diff --git a/internal/checker/pseudotypenodebuilder.go b/internal/checker/pseudotypenodebuilder.go index cf8046803f9..e9d8ddd7da9 100644 --- a/internal/checker/pseudotypenodebuilder.go +++ b/internal/checker/pseudotypenodebuilder.go @@ -184,7 +184,15 @@ func (b *NodeBuilderImpl) pseudoTypeToNode(t *pseudochecker.PseudoType) *ast.Nod if len(d.TypeParameters) > 0 { res := make([]*ast.Node, 0, len(d.TypeParameters)) for _, tp := range d.TypeParameters { - res = append(res, b.reuseNode(tp.AsNode())) + node := tp.AsNode() + reused := b.reuseNode(node) + if reused == nil { + // Reuse fails when the constraint references a name inaccessible from the emit target. + // A nil here would reach the printer, which dereferences it in NodeList.HasTrailingComma. + b.ctx.tracker.ReportInferenceFallback(node) + reused = b.typeParameterToDeclaration(b.ch.getDeclaredTypeOfTypeParameter(node.Symbol())) + } + res = append(res, reused) } typeParams = b.f.NewNodeList(res) } diff --git a/internal/execute/tsctests/tsc_test.go b/internal/execute/tsctests/tsc_test.go index f36349a4910..48cc1880d37 100644 --- a/internal/execute/tsctests/tsc_test.go +++ b/internal/execute/tsctests/tsc_test.go @@ -841,6 +841,40 @@ func TestTscDeclarationEmit(t *testing.T) { }, }, }, + { + // The declaration signature computed for b.ts inlines `setField` structurally. Its type + // parameter cannot be reused, because rewriting `typeof state` hits an inaccessible + // `unique symbol`. The pseudo type node builder used to store the resulting nil in the + // type parameter list, which crashed the printer. + subScenario: "dts signature update with a type parameter that cannot be reused", + files: FileMap{ + "/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(` + { + "compilerOptions": { + "strict": true, + "incremental": true, + "skipLibCheck": true, + "skipDefaultLibCheck": true, + }, + }`), + "/home/src/workspaces/project/a.ts": stringtestutil.Dedent(` + declare const brand: unique symbol; + const state = { name: "", count: 0, [brand]: true }; + export const api = { + setField: (key: K, value: (typeof state)[K]): void => { + state[key] = value; + }, + };`), + "/home/src/workspaces/project/b.ts": stringtestutil.Dedent(` + import { api } from "./a"; + export const merged = { ...api };`), + }, + edits: []*tscEdit{ + newTscEdit("modify b.ts", func(sys *TestSys) { + sys.appendFile("/home/src/workspaces/project/b.ts", "\nexport const touched = 1;") + }), + }, + }, { subScenario: "when using Windows paths and uppercase letters", files: FileMap{ diff --git a/testdata/baselines/reference/tsc/declarationEmit/dts-signature-update-with-a-type-parameter-that-cannot-be-reused.js b/testdata/baselines/reference/tsc/declarationEmit/dts-signature-update-with-a-type-parameter-that-cannot-be-reused.js new file mode 100644 index 00000000000..0b7cd8b142f --- /dev/null +++ b/testdata/baselines/reference/tsc/declarationEmit/dts-signature-update-with-a-type-parameter-that-cannot-be-reused.js @@ -0,0 +1,226 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/a.ts] *new* +declare const brand: unique symbol; +const state = { name: "", count: 0, [brand]: true }; +export const api = { + setField: (key: K, value: (typeof state)[K]): void => { + state[key] = value; + }, +}; +//// [/home/src/workspaces/project/b.ts] *new* +import { api } from "./a"; +export const merged = { ...api }; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{ + "compilerOptions": { + "strict": true, + "incremental": true, + "skipLibCheck": true, + "skipDefaultLibCheck": true, + }, +} + +tsgo +ExitStatus:: Success +Output:: +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/a.js] *new* +const state = { name: "", count: 0, [brand]: true }; +export const api = { + setField: (key, value) => { + state[key] = value; + }, +}; + +//// [/home/src/workspaces/project/b.js] *new* +import { api } from "./a"; +export const merged = { ...api }; + +//// [/home/src/workspaces/project/tsconfig.tsbuildinfo] *new* +{"version":"FakeTSVersion","root":[[2,3]],"fileNames":["lib.es2025.full.d.ts","./a.ts","./b.ts"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},"420f0324953654bb26e57da1857dc6a2-declare const brand: unique symbol;\nconst state = { name: \"\", count: 0, [brand]: true };\nexport const api = {\n setField: (key: K, value: (typeof state)[K]): void => {\n state[key] = value;\n },\n};","de62ab78d5ba64a5b325bfebf0f2e8d3-import { api } from \"./a\";\nexport const merged = { ...api };"],"fileIdsList":[[2]],"options":{"skipLibCheck":true,"strict":true,"skipDefaultLibCheck":true},"referencedMap":[[3,1]]} +//// [/home/src/workspaces/project/tsconfig.tsbuildinfo.readable.baseline.txt] *new* +{ + "version": "FakeTSVersion", + "root": [ + { + "files": [ + "./a.ts", + "./b.ts" + ], + "original": [ + 2, + 3 + ] + } + ], + "fileNames": [ + "lib.es2025.full.d.ts", + "./a.ts", + "./b.ts" + ], + "fileInfos": [ + { + "fileName": "lib.es2025.full.d.ts", + "version": "8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };", + "signature": "8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };", + "affectsGlobalScope": true, + "impliedNodeFormat": "CommonJS", + "original": { + "version": "8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };", + "affectsGlobalScope": true, + "impliedNodeFormat": 1 + } + }, + { + "fileName": "./a.ts", + "version": "420f0324953654bb26e57da1857dc6a2-declare const brand: unique symbol;\nconst state = { name: \"\", count: 0, [brand]: true };\nexport const api = {\n setField: (key: K, value: (typeof state)[K]): void => {\n state[key] = value;\n },\n};", + "signature": "420f0324953654bb26e57da1857dc6a2-declare const brand: unique symbol;\nconst state = { name: \"\", count: 0, [brand]: true };\nexport const api = {\n setField: (key: K, value: (typeof state)[K]): void => {\n state[key] = value;\n },\n};", + "impliedNodeFormat": "CommonJS" + }, + { + "fileName": "./b.ts", + "version": "de62ab78d5ba64a5b325bfebf0f2e8d3-import { api } from \"./a\";\nexport const merged = { ...api };", + "signature": "de62ab78d5ba64a5b325bfebf0f2e8d3-import { api } from \"./a\";\nexport const merged = { ...api };", + "impliedNodeFormat": "CommonJS" + } + ], + "fileIdsList": [ + [ + "./a.ts" + ] + ], + "options": { + "skipLibCheck": true, + "strict": true, + "skipDefaultLibCheck": true + }, + "referencedMap": { + "./b.ts": [ + "./a.ts" + ] + }, + "size": 1378 +} + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/a.ts +*refresh* /home/src/workspaces/project/b.ts +Signatures:: + + +Edit [0]:: modify b.ts +//// [/home/src/workspaces/project/b.ts] *modified* +import { api } from "./a"; +export const merged = { ...api }; +export const touched = 1; + +tsgo +ExitStatus:: Success +Output:: +//// [/home/src/workspaces/project/b.js] *modified* +import { api } from "./a"; +export const merged = { ...api }; +export const touched = 1; + +//// [/home/src/workspaces/project/tsconfig.tsbuildinfo] *modified* +{"version":"FakeTSVersion","root":[[2,3]],"fileNames":["lib.es2025.full.d.ts","./a.ts","./b.ts"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},"420f0324953654bb26e57da1857dc6a2-declare const brand: unique symbol;\nconst state = { name: \"\", count: 0, [brand]: true };\nexport const api = {\n setField: (key: K, value: (typeof state)[K]): void => {\n state[key] = value;\n },\n};",{"version":"6fa50051a3b923ced64e589b1e168966-import { api } from \"./a\";\nexport const merged = { ...api };\nexport const touched = 1;","signature":"49d11e9dbd07ad5b91e829e73838e2a1-export declare const merged: {\n setField: (key: K, value: ({\n name: string;\n count: number;\n [brand]: boolean;\n })[K]) => void;\n};\nexport declare const touched = 1;\n\n(40,6): error2527: The_inferred_type_of_0_references_an_inaccessible_1_type_A_type_annotation_is_necessary_2527\nmerged\nunique symbol\n\n(40,6): error4023: Exported_variable_0_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named_4023\nmerged\nbrand\n\"/home/src/workspaces/project/a\"\n","impliedNodeFormat":1}],"fileIdsList":[[2]],"options":{"skipLibCheck":true,"strict":true,"skipDefaultLibCheck":true},"referencedMap":[[3,1]]} +//// [/home/src/workspaces/project/tsconfig.tsbuildinfo.readable.baseline.txt] *modified* +{ + "version": "FakeTSVersion", + "root": [ + { + "files": [ + "./a.ts", + "./b.ts" + ], + "original": [ + 2, + 3 + ] + } + ], + "fileNames": [ + "lib.es2025.full.d.ts", + "./a.ts", + "./b.ts" + ], + "fileInfos": [ + { + "fileName": "lib.es2025.full.d.ts", + "version": "8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };", + "signature": "8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };", + "affectsGlobalScope": true, + "impliedNodeFormat": "CommonJS", + "original": { + "version": "8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };", + "affectsGlobalScope": true, + "impliedNodeFormat": 1 + } + }, + { + "fileName": "./a.ts", + "version": "420f0324953654bb26e57da1857dc6a2-declare const brand: unique symbol;\nconst state = { name: \"\", count: 0, [brand]: true };\nexport const api = {\n setField: (key: K, value: (typeof state)[K]): void => {\n state[key] = value;\n },\n};", + "signature": "420f0324953654bb26e57da1857dc6a2-declare const brand: unique symbol;\nconst state = { name: \"\", count: 0, [brand]: true };\nexport const api = {\n setField: (key: K, value: (typeof state)[K]): void => {\n state[key] = value;\n },\n};", + "impliedNodeFormat": "CommonJS" + }, + { + "fileName": "./b.ts", + "version": "6fa50051a3b923ced64e589b1e168966-import { api } from \"./a\";\nexport const merged = { ...api };\nexport const touched = 1;", + "signature": "49d11e9dbd07ad5b91e829e73838e2a1-export declare const merged: {\n setField: (key: K, value: ({\n name: string;\n count: number;\n [brand]: boolean;\n })[K]) => void;\n};\nexport declare const touched = 1;\n\n(40,6): error2527: The_inferred_type_of_0_references_an_inaccessible_1_type_A_type_annotation_is_necessary_2527\nmerged\nunique symbol\n\n(40,6): error4023: Exported_variable_0_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named_4023\nmerged\nbrand\n\"/home/src/workspaces/project/a\"\n", + "impliedNodeFormat": "CommonJS", + "original": { + "version": "6fa50051a3b923ced64e589b1e168966-import { api } from \"./a\";\nexport const merged = { ...api };\nexport const touched = 1;", + "signature": "49d11e9dbd07ad5b91e829e73838e2a1-export declare const merged: {\n setField: (key: K, value: ({\n name: string;\n count: number;\n [brand]: boolean;\n })[K]) => void;\n};\nexport declare const touched = 1;\n\n(40,6): error2527: The_inferred_type_of_0_references_an_inaccessible_1_type_A_type_annotation_is_necessary_2527\nmerged\nunique symbol\n\n(40,6): error4023: Exported_variable_0_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named_4023\nmerged\nbrand\n\"/home/src/workspaces/project/a\"\n", + "impliedNodeFormat": 1 + } + } + ], + "fileIdsList": [ + [ + "./a.ts" + ] + ], + "options": { + "skipLibCheck": true, + "strict": true, + "skipDefaultLibCheck": true + }, + "referencedMap": { + "./b.ts": [ + "./a.ts" + ] + }, + "size": 2037 +} + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/b.ts +Signatures:: +(computed .d.ts) /home/src/workspaces/project/b.ts