diff --git a/.chronus/changes/inifite-loop-2025-4-27-14-20-40.md b/.chronus/changes/inifite-loop-2025-4-27-14-20-40.md new file mode 100644 index 00000000000..7fa98791ed0 --- /dev/null +++ b/.chronus/changes/inifite-loop-2025-4-27-14-20-40.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/compiler" +--- + +Fix infinite recursion when navigating paging properties by detecting and handling circular model references. diff --git a/packages/compiler/src/lib/paging.ts b/packages/compiler/src/lib/paging.ts index 2ec0efe5f79..7a204276d6a 100644 --- a/packages/compiler/src/lib/paging.ts +++ b/packages/compiler/src/lib/paging.ts @@ -298,21 +298,24 @@ function navigateProperties( type: Type, callback: (prop: ModelProperty, path: ModelProperty[]) => void, path: ModelProperty[] = [], + visited: Set = new Set(), ): void { + if (visited.has(type)) return; + visited.add(type); switch (type.kind) { case "Model": for (const prop of type.properties.values()) { callback(prop, [...path, prop]); - navigateProperties(prop.type, callback, [...path, prop]); + navigateProperties(prop.type, callback, [...path, prop], visited); } break; case "Union": for (const member of type.variants.values()) { - navigateProperties(member, callback, path); + navigateProperties(member, callback, path, visited); } break; case "UnionVariant": - navigateProperties(type.type, callback, path); + navigateProperties(type.type, callback, path, visited); break; } } diff --git a/packages/compiler/test/decorators/paging.test.ts b/packages/compiler/test/decorators/paging.test.ts index 0e5892f6a8e..10d99bafac0 100644 --- a/packages/compiler/test/decorators/paging.test.ts +++ b/packages/compiler/test/decorators/paging.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it } from "vitest"; import { ignoreDiagnostics, ModelProperty, Operation } from "../../src/index.js"; import { getPagingOperation, PagingOperation } from "../../src/lib/paging.js"; -import { expectDiagnostics } from "../../src/testing/expect.js"; +import { expectDiagnosticEmpty, expectDiagnostics } from "../../src/testing/expect.js"; import { createTestRunner } from "../../src/testing/test-host.js"; import { BasicTestRunner } from "../../src/testing/types.js"; @@ -39,7 +39,20 @@ it("emit error if missing pageItems property", async () => { }); }); -describe("emit conflict diagnostic if multiple properties are annotated with teh same property marker", () => { +it("@list decorator handle recursive models without infinite loop", async () => { + const diagnostics = await runner.diagnose(` + model MyPage { + selfRef?: MyPage; + @pageItems items: string[]; + @nextLink next: string; + } + + @list op foo(): MyPage; + `); + expectDiagnosticEmpty(diagnostics); +}); + +describe("emit conflict diagnostic if multiple properties are annotated with the same property marker", () => { it.each([ ["offset", "int32"], ["pageSize", "int32"],