Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
56 changes: 40 additions & 16 deletions packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
142 changes: 142 additions & 0 deletions packages/compiler/test/checker/references.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T = string> { prop: T; }
alias A<T = string> = M<T>;

@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<T = string> { prop: T; }
alias A<U = boolean> = M<U>;

@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<T> { prop: T; }

alias A<T> = M<T>;
alias B<T = boolean> = A<T>;

@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 = string> = { 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: 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", () => {
Expand Down Expand Up @@ -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<T = string> { o(): T; }
alias A<T = string> = I<T>;

@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<T = string> { o(): T; }
alias A<U = boolean> = I<U>;

@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<T> { o(): T; }
alias A<T> = I<T>;
alias B<T = boolean> = A<T>;

@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 () => {
Expand Down
Loading