From 211b99411ff4f23debeabfdb7222b02bc1fdd298 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 16 Oct 2025 17:05:06 -0700 Subject: [PATCH 1/6] Fix referencing template instance members --- packages/compiler/src/core/checker.ts | 53 ++++++++++++++++--- .../compiler/test/checker/interface.test.ts | 16 ++++++ 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 3802783dc4f..cffa1b98e79 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3102,6 +3102,25 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return undefined; } base = aliasedSym; + } else if (options.checkTemplateTypes && isTemplatedNode(getSymNode(base))) { + const aliasedSym = getContainerTemplateSymbol(base, node.base, mapper); + if (!aliasedSym) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-ref", + messageId: "node", + format: { + id: node.id.sv, + nodeName: base.declarations[0] + ? SyntaxKind[base.declarations[0].kind] + : "Unknown node", + }, + target: node, + }), + ); + return undefined; + } + base = aliasedSym; } return resolveMemberInContainer(base, node, options); } @@ -3226,23 +3245,43 @@ export function createChecker(program: Program, resolver: NameResolver): Checker // Otherwise for templates we need to get the type and retrieve the late bound symbol. const aliasType = getTypeForNode(node as AliasStatementNode, mapper); - if (isErrorType(aliasType)) { + return lateBindContainer(aliasType, aliasSymbol); + } + + /** Check case where a template type member is referenced like + * ``` + * model Foo {t: T} + * model Test { t: Foo.t } // check `Foo` is correctly used as template + * ``` + */ + function getContainerTemplateSymbol( + sym: Sym, + node: MemberExpressionNode | IdentifierNode, + mapper: TypeMapper | undefined, + ): Sym | undefined { + // Otherwise for templates we need to get the type and retrieve the late bound symbol. + const type = checkTypeReferenceSymbol(sym, node, mapper); + return lateBindContainer(type, sym); + } + + function lateBindContainer(type: Type, sym: Sym) { + if (isErrorType(type)) { return undefined; } - switch (aliasType.kind) { + switch (type.kind) { case "Model": case "Interface": case "Union": - if (isTemplateInstance(aliasType)) { + if (isTemplateInstance(type)) { // this is an alias for some instantiation, so late-bind the instantiation - lateBindMemberContainer(aliasType); - return aliasType.symbol!; + lateBindMemberContainer(type); + return type.symbol!; } // fallthrough default: // get the symbol from the node aliased type's node, or just return the base // if it doesn't have a symbol (which will likely result in an error later on) - return getMergedSymbol(aliasType.node!.symbol) ?? aliasSymbol; + return getMergedSymbol(type.node!.symbol) ?? sym; } } @@ -5116,7 +5155,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker */ function checkAugmentDecorator(node: AugmentDecoratorStatementNode) { // This will validate the target type is pointing to a valid ref. - resolveTypeReferenceSym(node.targetType, undefined); + resolveTypeReferenceSym(node.targetType, undefined, { checkTemplateTypes: false }); const links = resolver.getNodeLinks(node.targetType); if (links.isTemplateInstantiation) { program.reportDiagnostic( diff --git a/packages/compiler/test/checker/interface.test.ts b/packages/compiler/test/checker/interface.test.ts index 114dd4513a6..8c62c401b1e 100644 --- a/packages/compiler/test/checker/interface.test.ts +++ b/packages/compiler/test/checker/interface.test.ts @@ -10,6 +10,7 @@ import { createTestRunner, expectDiagnostics, } from "../../src/testing/index.js"; +import { Tester } from "../tester.js"; describe("compiler: interfaces", () => { let testHost: TestHost; @@ -250,6 +251,21 @@ describe("compiler: interfaces", () => { }); }); + it("report error if trying to instantiate a templated interface without providing type arguments", async () => { + const [{ pos }, diagnostics] = await Tester.compileAndDiagnose(` + interface Foo { + bar(): T; + } + op test is /*Foo*/Foo.bar; + `); + + expectDiagnostics(diagnostics, { + code: "invalid-template-args", + message: "Template argument 'T' is required and not specified.", + pos: pos.Foo.pos, + }); + }); + describe("templated operations", () => { it("can instantiate template operation inside non-templated interface", async () => { const { Foo, bar } = (await runner.compile(` From 12fcb6a6adeeb12241d2e0bca6a521ff33c19c31 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 16 Oct 2025 17:08:07 -0700 Subject: [PATCH 2/6] change --- ...mplate-instance-member-2025-9-16-17-8-1.md | 7 +++++ packages/compiler/src/core/checker.ts | 26 +++++++++++++------ 2 files changed, 25 insertions(+), 8 deletions(-) create mode 100644 .chronus/changes/fix-ref-template-instance-member-2025-9-16-17-8-1.md diff --git a/.chronus/changes/fix-ref-template-instance-member-2025-9-16-17-8-1.md b/.chronus/changes/fix-ref-template-instance-member-2025-9-16-17-8-1.md new file mode 100644 index 00000000000..b253b0662e5 --- /dev/null +++ b/.chronus/changes/fix-ref-template-instance-member-2025-9-16-17-8-1.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/compiler" +--- + +Correctly report error when trying to reference member of template without using the arguments \ No newline at end of file diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index cffa1b98e79..ecbcb653a3c 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3027,11 +3027,15 @@ export function createChecker(program: Program, resolver: NameResolver): Checker typeof options === "boolean" ? { ...defaultSymbolResolutionOptions, resolveDecorators: options } : { ...defaultSymbolResolutionOptions, ...(options ?? {}) }; - if (mapper === undefined && resolvedOptions.checkTemplateTypes && referenceSymCache.has(node)) { + if ( + mapper === undefined && + !resolvedOptions.resolveDeclarationOfTemplate && + referenceSymCache.has(node) + ) { return referenceSymCache.get(node); } const sym = resolveTypeReferenceSymInternal(node, mapper, resolvedOptions); - if (resolvedOptions.checkTemplateTypes) { + if (!resolvedOptions.resolveDeclarationOfTemplate) { referenceSymCache.set(node, sym); } return sym; @@ -3102,7 +3106,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return undefined; } base = aliasedSym; - } else if (options.checkTemplateTypes && isTemplatedNode(getSymNode(base))) { + } else if (!options.resolveDeclarationOfTemplate && isTemplatedNode(getSymNode(base))) { const aliasedSym = getContainerTemplateSymbol(base, node.base, mapper); if (!aliasedSym) { reportCheckerDiagnostic( @@ -5155,7 +5159,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker */ function checkAugmentDecorator(node: AugmentDecoratorStatementNode) { // This will validate the target type is pointing to a valid ref. - resolveTypeReferenceSym(node.targetType, undefined, { checkTemplateTypes: false }); + resolveTypeReferenceSym(node.targetType, undefined, { resolveDeclarationOfTemplate: false }); const links = resolver.getNodeLinks(node.targetType); if (links.isTemplateInstantiation) { program.reportDiagnostic( @@ -6748,15 +6752,21 @@ interface SymbolResolutionOptions { resolveDecorators: boolean; /** - * Should the symbol resolution instantiate templates and do a late bind of symbols. - * @default true + * When resolving a symbol should it resolve to the declaration or template instance for ambigous cases + * ```tsp + * model Foo {} + * ``` + * + * Does `Foo` reference to the `Foo` or `Foo` instance. By default it is the instance. Only case looking for declaration are augment decorator target + * + * @default false */ - checkTemplateTypes: boolean; + resolveDeclarationOfTemplate: boolean; } const defaultSymbolResolutionOptions: SymbolResolutionOptions = { resolveDecorators: false, - checkTemplateTypes: true, + resolveDeclarationOfTemplate: false, }; function printTypeReferenceNode( From 4bf1a3e20cb959f46b3edcd4c4a26c426d3414b9 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 16 Oct 2025 20:14:03 -0700 Subject: [PATCH 3/6] fix --- packages/compiler/src/core/checker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index ecbcb653a3c..deb424f6d59 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -5159,7 +5159,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker */ function checkAugmentDecorator(node: AugmentDecoratorStatementNode) { // This will validate the target type is pointing to a valid ref. - resolveTypeReferenceSym(node.targetType, undefined, { resolveDeclarationOfTemplate: false }); + resolveTypeReferenceSym(node.targetType, undefined, { resolveDeclarationOfTemplate: true }); const links = resolver.getNodeLinks(node.targetType); if (links.isTemplateInstantiation) { program.reportDiagnostic( From d8a63fb1af4ca59931ea23cd9f77a6dc56387624 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 17 Oct 2025 11:06:19 -0700 Subject: [PATCH 4/6] fix typo --- packages/compiler/src/core/checker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index deb424f6d59..392cd503628 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -6752,7 +6752,7 @@ interface SymbolResolutionOptions { resolveDecorators: boolean; /** - * When resolving a symbol should it resolve to the declaration or template instance for ambigous cases + * When resolving a symbol should it resolve to the declaration or template instance for ambiguous cases * ```tsp * model Foo {} * ``` From 39fa3514965a43fff9a01377fb01a2b7e55e6732 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 17 Oct 2025 11:26:03 -0700 Subject: [PATCH 5/6] don't crash --- packages/compiler/src/core/checker.ts | 17 ++++++++++++++++- .../compiler/test/checker/interface.test.ts | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 392cd503628..95b6d4784fa 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3263,8 +3263,23 @@ export function createChecker(program: Program, resolver: NameResolver): Checker node: MemberExpressionNode | IdentifierNode, mapper: TypeMapper | undefined, ): Sym | undefined { - // Otherwise for templates we need to get the type and retrieve the late bound symbol. + if (pendingResolutions.has(sym, ResolutionKind.Type)) { + if (mapper === undefined) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "circular-alias-type", + format: { typeName: sym.name }, + target: node, + }), + ); + } + return undefined; + } + + pendingResolutions.start(sym, ResolutionKind.Type); const type = checkTypeReferenceSymbol(sym, node, mapper); + pendingResolutions.finish(sym, ResolutionKind.Type); + return lateBindContainer(type, sym); } diff --git a/packages/compiler/test/checker/interface.test.ts b/packages/compiler/test/checker/interface.test.ts index 8c62c401b1e..519b2db2b83 100644 --- a/packages/compiler/test/checker/interface.test.ts +++ b/packages/compiler/test/checker/interface.test.ts @@ -266,6 +266,24 @@ describe("compiler: interfaces", () => { }); }); + it("report error if trying to reference another property in the same template", async () => { + const [{ pos }, diagnostics] = await Tester.compileAndDiagnose(` + interface Foo { + bar(): T; + baz(): /*Foo*/Foo.bar; + } + `); + + expectDiagnostics(diagnostics, [ + { + code: "invalid-template-args", + message: "Template argument 'T' is required and not specified.", + pos: pos.Foo.pos, + }, + { code: "invalid-ref" }, + ]); + }); + describe("templated operations", () => { it("can instantiate template operation inside non-templated interface", async () => { const { Foo, bar } = (await runner.compile(` From b18a28b28060377cb2cb41a60ec5f17473eb51a7 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 17 Oct 2025 13:15:19 -0700 Subject: [PATCH 6/6] Fix --- packages/compiler/src/core/checker.ts | 27 ++++------ .../compiler/test/checker/interface.test.ts | 50 +++++++++++++------ 2 files changed, 45 insertions(+), 32 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 95b6d4784fa..5f84b0e085e 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3107,24 +3107,11 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } base = aliasedSym; } else if (!options.resolveDeclarationOfTemplate && isTemplatedNode(getSymNode(base))) { - const aliasedSym = getContainerTemplateSymbol(base, node.base, mapper); - if (!aliasedSym) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "invalid-ref", - messageId: "node", - format: { - id: node.id.sv, - nodeName: base.declarations[0] - ? SyntaxKind[base.declarations[0].kind] - : "Unknown node", - }, - target: node, - }), - ); + const baseSym = getContainerTemplateSymbol(base, node.base, mapper); + if (!baseSym) { return undefined; } - base = aliasedSym; + base = baseSym; } return resolveMemberInContainer(base, node, options); } @@ -5646,6 +5633,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ): Map { const ownMembers = new Map(); + // Preregister each operation sym links instantiation to make sure there is no race condition when instantiating templated interface + for (const opNode of node.operations) { + const symbol = getSymbolForMember(opNode); + const links = symbol && getSymbolLinks(symbol); + if (links) { + links.instantiations = new TypeInstantiationMap(); + } + } for (const opNode of node.operations) { const opType = checkOperation(opNode, mapper, interfaceType); if (ownMembers.has(opType.name)) { diff --git a/packages/compiler/test/checker/interface.test.ts b/packages/compiler/test/checker/interface.test.ts index 519b2db2b83..367ae54782f 100644 --- a/packages/compiler/test/checker/interface.test.ts +++ b/packages/compiler/test/checker/interface.test.ts @@ -253,35 +253,53 @@ describe("compiler: interfaces", () => { it("report error if trying to instantiate a templated interface without providing type arguments", async () => { const [{ pos }, diagnostics] = await Tester.compileAndDiagnose(` - interface Foo { + interface Base { bar(): T; } - op test is /*Foo*/Foo.bar; + op test is /*Base*/Base.bar; `); expectDiagnostics(diagnostics, { code: "invalid-template-args", message: "Template argument 'T' is required and not specified.", - pos: pos.Foo.pos, + pos: pos.Base.pos, }); }); - it("report error if trying to reference another property in the same template", async () => { - const [{ pos }, diagnostics] = await Tester.compileAndDiagnose(` - interface Foo { - bar(): T; - baz(): /*Foo*/Foo.bar; + describe("report error if trying to reference another op in the same template", () => { + it("before", async () => { + const [{ pos }, diagnostics] = await Tester.compileAndDiagnose(` + interface Base { + Custom(): T; + Default is /*Base*/Base.Custom; } `); - expectDiagnostics(diagnostics, [ - { - code: "invalid-template-args", - message: "Template argument 'T' is required and not specified.", - pos: pos.Foo.pos, - }, - { code: "invalid-ref" }, - ]); + expectDiagnostics(diagnostics, [ + { + code: "invalid-template-args", + message: "Template argument 'A' is required and not specified.", + pos: pos.Base.pos, + }, + ]); + }); + + it("after", async () => { + const [{ pos }, diagnostics] = await Tester.compileAndDiagnose(` + interface Base { + Default is /*Base*/Base.Custom; + Custom(): T; + } + `); + + expectDiagnostics(diagnostics, [ + { + code: "invalid-template-args", + message: "Template argument 'A' is required and not specified.", + pos: pos.Base.pos, + }, + ]); + }); }); describe("templated operations", () => {