Skip to content

Add typekit to list types under a container (namespace/interface) #7391

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/yellow-bees-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we use @chronus/chronus for changelog

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use @chronus/chronus for the changelog in 747a22a.

"@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.
115 changes: 115 additions & 0 deletions packages/compiler/src/core/helpers/type-utils.ts
Original file line number Diff line number Diff line change
@@ -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<T extends Type = Type>(
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;
}
27 changes: 27 additions & 0 deletions packages/compiler/src/typekit/kits/type.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getLocationContext } from "../../core/helpers/location-context.js";
import { listTypesUnder } from "../../core/helpers/type-utils.js";
import {
getMaxItems,
getMaxLength,
Expand All @@ -13,6 +14,7 @@ import { isNeverType } from "../../core/type-utils.js";
import {
Entity,
Enum,
Interface,
Model,
Namespace,
Node,
Expand Down Expand Up @@ -164,6 +166,28 @@ export interface TypeTypekit {
kind?: K,
) => K extends Type["kind"] ? Extract<Type, { kind: K }> : 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<T extends Type = Type>(
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 {
Expand Down Expand Up @@ -345,5 +369,8 @@ defineKit<TypekitExtension>({
}
return [type, diagnostics];
}),
listUnder(container, filter) {
return listTypesUnder(container, filter);
},
},
});
75 changes: 75 additions & 0 deletions packages/compiler/test/core/helpers/type-utils.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
92 changes: 92 additions & 0 deletions packages/compiler/test/typekit/type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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);
});
});
Loading