From 98a46091b5a88e5fd0c5c824f95f01013874dcc9 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Sat, 22 Aug 2026 06:50:51 +0530 Subject: [PATCH] fix(syntax): allow block comments between members `skipTrivia` skipped `// line comments` but not `/* block comments */`, so one written between two 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 parsed or did not depending on whether it happened to sit inside a block. `startsWithBlockComment` now skips only whitespace and line comments, so `props {}` keeps refusing block comments with its own explained error rather than silently swallowing one and dropping the declaration after it. Co-Authored-By: Claude Opus 5 --- packages/syntax/src/tokenizer.ts | 39 +++++++++++++++++++--- packages/syntax/test/syntax.test.ts | 51 +++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) 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 {
{x}
} +} +`; + expect(parse(page).name).toBe("P"); + + const component = `component C { + /* A note about this component. */ + state y = 2 + 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 {
ok
} +} +`; + expect(parse(spaced).name).toBe("Q"); +}); + +// Block comments inside props {} stay a deliberate, explained error -- the +// props parser cannot represent them, and silently skipping one would drop a +// declaration the author believed was there. +test("a block comment inside props is still refused with its own message", () => { + const source = `component C { + props { + /* not allowed here */ + name: string + } + view {

{name}

} +} +`; + expect(() => parse(source)).toThrow("Block comments are not allowed inside props"); +});