From ddfcbc86512e74746350cc952cfaa3a2d48f82d2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 00:48:50 +0000 Subject: [PATCH 1/5] Initial plan From 3dde7aad511a507d20e0235de3bfe4b25fac9708 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 01:13:10 +0000 Subject: [PATCH 2/5] fix(compiler): detect model spread cycles and allow recursive model-expression aliases Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/00744925-833d-4200-87a1-e286c26a0369 Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/compiler/src/core/checker.ts | 30 +++++++++++++++++++ packages/compiler/test/checker/alias.test.ts | 12 ++++++++ packages/compiler/test/checker/spread.test.ts | 2 +- 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 20422c0a7eb..68b33b752de 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -529,6 +529,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker * Key is the SymId of a node. It can be retrieved with getNodeSymId(node) */ const pendingResolutions = new PendingResolutions(); + const spreadResolutionAncestors = new Map>(); const postCheckValidators: ValidatorFn[] = []; const typespecNamespaceBinding = resolver.symbols.global.exports!.get("TypeSpec"); @@ -6473,6 +6474,32 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } return undefined; } + + const modelAncestors = spreadResolutionAncestors.get(modelSymId); + if (targetSym && modelAncestors?.has(targetSym)) { + if (ctx.mapper === undefined) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "spread-model", + messageId: "selfSpread", + target: target, + }), + ); + } + return undefined; + } + + if (targetSym) { + const targetAncestors = spreadResolutionAncestors.get(targetSym) ?? new Set(); + if (!spreadResolutionAncestors.has(targetSym)) { + spreadResolutionAncestors.set(targetSym, targetAncestors); + } + targetAncestors.add(modelSymId); + for (const ancestor of modelAncestors ?? []) { + targetAncestors.add(ancestor); + } + } + const type = getTypeForNode(target, ctx); return type; } @@ -7267,6 +7294,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const aliasSymId = getNodeSym(node); if (pendingResolutions.has(aliasSymId, ResolutionKind.Type)) { + if (node.value.kind === SyntaxKind.ModelExpression) { + return getTypeForNode(node.value, ctx); + } if (ctx.mapper === undefined) { reportCheckerDiagnostic( createDiagnostic({ diff --git a/packages/compiler/test/checker/alias.test.ts b/packages/compiler/test/checker/alias.test.ts index 2ba57aef012..e2165155241 100644 --- a/packages/compiler/test/checker/alias.test.ts +++ b/packages/compiler/test/checker/alias.test.ts @@ -149,6 +149,18 @@ describe("compiler: aliases", () => { strictEqual(expr.namespace, Foo); }); + it("doesn't emit diagnostics for recursive aliases through model expressions", async () => { + const diagnostics = await Tester.diagnose(` + alias A = { + a: B; + }; + alias B = { + a: A; + }; + `); + expectDiagnosticEmpty(diagnostics); + }); + it("emit diagnostics if assign itself", async () => { const diagnostics = await Tester.diagnose(` alias A = A; diff --git a/packages/compiler/test/checker/spread.test.ts b/packages/compiler/test/checker/spread.test.ts index 126b147a885..018cc22f2fd 100644 --- a/packages/compiler/test/checker/spread.test.ts +++ b/packages/compiler/test/checker/spread.test.ts @@ -113,7 +113,7 @@ describe("circular reference", () => { }); // https://github.com/microsoft/typespec/issues/7956 - it.skip("emit diagnostic if models spread each other", async () => { + it("emit diagnostic if models spread each other", async () => { const diagnostics = await Tester.diagnose(` model Foo { ...Bar } model Bar { ...Foo } From f57d3fa1bd46b608b9bcdb2502176d5d90a2f62e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 01:18:55 +0000 Subject: [PATCH 3/5] chore(compiler): address review nits in spread cycle tracking Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/00744925-833d-4200-87a1-e286c26a0369 Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/compiler/src/core/checker.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 68b33b752de..ee9c1b5ead2 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -6475,6 +6475,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return undefined; } + // Ancestors are models that already depend on this model via spread. const modelAncestors = spreadResolutionAncestors.get(modelSymId); if (targetSym && modelAncestors?.has(targetSym)) { if (ctx.mapper === undefined) { @@ -6490,8 +6491,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } if (targetSym) { - const targetAncestors = spreadResolutionAncestors.get(targetSym) ?? new Set(); - if (!spreadResolutionAncestors.has(targetSym)) { + let targetAncestors = spreadResolutionAncestors.get(targetSym); + if (!targetAncestors) { + targetAncestors = new Set(); spreadResolutionAncestors.set(targetSym, targetAncestors); } targetAncestors.add(modelSymId); From d5224a24c0cd723efd2eef6d5bb7f94b1f0d427a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 20:46:08 +0000 Subject: [PATCH 4/5] chore: add changelog entry for compiler spread cycle fix Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/4bab9fe4-0624-4803-9e15-28321acb1410 Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- ...lot-fix-model-circular-references-2026-4-14-20-45-42.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/copilot-fix-model-circular-references-2026-4-14-20-45-42.md diff --git a/.chronus/changes/copilot-fix-model-circular-references-2026-4-14-20-45-42.md b/.chronus/changes/copilot-fix-model-circular-references-2026-4-14-20-45-42.md new file mode 100644 index 00000000000..1e4c05476ed --- /dev/null +++ b/.chronus/changes/copilot-fix-model-circular-references-2026-4-14-20-45-42.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/compiler" +--- + +Validate circular model spread chains without over-reporting recursive model-expression aliases. \ No newline at end of file From c5366f493acd211478222a2bdbd963fb435f5bd3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 20:46:41 +0000 Subject: [PATCH 5/5] chore: refine changelog message wording Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/4bab9fe4-0624-4803-9e15-28321acb1410 Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- .../copilot-fix-model-circular-references-2026-4-14-20-45-42.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.chronus/changes/copilot-fix-model-circular-references-2026-4-14-20-45-42.md b/.chronus/changes/copilot-fix-model-circular-references-2026-4-14-20-45-42.md index 1e4c05476ed..dd682cc84ba 100644 --- a/.chronus/changes/copilot-fix-model-circular-references-2026-4-14-20-45-42.md +++ b/.chronus/changes/copilot-fix-model-circular-references-2026-4-14-20-45-42.md @@ -4,4 +4,4 @@ packages: - "@typespec/compiler" --- -Validate circular model spread chains without over-reporting recursive model-expression aliases. \ No newline at end of file +Fixed the compiler to correctly detect circular model spread chains while preserving support for recursive model-expression aliases.