From d2341e3f2977088d09a6f0063960a3d40d245cdf Mon Sep 17 00:00:00 2001 From: andre-fajar-n Date: Sun, 11 Oct 2026 08:44:36 +0700 Subject: [PATCH 1/2] fix: Fix nil pointer panic in getLateBoundSymbol under --noCheck --- tsc/internal/checker/checker.go | 12 ++++++++---- .../noCheckExpandoElementAccessAssignment.js | 16 ++++++++++++++++ ...oCheckExpandoElementAccessAssignment.symbols | 13 +++++++++++++ .../noCheckExpandoElementAccessAssignment.types | 17 +++++++++++++++++ .../noCheckExpandoElementAccessAssignment.ts | 8 ++++++++ 5 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignment.js create mode 100644 tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignment.symbols create mode 100644 tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignment.types create mode 100644 tsc/testdata/tests/cases/compiler/noCheckExpandoElementAccessAssignment.ts diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 345ea00ce1437..b9208e3fb4ebd 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -14691,10 +14691,14 @@ func (c *Checker) getLateBoundSymbol(symbol *ast.Symbol) *ast.Symbol { if links.lateSymbol == nil && core.Some(symbol.Declarations(), c.hasLateBindableName) { // force late binding of members/exports. This will set the late-bound symbol parent := c.getMergedSymbol(symbol.Parent()) - if core.Some(symbol.Declarations(), ast.HasStaticModifier) { - c.getExportsOfSymbol(parent) - } else { - c.getMembersOfSymbol(parent) + // Parent can be nil for top-level script expandos (SourceFile has no symbol) when + // checking was skipped (--noCheck) and late binding has not run yet. + if parent != nil { + if core.Some(symbol.Declarations(), ast.HasStaticModifier) { + c.getExportsOfSymbol(parent) + } else { + c.getMembersOfSymbol(parent) + } } } if links.lateSymbol == nil { diff --git a/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignment.js b/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignment.js new file mode 100644 index 0000000000000..0b1f387e18dfc --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignment.js @@ -0,0 +1,16 @@ +//// [tests/cases/compiler/noCheckExpandoElementAccessAssignment.ts] //// + +//// [noCheckExpandoElementAccessAssignment.ts] +const k = "bar"; +foo[k] = 1; +function foo() {} + + + + +//// [noCheckExpandoElementAccessAssignment.d.ts] +declare const k = "bar"; +declare function foo(): void; +declare namespace foo { + var bar: number; +} diff --git a/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignment.symbols b/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignment.symbols new file mode 100644 index 0000000000000..6915aec55afa0 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignment.symbols @@ -0,0 +1,13 @@ +//// [tests/cases/compiler/noCheckExpandoElementAccessAssignment.ts] //// + +=== noCheckExpandoElementAccessAssignment.ts === +const k = "bar"; +>k : Symbol(k, Decl(noCheckExpandoElementAccessAssignment.ts, 0, 5)) + +foo[k] = 1; +>foo : Symbol(foo, Decl(noCheckExpandoElementAccessAssignment.ts, 1, 11)) +>k : Symbol(k, Decl(noCheckExpandoElementAccessAssignment.ts, 0, 5)) + +function foo() {} +>foo : Symbol(foo, Decl(noCheckExpandoElementAccessAssignment.ts, 1, 11)) + diff --git a/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignment.types b/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignment.types new file mode 100644 index 0000000000000..4dbd3c67ddde0 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignment.types @@ -0,0 +1,17 @@ +//// [tests/cases/compiler/noCheckExpandoElementAccessAssignment.ts] //// + +=== noCheckExpandoElementAccessAssignment.ts === +const k = "bar"; +>k : "bar" +>"bar" : "bar" + +foo[k] = 1; +>foo[k] = 1 : 1 +>foo[k] : number +>foo : { (): void; bar: number; } +>k : "bar" +>1 : 1 + +function foo() {} +>foo : { (): void; bar: number; } + diff --git a/tsc/testdata/tests/cases/compiler/noCheckExpandoElementAccessAssignment.ts b/tsc/testdata/tests/cases/compiler/noCheckExpandoElementAccessAssignment.ts new file mode 100644 index 0000000000000..5cf68fc81395b --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/noCheckExpandoElementAccessAssignment.ts @@ -0,0 +1,8 @@ +// @target: es2015 +// @noCheck: true +// @emitDeclarationOnly: true +// @declaration: true + +const k = "bar"; +foo[k] = 1; +function foo() {} From 5e9b6cc88e224b6c09c73e0a72029d64e3e8245e Mon Sep 17 00:00:00 2001 From: andre-fajar-n Date: Tue, 20 Oct 2026 15:17:15 +0700 Subject: [PATCH 2/2] Bind expando parent to host and resolve exports in checker Co-authored-by: Cursor --- tsc/internal/binder/binder.go | 6 ++++++ tsc/internal/checker/checker.go | 15 +++++++-------- ...xpandoElementAccessAssignmentInitializer.js | 16 ++++++++++++++++ ...oElementAccessAssignmentInitializer.symbols | 13 +++++++++++++ ...ndoElementAccessAssignmentInitializer.types | 18 ++++++++++++++++++ ...heckExpandoElementAccessAssignmentModule.js | 15 +++++++++++++++ ...xpandoElementAccessAssignmentModule.symbols | 13 +++++++++++++ ...kExpandoElementAccessAssignmentModule.types | 17 +++++++++++++++++ ...xpandoElementAccessAssignmentInitializer.ts | 8 ++++++++ ...heckExpandoElementAccessAssignmentModule.ts | 8 ++++++++ 10 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentInitializer.js create mode 100644 tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentInitializer.symbols create mode 100644 tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentInitializer.types create mode 100644 tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentModule.js create mode 100644 tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentModule.symbols create mode 100644 tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentModule.types create mode 100644 tsc/testdata/tests/cases/compiler/noCheckExpandoElementAccessAssignmentInitializer.ts create mode 100644 tsc/testdata/tests/cases/compiler/noCheckExpandoElementAccessAssignmentModule.ts diff --git a/tsc/internal/binder/binder.go b/tsc/internal/binder/binder.go index a95bb19ecdff7..cce1fec94be0d 100644 --- a/tsc/internal/binder/binder.go +++ b/tsc/internal/binder/binder.go @@ -1064,6 +1064,12 @@ func (b *Binder) bindDeferredExpandoAssignment(node *ast.Node) { if symbol = getInitializerSymbol(symbol); symbol != nil { if ast.HasDynamicName(node) { b.bindAnonymousDeclaration(node, ast.SymbolFlagsProperty|ast.SymbolFlagsAssignment, ast.InternalSymbolNameComputed) + // Point the anonymous computed assignment at the expando host. For top-level + // scripts, bindAnonymousDeclaration would otherwise set parent to a nil + // SourceFile symbol, which breaks getLateBoundSymbol under --noCheck. + if declSym := node.Symbol(); declSym != nil { + declSym.SetParent(symbol) + } b.addLateBoundAssignmentDeclarationToSymbol(node, symbol) } else { // We declare expandos only when there are no non-expando declarations for that name. diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index c4f09876bb20e..ab940ffbd231b 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -14687,14 +14687,13 @@ func (c *Checker) getLateBoundSymbol(symbol *ast.Symbol) *ast.Symbol { if links.lateSymbol == nil && core.Some(symbol.Declarations(), c.hasLateBindableName) { // force late binding of members/exports. This will set the late-bound symbol parent := c.getMergedSymbol(symbol.Parent()) - // Parent can be nil for top-level script expandos (SourceFile has no symbol) when - // checking was skipped (--noCheck) and late binding has not run yet. - if parent != nil { - if core.Some(symbol.Declarations(), ast.HasStaticModifier) { - c.getExportsOfSymbol(parent) - } else { - c.getMembersOfSymbol(parent) - } + // Function/namespace expandos are stored on exports (see AssignmentDeclaration), + // not members. BinaryExpression declarations never have a static modifier, so + // Assignment is the signal to resolve exports. + if core.Some(symbol.Declarations(), ast.HasStaticModifier) || symbol.Flags()&ast.SymbolFlagsAssignment != 0 { + c.getExportsOfSymbol(parent) + } else { + c.getMembersOfSymbol(parent) } } if links.lateSymbol == nil { diff --git a/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentInitializer.js b/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentInitializer.js new file mode 100644 index 0000000000000..d051f353d4b16 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentInitializer.js @@ -0,0 +1,16 @@ +//// [tests/cases/compiler/noCheckExpandoElementAccessAssignmentInitializer.ts] //// + +//// [noCheckExpandoElementAccessAssignmentInitializer.ts] +const k = "bar"; +const foo = () => {}; +foo[k] = 1; + + + + +//// [noCheckExpandoElementAccessAssignmentInitializer.d.ts] +declare const k = "bar"; +declare function foo(): void; +declare namespace foo { + var bar: number; +} diff --git a/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentInitializer.symbols b/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentInitializer.symbols new file mode 100644 index 0000000000000..4fb0506542cee --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentInitializer.symbols @@ -0,0 +1,13 @@ +//// [tests/cases/compiler/noCheckExpandoElementAccessAssignmentInitializer.ts] //// + +=== noCheckExpandoElementAccessAssignmentInitializer.ts === +const k = "bar"; +>k : Symbol(k, Decl(noCheckExpandoElementAccessAssignmentInitializer.ts, 0, 5)) + +const foo = () => {}; +>foo : Symbol(foo, Decl(noCheckExpandoElementAccessAssignmentInitializer.ts, 1, 5)) + +foo[k] = 1; +>foo : Symbol(foo, Decl(noCheckExpandoElementAccessAssignmentInitializer.ts, 1, 5)) +>k : Symbol(k, Decl(noCheckExpandoElementAccessAssignmentInitializer.ts, 0, 5)) + diff --git a/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentInitializer.types b/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentInitializer.types new file mode 100644 index 0000000000000..321d46d610070 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentInitializer.types @@ -0,0 +1,18 @@ +//// [tests/cases/compiler/noCheckExpandoElementAccessAssignmentInitializer.ts] //// + +=== noCheckExpandoElementAccessAssignmentInitializer.ts === +const k = "bar"; +>k : "bar" +>"bar" : "bar" + +const foo = () => {}; +>foo : { (): void; bar: number; } +>() => {} : { (): void; bar: number; } + +foo[k] = 1; +>foo[k] = 1 : 1 +>foo[k] : number +>foo : { (): void; bar: number; } +>k : "bar" +>1 : 1 + diff --git a/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentModule.js b/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentModule.js new file mode 100644 index 0000000000000..bcbd119670ea3 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentModule.js @@ -0,0 +1,15 @@ +//// [tests/cases/compiler/noCheckExpandoElementAccessAssignmentModule.ts] //// + +//// [noCheckExpandoElementAccessAssignmentModule.ts] +const k = "bar"; +export function foo() {} +foo[k] = 1; + + + + +//// [noCheckExpandoElementAccessAssignmentModule.d.ts] +export declare function foo(): void; +export declare namespace foo { + var bar: number; +} diff --git a/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentModule.symbols b/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentModule.symbols new file mode 100644 index 0000000000000..ef07eb5027a9b --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentModule.symbols @@ -0,0 +1,13 @@ +//// [tests/cases/compiler/noCheckExpandoElementAccessAssignmentModule.ts] //// + +=== noCheckExpandoElementAccessAssignmentModule.ts === +const k = "bar"; +>k : Symbol(k, Decl(noCheckExpandoElementAccessAssignmentModule.ts, 0, 5)) + +export function foo() {} +>foo : Symbol(foo, Decl(noCheckExpandoElementAccessAssignmentModule.ts, 0, 16)) + +foo[k] = 1; +>foo : Symbol(foo, Decl(noCheckExpandoElementAccessAssignmentModule.ts, 0, 16)) +>k : Symbol(k, Decl(noCheckExpandoElementAccessAssignmentModule.ts, 0, 5)) + diff --git a/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentModule.types b/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentModule.types new file mode 100644 index 0000000000000..67100918b2ad5 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/noCheckExpandoElementAccessAssignmentModule.types @@ -0,0 +1,17 @@ +//// [tests/cases/compiler/noCheckExpandoElementAccessAssignmentModule.ts] //// + +=== noCheckExpandoElementAccessAssignmentModule.ts === +const k = "bar"; +>k : "bar" +>"bar" : "bar" + +export function foo() {} +>foo : { (): void; bar: number; } + +foo[k] = 1; +>foo[k] = 1 : 1 +>foo[k] : number +>foo : { (): void; bar: number; } +>k : "bar" +>1 : 1 + diff --git a/tsc/testdata/tests/cases/compiler/noCheckExpandoElementAccessAssignmentInitializer.ts b/tsc/testdata/tests/cases/compiler/noCheckExpandoElementAccessAssignmentInitializer.ts new file mode 100644 index 0000000000000..4bbe3e7a074ed --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/noCheckExpandoElementAccessAssignmentInitializer.ts @@ -0,0 +1,8 @@ +// @target: es2015 +// @noCheck: true +// @emitDeclarationOnly: true +// @declaration: true + +const k = "bar"; +const foo = () => {}; +foo[k] = 1; diff --git a/tsc/testdata/tests/cases/compiler/noCheckExpandoElementAccessAssignmentModule.ts b/tsc/testdata/tests/cases/compiler/noCheckExpandoElementAccessAssignmentModule.ts new file mode 100644 index 0000000000000..a5a5c41aa4140 --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/noCheckExpandoElementAccessAssignmentModule.ts @@ -0,0 +1,8 @@ +// @target: es2015 +// @noCheck: true +// @emitDeclarationOnly: true +// @declaration: true + +const k = "bar"; +export function foo() {} +foo[k] = 1;