Skip to content

fix(parser): let with binding list on the next line is one declaration (#59) - #68

Merged
ericsssan merged 2 commits into
mainfrom
fix/59-let-asi
Jun 25, 2026
Merged

fix(parser): let with binding list on the next line is one declaration (#59)#68
ericsssan merged 2 commits into
mainfrom
fix/59-let-asi

Conversation

@ericsssan

Copy link
Copy Markdown
Owner

Fixes #59.

Problem

At statement start in non-strict JS, let followed by a newline then a binding was mis-parsed — ASI fired after let:

let
    x = 1

es-parser produced ExpressionStatement(Identifier "let") + ExpressionStatement(x = 1) (assignment to an undeclared global) — no VariableDeclaration. espree / @typescript-eslint / Node all parse this as one let x = 1.

The .kw_let handler only treated {/let/yield/await as declarations across a newline; a plain identifier or [ fell through to the expression path.

Fix

There is no [no LineTerminator here] restriction between let and its BindingList, so an identifier / [-pattern / \u-escaped binding name continues the LexicalDeclaration — ASI must not fire. (The ExpressionStatement lookahead forbids let [, and let <id> has no valid expression parse.) Treat those next-tokens as declarations even across a newline. let\n {…} already worked; reserved operator words (instanceof, in, …) correctly remain expressions.

Validation

  • New tests: let\n x = 1, multi-declarator let\n x = {}, y = {}, array/object destructuring on the next line all parse as one declaration with declarators; let\n instanceof x stays an expression; and x is now a let binding (not an undeclared global) that usages resolve to.
  • test262 (the JS/ASI authority): 3966/3966 must-parse · 1389/1389 must-reject. Babel 1928/1928 · 1548/1548. TS conformance 17910/17913 · 1210/1223. Semantic sweep byte-identical, 0 crashes.

#59)

At statement start in non-strict JS, `let` followed by a newline then an
identifier or `[` was mis-parsed: ASI fired after `let`, producing a bare
`Identifier` ExpressionStatement plus a separate assignment, instead of a single
LexicalDeclaration. There is no `[no LineTerminator here]` restriction between
`let` and its BindingList — an identifier / `[` pattern / `\u`-escaped binding
name continues the declaration, so ASI must not fire (the ExpressionStatement
lookahead forbids `let [`, and `let <id>` has no valid expression parse). Treat
those next-tokens as declarations even across a newline, matching espree /
@typescript-eslint / Node. (`let\n {` already worked; reserved operator words
like `instanceof` correctly remain expressions.)

Fixes scope analysis (the binding is now a `let`, not an undeclared global
assignment) and every rule keyed on VariableDeclaration (indent, no-var,
prefer-const, one-var, …). Validated: full suite green incl. test262
3966/3966 must-parse · 1389/1389 must-reject (the ASI authority); babel
1928/1928 · 1548/1548; TS conformance 17910/17913 · 1210/1223; semantic sweep
byte-identical, 0 crashes.
…stmt contexts

The statement-list `let\n <id>` → declaration change leaked into single-statement
bodies via delegation: `if (a) let\n x = 1`, `while`/`for`/`do` bodies, and
labeled items (`lbl: let\n x`) wrongly became lexical declarations (forbidden
there) instead of the `let` identifier expression. The single-statement handlers
(parseNonDeclStatement/parseIfBody) and parseLabeledStatement delegated those
cases to parseStatement, relying on the old expression contract — now broken.

Parse the `let` expression directly (parseExprOrLabeledStatement) for the
newline/non-binding cases in those contexts instead of re-dispatching through
parseStatement. Verified across if/while/for/do bodies + single and nested
labels: `let\n x` is an expression there, while top-level/block stays a
declaration; same-line `let x` in those positions still errors as before.

Tests: add the single-statement-context guard; tighten the binding ref count to
exactly 2 and pin that the array-destructuring (`let\n [a] = b`) form binds `a`.
Conformance unchanged (test262 3966/3966 · 1389/1389, babel 1928/1928, TS
17910/17913).
@ericsssan
ericsssan merged commit 755eacf into main Jun 25, 2026
2 checks passed
@ericsssan
ericsssan deleted the fix/59-let-asi branch June 25, 2026 14:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

parser: let declaration with binding list on next line mis-parses (ASI after let)

1 participant