diff --git a/.chronus/changes/witemple-msft-alias-member-default-instance-2026-1-10-13-31-6.md b/.chronus/changes/witemple-msft-alias-member-default-instance-2026-1-10-13-31-6.md new file mode 100644 index 00000000000..496f70842e4 --- /dev/null +++ b/.chronus/changes/witemple-msft-alias-member-default-instance-2026-1-10-13-31-6.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/compiler" +--- + +Fixed an issue where referencing a member of a templated alias with defaultable parameters would fail to instantiate the alias, leaking template parameters. \ No newline at end of file diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 6bc130f0af5..412963497fb 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3283,24 +3283,48 @@ export function createChecker(program: Program, resolver: NameResolver): Checker // when resolving a type reference based on an alias, unwrap the alias. if (base.flags & SymbolFlags.Alias) { - const aliasedSym = getAliasedSymbol(ctx, base); - 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, - }), + if (!options.resolveDeclarationOfTemplate && isTemplatedNode(getSymNode(base))) { + // This is a bare identifier reference to a templated alias, so we need to actually check this type. + const ty = checkTypeReferenceSymbol( + ctx.withMapper(undefined), + base, + node.base, + /* instantiateTemplates */ true, ); - return undefined; + base = lateBindContainer(ty, base); + + if (base?.members) { + switch (ty.kind) { + case "Model": + case "Union": + case "Interface": + case "Enum": + case "Scalar": + lateBindMembers(ty); + } + } + + if (!base) return undefined; + } else { + const aliasedSym = getAliasedSymbol(ctx, base); + 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; } - base = aliasedSym; } else if (!options.resolveDeclarationOfTemplate && isTemplatedNode(getSymNode(base))) { const baseSym = getContainerTemplateSymbol(ctx, base, node.base); if (!baseSym) { diff --git a/packages/compiler/test/checker/references.test.ts b/packages/compiler/test/checker/references.test.ts index 1d6a58b7950..b721394cb63 100644 --- a/packages/compiler/test/checker/references.test.ts +++ b/packages/compiler/test/checker/references.test.ts @@ -260,6 +260,97 @@ describe("compiler: references", () => { strictEqual(Foo.properties.get("a")!.type, Foo.properties.get("b")); }); }); + + it("member reference via templated alias with default parameters", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + model M { prop: T; } + alias A = M; + + @test model X { y: A.prop; } + `, + ); + + const { X } = (await testHost.compile("./main.tsp")) as { X: Model }; + const y = X.properties.get("y")!; + strictEqual(y.type.kind, "ModelProperty"); + strictEqual(y.type.type.kind, "Scalar"); + strictEqual(y.type.type.name, "string"); + }); + + it("member reference via templated alias with different alias defaults", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + model M { prop: T; } + alias A = M; + + @test model X { y: A.prop; } + `, + ); + + const { X } = (await testHost.compile("./main.tsp")) as { X: Model }; + const y = X.properties.get("y")!; + strictEqual(y.type.kind, "ModelProperty"); + strictEqual(y.type.type.kind, "Scalar"); + strictEqual(y.type.type.name, "boolean"); + }); + + it("member reference via alias-of-alias (templated, defaultable)", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + model M { prop: T; } + + alias A = M; + alias B = A; + + @test model X { y: B.prop; } + `, + ); + + const { X } = (await testHost.compile("./main.tsp")) as { X: Model }; + const y = X.properties.get("y")!; + strictEqual(y.type.kind, "ModelProperty"); + strictEqual(y.type.type.kind, "Scalar"); + strictEqual(y.type.type.name, "boolean"); + }); + + it("member reference via templated alias to model literal with default argument", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + alias A = { t: T; }; + @test model Example { prop: A.t } + `, + ); + + const { Example } = (await testHost.compile("./main.tsp")) as { Example: Model }; + const prop = Example.properties.get("prop")!; + strictEqual(prop.type.kind, "ModelProperty"); + strictEqual(prop.type.type.kind, "Scalar"); + strictEqual(prop.type.type.name, "string"); + }); + + it("reports an error when referencing an uninstantiated alias", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + alias A = { t: T; }; + @test model Example { prop: A.t } + `, + ); + + const diagnostics = await testHost.diagnose("./main.tsp"); + + expectDiagnostics(diagnostics, [ + { + code: "invalid-template-args", + message: "Template argument 'T' is required and not specified.", + }, + ]); + }); }); describe("enum members", () => { @@ -523,6 +614,57 @@ describe("compiler: references", () => { strictEqual(linkedValue, Foo.operations.get("a")); }); }); + + it("operation reference via templated alias with default parameters", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + interface I { o(): T; } + alias A = I; + + @test op example is A.o; + `, + ); + const { example } = (await testHost.compile("./main.tsp")) as { example: Operation }; + strictEqual(example.kind, "Operation"); + strictEqual(example.returnType.kind, "Scalar"); + strictEqual(example.returnType.name, "string"); + }); + + it("operation reference via templated alias with different alias defaults", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + interface I { o(): T; } + alias A = I; + + @test op example is A.o; + `, + ); + + const { example } = (await testHost.compile("./main.tsp")) as { example: Operation }; + strictEqual(example.kind, "Operation"); + strictEqual(example.returnType.kind, "Scalar"); + strictEqual(example.returnType.name, "boolean"); + }); + + it("operation reference via alias-of-alias (templated, defaultable)", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + interface I { o(): T; } + alias A = I; + alias B = A; + + @test op example is B.o; + `, + ); + + const { example } = (await testHost.compile("./main.tsp")) as { example: Operation }; + strictEqual(example.kind, "Operation"); + strictEqual(example.returnType.kind, "Scalar"); + strictEqual(example.returnType.name, "boolean"); + }); }); it("throws proper diagnostics", async () => {