Skip to content

Fix nil dereference crash during checker initialization when resolving array types#2970

Open
Andarist wants to merge 1 commit intomicrosoft:mainfrom
Andarist:fix/array-types-crash-during-init
Open

Fix nil dereference crash during checker initialization when resolving array types#2970
Andarist wants to merge 1 commit intomicrosoft:mainfrom
Andarist:fix/array-types-crash-during-init

Conversation

@Andarist
Copy link
Contributor

@Andarist Andarist commented Mar 3, 2026

Fixes #2953

Problem

During initializeChecker, the first merge loop merges file locals into the global symbol table. When two files declare declare module 'mymod' — one re-exporting a namespace import (import * as foo from 'foo'; export { foo }) and another exporting a conflicting symbol — mergeSymbol resolves the alias chain. Since esModuleInterop is always on in typescript-go, this triggers resolveESModuleSymbolgetTypeWithSyntheticDefaultImportTypegetSpreadType, which deeply resolves the module's type, including any string[] properties. This calls getArrayOrTupleTargetTypecreateDeferredTypeReference, which dereferences globalArrayType — still nil because we're in the merge loop, before global types are initialized.

The related PR #21563 fixed a similar class of bug for global augmentations, but that occurs in a later phase of initialization.

Fix

Introduce lazy initialization for globalArrayType and globalReadonlyArrayType via getGlobalArrayType() / getGlobalReadonlyArrayType() getters. These resolve the type on first access and cache it, so the crash path gets a valid type instead of nil.

When Array comes from a single lib file (e.g. @lib: es5), its symbol is non-transient. If a global augmentation later adds members (e.g. interface Array<T> { customMethod(): T }), mergeSymbol would clone the Array symbol — creating a new identity disconnected from the already-cached globalArrayType. This causes augmented members to be invisible or have leaked type parameters.

With multiple lib files (e.g. @lib: es2015), this doesn't happen because the natural multi-file merge already makes the Array symbol (and its type parameter members) transient before our lazy getter fires, so augmentation merges in-place into the same symbol identity.

ensureGlobalSymbolTransient replicates what the natural multi-file merge does for single-file libs: it clones the symbol to make it transient before any type resolution occurs on it. Crucially, it also clones the symbol's type parameter member symbols (e.g. T in Array<T>), ensuring that when a global augmentation later merges its T, it merges in-place into the already-transient T symbol — preserving the single type parameter identity that the InterfaceType uses for instantiation.

@@ -23578,13 +23572,44 @@ func (c *Checker) getArrayOrTupleTargetType(node *ast.Node) *Type {
elementType := c.getArrayElementTypeNode(node)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% sure about this fix so I refrained myself from doing more changes to reduce the noise.

If the fix gets accepted, I think it would be best to update:

  • all other raw c.globalReadonlyArrayType/c.globalArrayType to use the introduced getters
  • likely do the same with other global types that are referenced directly and not through getters

I noticed that in Strada more types were retrieved through such caching getters but that pattern seems to be gone from Corsa so perhaps it's something you want to actively avoid.

That said, Strada doesn't use this pattern for those global array types and it's prone to the very same crash

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.

Inlay hints triggers crash during checker initialization

1 participant