diff --git a/.chronus/changes/joheredi-is-builtin-2025-4-20-22-7-54.md b/.chronus/changes/joheredi-is-builtin-2025-4-20-22-7-54.md new file mode 100644 index 00000000000..aa2844b0720 --- /dev/null +++ b/.chronus/changes/joheredi-is-builtin-2025-4-20-22-7-54.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Introduce builtin.is(type: Type): boolean, which returns true for any type defined in the global TypeSpec namespace (i.e. built-in/standard library types). \ No newline at end of file diff --git a/packages/compiler/src/typekit/kits/builtin.ts b/packages/compiler/src/typekit/kits/builtin.ts index 7fb43448045..457dfc1dde0 100644 --- a/packages/compiler/src/typekit/kits/builtin.ts +++ b/packages/compiler/src/typekit/kits/builtin.ts @@ -1,4 +1,4 @@ -import type { Scalar } from "../../core/types.js"; +import type { Namespace, Scalar, Type } from "../../core/types.js"; import { defineKit } from "../define-kit.js"; /** @@ -6,6 +6,12 @@ import { defineKit } from "../define-kit.js"; * @typekit builtin */ export interface BuiltinKit { + /** + * Checks if the given type is a built-in type. + * A type is considered built-in if it is defined in the TypeSpec namespace. + */ + is(type: Type): boolean; + /** * Accessor for the string builtin type. */ @@ -142,6 +148,36 @@ declare module "../define-kit.js" { defineKit({ builtin: { + is(type: Type): boolean { + // Model property does not have a namespace, + // so we will use the Model where it is defined. + if (type.kind === "ModelProperty" && type.model) { + type = type.model; + } + + // Consider the type not built-in if it has no namespace + // TypeKit might create types without a namespace + const ns = (type as any).namespace as Namespace | undefined; + if (!ns) { + return false; + } + + const globalNs = this.program.getGlobalNamespaceType(); + // If it’s already in the global root, it's not in global.TypeSpec + if (ns === globalNs) { + return false; + } + + // Find the immediate child of `globalNs` in this namespace's ancestry. + const topLevel = getImmediateChildOfGlobal(ns, globalNs); + if (!topLevel) { + return false; + } + + // Finally, compare by identity + const typeSpecNs = globalNs.namespaces.get("TypeSpec"); + return topLevel === typeSpecNs; + }, get string(): Scalar { return this.program.checker.getStdType("string"); }, @@ -219,3 +255,16 @@ defineKit({ }, }, }); + +/** + * Walks from `ns` up to `root`, returning the first namespace + * whose parent is `root`. Returns undefined if `root` is not + * actually an ancestor. + */ +function getImmediateChildOfGlobal(ns: Namespace, root: Namespace): Namespace | undefined { + let current: Namespace | undefined = ns; + while (current.namespace && current.namespace !== root) { + current = current.namespace; + } + return current?.namespace === root ? current : undefined; +} diff --git a/packages/compiler/test/typekit/builtin.test.ts b/packages/compiler/test/typekit/builtin.test.ts index e0337c9e3c2..4a99dabd451 100644 --- a/packages/compiler/test/typekit/builtin.test.ts +++ b/packages/compiler/test/typekit/builtin.test.ts @@ -1,5 +1,11 @@ -import { beforeAll, expect, it } from "vitest"; -import { Program } from "../../src/index.js"; +import { beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { + unsafe_mutateSubgraphWithNamespace, + unsafe_MutatorWithNamespace, +} from "../../src/experimental/index.js"; +import { Model, ModelProperty, Namespace, Program, Scalar, Union } from "../../src/index.js"; +import { createTestRunner } from "../../src/testing/test-host.js"; +import { BasicTestRunner } from "../../src/testing/types.js"; import { $ } from "../../src/typekit/index.js"; import { createContextMock } from "./utils.js"; @@ -159,3 +165,270 @@ it("can get the builtin utcDateTime type", async () => { expect(utcDateTimeType).toBeDefined(); expect(utcDateTimeType.name).toBe("utcDateTime"); }); + +describe("builtin.is() tests", () => { + let runner: BasicTestRunner; + + beforeEach(async () => { + runner = await createTestRunner(); + }); + + it("simple model with a string property", async () => { + // ------------------------------------------------------------ + // - Foo (Model) → false + // - Foo.bar (ModelProperty) → false + // - type of Foo.bar (intrinsic) → true + // ------------------------------------------------------------ + const { Foo, bar } = (await runner.compile( + ` + @test model Foo { + @test bar: string; + } + `, + )) as { Foo: Model; bar: ModelProperty }; + + program = runner.program; + const _$ = $(program); + + expect(_$.builtin.is(Foo)).toBe(false); + expect(_$.builtin.is(bar)).toBe(false); + expect(_$.builtin.is(bar.type)).toBe(true); + }); + + it("Model Property reference", async () => { + // ------------------------------------------------------------ + // - Foo (Model) → false + // - Foo.bar (ModelProperty) → false + // - type of Foo.bar (ModelProperty) → false + // - type of Foo.bar.type (Intrinsic) → true + // ------------------------------------------------------------ + const { Foo, bar } = (await runner.compile( + ` + @test model Bar { + prop: string + } + + @test model Foo { + @test bar: Bar.prop; + } + `, + )) as { Foo: Model; bar: ModelProperty }; + + program = runner.program; + const _$ = $(program); + + expect(_$.builtin.is(Foo)).toBe(false); + expect(_$.builtin.is(bar)).toBe(false); + expect(_$.builtin.is(bar.type)).toBe(false); + expect(_$.builtin.is((bar.type as ModelProperty).type)).toBe(true); + }); + + it("model property is a union of string | int32", async () => { + // ------------------------------------------------------------ + // - Foo (Model) → false + // - Foo.bar (ModelProperty) → false + // - Foo.bar.type (Union) → false + // - each variant (UnionVariant) → false + // - variant.type (intrinsic) → true + // ------------------------------------------------------------ + const { Foo, bar } = (await runner.compile( + ` + @test model Foo { + @test bar: string | int32; + } + `, + )) as { Foo: Model; bar: ModelProperty }; + + program = runner.program; + const _$ = $(program); + + expect(_$.builtin.is(Foo)).toBe(false); + expect(_$.builtin.is(bar)).toBe(false); + expect(_$.builtin.is(bar.type)).toBe(false); + const [variant1, variant2] = [...(bar.type as Union).variants.values()]; + expect(_$.builtin.is(variant1)).toBe(false); + expect(_$.builtin.is(variant2)).toBe(false); + expect(_$.builtin.is(variant1.type)).toBe(true); + expect(_$.builtin.is(variant2.type)).toBe(true); + }); + + it("works with enums and models", async () => { + // ------------------------------------------------------------------- + // - FooEnum (Enum variants) → false + // - Foo.bar (ModelProperty of FooEnum) → false + // - Foo.bar.type (Enum) → false + // - Baz.baz (ModelProperty of FooEnum.one)→ false + // - Baz.baz.type (EnumMember) → false + // ------------------------------------------------------------------- + const { Foo, bar, Baz } = (await runner.compile( + ` + @test enum FooEnum { + one: "1"; + two: "2"; + }; + + @test model Foo { + @test bar: FooEnum; + } + @test model Baz { + @test baz: FooEnum.one; + } + `, + )) as { Foo: Model; bar: ModelProperty; Baz: Model }; + + program = runner.program; + const _$ = $(program); + + expect(_$.builtin.is(Foo)).toBe(false); + expect(_$.builtin.is(bar)).toBe(false); + expect(_$.builtin.is(bar.type)).toBe(false); + expect(_$.builtin.is(Baz)).toBe(false); + expect(_$.builtin.is(Baz.properties.get("baz")!)).toBe(false); + expect(_$.builtin.is(Baz.properties.get("baz")!.type)).toBe(false); + }); + + it("scalar extends an intrinsic (string)", async () => { + // ------------------------------------------------------------ + // - NotBuiltin (Scalar) → false + // - NotBuiltin.baseScalar (string) → true + // ------------------------------------------------------------ + const { NotBuiltin } = (await runner.compile( + ` + @test scalar NotBuiltin extends string; + `, + )) as { NotBuiltin: Scalar }; + + program = runner.program; + const _$ = $(program); + expect(_$.builtin.is(NotBuiltin)).toBe(false); + expect(_$.builtin.is(NotBuiltin.baseScalar!)).toBe(true); + }); + + it("alias an intrinsic then extend it", async () => { + // ------------------------------------------------------------ + // - NotBuiltin (Scalar extends alias) → false + // - NotBuiltin.baseScalar (int builtin)→ true + // ------------------------------------------------------------ + const { NotBuiltin } = (await runner.compile( + ` + alias UnixTimeStamp32 = unixTimestamp32; + @test scalar NotBuiltin extends UnixTimeStamp32; + `, + )) as { NotBuiltin: Scalar }; + + program = runner.program; + const _$ = $(program); + expect(_$.builtin.is(NotBuiltin)).toBe(false); + expect(_$.builtin.is(NotBuiltin.baseScalar!)).toBe(true); + }); + + it("should recognize a model in global.TypeSpec", async () => { + // ------------------------------------------------------------ + // A non-typespec model references a ModelProperty defined in TypeSpec + // ------------------------------------------------------------ + const { InTypeSpec, Foo } = (await runner.compile(` + @test namespace TypeSpec { + @test model InTypeSpec { + @test foo: string; + } + } + + @test model Foo { + @test bar: TypeSpec.InTypeSpec.foo; + } + `)) as { InTypeSpec: Model; Foo: Model }; + + const program = runner.program; + const _$ = $(program); + + expect(_$.builtin.is(InTypeSpec)).toBe(true); + + const prop = InTypeSpec.properties.get("foo")!; + expect(_$.builtin.is(prop)).toBe(true); + + expect(_$.builtin.is(prop.type)).toBe(true); + + expect(_$.builtin.is(Foo)).toBe(false); + const bar = Foo.properties.get("bar") as ModelProperty; + expect(_$.builtin.is(bar)).toBe(false); // bar is defined outside TypeSpec + expect(_$.builtin.is(bar.type)).toBe(true); // TypeSpec.InTypeSpec.foo is deifined inside TypeSpec + }); + + it("should NOT recognize a nested TypeSpec namespace", async () => { + // ---------------------------------------------------------------- + // A namespace called "TypeSpec" but nested inside another namespace + // ---------------------------------------------------------------- + const { InNested } = (await runner.compile(` + @test namespace Outer { + @test namespace TypeSpec { + @test model InNested { + @test bar: int32; + } + } + } + `)) as { InNested: Model }; + + const program = runner.program; + const _$ = $(program); + + // Although the local namespace is called "TypeSpec", + // it's not direct child of the global namespace → false + expect(_$.builtin.is(InNested)).toBe(false); + }); + + it("should always return true for pure intrinsics", async () => { + const _$ = $(program); + const stringType = _$.builtin.string; + + expect(_$.builtin.is(stringType)).toBe(true); + + const int32Type = _$.builtin.int32; + expect(_$.builtin.is(int32Type)).toBe(true); + }); + + it("should work with mutators", async () => { + // ------------------------------------------------------------ + // A non-typespec model references a ModelProperty defined in TypeSpec + // ------------------------------------------------------------ + const mutator: unsafe_MutatorWithNamespace = { + name: "test", + Namespace: { + mutate: (_ns, clone) => { + clone.models.delete("Bar"); + }, + }, + }; + + (await runner.compile(` + namespace TypeSpec { + model InTypeSpec { + foo: string; + } + } + + model Foo { + bar: TypeSpec.InTypeSpec.foo; + } + + model Bar {} + `)) as { InTypeSpec: Model; Foo: Model }; + + const program = runner.program; + const _$ = $(program); + + const globalNs = program.getGlobalNamespaceType(); + const mutatedGlobalNs = unsafe_mutateSubgraphWithNamespace(program, [mutator], globalNs) + .type as Namespace; + + const Foo = mutatedGlobalNs.models.get("Foo")!; + const Bar = mutatedGlobalNs.models.get("Bar"); + + expect(Bar).toBeUndefined(); + expect(Foo).toBeDefined(); + + expect(_$.builtin.is(Foo)).toBe(false); + expect(_$.builtin.is(Foo.properties.get("bar")!)).toBe(false); + expect(_$.builtin.is(Foo.properties.get("bar")!.type)).toBe(true); + expect(_$.builtin.is((Foo.properties.get("bar")!.type as ModelProperty).type)).toBe(true); + }); +});