Skip to content

fix(parser,types): clear the ParseError long tail + Cluster A logical-assignment narrowing#1260

Merged
nickna merged 5 commits into
mainfrom
worktree-epic-99
Jul 5, 2026
Merged

fix(parser,types): clear the ParseError long tail + Cluster A logical-assignment narrowing#1260
nickna merged 5 commits into
mainfrom
worktree-epic-99

Conversation

@nickna

@nickna nickna commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to #1259 (merged) — continuation of epic #99 / #80 Track-1 (TS conformance). #1259 was merged before this branch's later commits were pushed, so this PR carries the remainder, across two pieces of work:

1. Clearing the ParseError long tail (7 tests)

Seven unrelated, independently-scoped parser gaps found while clearing what was left of the ParseError bucket:

  • Object-type/interface members separated only by a newline (no ;/,) now use the same ASI fallback as top-level interface members (bar(): { baz: T } followed by baz: T on the next line).
  • A parenthesized member/index expression is a valid assignment target for =, compound, and logical-assignment operators alike ((a.b) &&= v) — parens don't change whether an expression is a reference; unwrap them (recursively) before dispatch.
  • async is a contextual keyword, not exclusively a function/arrow marker: only commit to parsing an async function/arrow when it's immediately followed by function/</(; otherwise treat it as a plain identifier reference (if (async), a destructured async binding).
  • import.meta used as a statement (import.meta.foo();) was routed to the static import-declaration parser because the statement dispatcher only excluded dynamic-import's import(, not import. — both are expressions, not declarations.
  • export { x as y } (a bare specifier list, no declaration) inside declare global/declare module bodies was unhandled — only export <declaration> forms were.
  • export * as ns from './mod' (ES2020 namespace re-export) wasn't recognized; only bare export * from was.
  • An untagged template with an invalid escape sequence (`\xtraordinary`) is a real syntax error (TS1125) but a recoverable one — tsc keeps checking the rest of the file. The parser now substitutes an empty string for that part and records the offending line(s) instead of aborting the parse; the checker reports TS1125 there. Tagged templates are unaffected.

2. Cluster A: compound logical-assignment narrowing (logicalAssignment4/11)

Three gaps in CheckLogicalAssign / the if-condition type-guard analyzer, all needed together:

  • Post-assignment narrowing: results ||= []; results.push(100) (statement form) wasn't narrowed for the second statement — CheckLogicalAssign now installs the assignment's result type into the environment the same way CheckAssign already does for plain x = v.
  • &&= short-circuit narrowing within its own RHS: thing &&= thing.original spuriously flagged thing as possibly undefined on a read &&='s own short-circuit already guarantees is safe — narrow the LHS to truthy while checking the RHS, mirroring plain &&.
  • if (x OP= y) as a type-guard condition (new AnalyzeLogicalAssignGuard): the LHS narrows like a bare if (x); for &&= specifically, a truthy result additionally proves the RHS ran and was itself truthy, so a bare-identifier RHS narrows too (verified against tsc's own baseline — ||=/??= do NOT get this).
  • Also fixes a real pre-existing bug surfaced along the way: the "pick the wider of two candidate types" pattern in CheckLogicalAssign's result-type computation had its two IsCompatible branches' return values swapped, collapsing a union to its narrower constituent instead of the wider one.

Not fixed, left as separate follow-ups (distinct root causes from narrowing): logicalAssignment5 needs a new checker feature — TS2722 "cannot invoke an object which is possibly undefined" doesn't exist anywhere in the codebase. logicalAssignment6/7 need argument-compatibility checking to continue after a nullable-receiver diagnostic fires, instead of stopping there.

Result: TS conformance subset baseline 203 → 211 Pass, 7 → 0 ParseError, 0 regressions across both pieces of work.

Test plan

  • dotnet test (SharpTS.Tests) — 15452/15452 passing
  • SharpTS.Test262 interpreted + compiled baselines — 0 failures
  • SharpTS.TypeScriptConformance baseline — 211 Pass / 69 Fail / 0 ParseError / 17 Skipped / 1 TypeCheckError, 0 regressions vs. committed baseline

nickna added 5 commits July 5, 2026 14:34
Seven unrelated, independently-scoped parser gaps found while triaging
the last of the #99 Track-1 ParseError bucket:

- Object-type/interface members separated only by a newline (no ';'/',')
  now use the same ASI fallback as top-level interface members
  (`bar(): { baz: T }` followed by `baz: T` on the next line).
- A parenthesized member/index expression is a valid assignment target
  for '=', compound, and logical-assignment operators alike
  (`(a.b) &&= v`) — parens don't change whether an expression is a
  reference; unwrap them (recursively) before dispatch.
- `async` is a contextual keyword, not exclusively a function/arrow
  marker: only commit to parsing an async function/arrow when it's
  immediately followed by `function`/`<`/`(`; otherwise treat it as a
  plain identifier reference (`if (async)`, a destructured `async`
  binding).
- `import.meta` used as a statement (`import.meta.foo();`) was routed
  to the *static* import-declaration parser because the statement
  dispatcher only excluded dynamic-import's `import(`, not
  `import.` — both are expressions, not declarations.
- `export { x as y }` (a bare specifier list, no declaration) inside
  `declare global`/`declare module` bodies was unhandled — only
  `export <declaration>` forms were.
- `export * as ns from './mod'` (ES2020 namespace re-export) wasn't
  recognized; only bare `export * from` was. Threads a new
  Stmt.Export.NamespaceExportName through parsing (the re-export
  binding step is a no-op for single-file checking either way).
- An untagged template with an invalid escape sequence
  (`` `\xtraordinary` ``) is a real syntax error (TS1125) but a
  recoverable one — tsc keeps checking the rest of the file. The
  parser now substitutes an empty string for that part and records
  the offending line(s) on the AST node instead of aborting the parse;
  the checker reports TS1125 there. Tagged templates are unaffected
  (their raw form already tolerated this).

Baseline: 7 ParseError -> 0 (296-test subset fully parses now); 3
flip straight to Pass, the rest land in Fail (real, now-measurable
type-checking gaps instead of a fatal abort). 0 regressions.
203 -> 207 Pass, 7 -> 0 ParseError. 0 regressions.
…if-guards

Three gaps in CheckLogicalAssign / the if-condition type-guard analyzer,
all needed together to clear logicalAssignment4's remaining diagnostics:

- Post-assignment narrowing: `results ||= []; results.push(100)` (the
  statement form, not chained directly onto the assignment) wasn't
  narrowed for the second statement at all. CheckLogicalAssign now
  installs the assignment's result type into the environment the same
  way CheckAssign already does for a plain `x = v`.
- `&&=` only evaluates/assigns its RHS when the LHS is truthy — narrow
  the LHS to its truthy constituents while checking the RHS (mirrors
  plain `&&`'s CheckLogical). Without this, `thing &&= thing.original`
  spuriously flagged `thing` as possibly undefined on a read `&&=`'s
  own short-circuit already guarantees is safe.
- `if (x OP= y)` is now recognized as a type-guard condition (new
  AnalyzeLogicalAssignGuard): the LHS narrows the same way a bare
  `if (x)` would, using its post-assignment type. For `&&=`
  specifically, a truthy result additionally proves the RHS ran and
  was itself truthy — if the RHS is a bare identifier, narrow that
  too (`||=`/`??=` don't get this: their truthy branch is also
  reachable via an already-truthy/non-nullish LHS that never touched
  the RHS).

Also fixes a real pre-existing bug surfaced while wiring the above: the
"pick the wider of two candidate types" pattern in CheckLogicalAssign's
resultType computation had its two IsCompatible branches' return values
swapped, collapsing a union to its narrower constituent instead of the
wider one. Scoped to CheckLogicalAssign only — CheckLogical (plain
&&/||) has the identical pattern and is unaudited.

Flips logicalAssignment4 and (unplanned bonus) logicalAssignment11 from
Fail to Pass, 0 regressions. logicalAssignment5 needs a distinct new
checker feature entirely (TS2722 "cannot invoke possibly undefined" does
not exist anywhere in the codebase); logicalAssignment6/7 need argument
compatibility checking to continue after a nullable-receiver diagnostic
fires, instead of stopping there — both left as separate follow-ups.
@nickna nickna changed the title fix(parser): clear the remaining TS conformance ParseError long tail fix(parser,types): clear the ParseError long tail + Cluster A logical-assignment narrowing Jul 5, 2026
@nickna nickna merged commit 89c1f36 into main Jul 5, 2026
2 checks passed
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.

1 participant