Skip to content

typekits - add $.type.resolve, $.value.resolve, and update $.resolve to support returning values #7167

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

Merged
merged 7 commits into from
Apr 29, 2025
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

Adds `$.value.resolve` and `$.type.resolve` typekits, and updated `$.resolve` to return values or types, instead of just types
19 changes: 19 additions & 0 deletions packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,13 @@ export interface Checker {
* @internal use program.resolveTypeReference
*/
resolveTypeReference(node: TypeReferenceNode): [Type | undefined, readonly Diagnostic[]];
/**
* Check and resolve a type or value for the given type reference node.
* @param node Node.
* @returns Resolved type and diagnostics if there was an error.
* @internal
*/
resolveTypeOrValueReference(node: TypeReferenceNode): [Entity | undefined, readonly Diagnostic[]];

/** @internal */
getValueForNode(node: Node): Value | null;
Expand Down Expand Up @@ -363,6 +370,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
isStdType,
getStdType,
resolveTypeReference,
resolveTypeOrValueReference,
getValueForNode,
getTypeOrValueForNode,
getValueExactType,
Expand Down Expand Up @@ -1059,6 +1067,17 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
return checkNode(node.argument, mapper);
}

function resolveTypeOrValueReference(
node: TypeReferenceNode | MemberExpressionNode | IdentifierNode,
): [Entity | undefined, readonly Diagnostic[]] {
const oldDiagnosticHook = onCheckerDiagnostic;
const diagnostics: Diagnostic[] = [];
onCheckerDiagnostic = (x: Diagnostic) => diagnostics.push(x);
const entity = checkTypeOrValueReference(node, undefined, false);
onCheckerDiagnostic = oldDiagnosticHook;
return [entity === errorType ? undefined : entity, diagnostics];
}

function resolveTypeReference(
node: TypeReferenceNode,
): [Type | undefined, readonly Diagnostic[]] {
Expand Down
19 changes: 19 additions & 0 deletions packages/compiler/src/core/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ export interface Program {

resolveTypeReference(reference: string): [Type | undefined, readonly Diagnostic[]];

/** @internal */
resolveTypeOrValueReference(reference: string): [Entity | undefined, readonly Diagnostic[]];

/** Return location context of the given source file. */
getSourceFileLocationContext(sourceFile: SourceFile): LocationContext;

Expand Down Expand Up @@ -233,6 +236,8 @@ async function createProgram(
},
getGlobalNamespaceType,
resolveTypeReference,
/** @internal */
resolveTypeOrValueReference,
getSourceFileLocationContext,
projectRoot: getDirectoryPath(options.config ?? resolvedMain ?? ""),
};
Expand Down Expand Up @@ -907,6 +912,20 @@ async function createProgram(
resolver.resolveTypeReference(node);
return program.checker.resolveTypeReference(node);
}

function resolveTypeOrValueReference(
reference: string,
): [Entity | undefined, readonly Diagnostic[]] {
const [node, parseDiagnostics] = parseStandaloneTypeReference(reference);
if (parseDiagnostics.length > 0) {
return [undefined, parseDiagnostics];
}
const binder = createBinder(program);
binder.bindNode(node);
mutate(node).parent = resolver.symbols.global.declarations[0];
resolver.resolveTypeReference(node);
return program.checker.resolveTypeOrValueReference(node);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,18 @@ import { ignoreDiagnostics } from "../../core/diagnostics.js";
import { type Diagnostic } from "../../core/types.js";
import { Typekit } from "./define-kit.js";

/**
* Represents a function that can return diagnostics along with its primary result.
* @template P The parameters of the function.
* @template R The primary return type of the function.
*/
export type DiagnosableFunction<P extends unknown[], R> = (
...args: P
) => [R, readonly Diagnostic[]];

/**
* Represents the enhanced function returned by `createDiagnosable`.
* This function, when called directly, ignores diagnostics.
* It also has a `withDiagnostics` method to access the original function's behavior.
* @template P The parameters of the function.
* @template R The primary return type of the function.
* @template F The function type to be wrapped. This should not include diagnostics on the return type.
*/
export type Diagnosable<F> = F extends (...args: infer P extends unknown[]) => infer R
? {
(...args: P): R;
/**
* Returns a tuple of its primary result and any diagnostics.
*/
withDiagnostics: DiagnosableFunction<P, R>;
}
: never;
export type Diagnosable<F extends (...args: any[]) => unknown> = F & {
/**
* Returns a tuple of its primary result and any diagnostics.
*/
withDiagnostics: (...args: Parameters<F>) => [ReturnType<F>, readonly Diagnostic[]];
};

/**
* Creates a diagnosable function wrapper.
Expand Down
7 changes: 3 additions & 4 deletions packages/compiler/src/experimental/typekit/kits/resolve.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Type } from "../../../core/types.js";
import { Entity } from "../../../core/types.js";
import { createDiagnosable, Diagnosable } from "../create-diagnosable.js";
import { defineKit } from "../define-kit.js";

export type ResolveKit = Diagnosable<(reference: string) => Type | undefined>;
export type ResolveKit = Diagnosable<(reference: string) => Entity | undefined>;

interface TypekitExtension {
/**
Expand All @@ -22,7 +22,6 @@ declare module "../define-kit.js" {

defineKit<TypekitExtension>({
resolve: createDiagnosable(function (reference) {
// Directly use the program's resolveTypeReference method
return this.program.resolveTypeReference(reference);
return this.program.resolveTypeOrValueReference(reference);
}),
});
23 changes: 23 additions & 0 deletions packages/compiler/src/experimental/typekit/kits/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,22 @@ export interface TypeTypekit {
isAssignableTo: Diagnosable<
(source: Type, target: Entity, diagnosticTarget?: Entity | Node) => boolean
>;

/**
* Resolve a type reference to a TypeSpec type.
* By default any diagnostics are ignored.
*
* If a `kind` is provided, it will check if the resolved type matches the expected kind
* and throw an error if it doesn't.
*
* Call `type.resolve.withDiagnostics("reference")` to get a tuple containing the resolved type and any diagnostics.
*/
resolve: Diagnosable<
<K extends Type["kind"] | undefined>(
reference: string,
kind?: K,
) => K extends Type["kind"] ? Extract<Type, { kind: K }> : undefined
>;
}

interface TypekitExtension {
Expand Down Expand Up @@ -336,5 +352,12 @@ defineKit<TypekitExtension>({
isAssignableTo: createDiagnosable(function (source, target, diagnosticTarget) {
return this.program.checker.isTypeAssignableTo(source, target, diagnosticTarget ?? source);
}),
resolve: createDiagnosable(function (reference, kind) {
const [type, diagnostics] = this.program.resolveTypeReference(reference);
if (type && kind && type.kind !== kind) {
throw new Error(`Type kind mismatch: expected ${kind}, got ${type.kind}`);
}
return [type, diagnostics];
}),
},
});
27 changes: 27 additions & 0 deletions packages/compiler/src/experimental/typekit/kits/value.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Numeric } from "../../../core/numeric.js";
import { isValue } from "../../../core/type-utils.js";
import type {
ArrayValue,
BooleanValue,
Expand Down Expand Up @@ -107,6 +108,22 @@ export interface ValueKit {
isAssignableTo: Diagnosable<
(source: Value, target: Entity, diagnosticTarget?: Entity | Node) => boolean
>;

/**
* Resolve a value reference to a TypeSpec value.
* By default any diagnostics are ignored.
*
* If a `kind` is provided, it will check if the resolved value matches the expected kind
* and throw an error if it doesn't.
*
* Call `value.resolve.withDiagnostics("reference")` to get a tuple containing the resolved value and any diagnostics.
*/
resolve: Diagnosable<
<K extends Value["valueKind"] | undefined>(
reference: string,
kind?: K,
) => K extends Value["valueKind"] ? Extract<Value, { valueKind: K }> : undefined
>;
}

interface TypekitExtension {
Expand Down Expand Up @@ -201,5 +218,15 @@ defineKit<TypekitExtension>({
isAssignableTo: createDiagnosable(function (source, target, diagnosticTarget) {
return this.program.checker.isTypeAssignableTo(source, target, diagnosticTarget ?? source);
}),
resolve: createDiagnosable(function (reference, kind) {
const [value, diagnostics] = this.program.resolveTypeOrValueReference(reference);
if (value && !isValue(value)) {
return [undefined, diagnostics];
}
if (value && kind && value.valueKind !== kind) {
throw new Error(`Value kind mismatch: expected ${kind}, got ${value.valueKind}`);
}
return [value, diagnostics];
}),
},
});
17 changes: 14 additions & 3 deletions packages/compiler/test/experimental/typekit/resolve.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { beforeEach, expect, it } from "vitest";
import { IntrinsicType } from "../../../src/core/types.js";
import { $ } from "../../../src/experimental/typekit/index.js";
import { isValue } from "../../../src/index.js";
import { createTestHost } from "../../../src/testing/test-host.js";
import { createTestWrapper } from "../../../src/testing/test-utils.js";
import { BasicTestRunner } from "../../../src/testing/types.js";
Expand All @@ -15,14 +15,25 @@ it("resolve resolves existing types", async () => {
const tk = $(runner.program);
const stringType = tk.resolve("TypeSpec.string");
expect(stringType).toBeDefined();
expect(stringType?.kind).toBe("Scalar");
expect((stringType as IntrinsicType).name).toBe("string");
expect(tk.builtin.string).toBe(stringType);

const [stringTypeDiag, diagnostics] = tk.resolve.withDiagnostics("TypeSpec.string");
expect(stringTypeDiag).toBe(stringType);
expect(diagnostics).toHaveLength(0);
});

it("resolve resolves existing values", async () => {
await runner.compile(`const stringValue = "test";`);
const tk = $(runner.program);
const stringValue = tk.resolve("stringValue");
expect(stringValue).toBeDefined();
expect(isValue(stringValue!)).toBe(true);

const [stringValueDiag, diagnostics] = tk.resolve.withDiagnostics("stringValue");
expect(isValue(stringValueDiag!)).toBe(true);
expect(diagnostics).toHaveLength(0);
});

it("resolve returns undefined and diagnostics for invalid references", async () => {
await runner.compile("");
const unknownType = $(runner.program).resolve("UnknownModel");
Expand Down
62 changes: 61 additions & 1 deletion packages/compiler/test/experimental/typekit/type.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { assert, describe, expect, it } from "vitest";
import { Enum, Model, Namespace, Scalar, Union } from "../../../src/core/types.js";
import { $ } from "../../../src/experimental/typekit/index.js";
import { isTemplateInstance } from "../../../src/index.js";
Expand Down Expand Up @@ -539,3 +539,63 @@ describe("isAssignableTo", () => {
expectDiagnosticEmpty(validTest[1]);
});
});

describe("resolve", () => {
it("resolves to the value type", async () => {
const {
context: { program },
} = await getTypes(
`
alias stringLiteral = "hello";
alias aliasedLiteral = stringLiteral;
enum Foo { one: 1, two: 2 }
const aValue = "value";
`,
[],
);

const tk = $(program);
const stringLiteral = tk.type.resolve("stringLiteral");
assert(tk.literal.isString(stringLiteral!));
expect(stringLiteral.value).toBe("hello");

const aliasedLiteral = tk.type.resolve("aliasedLiteral", "String");
expect(aliasedLiteral!.value).toBe("hello");

const enumMember = tk.type.resolve("Foo.one", "EnumMember");
expect(tk.enumMember.is(enumMember!)).toBe(true);

// Not actually a type
const confusedValue = tk.type.resolve("aValue");
expect(confusedValue).toBeUndefined();
});

it("throws an error for incorrect kind assertion", async () => {
const {
context: { program },
} = await getTypes(
`
alias stringLiteral = "hello";
`,
[],
);

const tk = $(program);
expect(() => tk.type.resolve("stringLiteral", "Boolean")).toThrow(
"Type kind mismatch: expected Boolean, got String",
);
});

it("returns undefined and diagnostics for invalid references", async () => {
const {
context: { program },
} = await getTypes(``, []);

const tk = $(program);
const [unknownType, diagnostics] = tk.type.resolve.withDiagnostics("unknownType");
expect(unknownType).toBeUndefined();
expectDiagnostics(diagnostics, {
code: "invalid-ref",
});
});
});
Loading
Loading