Skip to content

fix(semantic): import-equals declares an import_binding symbol (#64) - #66

Merged
ericsssan merged 2 commits into
mainfrom
fix/64-import-equals-binding
Jun 25, 2026
Merged

fix(semantic): import-equals declares an import_binding symbol (#64)#66
ericsssan merged 2 commits into
mainfrom
fix/64-import-equals-binding

Conversation

@ericsssan

Copy link
Copy Markdown
Owner

Fixes #64.

Problem

The import X = require("./m") / import X = Y.Z (TSImportEqualsDeclaration) branch advanced past the binding name without creating a node or declaring it — unlike import * as X and default imports, which both emitDeclare(.import_binding, …). So X had no symbol:

  • symbolForIdentRef(X) returned null → references fell through to a name-keyed fallback and collided with same-named classes/locals (class C + import C = … both resolving to typeof C).
  • The binding was unanchored — it rendered as <no-node> in the ez-checker type oracle.

Fix

Capture the binding token, create its identifier node, and emitDeclare(.import_binding, local_node), mirroring the namespace/default-import paths. import_decl already uses lhs as the import-equals discriminator (.none) and rhs as the module reference, so the binding has no data slot — anchor it under the declaration via parent_fixups so it's reachable in the parent tree.

Validation

  • New tests: import X = require(...) and import X = Y.Z each declare an import_binding symbol that usages resolve to; the import C = N.C; class D extends C {} collision case resolves extends C to the import; and a parser test that the binding identifier is created and parented to the declaration.
  • TS conformance 17910/17913 · 1210/1223 (baseline); test262 3966/3966 · 1389/1389.
  • Semantic sweep over 19,233 files: 0 crashes, +1215 import_binding symbols and no other structural change (scopes/refs/diagnostics unchanged) — exactly the previously-missing import-equals bindings.

The `import X = require(...)` / `import X = Y.Z` branch in parseImportDeclaration
advanced past the binding name without creating a node or declaring it — unlike
`import * as X` and default imports — so `X` had no symbol. References to `X`
then fell through to a name-keyed fallback and collided with same-named
classes/locals (e.g. `class C` and `import C = …` both resolving to `typeof C`).

Capture the binding token, create its identifier node, and
`emitDeclare(.import_binding, local_node)`, mirroring the namespace/default
paths. `import_decl` already uses lhs as the import-equals discriminator (.none)
and rhs as the module reference, so the binding has no data slot — anchor it
under the declaration via parent_fixups so it is reachable in the parent tree
(previously it rendered as `<no-node>`).

Validated: full suite green; TS conformance 17910/17913 · 1210/1223;
test262 3966/3966 · 1389/1389; semantic sweep 0 crashes, +1215 import_binding
symbols and no other structural change (scopes/refs/diagnostics unchanged).
…ds; strengthen tests

Per review: emit the binding declare AFTER the TS1141/reserved-word guards (not
before) so a rejected `import X = require(nonLiteral)` leaves no orphaned phantom
symbol on the error path. Tests: tighten ref counts to exact (== 1), add the
deep-qualified `import X = Y.Z.W` case, a negative control that the module
reference's own identifiers (`Y`/`Z`) are not declared as bindings, and assert
exactly one identifier child is anchored to the declaration.
@ericsssan
ericsssan merged commit 63104c7 into main Jun 25, 2026
2 checks passed
@ericsssan
ericsssan deleted the fix/64-import-equals-binding branch June 25, 2026 06:42
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.

import-equals (import X = require() / import X = Y.Z) emits no binding symbol

1 participant