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:
@@ -163,8 +163,13 @@ export class Lexer {
|
|||||||
pos = 0;
|
pos = 0;
|
||||||
constructor(public readonly src: string) {}
|
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;
|
const { src } = this;
|
||||||
while (this.pos < src.length) {
|
while (this.pos < src.length) {
|
||||||
const c = src[this.pos]!;
|
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 {
|
startsWithBlockComment(): boolean {
|
||||||
this.skipTrivia();
|
this.skipWhitespaceAndLineComments();
|
||||||
return this.src[this.pos] === "/" && this.src[this.pos + 1] === "*";
|
return this.src[this.pos] === "/" && this.src[this.pos + 1] === "*";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -286,3 +286,54 @@ test("division is not treated as a regex literal", () => {
|
|||||||
`;
|
`;
|
||||||
expect(parse(source).name).toBe("P");
|
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");
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user