Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/compiler"
---

Fixed the compiler to correctly detect circular model spread chains while preserving support for recursive model-expression aliases.
33 changes: 32 additions & 1 deletion packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Sym, Set<Sym>>();
const postCheckValidators: ValidatorFn[] = [];

const typespecNamespaceBinding = resolver.symbols.global.exports!.get("TypeSpec");
Expand Down Expand Up @@ -6532,6 +6533,34 @@ 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) {
reportCheckerDiagnostic(
createDiagnostic({
code: "spread-model",
messageId: "selfSpread",
target: target,
}),
);
}
return undefined;
}

if (targetSym) {
let targetAncestors = spreadResolutionAncestors.get(targetSym);
if (!targetAncestors) {
targetAncestors = new Set<Sym>();
spreadResolutionAncestors.set(targetSym, targetAncestors);
}
targetAncestors.add(modelSymId);
for (const ancestor of modelAncestors ?? []) {
targetAncestors.add(ancestor);
}
}

const type = getTypeForNode(target, ctx);
return type;
}
Expand Down Expand Up @@ -7360,7 +7389,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
return inProgressType;
}
}

if (node.value.kind === SyntaxKind.ModelExpression) {
return getTypeForNode(node.value, ctx);
}
if (ctx.mapper === undefined) {
reportCheckerDiagnostic(
createDiagnostic({
Expand Down
12 changes: 12 additions & 0 deletions packages/compiler/test/checker/alias.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/test/checker/spread.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
Loading