Skip to content

Commit 55835ce

Browse files
chrisradekChristopher Radek
andauthored
typekits - add $.type.resolve, $.value.resolve, and update $.resolve to support returning values (#7167)
The `$.type.resolve` and `$.value.resolve` versions of the typekits also support passing in a `kind` and will assert that the resolved type or value matches the passed in kind. Note: `$.resolve` wasn't actually available in the previous release, so changing the return value to allow returning `Value` is not a breaking change. --------- Co-authored-by: Christopher Radek <Christopher.Radek@microsoft.com>
1 parent 30ec04f commit 55835ce

File tree

10 files changed

+244
-30
lines changed

10 files changed

+244
-30
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: feature
3+
packages:
4+
- "@typespec/compiler"
5+
---
6+
7+
Adds `$.value.resolve` and `$.type.resolve` typekits, and updated `$.resolve` to return values or types, instead of just types

packages/compiler/src/core/checker.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,13 @@ export interface Checker {
256256
* @internal use program.resolveTypeReference
257257
*/
258258
resolveTypeReference(node: TypeReferenceNode): [Type | undefined, readonly Diagnostic[]];
259+
/**
260+
* Check and resolve a type or value for the given type reference node.
261+
* @param node Node.
262+
* @returns Resolved type and diagnostics if there was an error.
263+
* @internal
264+
*/
265+
resolveTypeOrValueReference(node: TypeReferenceNode): [Entity | undefined, readonly Diagnostic[]];
259266

260267
/** @internal */
261268
getValueForNode(node: Node): Value | null;
@@ -363,6 +370,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
363370
isStdType,
364371
getStdType,
365372
resolveTypeReference,
373+
resolveTypeOrValueReference,
366374
getValueForNode,
367375
getTypeOrValueForNode,
368376
getValueExactType,
@@ -1059,6 +1067,17 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
10591067
return checkNode(node.argument, mapper);
10601068
}
10611069

1070+
function resolveTypeOrValueReference(
1071+
node: TypeReferenceNode | MemberExpressionNode | IdentifierNode,
1072+
): [Entity | undefined, readonly Diagnostic[]] {
1073+
const oldDiagnosticHook = onCheckerDiagnostic;
1074+
const diagnostics: Diagnostic[] = [];
1075+
onCheckerDiagnostic = (x: Diagnostic) => diagnostics.push(x);
1076+
const entity = checkTypeOrValueReference(node, undefined, false);
1077+
onCheckerDiagnostic = oldDiagnosticHook;
1078+
return [entity === errorType ? undefined : entity, diagnostics];
1079+
}
1080+
10621081
function resolveTypeReference(
10631082
node: TypeReferenceNode,
10641083
): [Type | undefined, readonly Diagnostic[]] {

packages/compiler/src/core/program.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ export interface Program {
118118

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

121+
/** @internal */
122+
resolveTypeOrValueReference(reference: string): [Entity | undefined, readonly Diagnostic[]];
123+
121124
/** Return location context of the given source file. */
122125
getSourceFileLocationContext(sourceFile: SourceFile): LocationContext;
123126

@@ -233,6 +236,8 @@ async function createProgram(
233236
},
234237
getGlobalNamespaceType,
235238
resolveTypeReference,
239+
/** @internal */
240+
resolveTypeOrValueReference,
236241
getSourceFileLocationContext,
237242
projectRoot: getDirectoryPath(options.config ?? resolvedMain ?? ""),
238243
};
@@ -907,6 +912,20 @@ async function createProgram(
907912
resolver.resolveTypeReference(node);
908913
return program.checker.resolveTypeReference(node);
909914
}
915+
916+
function resolveTypeOrValueReference(
917+
reference: string,
918+
): [Entity | undefined, readonly Diagnostic[]] {
919+
const [node, parseDiagnostics] = parseStandaloneTypeReference(reference);
920+
if (parseDiagnostics.length > 0) {
921+
return [undefined, parseDiagnostics];
922+
}
923+
const binder = createBinder(program);
924+
binder.bindNode(node);
925+
mutate(node).parent = resolver.symbols.global.declarations[0];
926+
resolver.resolveTypeReference(node);
927+
return program.checker.resolveTypeOrValueReference(node);
928+
}
910929
}
911930

912931
/**

packages/compiler/src/experimental/typekit/create-diagnosable.ts

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,18 @@ import { ignoreDiagnostics } from "../../core/diagnostics.js";
22
import { type Diagnostic } from "../../core/types.js";
33
import { Typekit } from "./define-kit.js";
44

5-
/**
6-
* Represents a function that can return diagnostics along with its primary result.
7-
* @template P The parameters of the function.
8-
* @template R The primary return type of the function.
9-
*/
10-
export type DiagnosableFunction<P extends unknown[], R> = (
11-
...args: P
12-
) => [R, readonly Diagnostic[]];
13-
145
/**
156
* Represents the enhanced function returned by `createDiagnosable`.
167
* This function, when called directly, ignores diagnostics.
178
* It also has a `withDiagnostics` method to access the original function's behavior.
18-
* @template P The parameters of the function.
19-
* @template R The primary return type of the function.
9+
* @template F The function type to be wrapped. This should not include diagnostics on the return type.
2010
*/
21-
export type Diagnosable<F> = F extends (...args: infer P extends unknown[]) => infer R
22-
? {
23-
(...args: P): R;
24-
/**
25-
* Returns a tuple of its primary result and any diagnostics.
26-
*/
27-
withDiagnostics: DiagnosableFunction<P, R>;
28-
}
29-
: never;
11+
export type Diagnosable<F extends (...args: any[]) => unknown> = F & {
12+
/**
13+
* Returns a tuple of its primary result and any diagnostics.
14+
*/
15+
withDiagnostics: (...args: Parameters<F>) => [ReturnType<F>, readonly Diagnostic[]];
16+
};
3017

3118
/**
3219
* Creates a diagnosable function wrapper.

packages/compiler/src/experimental/typekit/kits/resolve.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { Type } from "../../../core/types.js";
1+
import { Entity } from "../../../core/types.js";
22
import { createDiagnosable, Diagnosable } from "../create-diagnosable.js";
33
import { defineKit } from "../define-kit.js";
44

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

77
interface TypekitExtension {
88
/**
@@ -22,7 +22,6 @@ declare module "../define-kit.js" {
2222

2323
defineKit<TypekitExtension>({
2424
resolve: createDiagnosable(function (reference) {
25-
// Directly use the program's resolveTypeReference method
26-
return this.program.resolveTypeReference(reference);
25+
return this.program.resolveTypeOrValueReference(reference);
2726
}),
2827
});

packages/compiler/src/experimental/typekit/kits/type.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,22 @@ export interface TypeTypekit {
150150
isAssignableTo: Diagnosable<
151151
(source: Type, target: Entity, diagnosticTarget?: Entity | Node) => boolean
152152
>;
153+
154+
/**
155+
* Resolve a type reference to a TypeSpec type.
156+
* By default any diagnostics are ignored.
157+
*
158+
* If a `kind` is provided, it will check if the resolved type matches the expected kind
159+
* and throw an error if it doesn't.
160+
*
161+
* Call `type.resolve.withDiagnostics("reference")` to get a tuple containing the resolved type and any diagnostics.
162+
*/
163+
resolve: Diagnosable<
164+
<K extends Type["kind"] | undefined>(
165+
reference: string,
166+
kind?: K,
167+
) => K extends Type["kind"] ? Extract<Type, { kind: K }> : undefined
168+
>;
153169
}
154170

155171
interface TypekitExtension {
@@ -336,5 +352,12 @@ defineKit<TypekitExtension>({
336352
isAssignableTo: createDiagnosable(function (source, target, diagnosticTarget) {
337353
return this.program.checker.isTypeAssignableTo(source, target, diagnosticTarget ?? source);
338354
}),
355+
resolve: createDiagnosable(function (reference, kind) {
356+
const [type, diagnostics] = this.program.resolveTypeReference(reference);
357+
if (type && kind && type.kind !== kind) {
358+
throw new Error(`Type kind mismatch: expected ${kind}, got ${type.kind}`);
359+
}
360+
return [type, diagnostics];
361+
}),
339362
},
340363
});

packages/compiler/src/experimental/typekit/kits/value.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Numeric } from "../../../core/numeric.js";
2+
import { isValue } from "../../../core/type-utils.js";
23
import type {
34
ArrayValue,
45
BooleanValue,
@@ -107,6 +108,22 @@ export interface ValueKit {
107108
isAssignableTo: Diagnosable<
108109
(source: Value, target: Entity, diagnosticTarget?: Entity | Node) => boolean
109110
>;
111+
112+
/**
113+
* Resolve a value reference to a TypeSpec value.
114+
* By default any diagnostics are ignored.
115+
*
116+
* If a `kind` is provided, it will check if the resolved value matches the expected kind
117+
* and throw an error if it doesn't.
118+
*
119+
* Call `value.resolve.withDiagnostics("reference")` to get a tuple containing the resolved value and any diagnostics.
120+
*/
121+
resolve: Diagnosable<
122+
<K extends Value["valueKind"] | undefined>(
123+
reference: string,
124+
kind?: K,
125+
) => K extends Value["valueKind"] ? Extract<Value, { valueKind: K }> : undefined
126+
>;
110127
}
111128

112129
interface TypekitExtension {
@@ -201,5 +218,15 @@ defineKit<TypekitExtension>({
201218
isAssignableTo: createDiagnosable(function (source, target, diagnosticTarget) {
202219
return this.program.checker.isTypeAssignableTo(source, target, diagnosticTarget ?? source);
203220
}),
221+
resolve: createDiagnosable(function (reference, kind) {
222+
const [value, diagnostics] = this.program.resolveTypeOrValueReference(reference);
223+
if (value && !isValue(value)) {
224+
return [undefined, diagnostics];
225+
}
226+
if (value && kind && value.valueKind !== kind) {
227+
throw new Error(`Value kind mismatch: expected ${kind}, got ${value.valueKind}`);
228+
}
229+
return [value, diagnostics];
230+
}),
204231
},
205232
});

packages/compiler/test/experimental/typekit/resolve.test.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { beforeEach, expect, it } from "vitest";
2-
import { IntrinsicType } from "../../../src/core/types.js";
32
import { $ } from "../../../src/experimental/typekit/index.js";
3+
import { isValue } from "../../../src/index.js";
44
import { createTestHost } from "../../../src/testing/test-host.js";
55
import { createTestWrapper } from "../../../src/testing/test-utils.js";
66
import { BasicTestRunner } from "../../../src/testing/types.js";
@@ -15,14 +15,25 @@ it("resolve resolves existing types", async () => {
1515
const tk = $(runner.program);
1616
const stringType = tk.resolve("TypeSpec.string");
1717
expect(stringType).toBeDefined();
18-
expect(stringType?.kind).toBe("Scalar");
19-
expect((stringType as IntrinsicType).name).toBe("string");
18+
expect(tk.builtin.string).toBe(stringType);
2019

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

25+
it("resolve resolves existing values", async () => {
26+
await runner.compile(`const stringValue = "test";`);
27+
const tk = $(runner.program);
28+
const stringValue = tk.resolve("stringValue");
29+
expect(stringValue).toBeDefined();
30+
expect(isValue(stringValue!)).toBe(true);
31+
32+
const [stringValueDiag, diagnostics] = tk.resolve.withDiagnostics("stringValue");
33+
expect(isValue(stringValueDiag!)).toBe(true);
34+
expect(diagnostics).toHaveLength(0);
35+
});
36+
2637
it("resolve returns undefined and diagnostics for invalid references", async () => {
2738
await runner.compile("");
2839
const unknownType = $(runner.program).resolve("UnknownModel");

packages/compiler/test/experimental/typekit/type.test.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, expect, it } from "vitest";
1+
import { assert, describe, expect, it } from "vitest";
22
import { Enum, Model, Namespace, Scalar, Union } from "../../../src/core/types.js";
33
import { $ } from "../../../src/experimental/typekit/index.js";
44
import { isTemplateInstance } from "../../../src/index.js";
@@ -539,3 +539,63 @@ describe("isAssignableTo", () => {
539539
expectDiagnosticEmpty(validTest[1]);
540540
});
541541
});
542+
543+
describe("resolve", () => {
544+
it("resolves to the value type", async () => {
545+
const {
546+
context: { program },
547+
} = await getTypes(
548+
`
549+
alias stringLiteral = "hello";
550+
alias aliasedLiteral = stringLiteral;
551+
enum Foo { one: 1, two: 2 }
552+
const aValue = "value";
553+
`,
554+
[],
555+
);
556+
557+
const tk = $(program);
558+
const stringLiteral = tk.type.resolve("stringLiteral");
559+
assert(tk.literal.isString(stringLiteral!));
560+
expect(stringLiteral.value).toBe("hello");
561+
562+
const aliasedLiteral = tk.type.resolve("aliasedLiteral", "String");
563+
expect(aliasedLiteral!.value).toBe("hello");
564+
565+
const enumMember = tk.type.resolve("Foo.one", "EnumMember");
566+
expect(tk.enumMember.is(enumMember!)).toBe(true);
567+
568+
// Not actually a type
569+
const confusedValue = tk.type.resolve("aValue");
570+
expect(confusedValue).toBeUndefined();
571+
});
572+
573+
it("throws an error for incorrect kind assertion", async () => {
574+
const {
575+
context: { program },
576+
} = await getTypes(
577+
`
578+
alias stringLiteral = "hello";
579+
`,
580+
[],
581+
);
582+
583+
const tk = $(program);
584+
expect(() => tk.type.resolve("stringLiteral", "Boolean")).toThrow(
585+
"Type kind mismatch: expected Boolean, got String",
586+
);
587+
});
588+
589+
it("returns undefined and diagnostics for invalid references", async () => {
590+
const {
591+
context: { program },
592+
} = await getTypes(``, []);
593+
594+
const tk = $(program);
595+
const [unknownType, diagnostics] = tk.type.resolve.withDiagnostics("unknownType");
596+
expect(unknownType).toBeUndefined();
597+
expectDiagnostics(diagnostics, {
598+
code: "invalid-ref",
599+
});
600+
});
601+
});

0 commit comments

Comments
 (0)