fix(syntax): do not let a regex literal unbalance a block

The brace scanner knew about strings and comments but had no case for regex
literals. A quote inside one opened a phantom string that swallowed every brace
until the next quote; a lone `{` or `}` inside one miscounted block depth. Both
failed the component with "Unbalanced braces" pointing at the block's first line.

`/-/g` parsed fine, which is why this went unnoticed -- it needs a quote or a
brace inside the pattern to bite.

Regex-vs-division is decided by scanning back to the last significant
character, erring towards division: mistaking division for a regex would
swallow code to the next `/` and lose any braces between. A regex cannot span a
newline, so an unterminated one on the line is treated as "not a regex", which
is what keeps a bare URL in view text intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 06:05:09 +05:30
co-authored by Claude Opus 5
parent 0ddb481159
commit 9746e8e875
2 changed files with 137 additions and 0 deletions
+51
View File
@@ -235,3 +235,54 @@ test("comments may contain apostrophes without unbalancing a block", () => {
// The URL in view text must survive: `//` is only a comment at line start.
expect(JSON.stringify(ast.view)).toContain("https://example.com/a//b");
});
// A regex literal is not a string and not a pair of braces. The brace scanner
// knew about quotes and comments but had no case for regexes, so a quote inside
// one opened a phantom string that swallowed every brace until the next quote,
// and a lone `{` or `}` inside one miscounted depth. Both failed the whole
// component -- with a green build in the reported case, because the damage
// landed in generated output rather than at parse time.
test("regex literals do not unbalance a block", () => {
const mk = (body: string) =>
`page P {
load server {
${body}
return { x };
}
view { <div>{x}</div> }
}
`;
// A quote inside a regex used to open a string that ran to the next quote.
expect(parse(mk(` const x = /it's/.test("its");`)).name).toBe("P");
expect(parse(mk(` const x = /"/.test("q");`)).name).toBe("P");
// A brace inside a regex used to be counted as block depth.
expect(parse(mk(` const x = /\{/.test("{");`)).name).toBe("P");
expect(parse(mk(` const x = /}/.test("}");`)).name).toBe("P");
// A brace quantifier is balanced, but must not be counted either.
expect(parse(mk(` const x = /^a{2,3}$/.test("aa");`)).name).toBe("P");
// A `/` inside a character class does not close the regex.
expect(parse(mk(` const x = /[/'"{]/.test("/");`)).name).toBe("P");
// The case that already worked must keep working.
expect(parse(mk(` const x = "a-b".replace(/-/g, " ");`)).name).toBe("P");
});
// Division must not be mistaken for a regex, or the scanner would swallow code
// from the `/` to the next one and lose any braces in between.
test("division is not treated as a regex literal", () => {
const source = `page P {
load server {
const half = 10 / 2;
const ratio = (a + b) / 2;
const each = items[0] / total;
if (half > 1) {
return { half };
}
return { half: 0 };
}
view { <div>{half}</div> }
}
`;
expect(parse(source).name).toBe("P");
});