Skip to content

Commit ba9b50c

Browse files
authored
[compiler] Add typekit support to check if a type is in a namespace (microsoft#7090)
This pull request introduces a new feature to the `@typespec/compiler` package, adding support for checking namespace membership using a new `inNamespace` method. The changes include updates to the core type system, implementation of the feature, and extensive test coverage to ensure correctness. Added a new `inNamespace` method to the `Type` TypeKit to determine if a type belongs to a specific namespace. This method handles various type scenarios, including models, enums, unions, and operations. (`packages/compiler/src/experimental/typekit/kits/type.ts`, [[1]](diffhunk://#diff-aaa7a0cba000f6acff7c2734746e5f87fcfd7676ee6ff0b49bbe11385194f854R124-R125) [[2]](diffhunk://#diff-aaa7a0cba000f6acff7c2734746e5f87fcfd7676ee6ff0b49bbe11385194f854R277-R313) Resolves microsoft#7036
1 parent f7f7004 commit ba9b50c

File tree

3 files changed

+242
-2
lines changed

3 files changed

+242
-2
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 TypeKit support for type.inNamespace to check namespace membership

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

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
getMinValueExclusive,
1515
} from "../../../core/intrinsic-type-state.js";
1616
import { isNeverType } from "../../../core/type-utils.js";
17-
import { Enum, Model, Scalar, Union, type Type } from "../../../core/types.js";
17+
import { Enum, Model, Namespace, Scalar, Union, type Type } from "../../../core/types.js";
1818
import { getDoc, getSummary, isErrorModel } from "../../../lib/decorators.js";
1919
import { resolveEncodedName } from "../../../lib/encoded-names.js";
2020
import { defineKit } from "../define-kit.js";
@@ -121,6 +121,15 @@ export interface TypeTypekit {
121121
* @returns True if the type is a user defined type, false otherwise.
122122
*/
123123
isUserDefined(type: Type): boolean;
124+
125+
/**
126+
* Checks if the given type is in the given namespace (directly or indirectly) by walking up the type's namespace chain.
127+
*
128+
* @param type The type to check.
129+
* @param namespace The namespace to check membership against.
130+
* @returns True if the type is in the namespace, false otherwise.
131+
*/
132+
inNamespace(type: Type, namespace: Namespace): boolean;
124133
}
125134

126135
interface TypekitExtension {
@@ -272,5 +281,37 @@ defineKit<TypekitExtension>({
272281
isUserDefined(type) {
273282
return getLocationContext(this.program, type).type === "project";
274283
},
284+
inNamespace(type: Type, namespace: Namespace): boolean {
285+
// A namespace is always in itself
286+
if (type === namespace) {
287+
return true;
288+
}
289+
290+
// Handle types with known containers
291+
switch (type.kind) {
292+
case "ModelProperty":
293+
if (type.model) {
294+
return this.type.inNamespace(type.model, namespace);
295+
}
296+
break;
297+
case "EnumMember":
298+
return this.type.inNamespace(type.enum, namespace);
299+
case "UnionVariant":
300+
return this.type.inNamespace(type.union, namespace);
301+
case "Operation":
302+
if (type.interface) {
303+
return this.type.inNamespace(type.interface, namespace);
304+
}
305+
// Operations that belong to a namespace directly will be handled in the generic case
306+
break;
307+
}
308+
309+
// Generic case handles all other types
310+
if ("namespace" in type && type.namespace) {
311+
return this.type.inNamespace(type.namespace, namespace);
312+
}
313+
// If we got this far, the type does not belong to the namespace
314+
return false;
315+
},
275316
},
276317
});

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

Lines changed: 193 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from "vitest";
2-
import { Enum, Model, Scalar, Union } from "../../../src/core/types.js";
2+
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";
55
import { getTypes } from "./utils.js";
@@ -281,3 +281,195 @@ it("isError can check if a type is an error model", async () => {
281281
expect($(program).type.isError(Error)).toBe(true);
282282
expect($(program).type.isError(Foo)).toBe(false);
283283
});
284+
285+
describe("inNamespace", () => {
286+
it("checks that a namespace belongs to itself", async () => {
287+
const {
288+
Root,
289+
context: { program },
290+
} = await getTypes(
291+
`
292+
namespace Root {}
293+
`,
294+
["Root"],
295+
);
296+
expect($(program).type.inNamespace(Root, Root as Namespace)).toBe(true);
297+
});
298+
299+
it("checks direct namespace membership", async () => {
300+
const {
301+
Root,
302+
context: { program },
303+
} = await getTypes(
304+
`
305+
namespace Root {
306+
namespace Child1 {
307+
namespace Child2 {}
308+
}
309+
}
310+
`,
311+
["Root"],
312+
);
313+
314+
const child1 = (Root as Namespace).namespaces.get("Child1");
315+
expect(child1).toBeDefined();
316+
const child2 = child1?.namespaces.get("Child2");
317+
expect(child2).toBeDefined();
318+
319+
expect($(program).type.inNamespace(child1!, Root as Namespace)).toBe(true);
320+
expect($(program).type.inNamespace(child2!, Root as Namespace)).toBe(true);
321+
});
322+
323+
it("checks model property namespace membership", async () => {
324+
const {
325+
Root,
326+
Outside,
327+
context: { program },
328+
} = await getTypes(
329+
`
330+
namespace Root {
331+
model Inside {
332+
prop: string;
333+
}
334+
}
335+
336+
model Outside {
337+
prop: string;
338+
}
339+
`,
340+
["Root", "Inside", "Outside"],
341+
);
342+
343+
const model1 = (Root as Namespace).models.get("Inside");
344+
expect(model1).toBeDefined();
345+
const prop1 = model1?.properties.get("prop");
346+
expect(prop1).toBeDefined();
347+
348+
const prop2 = (Outside as Model).properties.get("prop");
349+
expect(prop2).toBeDefined();
350+
351+
expect($(program).type.inNamespace(prop1!, Root as Namespace)).toBe(true);
352+
expect($(program).type.inNamespace(prop2!, Root as Namespace)).toBe(false);
353+
});
354+
355+
it("checks enum member namespace membership", async () => {
356+
const {
357+
Root,
358+
context: { program },
359+
} = await getTypes(
360+
`
361+
namespace Root {
362+
enum Test {
363+
A,
364+
B
365+
}
366+
}
367+
`,
368+
["Root"],
369+
);
370+
371+
const enum1 = (Root as Namespace).enums.get("Test");
372+
const enumMember = enum1?.members.get("A");
373+
expect(enumMember).toBeDefined();
374+
375+
expect($(program).type.inNamespace(enumMember!, Root as Namespace)).toBe(true);
376+
});
377+
378+
it("checks union variant namespace membership", async () => {
379+
const {
380+
Root,
381+
context: { program },
382+
} = await getTypes(
383+
`
384+
namespace Root {
385+
union Test {
386+
A: string,
387+
B: int32
388+
}
389+
}
390+
`,
391+
["Root"],
392+
);
393+
394+
const union = (Root as Namespace).unions.get("Test");
395+
const variant = union?.variants.get("A");
396+
expect(variant).toBeDefined();
397+
398+
expect($(program).type.inNamespace(variant!, Root as Namespace)).toBe(true);
399+
});
400+
401+
it("checks interface operation namespace membership", async () => {
402+
const {
403+
Root,
404+
context: { program },
405+
} = await getTypes(
406+
`
407+
namespace Root {
408+
interface Test {
409+
op myOp(): void;
410+
}
411+
}
412+
`,
413+
["Root"],
414+
);
415+
416+
const test = (Root as Namespace).interfaces.get("Test");
417+
const operation = test?.operations.get("myOp");
418+
expect(operation).toBeDefined();
419+
420+
expect($(program).type.inNamespace(operation!, Root as Namespace)).toBe(true);
421+
});
422+
423+
it("checks operations namespace membership", async () => {
424+
const {
425+
Root,
426+
context: { program },
427+
} = await getTypes(
428+
`
429+
namespace Root {
430+
op myOp(): void;
431+
}
432+
`,
433+
["Root"],
434+
);
435+
const operation = (Root as Namespace).operations.get("myOp");
436+
expect(operation).toBeDefined();
437+
438+
expect($(program).type.inNamespace(operation!, Root as Namespace)).toBe(true);
439+
});
440+
441+
it("returns false for types outside the namespace", async () => {
442+
const {
443+
Root,
444+
Outside,
445+
context: { program },
446+
} = await getTypes(
447+
`
448+
namespace Root {
449+
namespace Child1 {}
450+
}
451+
namespace Outside {}
452+
`,
453+
["Root", "Outside"],
454+
);
455+
const child1 = (Root as Namespace).namespaces.get("Child1");
456+
expect(child1).toBeDefined();
457+
458+
expect($(program).type.inNamespace(child1!, Outside as Namespace)).toBe(false);
459+
});
460+
461+
it("returns false for types without namespace", async () => {
462+
const {
463+
MyNamespace,
464+
context: { program },
465+
} = await getTypes(
466+
`
467+
namespace MyNamespace { }
468+
`,
469+
["MyNamespace"],
470+
);
471+
472+
const stringLiteral = $(program).literal.create("test");
473+
expect($(program).type.inNamespace(stringLiteral, MyNamespace as Namespace)).toBe(false);
474+
});
475+
});

0 commit comments

Comments
 (0)