diff --git a/packages/syntax/src/tokenizer.ts b/packages/syntax/src/tokenizer.ts index 52554d69..5a4d5629 100644 --- a/packages/syntax/src/tokenizer.ts +++ b/packages/syntax/src/tokenizer.ts @@ -163,8 +163,13 @@ export class Lexer { pos = 0; constructor(public readonly src: string) {} - /** Skip whitespace and `// line comments`. */ - private skipTrivia(): void { + /** + * Skip whitespace and `// line comments`, but NOT block comments. + * + * Kept separate from `skipTrivia` so `startsWithBlockComment` can still see a + * block comment that `skipTrivia` would otherwise consume. + */ + private skipWhitespaceAndLineComments(): void { const { src } = this; while (this.pos < src.length) { const c = src[this.pos]!; @@ -180,9 +185,35 @@ export class Lexer { } } - /** True when the next non-trivia characters open a block comment. */ + /** + * Skip whitespace, line comments and block comments. + * + * Block comments used to be skipped only inside a braced body, so one written + * between two members failed with a bare "Unexpected character '/'" -- the + * same comment parsed or did not depending on where it sat. + */ + private skipTrivia(): void { + const { src } = this; + while (this.pos < src.length) { + this.skipWhitespaceAndLineComments(); + if (src[this.pos] === "/" && src[this.pos + 1] === "*") { + const close = src.indexOf("*/", this.pos + 2); + this.pos = close === -1 ? src.length : close + 2; + continue; + } + break; + } + } + + /** + * True when the next non-trivia characters open a block comment. + * + * Deliberately skips only whitespace and line comments: `props {}` refuses + * block comments with an explained error, and that check must run before + * `skipTrivia` would swallow the comment and drop a declaration silently. + */ startsWithBlockComment(): boolean { - this.skipTrivia(); + this.skipWhitespaceAndLineComments(); return this.src[this.pos] === "/" && this.src[this.pos + 1] === "*"; } diff --git a/packages/syntax/test/syntax.test.ts b/packages/syntax/test/syntax.test.ts index 8115628c..efb7c469 100644 --- a/packages/syntax/test/syntax.test.ts +++ b/packages/syntax/test/syntax.test.ts @@ -286,3 +286,54 @@ test("division is not treated as a regex literal", () => { `; expect(parse(source).name).toBe("P"); }); + +// `skipTrivia` skipped `// line comments` but not `/* block comments */`, so a +// block comment between page or component members failed with a bare +// "Unexpected character '/'". Block comments inside a braced body already +// worked, which made the failure look arbitrary: the same comment moved a few +// lines parsed or did not depending on whether it sat inside a block. +test("block comments are allowed between members", () => { + const page = `page P { + /* Explains the state below. */ + state x = 1 + + /* Explains the view below. */ + view {
{y}
} +} +`; + expect(parse(component).name).toBe("C"); + + // A block comment before the opening brace, and a multi-line one. + const spaced = `/* Leading note. */ +page Q { + /* + * A multi-line note about the view. + */ + view {{name}
} +} +`; + expect(() => parse(source)).toThrow("Block comments are not allowed inside props"); +});