diff --git a/.chronus/changes/feature-compiler-semantic-walker-derived-models-2025-0-27-16-29-58.md b/.chronus/changes/feature-compiler-semantic-walker-derived-models-2025-0-27-16-29-58.md new file mode 100644 index 00000000000..4767aff0275 --- /dev/null +++ b/.chronus/changes/feature-compiler-semantic-walker-derived-models-2025-0-27-16-29-58.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Add option for semantic walker to visit model derived types \ No newline at end of file diff --git a/packages/compiler/src/core/semantic-walker.ts b/packages/compiler/src/core/semantic-walker.ts index 693313c6920..7b0e9d37619 100644 --- a/packages/compiler/src/core/semantic-walker.ts +++ b/packages/compiler/src/core/semantic-walker.ts @@ -27,6 +27,10 @@ export interface NavigationOptions { * Skip non instantiated templates. */ includeTemplateDeclaration?: boolean; + /** + * Visit derived types. + */ + visitDerivedTypes?: boolean; } export interface NamespaceNavigationOptions { @@ -253,6 +257,13 @@ function navigateModelType(model: Model, context: NavigationContext) { if (model.indexer && model.indexer.value) { navigateTypeInternal(model.indexer.value, context); } + + if (context.options.visitDerivedTypes) { + for (const derived of model.derivedModels) { + navigateModelType(derived, context); + } + } + context.emit("exitModel", model); } diff --git a/packages/compiler/test/semantic-walker.test.ts b/packages/compiler/test/semantic-walker.test.ts index 8d35b1c8835..ce86966de37 100644 --- a/packages/compiler/test/semantic-walker.test.ts +++ b/packages/compiler/test/semantic-walker.test.ts @@ -17,15 +17,23 @@ import { import { getProperty, navigateProgram, + navigateType, navigateTypesInNamespace, } from "../src/core/semantic-walker.js"; -import { TestHost, createTestHost } from "../src/testing/index.js"; +import { + BasicTestRunner, + TestHost, + createTestHost, + createTestRunner, +} from "../src/testing/index.js"; describe("compiler: semantic walker", () => { let host: TestHost; + let runner: BasicTestRunner; beforeEach(async () => { host = await createTestHost(); + runner = await createTestRunner(); }); function createCollector(customListener?: SemanticNodeListener) { @@ -138,6 +146,100 @@ describe("compiler: semantic walker", () => { return result; } + it("finds derived models", async () => { + const { Bird } = (await runner.compile(` + namespace Test; + + @discriminator("kind") + @test + model Bird { + kind: string; + wingspan: int32; + } + + model SeaGull extends Bird { + kind: "seagull"; + } + + model Sparrow extends Bird { + kind: "sparrow"; + } + + model Goose extends Bird { + kind: "goose"; + } + + model Eagle extends Bird { + kind: "eagle"; + friends?: Bird[]; + hate?: Record; + partner?: Bird; + } + `)) as { Bird: Model }; + + const visitedModels: Model[] = []; + navigateType( + Bird, + { + model(model) { + visitedModels.push(model); + }, + }, + { includeTemplateDeclaration: false, visitDerivedTypes: true }, + ); + + const expectedModels = ["Bird", "SeaGull", "Sparrow", "Goose", "Eagle"]; + strictEqual( + expectedModels.every((element) => visitedModels.map((m) => m.name).includes(element)), + true, + ); + }); + + it("doesn't visit derived models without the option", async () => { + const { Bird } = (await runner.compile(` + namespace Test; + + @discriminator("kind") + @test + model Bird { + kind: string; + wingspan: int32; + } + + model SeaGull extends Bird { + kind: "seagull"; + } + + model Sparrow extends Bird { + kind: "sparrow"; + } + + model Goose extends Bird { + kind: "goose"; + } + + model Eagle extends Bird { + kind: "eagle"; + friends?: Bird[]; + hate?: Record; + partner?: Bird; + } + `)) as { Bird: Model }; + + const visitedModels: Model[] = []; + navigateType( + Bird, + { + model(model) { + visitedModels.push(model); + }, + }, + { visitDerivedTypes: false }, + ); + + strictEqual(visitedModels.length, 1); + }); + it("finds models", async () => { const result = await runNavigator(` model Foo {