microsoft/typespec

Public

mirrored from https://github.com/microsoft/typespecAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/fix-missing-extension-methods

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

packages/asset-emitter/test/context.test.ts

553lines · modecode

1import type { Model, ModelProperty, Namespace, Program, Type } from "@typespec/compiler";
2import assert, { deepStrictEqual, ok, strictEqual } from "assert";
3import { describe, it } from "vitest";
4import {
5 type AssetEmitter,
6 CodeTypeEmitter,
7 type Context,
8 type EmitEntity,
9 type EmitterOutput,
10 TypeEmitter,
11 createAssetEmitter,
12} from "../src/index.js";
13import { emitTypeSpec, getHostForTypeSpecFile } from "./host.js";
14
15describe("emitter-framework: emitter context", () => {
16 describe("program context", () => {
17 it("should be initialized to empty state", async () => {
18 class Emitter extends CodeTypeEmitter {
19 modelDeclaration(model: Model, name: string): EmitterOutput<string> {
20 const context = this.emitter.getContext();
21 assert.deepStrictEqual(context, {});
22 return super.modelDeclaration(model, name);
23 }
24 }
25
26 await emitTypeSpec(Emitter, `model Foo { }`);
27 });
28
29 it("should set program state for the whole program", async () => {
30 class Emitter extends CodeTypeEmitter {
31 programContext(program: Program) {
32 return {
33 inProgram: true,
34 };
35 }
36 modelDeclaration(model: Model, name: string): EmitterOutput<string> {
37 const context = this.emitter.getContext();
38 assert.deepStrictEqual(context, { inProgram: true });
39 return super.modelDeclaration(model, name);
40 }
41 }
42
43 await emitTypeSpec(Emitter, `model Foo { }`);
44 });
45 });
46
47 describe("namespace context", () => {
48 it("should set context for everything inside the namespace", async () => {
49 class Emitter extends CodeTypeEmitter {
50 namespaceContext(namespace: Namespace): Context {
51 return { inNamespace: true };
52 }
53
54 namespace(namespace: Namespace): EmitterOutput<string> {
55 assert.deepStrictEqual(this.emitter.getContext(), {
56 inNamespace: true,
57 });
58
59 return super.namespace(namespace);
60 }
61 }
62
63 await emitTypeSpec(Emitter, `namespace Foo { }`);
64 });
65
66 it("should set context for everything inside the namespace, multiple namespaces", async () => {
67 class Emitter extends CodeTypeEmitter {
68 namespaceContext(namespace: Namespace): Context {
69 return { inNamespace: namespace.name };
70 }
71
72 namespace(namespace: Namespace): EmitterOutput<string> {
73 assert.deepStrictEqual(this.emitter.getContext(), {
74 inNamespace: namespace.name,
75 });
76
77 return super.namespace(namespace);
78 }
79 }
80
81 await emitTypeSpec(Emitter, `namespace Foo { } namespace Bar { }`, {
82 namespaceContext: 2,
83 namespace: 2,
84 });
85 });
86
87 it("should set context for everything inside the namespace, nested namespaces", async () => {
88 class Emitter extends CodeTypeEmitter {
89 namespaceContext(namespace: Namespace): Context {
90 const newState: Record<string, boolean> = {};
91 if (namespace.name === "Foo") {
92 newState.foo = true;
93 } else {
94 newState.bar = true;
95 }
96
97 return newState;
98 }
99
100 namespace(namespace: Namespace): EmitterOutput<string> {
101 const expectedContext: Record<string, boolean> = { foo: true };
102
103 if (namespace.name === "Bar") {
104 expectedContext.bar = true;
105 }
106
107 assert.deepStrictEqual(
108 this.emitter.getContext(),
109 expectedContext,
110 "context for namespace " + namespace.name,
111 );
112
113 return super.namespace(namespace);
114 }
115 }
116
117 await emitTypeSpec(Emitter, `namespace Foo { namespace Bar { } }`, {
118 namespaceContext: 2,
119 namespace: 2,
120 });
121 });
122 });
123
124 describe("model context", () => {
125 it("sets model context for models and properties", async () => {
126 class Emitter extends CodeTypeEmitter {
127 modelDeclarationContext(model: Model, name: string): Context {
128 return {
129 inModel: true,
130 };
131 }
132 modelDeclaration(model: Model, name: string): EmitterOutput<string> {
133 assert.deepStrictEqual(this.emitter.getContext(), {
134 inModel: true,
135 });
136 return super.modelDeclaration(model, name);
137 }
138
139 modelPropertyLiteral(property: ModelProperty): EmitterOutput<string> {
140 assert.deepStrictEqual(this.emitter.getContext(), {
141 inModel: true,
142 });
143 return super.modelPropertyLiteral(property);
144 }
145 }
146
147 await emitTypeSpec(
148 Emitter,
149 `model Foo {
150 prop: string;
151 }`,
152 );
153 });
154
155 it("sets model context for nested model literals", async () => {
156 class Emitter extends CodeTypeEmitter {
157 modelDeclarationContext(model: Model, name: string): Context {
158 return {
159 inModel: true,
160 };
161 }
162
163 modelLiteral(model: Model): EmitterOutput<string> {
164 assert.deepStrictEqual(this.emitter.getContext(), {
165 inModel: true,
166 });
167
168 return super.modelLiteral(model);
169 }
170 }
171
172 await emitTypeSpec(
173 Emitter,
174 `model Foo {
175 prop: {
176 nested: true
177 };
178 }`,
179 );
180 });
181 });
182
183 describe("references", () => {
184 it("namespace context is preserved for models in that namespace even with references", async () => {
185 class TestEmitter extends CodeTypeEmitter {
186 namespaceContext(namespace: Namespace): Context {
187 return {
188 inANamespace: namespace.name === "A",
189 };
190 }
191
192 modelDeclaration(model: Model, name: string): EmitterOutput<string> {
193 const context = this.emitter.getContext();
194 if (name === "Foo") {
195 assert(context.inANamespace);
196 } else {
197 assert(!context.inANamespace);
198 }
199
200 return super.modelDeclaration(model, name);
201 }
202 }
203
204 await emitTypeSpec(
205 TestEmitter,
206 `
207 model Bar { prop: A.Foo };
208 namespace A {
209 model Foo { prop: string };
210 }
211 `,
212 {
213 namespaceContext: 2,
214 modelDeclaration: 2,
215 },
216 );
217 });
218 });
219
220 describe("reference context", () => {
221 it("propagates reference context", async () => {
222 const seenContexts: Set<boolean> = new Set();
223 const propSeenContexts: Set<boolean> = new Set();
224
225 class TestEmitter extends CodeTypeEmitter {
226 namespaceReferenceContext(namespace: Namespace): Context {
227 if (namespace.name === "Foo") {
228 return { refFromNs: true };
229 }
230 return {};
231 }
232
233 modelDeclaration(model: Model, name: string): EmitterOutput<string> {
234 const context = this.emitter.getContext();
235 if (model.name === "N") {
236 seenContexts.add(context.refFromNs ?? false);
237 }
238 return super.modelDeclaration(model, name);
239 }
240
241 modelPropertyLiteral(property: ModelProperty): EmitterOutput<string> {
242 const context = this.emitter.getContext();
243 if (property.name === "test") {
244 propSeenContexts.add(context.refFromNs ?? false);
245 }
246 return super.modelPropertyLiteral(property);
247 }
248 }
249
250 await emitTypeSpec(
251 TestEmitter,
252 `
253 namespace Foo {
254 model M { x: Bar.N }
255 }
256 namespace Bar {
257 model N {
258 test: string;
259 }
260 }
261 `,
262 {
263 namespaceReferenceContext: 3,
264 modelDeclaration: 3,
265 modelPropertyLiteral: 3,
266 },
267 );
268
269 assert(seenContexts.has(true), "N has ref context");
270 assert(seenContexts.has(false), "N doesn't ref context also");
271 });
272
273 it("propagates reference context across multiple references", async () => {
274 let seenContext: Context;
275 class TestEmitter extends CodeTypeEmitter {
276 namespaceReferenceContext(namespace: Namespace): Context {
277 if (namespace.name === "Foo") {
278 return { refFromFoo: true };
279 } else if (namespace.name === "Bar") {
280 return { refFromBar: true };
281 }
282
283 return {};
284 }
285
286 modelPropertyLiteral(property: ModelProperty): EmitterOutput<string> {
287 const context = this.emitter.getContext();
288 if (property.name === "prop") {
289 seenContext = context;
290 }
291 return super.modelPropertyLiteral(property);
292 }
293 }
294 const code = `
295 namespace Foo {
296 model M { x: Bar.N }
297 }
298 namespace Bar {
299 model N {
300 test: Baz.O;
301 }
302 }
303 namespace Baz {
304 model O {
305 prop: string;
306 }
307 }
308 `;
309
310 const host = await getHostForTypeSpecFile(code);
311 const emitter = createAssetEmitter(host.program, TestEmitter, {
312 emitterOutputDir: "tsp-output",
313 options: {},
314 } as any);
315
316 await emitter.emitType(host.program.resolveTypeReference("Foo")[0]!);
317
318 assert.deepStrictEqual(seenContext!, { refFromFoo: true, refFromBar: true });
319 });
320
321 it("doesn't emit model multiple times when reference context is the same", async () => {
322 class TestEmitter extends CodeTypeEmitter {
323 modelDeclarationReferenceContext(model: Model): Context {
324 if (model.name === "Qux") {
325 return {};
326 }
327 return { ref: true };
328 }
329
330 modelDeclaration(model: Model, name: string): EmitterOutput<string> {
331 return super.modelDeclaration(model, name);
332 }
333 }
334
335 await emitTypeSpec(
336 TestEmitter,
337 `
338 model Foo { x: Qux }
339 model Bar { x: Qux }
340 model Qux { }
341 `,
342 {
343 modelDeclarationReferenceContext: 4,
344 modelDeclaration: 4,
345 },
346 );
347 });
348 });
349
350 describe("setting context via emitTypeReference", () => {
351 async function emitType(
352 Emitter: typeof TypeEmitter<any>,
353 code: string,
354 ref: string,
355 referenceContext?: Record<string, any>,
356 ): Promise<EmitEntity<any>> {
357 const host = await getHostForTypeSpecFile(code);
358 const emitter = createAssetEmitter(host.program, Emitter, {
359 emitterOutputDir: "tsp-output",
360 options: {},
361 } as any);
362 const type = host.program.resolveTypeReference(ref)[0]!;
363 ok(type, `Expected to have found reference ${ref}`);
364 return emitter.emitType(type, { referenceContext });
365 }
366
367 function objTypeReference(
368 emitter: AssetEmitter<any>,
369 target: Type,
370 contextValue: string | undefined,
371 ) {
372 return (
373 emitter.emitTypeReference(target, {
374 referenceContext: contextValue ? { contextValue } : {},
375 }) as any
376 ).value;
377 }
378
379 it("set reference context value when calling emitTypeReference", async () => {
380 class TestEmitter extends TypeEmitter<any, any> {
381 modelDeclaration(model: Model, name: string): EmitterOutput<any> {
382 if (model.name === "Foo") {
383 const prop = model.properties.get("prop")!.type;
384
385 return {
386 context1: objTypeReference(this.emitter, prop, "context1"),
387 context2: objTypeReference(this.emitter, prop, "context2"),
388 noSet: objTypeReference(this.emitter, prop, undefined),
389 };
390 }
391 return this.emitter.getContext().contextValue;
392 }
393 }
394
395 const result = await emitType(
396 TestEmitter,
397 `
398 model Foo { prop: Bar }
399 model Bar {}
400 `,
401 "Foo",
402 );
403 strictEqual(result.kind, "code");
404 deepStrictEqual(result.value, {
405 context1: "context1",
406 context2: "context2",
407 noSet: undefined,
408 });
409 });
410
411 it("set reference context on model properties ", async () => {
412 class TestEmitter extends TypeEmitter<any, any> {
413 modelDeclaration(model: Model, name: string): EmitterOutput<any> {
414 if (model.name === "Foo") {
415 const prop = model.properties.get("prop")!;
416
417 return {
418 context1: objTypeReference(this.emitter, prop, "context1"),
419 context2: objTypeReference(this.emitter, prop, "context2"),
420 noSet: objTypeReference(this.emitter, prop, undefined),
421 };
422 }
423 return this.emitter.getContext().contextValue;
424 }
425 }
426
427 const result = await emitType(
428 TestEmitter,
429 `
430 model Foo { prop: Bar }
431 model Bar {}
432 `,
433 "Foo",
434 );
435 strictEqual(result.kind, "code");
436 deepStrictEqual(result.value, {
437 context1: "context1",
438 context2: "context2",
439 noSet: undefined,
440 });
441 });
442
443 it("merge with incoming reference context", async () => {
444 class TestEmitter extends TypeEmitter<any, any> {
445 modelDeclaration(model: Model, name: string): EmitterOutput<any> {
446 if (model.name === "Foo") {
447 const prop = model.properties.get("prop")!.type;
448 return {
449 context1: objTypeReference(this.emitter, prop, "context1"),
450 };
451 }
452 return {
453 contextValue: this.emitter.getContext().contextValue,
454 incoming: this.emitter.getContext().incoming,
455 };
456 }
457 }
458
459 const result = await emitType(
460 TestEmitter,
461 `
462 model Foo { prop: Bar }
463 model Bar {}
464 `,
465 "Foo",
466 { incoming: "incoming-value" },
467 );
468 strictEqual(result.kind, "code");
469 deepStrictEqual(result.value, {
470 context1: {
471 contextValue: "context1",
472 incoming: "incoming-value",
473 },
474 });
475 });
476
477 it("ReferenceContext hook always wins", async () => {
478 class TestEmitter extends TypeEmitter<any, any> {
479 modelDeclarationReferenceContext(model: Model, name: string): Context {
480 return { contextValue: "context-override" };
481 }
482 modelDeclaration(model: Model, name: string): EmitterOutput<any> {
483 if (model.name === "Foo") {
484 const prop = model.properties.get("prop")!.type;
485 return {
486 context1: objTypeReference(this.emitter, prop, "context1"),
487 };
488 }
489 return this.emitter.getContext().contextValue;
490 }
491 }
492
493 const result = await emitType(
494 TestEmitter,
495 `
496 model Foo { prop: Bar }
497 model Bar {}
498 `,
499 "Foo",
500 );
501 strictEqual(result.kind, "code");
502 deepStrictEqual(result.value, {
503 context1: "context-override",
504 });
505 });
506 });
507
508 describe("instantiation context", () => {
509 it("restores context when after referencing a type with a circular reference", async () => {
510 class Emitter extends CodeTypeEmitter {
511 programContext(program: Program): Context {
512 return {
513 scope: this.emitter.createSourceFile("foo.txt").globalScope,
514 };
515 }
516 modelDeclarationContext(model: Model, name: string): Context {
517 return {
518 inModel: name,
519 };
520 }
521 modelDeclaration(model: Model, name: string): EmitterOutput<string> {
522 super.modelDeclaration(model, name);
523 return this.emitter.result.declaration(name, "Declaration for model " + name);
524 }
525 modelPropertyLiteralContext(property: ModelProperty): Context {
526 return { inProp: property.name };
527 }
528 modelPropertyLiteral(property: ModelProperty): EmitterOutput<string> {
529 const beforeContext = this.emitter.getContext();
530 const res = super.modelPropertyLiteral(property);
531 assert.deepStrictEqual(beforeContext, this.emitter.getContext());
532 return res;
533 }
534 }
535
536 await emitTypeSpec(
537 Emitter,
538 `
539 model A {
540 a: B;
541 }
542
543 model B {
544 b: B;
545 }
546
547 `,
548 {},
549 false,
550 );
551 });
552 });
553});
554