fix(parser): skip interpolated templates in disabled-feature recovery#889
Conversation
When the parser rejects a disabled traditional `for (;;)` loop, it records
the warning and skips the loop's header and body via the error-recovery
routines (SkipBalancedParens, SkipBlock, SkipUntilSemicolon). Those routines
counted braces token by token, but the `}` that closes a `${ ... }` template
substitution is lexed as a plain RightBrace under the default goal — the
TemplateMiddle/TemplateTail span is only produced when the lexer is invoked
with the template-tail goal *after* that `}`. So the skip miscounted the
substitution's `}` as the structural brace ending the skipped region, stopped
early, and the trailing backtick was re-scanned as a fresh, unterminated
template literal running to EOF. The result was a misleading
"Unterminated template literal" error at EOF instead of the real
"enable --compat-traditional-for-loop" diagnostic at the `for` keyword.
Add a shared SkipTemplateLiteral helper that skips a complete template literal
as a unit — recursing through nested substitutions and templates, balancing
inner object/block braces, and re-lexing each continuation span with the
template-tail goal (as ParseTemplateLiteral does). Route SkipBlock,
SkipBalancedParens, and SkipUntilSemicolon through it, which fixes the whole
class of disabled-feature recovery skips (for-loop body/header/non-block body,
and the sibling while / do-while constructs), not just the reported case.
Tests (both verified red without the parser change):
- tests/language/statements/unsupported-features.js: a disabled for-loop with
an interpolated template (block body, non-block body, header, nested) is
skipped as a clean no-op and subsequent code runs.
- scripts/test-cli-parser.ts: recovery succeeds in interpreter and bytecode
mode, never regresses to "Unterminated template literal", and the
human-readable diagnostic references --compat-traditional-for-loop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds template-literal-aware parser recovery and expands regression tests for disabled-feature parsing with interpolated template literals in Template-literal error recovery for disabled loops
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
scripts/test-cli-parser.ts (1)
191-255: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCover the shared
while/do...whilerecovery path too.
SkipTemplateLiteralis now exercised here only through disabledforrecovery, but the sameSkipBalancedParenspath also drives disabledwhileanddo...whilerecovery insource/units/Goccia.Parser.pas. Adding one regression per construct would lock down the broader contract this parser change now depends on.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/test-cli-parser.ts` around lines 191 - 255, The recovery coverage here only exercises the disabled traditional for-loop path, but the same SkipBalancedParens/SkipTemplateLiteral flow is also used by disabled while and do...while recovery in Goccia.Parser. Extend the recoveryCases in test-cli-parser.ts with one regression each for while and do...while template-literal recovery, and keep the same assertions around json.ok, exitCode, expected output, and the compat-flag diagnostic so the shared parser contract stays covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@scripts/test-cli-parser.ts`:
- Around line 191-255: The recovery coverage here only exercises the disabled
traditional for-loop path, but the same SkipBalancedParens/SkipTemplateLiteral
flow is also used by disabled while and do...while recovery in Goccia.Parser.
Extend the recoveryCases in test-cli-parser.ts with one regression each for
while and do...while template-literal recovery, and keep the same assertions
around json.ok, exitCode, expected output, and the compat-flag diagnostic so the
shared parser contract stays covered.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: a4db99c8-bb3b-4d37-9f50-010b63fae0de
📒 Files selected for processing (3)
scripts/test-cli-parser.tssource/units/Goccia.Parser.pastests/language/statements/unsupported-features.js
Suite TimingTest Runner (interpreted: 10,962 passed; bytecode: 10,962 passed)
MemoryGC rows aggregate the main thread plus all worker thread-local GCs. Test runner worker shutdown frees thread-local heaps in bulk; that shutdown reclamation is not counted as GC collections or collected objects.
Benchmarks (interpreted: 436; bytecode: 436)
MemoryGC rows aggregate the main thread plus all worker thread-local GCs. Benchmark runner performs explicit between-file collections, so collection and collected-object counts can be much higher than the test runner.
Measured on ubuntu-latest x64. |
Benchmark Results436 benchmarks · PR vs same-runner Interpreted: 🟢 44 improved · 🔴 50 regressed · 342 unchanged · avg +2.6% Typical per-run noise (median variance): interpreted ±4.3%, bytecode ±2.1%. Deltas within noise overlap and read as unchanged. arraybuffer.js — Interp: 14 unch. · avg -0.1% · Bytecode: 14 unch. · avg -1.3%
arrays.js — Interp: 🔴 6, 13 unch. · avg -1.2% · Bytecode: 🟢 1, 🔴 2, 16 unch. · avg +0.4%
async-await.js — Interp: 6 unch. · avg +1.5% · Bytecode: 🔴 1, 5 unch. · avg -0.1%
async-generators.js — Interp: 2 unch. · avg -2.9% · Bytecode: 2 unch. · avg -0.2%
atomics.js — Interp: 🔴 1, 5 unch. · avg -1.7% · Bytecode: 6 unch. · avg -1.7%
base64.js — Interp: 🟢 3, 7 unch. · avg +2.5% · Bytecode: 🟢 2, 🔴 1, 7 unch. · avg +0.5%
classes.js — Interp: 🟢 1, 🔴 4, 26 unch. · avg -1.9% · Bytecode: 🟢 4, 27 unch. · avg +1.6%
closures.js — Interp: 11 unch. · avg -0.5% · Bytecode: 11 unch. · avg +0.9%
collections.js — Interp: 🔴 1, 11 unch. · avg +1.1% · Bytecode: 12 unch. · avg +1.3%
csv.js — Interp: 🟢 2, 11 unch. · avg +4.8% · Bytecode: 🔴 1, 12 unch. · avg -0.5%
destructuring.js — Interp: 🟢 2, 🔴 4, 16 unch. · avg -2.0% · Bytecode: 🟢 3, 19 unch. · avg +1.0%
fibonacci.js — Interp: 🟢 1, 🔴 1, 6 unch. · avg -1.5% · Bytecode: 8 unch. · avg +0.5%
float16array.js — Interp: 🟢 7, 🔴 3, 22 unch. · avg +0.5% · Bytecode: 🔴 1, 31 unch. · avg +0.7%
for-in/for-in.js — Interp: 🟢 3 · avg +242.3% · Bytecode: 🟢 3 · avg +322.1%
for-of.js — Interp: 🔴 6, 1 unch. · avg -6.4% · Bytecode: 🔴 1, 6 unch. · avg -0.6%
generators.js — Interp: 4 unch. · avg -0.9% · Bytecode: 4 unch. · avg -3.3%
intl.js — Interp: 🟢 1, 🔴 1, 4 unch. · avg -1.3% · Bytecode: 6 unch. · avg +2.4%
iterators.js — Interp: 🟢 1, 🔴 5, 36 unch. · avg -0.3% · Bytecode: 🟢 2, 🔴 2, 38 unch. · avg +1.2%
json.js — Interp: 🟢 3, 20 unch. · avg +0.0% · Bytecode: 🟢 1, 🔴 2, 20 unch. · avg +0.0%
jsx.jsx — Interp: 🟢 1, 🔴 2, 18 unch. · avg +0.4% · Bytecode: 🔴 2, 19 unch. · avg -2.0%
modules.js — Interp: 9 unch. · avg -1.1% · Bytecode: 9 unch. · avg +1.6%
numbers.js — Interp: 🟢 2, 9 unch. · avg +1.8% · Bytecode: 🟢 1, 🔴 2, 8 unch. · avg +1.1%
objects.js — Interp: 🔴 1, 6 unch. · avg -1.2% · Bytecode: 7 unch. · avg -3.8%
promises.js — Interp: 🟢 2, 10 unch. · avg +5.0% · Bytecode: 🔴 1, 11 unch. · avg -0.1%
property-access.js — Interp: 5 unch. · avg -4.5% · Bytecode: 🟢 1, 4 unch. · avg +4.9%
regexp.js — Interp: 🟢 1, 🔴 1, 9 unch. · avg -1.4% · Bytecode: 🟢 1, 🔴 3, 7 unch. · avg -2.3%
strings.js — Interp: 🟢 1, 🔴 3, 15 unch. · avg -0.1% · Bytecode: 🟢 3, 🔴 1, 15 unch. · avg +1.3%
temporal.js — Interp: 🔴 1, 5 unch. · avg -2.8% · Bytecode: 🔴 2, 4 unch. · avg -3.2%
tsv.js — Interp: 🟢 1, 8 unch. · avg +3.8% · Bytecode: 🟢 1, 8 unch. · avg +1.8%
typed-arrays.js — Interp: 🟢 2, 🔴 7, 13 unch. · avg -3.3% · Bytecode: 🟢 1, 🔴 5, 16 unch. · avg -0.0%
uint8array-encoding.js — Interp: 🟢 7, 11 unch. · avg +26.4% · Bytecode: 🟢 4, 🔴 1, 13 unch. · avg -0.2%
weak-collections.js — Interp: 🟢 3, 🔴 3, 9 unch. · avg +5.1% · Bytecode: 🟢 3, 🔴 1, 11 unch. · avg +8.5%
Deterministic profile diffDeterministic profile diff: no significant changes. Measured on ubuntu-latest x64. Each PR run also builds the |
test262 Conformance
Areas closest to 100%
Per-test deltas (+2 / -0)Newly passing (2):
Steady-state failures are non-blocking; regressions vs the cached main baseline (lower total pass count, or any PASS → non-PASS transition) fail the conformance gate. Measured on ubuntu-latest x64, bytecode mode. Areas grouped by the first two test262 path components; minimum 25 attempted tests, areas already at 100% excluded. Δ vs main compares against the most recent cached |
The disabled while and do...while recovery paths share the same SkipBalancedParens / SkipBlock + SkipTemplateLiteral flow as the traditional for-loop, so extend the CLI parser recovery cases with one regression each: a while loop with an interpolated template in its condition and a do...while loop with one in its body. Each case now carries its own compat flag and the human-readable diagnostic assertion runs per case, so json.ok, exit code, expected output, and the compat-flag diagnostic stay covered across the whole shared parser contract. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
for (;;)loop containing an interpolated template literal (e.g.obj[`u${level}`] = level;) reported a misleadingSyntaxError: Unterminated template literalat EOF instead of the real "enable--compat-traditional-for-loop" diagnostic at theforkeyword.SkipBalancedParens/SkipBlock/SkipUntilSemicolon. These counted braces naively, but the}that closes a${ … }substitution is lexed as a plainRightBraceunder the default goal (theTemplateMiddle/TemplateTailspan is only produced when the lexer runs with the template-tail goal after that}). The skip miscounted the substitution's}as the structural brace ending the region, stopped early, and the trailing backtick was re-scanned as a fresh, unterminated template literal running to EOF.SkipTemplateLiteralhelper that skips a complete template literal as a unit — recursing through nested substitutions/templates, balancing inner object/block braces, and re-lexing each continuation span with the template-tail goal (mirroringParseTemplateLiteral). Route the three recovery skips through it.${}, and the siblingwhile/do-whileconstructs, while the enabled--compat-traditional-for-looppath still parses and executes templates correctly.Testing
Added tests (both verified red without the parser change):
tests/language/statements/unsupported-features.js— a disabled for-loop with an interpolated template (block body, non-block body, header, nested) is skipped as a clean no-op and subsequent code runs.scripts/test-cli-parser.ts— recovery succeeds in interpreter and bytecode mode, never regresses toUnterminated template literal, and the human-readable diagnostic references--compat-traditional-for-loop.Verification run:
bun scripts/test-cli-parser.ts./format.pas --checkSyntaxError: Unterminated template literal(0 passed / 1 failed) — confirms a genuine regression test🤖 Generated with Claude Code