diff --git a/.chronus/changes/fix-circular-template-constraint-2024-1-16-22-25-53.md b/.chronus/changes/fix-circular-template-constraint-2024-1-16-22-25-53.md new file mode 100644 index 0000000000..97e9b5cc05 --- /dev/null +++ b/.chronus/changes/fix-circular-template-constraint-2024-1-16-22-25-53.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@typespec/compiler" +--- + +Report error when having a circular template constraint e.g. `model Example` diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 42d8da6db2..f9c6688a7a 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -711,6 +711,7 @@ export function createChecker(program: Program): Checker { | AliasStatementNode | InterfaceStatementNode | OperationStatementNode + | TemplateParameterDeclarationNode | UnionStatementNode ): number { const symbol = @@ -750,6 +751,19 @@ export function createChecker(program: Program): Checker { const grandParentNode = parentNode.parent; const links = getSymbolLinks(node.symbol); + if (pendingResolutions.has(getNodeSymId(node), ResolutionKind.Constraint)) { + if (mapper === undefined) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "circular-constraint", + format: { typeName: node.id.sv }, + target: node.constraint!, + }) + ); + } + return errorType; + } + let type: TemplateParameter | undefined = links.declaredType as TemplateParameter; if (type === undefined) { if (grandParentNode) { @@ -770,7 +784,9 @@ export function createChecker(program: Program): Checker { }); if (node.constraint) { + pendingResolutions.start(getNodeSymId(node), ResolutionKind.Constraint); type.constraint = getTypeOrValueTypeForNode(node.constraint); + pendingResolutions.finish(getNodeSymId(node), ResolutionKind.Constraint); } if (node.default) { type.default = checkTemplateParameterDefault( @@ -6330,6 +6346,7 @@ const _assertReflectionNameToKind: Record = ReflectionName enum ResolutionKind { Type, BaseType, + Constraint, } class PendingResolutions { diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index d7b09b4752..b6edb9f520 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -854,6 +854,12 @@ const diagnostics = { default: paramMessage`Type '${"typeName"}' recursively references itself as a base type.`, }, }, + "circular-constraint": { + severity: "error", + messages: { + default: paramMessage`Type parameter '${"typeName"}' has a circular constraint.`, + }, + }, "circular-op-signature": { severity: "error", messages: { diff --git a/packages/compiler/test/checker/templates.test.ts b/packages/compiler/test/checker/templates.test.ts index 84d974d414..4538a72c53 100644 --- a/packages/compiler/test/checker/templates.test.ts +++ b/packages/compiler/test/checker/templates.test.ts @@ -428,6 +428,36 @@ describe("compiler: templates", () => { `); }); + it("emits diagnostic when constraint reference itself", async () => { + testHost.addTypeSpecFile("main.tsp", `model Test {}`); + + const diagnostics = await testHost.diagnose("main.tsp"); + expectDiagnostics(diagnostics, { + code: "circular-constraint", + message: "Type parameter 'A' has a circular constraint.", + }); + }); + + it("emits diagnostic when constraint reference other parameter in circular constraint", async () => { + testHost.addTypeSpecFile("main.tsp", `model Test {}`); + + const diagnostics = await testHost.diagnose("main.tsp"); + expectDiagnostics(diagnostics, { + code: "circular-constraint", + message: "Type parameter 'A' has a circular constraint.", + }); + }); + + it("emits diagnostic when constraint reference itself inside an expression", async () => { + testHost.addTypeSpecFile("main.tsp", `model Test {}`); + + const diagnostics = await testHost.diagnose("main.tsp"); + expectDiagnostics(diagnostics, { + code: "circular-constraint", + message: "Type parameter 'A' has a circular constraint.", + }); + }); + it("emit diagnostics if template default is not assignable to constraint", async () => { const { source, pos, end } = extractSquiggles(` model A { a: T }