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 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 06:50:51 +05:30
co-authored by Claude Opus 5
parent 2c50a07ed2
commit 98a46091b5
2 changed files with 86 additions and 4 deletions
+35 -4
View File
@@ -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] === "*";
}
+51
View File
@@ -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 { <div>{x}</div> }
}
`;
expect(parse(page).name).toBe("P");
const component = `component C {
/* A note about this component. */
state y = 2
view { <p>{y}</p> }
}
`;
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 { <div>ok</div> }
}
`;
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 { <p>{name}</p> }
}
`;
expect(() => parse(source)).toThrow("Block comments are not allowed inside props");
});