From 8e40584942d2d9268d20e8a7a0cbcae7b0e703cb Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:00:59 -0700 Subject: [PATCH 1/2] Add regression for cross-file computed-name visibility Record declaration emit retaining an otherwise unused namespace after serializing a computed property from another file. The referenced type is stripped, so the namespace should not survive in declaration output. --- ...PropertyNameSymbolStripInternal.errors.txt | 17 ++++++++++ ...ComputedPropertyNameSymbolStripInternal.js | 22 +++++++++++++ ...tedPropertyNameSymbolStripInternal.symbols | 27 ++++++++++++++++ ...putedPropertyNameSymbolStripInternal.types | 31 +++++++++++++++++++ ...ComputedPropertyNameSymbolStripInternal.ts | 17 ++++++++++ 5 files changed, 114 insertions(+) create mode 100644 tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.errors.txt create mode 100644 tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.js create mode 100644 tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.symbols create mode 100644 tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.types create mode 100644 tsc/testdata/tests/cases/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.ts diff --git a/tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.errors.txt b/tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.errors.txt new file mode 100644 index 0000000000000..451347ffcb04c --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.errors.txt @@ -0,0 +1,17 @@ +index.ts(3,14): error TS4023: Exported variable 'foo' has or is using name 'Foo' from external module "type" but cannot be named. + + +==== type.ts (0 errors) ==== + namespace Foo { + export const sym = Symbol(); + } + /** @internal */ + export type Type = { x?: { [Foo.sym]: 0 } }; + +==== index.ts (1 errors) ==== + import { type Type } from "./type"; + + export const foo = { ...({} as Type) }; + ~~~ +!!! error TS4023: Exported variable 'foo' has or is using name 'Foo' from external module "type" but cannot be named. + \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.js b/tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.js new file mode 100644 index 0000000000000..c0de031b073b0 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.js @@ -0,0 +1,22 @@ +//// [tests/cases/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.ts] //// + +//// [type.ts] +namespace Foo { + export const sym = Symbol(); +} +/** @internal */ +export type Type = { x?: { [Foo.sym]: 0 } }; + +//// [index.ts] +import { type Type } from "./type"; + +export const foo = { ...({} as Type) }; + + + + +//// [type.d.ts] +declare namespace Foo { + const sym: unique symbol; +} +export {}; diff --git a/tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.symbols b/tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.symbols new file mode 100644 index 0000000000000..5372163bf2beb --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.symbols @@ -0,0 +1,27 @@ +//// [tests/cases/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.ts] //// + +=== type.ts === +namespace Foo { +>Foo : Symbol(Foo, Decl(type.ts, 0, 0)) + + export const sym = Symbol(); +>sym : Symbol(sym, Decl(type.ts, 1, 16)) +>Symbol : Symbol(Symbol, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.symbol.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --)) +} +/** @internal */ +export type Type = { x?: { [Foo.sym]: 0 } }; +>Type : Symbol(Type, Decl(type.ts, 2, 1)) +>x : Symbol(x, Decl(type.ts, 4, 20)) +>[Foo.sym] : Symbol([Foo.sym], Decl(type.ts, 4, 26)) +>Foo.sym : Symbol(Foo.sym, Decl(type.ts, 1, 16)) +>Foo : Symbol(Foo, Decl(type.ts, 0, 0)) +>sym : Symbol(Foo.sym, Decl(type.ts, 1, 16)) + +=== index.ts === +import { type Type } from "./type"; +>Type : Symbol(Type, Decl(index.ts, 0, 8)) + +export const foo = { ...({} as Type) }; +>foo : Symbol(foo, Decl(index.ts, 2, 12)) +>Type : Symbol(Type, Decl(index.ts, 0, 8)) + diff --git a/tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.types b/tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.types new file mode 100644 index 0000000000000..0af68049e75bc --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.types @@ -0,0 +1,31 @@ +//// [tests/cases/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.ts] //// + +=== type.ts === +namespace Foo { +>Foo : typeof Foo + + export const sym = Symbol(); +>sym : unique symbol +>Symbol() : unique symbol +>Symbol : SymbolConstructor +} +/** @internal */ +export type Type = { x?: { [Foo.sym]: 0 } }; +>Type : Type +>x : { [Foo.sym]: 0; } | undefined +>[Foo.sym] : 0 +>Foo.sym : unique symbol +>Foo : typeof Foo +>sym : unique symbol + +=== index.ts === +import { type Type } from "./type"; +>Type : any + +export const foo = { ...({} as Type) }; +>foo : { x?: { [Foo.sym]: 0; }; } +>{ ...({} as Type) } : { x?: { [Foo.sym]: 0; }; } +>({} as Type) : Type +>{} as Type : Type +>{} : {} + diff --git a/tsc/testdata/tests/cases/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.ts b/tsc/testdata/tests/cases/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.ts new file mode 100644 index 0000000000000..4c2a3b40cea4f --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.ts @@ -0,0 +1,17 @@ +// @target: es2015 +// @strict: true +// @declaration: true +// @emitDeclarationOnly: true +// @stripInternal: true + +// @filename: type.ts +namespace Foo { + export const sym = Symbol(); +} +/** @internal */ +export type Type = { x?: { [Foo.sym]: 0 } }; + +// @filename: index.ts +import { type Type } from "./type"; + +export const foo = { ...({} as Type) }; From 1553ddd7671c77d20662bdbc67b652e43cc612c1 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:04:05 -0700 Subject: [PATCH 2/2] Keep computed-name reconstruction out of symbol tracking Computed property references are already checked in the destination scope. Reconstructing their source spelling must not track a second reference in the source scope, which could paint declarations visible for another file's emitter. Separate expression construction from reference tracking and use the untracked path when reconstructing computed names. Update the regression baseline so the otherwise unused namespace is no longer emitted. --- tsc/internal/checker/nodebuilderimpl.go | 13 ++++++++++--- ...onEmitComputedPropertyNameSymbolStripInternal.js | 3 --- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/tsc/internal/checker/nodebuilderimpl.go b/tsc/internal/checker/nodebuilderimpl.go index 0f0a010195363..50732bbfa9d6b 100644 --- a/tsc/internal/checker/nodebuilderimpl.go +++ b/tsc/internal/checker/nodebuilderimpl.go @@ -594,7 +594,7 @@ func (b *NodeBuilderImpl) symbolToNode(symbol *ast.Symbol, meaning ast.SymbolFla if nameType != nil && nameType.flags&(TypeFlagsEnumLiteral|TypeFlagsUniqueESSymbol) != 0 { oldEnclosing := b.ctx.enclosingDeclaration b.ctx.enclosingDeclaration = nameType.symbol.ValueDeclaration - result := b.f.NewComputedPropertyName(b.symbolToExpression(nameType.symbol, meaning)) + result := b.f.NewComputedPropertyName(b.symbolToExpressionWorker(nameType.symbol, meaning)) b.ctx.enclosingDeclaration = oldEnclosing return result } @@ -846,7 +846,12 @@ func (b *NodeBuilderImpl) createAccessFromSymbolChain(chain []*ast.Symbol, index } func (b *NodeBuilderImpl) symbolToExpression(symbol *ast.Symbol, mask ast.SymbolFlags) *ast.Expression { - chain := b.lookupSymbolChain(symbol, mask, false) + b.ctx.tracker.TrackSymbol(symbol, b.ctx.enclosingDeclaration, mask) + return b.symbolToExpressionWorker(symbol, mask) +} + +func (b *NodeBuilderImpl) symbolToExpressionWorker(symbol *ast.Symbol, mask ast.SymbolFlags) *ast.Expression { + chain := b.lookupSymbolChainWorker(symbol, mask, false) return b.createExpressionFromSymbolChain(chain, len(chain)-1) } @@ -2563,7 +2568,9 @@ func (b *NodeBuilderImpl) getPropertyNameNodeForSymbolFromNameType(symbol *ast.S return b.createPropertyNameNodeForIdentifierOrLiteral(name, singleQuote, stringNamed, isMethod, symbol) } if nameType.flags&TypeFlagsUniqueESSymbol != 0 { - return b.f.NewComputedPropertyName(b.symbolToExpression(nameType.AsUniqueESSymbolType().symbol, ast.SymbolFlagsValue)) + // The reference was tracked in the destination scope by trackComputedName. + // Reconstructing its spelling in the source scope must not paint that scope's declarations visible. + return b.f.NewComputedPropertyName(b.symbolToExpressionWorker(nameType.AsUniqueESSymbolType().symbol, ast.SymbolFlagsValue)) } return nil } diff --git a/tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.js b/tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.js index c0de031b073b0..c7649953186e8 100644 --- a/tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.js +++ b/tsc/testdata/baselines/reference/compiler/declarationEmitComputedPropertyNameSymbolStripInternal.js @@ -16,7 +16,4 @@ export const foo = { ...({} as Type) }; //// [type.d.ts] -declare namespace Foo { - const sym: unique symbol; -} export {};