Skip to content

Commit 9e9e40d

Browse files
Correctly report error when trying to reference template member without arguments (#8751)
fix #8750 Azure pr fixing the invalid use Azure/typespec-azure#3410
1 parent b56fc20 commit 9e9e40d

File tree

3 files changed

+131
-13
lines changed

3 files changed

+131
-13
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: fix
3+
packages:
4+
- "@typespec/compiler"
5+
---
6+
7+
Correctly report error when trying to reference member of template without using the arguments

packages/compiler/src/core/checker.ts

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3027,11 +3027,15 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
30273027
typeof options === "boolean"
30283028
? { ...defaultSymbolResolutionOptions, resolveDecorators: options }
30293029
: { ...defaultSymbolResolutionOptions, ...(options ?? {}) };
3030-
if (mapper === undefined && resolvedOptions.checkTemplateTypes && referenceSymCache.has(node)) {
3030+
if (
3031+
mapper === undefined &&
3032+
!resolvedOptions.resolveDeclarationOfTemplate &&
3033+
referenceSymCache.has(node)
3034+
) {
30313035
return referenceSymCache.get(node);
30323036
}
30333037
const sym = resolveTypeReferenceSymInternal(node, mapper, resolvedOptions);
3034-
if (resolvedOptions.checkTemplateTypes) {
3038+
if (!resolvedOptions.resolveDeclarationOfTemplate) {
30353039
referenceSymCache.set(node, sym);
30363040
}
30373041
return sym;
@@ -3102,6 +3106,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
31023106
return undefined;
31033107
}
31043108
base = aliasedSym;
3109+
} else if (!options.resolveDeclarationOfTemplate && isTemplatedNode(getSymNode(base))) {
3110+
const baseSym = getContainerTemplateSymbol(base, node.base, mapper);
3111+
if (!baseSym) {
3112+
return undefined;
3113+
}
3114+
base = baseSym;
31053115
}
31063116
return resolveMemberInContainer(base, node, options);
31073117
}
@@ -3226,23 +3236,58 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
32263236

32273237
// Otherwise for templates we need to get the type and retrieve the late bound symbol.
32283238
const aliasType = getTypeForNode(node as AliasStatementNode, mapper);
3229-
if (isErrorType(aliasType)) {
3239+
return lateBindContainer(aliasType, aliasSymbol);
3240+
}
3241+
3242+
/** Check case where a template type member is referenced like
3243+
* ```
3244+
* model Foo<T> {t: T}
3245+
* model Test { t: Foo.t } // check `Foo` is correctly used as template
3246+
* ```
3247+
*/
3248+
function getContainerTemplateSymbol(
3249+
sym: Sym,
3250+
node: MemberExpressionNode | IdentifierNode,
3251+
mapper: TypeMapper | undefined,
3252+
): Sym | undefined {
3253+
if (pendingResolutions.has(sym, ResolutionKind.Type)) {
3254+
if (mapper === undefined) {
3255+
reportCheckerDiagnostic(
3256+
createDiagnostic({
3257+
code: "circular-alias-type",
3258+
format: { typeName: sym.name },
3259+
target: node,
3260+
}),
3261+
);
3262+
}
3263+
return undefined;
3264+
}
3265+
3266+
pendingResolutions.start(sym, ResolutionKind.Type);
3267+
const type = checkTypeReferenceSymbol(sym, node, mapper);
3268+
pendingResolutions.finish(sym, ResolutionKind.Type);
3269+
3270+
return lateBindContainer(type, sym);
3271+
}
3272+
3273+
function lateBindContainer(type: Type, sym: Sym) {
3274+
if (isErrorType(type)) {
32303275
return undefined;
32313276
}
3232-
switch (aliasType.kind) {
3277+
switch (type.kind) {
32333278
case "Model":
32343279
case "Interface":
32353280
case "Union":
3236-
if (isTemplateInstance(aliasType)) {
3281+
if (isTemplateInstance(type)) {
32373282
// this is an alias for some instantiation, so late-bind the instantiation
3238-
lateBindMemberContainer(aliasType);
3239-
return aliasType.symbol!;
3283+
lateBindMemberContainer(type);
3284+
return type.symbol!;
32403285
}
32413286
// fallthrough
32423287
default:
32433288
// get the symbol from the node aliased type's node, or just return the base
32443289
// if it doesn't have a symbol (which will likely result in an error later on)
3245-
return getMergedSymbol(aliasType.node!.symbol) ?? aliasSymbol;
3290+
return getMergedSymbol(type.node!.symbol) ?? sym;
32463291
}
32473292
}
32483293

@@ -5116,7 +5161,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
51165161
*/
51175162
function checkAugmentDecorator(node: AugmentDecoratorStatementNode) {
51185163
// This will validate the target type is pointing to a valid ref.
5119-
resolveTypeReferenceSym(node.targetType, undefined);
5164+
resolveTypeReferenceSym(node.targetType, undefined, { resolveDeclarationOfTemplate: true });
51205165
const links = resolver.getNodeLinks(node.targetType);
51215166
if (links.isTemplateInstantiation) {
51225167
program.reportDiagnostic(
@@ -5588,6 +5633,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
55885633
): Map<string, Operation> {
55895634
const ownMembers = new Map<string, Operation>();
55905635

5636+
// Preregister each operation sym links instantiation to make sure there is no race condition when instantiating templated interface
5637+
for (const opNode of node.operations) {
5638+
const symbol = getSymbolForMember(opNode);
5639+
const links = symbol && getSymbolLinks(symbol);
5640+
if (links) {
5641+
links.instantiations = new TypeInstantiationMap();
5642+
}
5643+
}
55915644
for (const opNode of node.operations) {
55925645
const opType = checkOperation(opNode, mapper, interfaceType);
55935646
if (ownMembers.has(opType.name)) {
@@ -6709,15 +6762,21 @@ interface SymbolResolutionOptions {
67096762
resolveDecorators: boolean;
67106763

67116764
/**
6712-
* Should the symbol resolution instantiate templates and do a late bind of symbols.
6713-
* @default true
6765+
* When resolving a symbol should it resolve to the declaration or template instance for ambiguous cases
6766+
* ```tsp
6767+
* model Foo<T = string> {}
6768+
* ```
6769+
*
6770+
* 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
6771+
*
6772+
* @default false
67146773
*/
6715-
checkTemplateTypes: boolean;
6774+
resolveDeclarationOfTemplate: boolean;
67166775
}
67176776

67186777
const defaultSymbolResolutionOptions: SymbolResolutionOptions = {
67196778
resolveDecorators: false,
6720-
checkTemplateTypes: true,
6779+
resolveDeclarationOfTemplate: false,
67216780
};
67226781

67236782
function printTypeReferenceNode(

packages/compiler/test/checker/interface.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
createTestRunner,
1111
expectDiagnostics,
1212
} from "../../src/testing/index.js";
13+
import { Tester } from "../tester.js";
1314

1415
describe("compiler: interfaces", () => {
1516
let testHost: TestHost;
@@ -250,6 +251,57 @@ describe("compiler: interfaces", () => {
250251
});
251252
});
252253

254+
it("report error if trying to instantiate a templated interface without providing type arguments", async () => {
255+
const [{ pos }, diagnostics] = await Tester.compileAndDiagnose(`
256+
interface Base<T> {
257+
bar(): T;
258+
}
259+
op test is /*Base*/Base.bar;
260+
`);
261+
262+
expectDiagnostics(diagnostics, {
263+
code: "invalid-template-args",
264+
message: "Template argument 'T' is required and not specified.",
265+
pos: pos.Base.pos,
266+
});
267+
});
268+
269+
describe("report error if trying to reference another op in the same template", () => {
270+
it("before", async () => {
271+
const [{ pos }, diagnostics] = await Tester.compileAndDiagnose(`
272+
interface Base<A> {
273+
Custom<T>(): T;
274+
Default is /*Base*/Base.Custom<A>;
275+
}
276+
`);
277+
278+
expectDiagnostics(diagnostics, [
279+
{
280+
code: "invalid-template-args",
281+
message: "Template argument 'A' is required and not specified.",
282+
pos: pos.Base.pos,
283+
},
284+
]);
285+
});
286+
287+
it("after", async () => {
288+
const [{ pos }, diagnostics] = await Tester.compileAndDiagnose(`
289+
interface Base<A> {
290+
Default is /*Base*/Base.Custom<A>;
291+
Custom<T>(): T;
292+
}
293+
`);
294+
295+
expectDiagnostics(diagnostics, [
296+
{
297+
code: "invalid-template-args",
298+
message: "Template argument 'A' is required and not specified.",
299+
pos: pos.Base.pos,
300+
},
301+
]);
302+
});
303+
});
304+
253305
describe("templated operations", () => {
254306
it("can instantiate template operation inside non-templated interface", async () => {
255307
const { Foo, bar } = (await runner.compile(`

0 commit comments

Comments
 (0)