diff --git a/.changeset/yellow-bees-float.md b/.changeset/yellow-bees-float.md new file mode 100644 index 00000000000..dd7884e999b --- /dev/null +++ b/.changeset/yellow-bees-float.md @@ -0,0 +1,6 @@ +--- +"@typespec/compiler": minor +--- + +@chronus/chronus +Added `$.type.listUnder()` utility function to allow listing all types under a container (namespace/interface) that match a filter criteria. \ No newline at end of file diff --git a/packages/compiler/src/core/helpers/type-utils.ts b/packages/compiler/src/core/helpers/type-utils.ts new file mode 100644 index 00000000000..6db664260a4 --- /dev/null +++ b/packages/compiler/src/core/helpers/type-utils.ts @@ -0,0 +1,115 @@ +import { Interface, Namespace, Type } from "../types.js"; +import { isTemplateDeclaration } from "../type-utils.js"; + +/** + * List types under the given container. Will list types recursively. + * @param container Container. + * @param filter Function to filter types. + */ +export function listTypesUnder( + container: Namespace | Interface, + filter: (type: Type) => type is T, +): T[]; + +/** + * List types under the given container. Will list types recursively. + * @param container Container. + * @param filter Function to filter types. + */ +export function listTypesUnder( + container: Namespace | Interface, + filter: (type: Type) => boolean, +): Type[]; + +export function listTypesUnder( + container: Namespace | Interface, + filter: (type: Type) => boolean, +): Type[] { + const types: Type[] = []; + + function addTypes(current: Namespace | Interface) { + if (isTemplateDeclaration(current)) { + // Skip template types + return; + } + + // For interfaces, we only have operations + if (current.kind === "Interface") { + for (const op of current.operations.values()) { + if (filter(op)) { + types.push(op); + } + } + return; + } + + // For namespaces, check all contained type collections + const namespace = current; + + // Check models + for (const model of namespace.models.values()) { + if (filter(model)) { + types.push(model); + } + } + + // Check operations + for (const op of namespace.operations.values()) { + if (filter(op)) { + types.push(op); + } + } + + // Check scalars + for (const scalar of namespace.scalars.values()) { + if (filter(scalar)) { + types.push(scalar); + } + } + + // Check enums + for (const enumType of namespace.enums.values()) { + if (filter(enumType)) { + types.push(enumType); + } + } + + // Check unions + for (const union of namespace.unions.values()) { + if (filter(union)) { + types.push(union); + } + } + + // Check interfaces + for (const iface of namespace.interfaces.values()) { + if (filter(iface)) { + types.push(iface); + } + } + + // Recursively check sub-namespaces + for (const subNamespace of namespace.namespaces.values()) { + if ( + !( + subNamespace.name === "Prototypes" && + subNamespace.namespace?.name === "TypeSpec" && + subNamespace.namespace.namespace?.name === "" + ) + ) { + if (filter(subNamespace)) { + types.push(subNamespace); + } + addTypes(subNamespace); + } + } + + // Recursively check operations in interfaces + for (const iface of namespace.interfaces.values()) { + addTypes(iface); + } + } + + addTypes(container); + return types; +} \ No newline at end of file diff --git a/packages/compiler/src/typekit/kits/type.ts b/packages/compiler/src/typekit/kits/type.ts index 14ec7d759c9..b2e1135f082 100644 --- a/packages/compiler/src/typekit/kits/type.ts +++ b/packages/compiler/src/typekit/kits/type.ts @@ -1,4 +1,5 @@ import { getLocationContext } from "../../core/helpers/location-context.js"; +import { listTypesUnder } from "../../core/helpers/type-utils.js"; import { getMaxItems, getMaxLength, @@ -13,6 +14,7 @@ import { isNeverType } from "../../core/type-utils.js"; import { Entity, Enum, + Interface, Model, Namespace, Node, @@ -164,6 +166,28 @@ export interface TypeTypekit { kind?: K, ) => K extends Type["kind"] ? Extract : undefined >; + + /** + * List all types under the given container that satisfy the filter criteria. + * @param container The container to inspect. + * @param filter A filter function to select specific types. + * @returns An array of types that match the filter criteria. + */ + listUnder( + container: Namespace | Interface, + filter: (type: Type) => type is T, + ): T[]; + + /** + * List all types under the given container that satisfy the filter criteria. + * @param container The container to inspect. + * @param filter A filter function to select specific types. + * @returns An array of types that match the filter criteria. + */ + listUnder( + container: Namespace | Interface, + filter: (type: Type) => boolean, + ): Type[]; } interface TypekitExtension { @@ -345,5 +369,8 @@ defineKit({ } return [type, diagnostics]; }), + listUnder(container, filter) { + return listTypesUnder(container, filter); + }, }, }); diff --git a/packages/compiler/test/core/helpers/type-utils.test.ts b/packages/compiler/test/core/helpers/type-utils.test.ts new file mode 100644 index 00000000000..69d8e96f679 --- /dev/null +++ b/packages/compiler/test/core/helpers/type-utils.test.ts @@ -0,0 +1,75 @@ +import { Interface, Model, Namespace, Operation } from "../../../src/core/types.js"; +import { createTestHost, TestHost } from "../../../src/testing/index.js"; +import { listTypesUnder } from "../../../src/core/helpers/type-utils.js"; + +let host: TestHost; + +beforeEach(async () => { + host = await createTestHost(); +}); + +it("lists all types in a namespace", async () => { + const testCode = ` + namespace A { + model M1 {} + model M2 {} + } + `; + + const { A } = (await host.compile(testCode)) as { A: Namespace }; + const types = listTypesUnder(A, (t) => true); + + // Should include 2 models plus namespace A + expect(types.length).toBe(3); +}); + +it("filters models", async () => { + const testCode = ` + namespace A { + model M1 {} + model M2 {} + scalar S {} + } + `; + + const { A } = (await host.compile(testCode)) as { A: Namespace }; + const models = listTypesUnder(A, (t): t is Model => t.kind === "Model"); + + expect(models.length).toBe(2); + expect(models.every(m => m.kind === "Model")).toBe(true); +}); + +it("handles nested namespaces", async () => { + const testCode = ` + namespace A { + model M1 {} + + namespace B { + model M2 {} + } + } + `; + + const { A } = (await host.compile(testCode)) as { A: Namespace }; + const models = listTypesUnder(A, (t): t is Model => t.kind === "Model"); + + expect(models.length).toBe(2); +}); + +it("handles interfaces and operations", async () => { + const testCode = ` + namespace A { + interface I1 { + op1(): void; + op2(): string; + } + } + `; + + const { A } = (await host.compile(testCode)) as { A: Namespace }; + const operations = listTypesUnder(A, (t): t is Operation => t.kind === "Operation"); + const interfaces = listTypesUnder(A, (t): t is Interface => t.kind === "Interface"); + + expect(operations.length).toBe(2); + expect(interfaces.length).toBe(1); +}); \ No newline at end of file diff --git a/packages/compiler/test/typekit/type.test.ts b/packages/compiler/test/typekit/type.test.ts index 46c422de864..bf3a57e1fda 100644 --- a/packages/compiler/test/typekit/type.test.ts +++ b/packages/compiler/test/typekit/type.test.ts @@ -4,6 +4,7 @@ import { isTemplateInstance } from "../../src/index.js"; import { expectDiagnosticEmpty, expectDiagnostics } from "../../src/testing/expect.js"; import { $ } from "../../src/typekit/index.js"; import { getAssignables, getTypes } from "./utils.js"; +import { TestHost, createTestHost } from "../../src/testing/index.js"; describe("is", () => { it("checks if an entity is a type", async () => { @@ -667,3 +668,94 @@ describe("resolve", () => { }); }); }); + +describe("listUnder", () => { + let host: TestHost; + + beforeEach(async () => { + host = await createTestHost(); + }); + + it("lists all types under a namespace", async () => { + const testCode = ` + namespace A { + model M1 {} + model M2 {} + + @doc("Some doc") + model M3 {} + } + `; + + const { A } = (await host.compile(testCode)) as { A: Namespace }; + const types = host.program.typespecType.listUnder(A, t => true); + + // Should include all 3 models plus the namespace itself when filter is 'true' + expect(types.length).toBe(4); + }); + + it("filters types with the provided filter function", async () => { + const testCode = ` + namespace A { + model M1 {} + + @doc("Some doc") + model M2 {} + + @deprecated + model M3 {} + } + `; + + const { A } = (await host.compile(testCode)) as { A: Namespace }; + + // Filter only models with @doc decorator + const types = host.program.typespecType.listUnder(A, t => { + return t.kind === "Model" && !!t.decorators.find(d => d.decorator.name === "@doc"); + }); + + expect(types.length).toBe(1); + expect((types[0] as Model).name).toBe("M2"); + }); + + it("recursively searches sub-namespaces", async () => { + const testCode = ` + namespace A { + model M1 {} + + namespace B { + model M2 {} + + namespace C { + model M3 {} + } + } + } + `; + + const { A } = (await host.compile(testCode)) as { A: Namespace }; + + // Should find all models in all namespaces + const types = host.program.typespecType.listUnder(A, t => t.kind === "Model"); + + expect(types.length).toBe(3); + }); + + it("finds operations in interfaces", async () => { + const testCode = ` + namespace A { + interface I1 { + op1(): void; + op2(): string; + } + } + `; + + const { A } = (await host.compile(testCode)) as { A: Namespace }; + + // Should find both operations + const types = host.program.typespecType.listUnder(A, t => t.kind === "Operation"); + + expect(types.length).toBe(2); + }); +});