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"
---

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