Skip to content

Commit fa20db1

Browse files
authored
Add type-usage and reachability analysis module (#71)
* Add type-usage reachability analysis module resolveTypeUsage walks all operations in a namespace tree and tracks which types are reachable from operation parameters (Input) vs return types (Output). Handles circular references, array elements, base models, union variants, enums, and scalars. When omitUnreachableTypes is false, all declared types are marked reachable without adding spurious Input/Output flags. Includes 13 unit tests covering all reachability scenarios.
1 parent 96267de commit fa20db1

3 files changed

Lines changed: 417 additions & 26 deletions

File tree

packages/graphql/src/type-usage.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import {
2+
isArrayModelType,
3+
navigateTypesInNamespace,
4+
type Namespace,
5+
type Operation,
6+
type Type,
7+
} from "@typespec/compiler";
8+
9+
/**
10+
* GraphQL-specific flags for type usage tracking (input vs output).
11+
*/
12+
export enum GraphQLTypeUsage {
13+
/** Type is used as an input (operation parameter or nested within one) */
14+
Input = "Input",
15+
/** Type is used as an output (operation return type or nested within one) */
16+
Output = "Output",
17+
}
18+
19+
export interface TypeUsageResolver {
20+
/** Get the set of usage flags for a type, or undefined if never referenced by an operation */
21+
getUsage(type: Type): Set<GraphQLTypeUsage> | undefined;
22+
/** Returns true if the type should not be included in the schema */
23+
isUnreachable(type: Type): boolean;
24+
}
25+
26+
/**
27+
* Walk all operations in a namespace tree to determine type reachability and
28+
* input/output classification.
29+
*
30+
* Produces two independent results:
31+
* - **Reachability**: whether a type should be included in the emitted schema.
32+
* - **Usage**: whether a type is used as Input, Output, or both.
33+
*
34+
* When `omitUnreachableTypes` is false, all types declared in the namespace
35+
* are considered reachable regardless of whether an operation references them.
36+
*/
37+
export function resolveTypeUsage(
38+
root: Namespace,
39+
omitUnreachableTypes: boolean,
40+
): TypeUsageResolver {
41+
// Two independent concerns tracked in a single walk:
42+
// reachableTypes — should this type appear in the schema?
43+
// usages — is this type used as Input, Output, or both?
44+
const reachableTypes = new Set<Type>();
45+
const usages = new Map<Type, Set<GraphQLTypeUsage>>();
46+
47+
addUsagesInNamespace(root, reachableTypes, usages);
48+
49+
// When all declared types should be emitted, mark them reachable.
50+
if (!omitUnreachableTypes) {
51+
const markReachable = (type: Type) => {
52+
reachableTypes.add(type);
53+
};
54+
navigateTypesInNamespace(root, {
55+
model: markReachable,
56+
scalar: markReachable,
57+
enum: markReachable,
58+
union: markReachable,
59+
});
60+
}
61+
62+
return {
63+
getUsage: (type: Type) => usages.get(type),
64+
isUnreachable: (type: Type) => !reachableTypes.has(type),
65+
};
66+
}
67+
68+
function trackUsage(
69+
reachableTypes: Set<Type>,
70+
usages: Map<Type, Set<GraphQLTypeUsage>>,
71+
type: Type,
72+
usage: GraphQLTypeUsage,
73+
) {
74+
reachableTypes.add(type);
75+
const existing = usages.get(type) ?? new Set();
76+
existing.add(usage);
77+
usages.set(type, existing);
78+
}
79+
80+
/**
81+
* Recursively walk a namespace and all sub-namespaces, tracking type usage
82+
* from operations.
83+
*/
84+
function addUsagesInNamespace(
85+
namespace: Namespace,
86+
reachableTypes: Set<Type>,
87+
usages: Map<Type, Set<GraphQLTypeUsage>>,
88+
): void {
89+
for (const subNamespace of namespace.namespaces.values()) {
90+
addUsagesInNamespace(subNamespace, reachableTypes, usages);
91+
}
92+
for (const iface of namespace.interfaces.values()) {
93+
for (const operation of iface.operations.values()) {
94+
addUsagesFromOperation(operation, reachableTypes, usages);
95+
}
96+
}
97+
for (const operation of namespace.operations.values()) {
98+
addUsagesFromOperation(operation, reachableTypes, usages);
99+
}
100+
}
101+
102+
/**
103+
* For a single operation, mark parameter types as Input and return type as Output.
104+
*/
105+
function addUsagesFromOperation(
106+
operation: Operation,
107+
reachableTypes: Set<Type>,
108+
usages: Map<Type, Set<GraphQLTypeUsage>>,
109+
): void {
110+
for (const param of operation.parameters.properties.values()) {
111+
navigateReferencedTypes(param.type, GraphQLTypeUsage.Input, reachableTypes, usages);
112+
}
113+
navigateReferencedTypes(operation.returnType, GraphQLTypeUsage.Output, reachableTypes, usages);
114+
}
115+
116+
/**
117+
* Recursively walk a type graph, tracking reachability and usage classification.
118+
* Handles circular references via a visited set.
119+
*/
120+
function navigateReferencedTypes(
121+
type: Type,
122+
usage: GraphQLTypeUsage,
123+
reachableTypes: Set<Type>,
124+
usages: Map<Type, Set<GraphQLTypeUsage>>,
125+
visited: Set<Type> = new Set(),
126+
): void {
127+
if (visited.has(type)) return;
128+
visited.add(type);
129+
130+
switch (type.kind) {
131+
case "Model":
132+
if (isArrayModelType(type)) {
133+
if (type.indexer?.value) {
134+
navigateReferencedTypes(type.indexer.value, usage, reachableTypes, usages, visited);
135+
}
136+
} else {
137+
// Note: Record<K, V> models land here but their indexer value type
138+
// is not navigated. That's intentional — we don't support Record
139+
// types in GraphQL.
140+
trackUsage(reachableTypes, usages, type, usage);
141+
for (const prop of type.properties.values()) {
142+
navigateReferencedTypes(prop.type, usage, reachableTypes, usages, visited);
143+
}
144+
if (type.baseModel) {
145+
navigateReferencedTypes(type.baseModel, usage, reachableTypes, usages, visited);
146+
}
147+
}
148+
break;
149+
150+
case "Union":
151+
trackUsage(reachableTypes, usages, type, usage);
152+
for (const variant of type.variants.values()) {
153+
navigateReferencedTypes(variant.type, usage, reachableTypes, usages, visited);
154+
}
155+
break;
156+
157+
case "Scalar":
158+
case "Enum":
159+
trackUsage(reachableTypes, usages, type, usage);
160+
break;
161+
162+
default:
163+
break;
164+
}
165+
}

packages/graphql/src/visibility-usage.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

0 commit comments

Comments
 (0)