Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
75 changes: 62 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,25 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
return undefined;
}
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,
}),
);
return undefined;
}
base = aliasedSym;
}
return resolveMemberInContainer(base, node, options);
}
Expand Down Expand Up @@ -3226,23 +3249,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: 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;
}
}

Expand Down Expand Up @@ -5116,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);
resolveTypeReferenceSym(node.targetType, undefined, { resolveDeclarationOfTemplate: true });
const links = resolver.getNodeLinks(node.targetType);
if (links.isTemplateInstantiation) {
program.reportDiagnostic(
Expand Down Expand Up @@ -6709,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 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
16 changes: 16 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,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<T> {
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(`
Expand Down
Loading