Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/compiler"
---

Correctly report error when trying to reference member of template without using the arguments
85 changes: 72 additions & 13 deletions packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -3102,6 +3106,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
return undefined;
}
base = aliasedSym;
} else if (!options.resolveDeclarationOfTemplate && isTemplatedNode(getSymNode(base))) {
const baseSym = getContainerTemplateSymbol(base, node.base, mapper);
if (!baseSym) {
return undefined;
}
base = baseSym;
}
return resolveMemberInContainer(base, node, options);
}
Expand Down Expand Up @@ -3226,23 +3236,58 @@ 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: 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 {
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);
}

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;
}
}

Expand Down Expand Up @@ -5116,7 +5161,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, { resolveDeclarationOfTemplate: true });
const links = resolver.getNodeLinks(node.targetType);
if (links.isTemplateInstantiation) {
program.reportDiagnostic(
Expand Down Expand Up @@ -5588,6 +5633,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
): Map<string, Operation> {
const ownMembers = new Map<string, Operation>();

// 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)) {
Expand Down Expand Up @@ -6709,15 +6762,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 ambiguous cases
* ```tsp
* model Foo<T = string> {}
* ```
*
* Does `Foo` reference to the `Foo<T>` or `Foo<string>` 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(
Expand Down
52 changes: 52 additions & 0 deletions packages/compiler/test/checker/interface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
createTestRunner,
expectDiagnostics,
} from "../../src/testing/index.js";
import { Tester } from "../tester.js";

describe("compiler: interfaces", () => {
let testHost: TestHost;
Expand Down Expand Up @@ -250,6 +251,57 @@ 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 Base<T> {
bar(): T;
}
op test is /*Base*/Base.bar;
`);

expectDiagnostics(diagnostics, {
code: "invalid-template-args",
message: "Template argument 'T' is required and not specified.",
pos: pos.Base.pos,
});
});

describe("report error if trying to reference another op in the same template", () => {
it("before", async () => {
const [{ pos }, diagnostics] = await Tester.compileAndDiagnose(`
interface Base<A> {
Custom<T>(): T;
Default is /*Base*/Base.Custom<A>;
}
`);

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<A> {
Default is /*Base*/Base.Custom<A>;
Custom<T>(): T;
}
`);

expectDiagnostics(diagnostics, [
{
code: "invalid-template-args",
message: "Template argument 'A' is required and not specified.",
pos: pos.Base.pos,
},
]);
});
});

describe("templated operations", () => {
it("can instantiate template operation inside non-templated interface", async () => {
const { Foo, bar } = (await runner.compile(`
Expand Down
Loading