Skip to content

Commit c769394

Browse files
committed
feat: add getReferences utility for extracting reference values from TypeScript symbols
1 parent 6f7adda commit c769394

1 file changed

Lines changed: 308 additions & 0 deletions

File tree

src/util/references.util.ts

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
import ts from "typescript";
2+
import {ReferencePath, ReferenceValue} from "@code0-tech/sagittarius-graphql-types";
3+
4+
/**
5+
* Extracts reference values from a collection of TypeScript symbols.
6+
*
7+
* This function processes symbols with specific prefixes (node_, p_, flow_) and creates
8+
* corresponding ReferenceValue objects. It filters out invalid symbols and symbols that
9+
* are declared after the current node position.
10+
*
11+
* @param checker - TypeScript TypeChecker instance for type information resolution
12+
* @param node - The VariableDeclaration context in which symbols are analyzed
13+
* @param paramType - The expected parameter type for property matching during extraction
14+
* @param symbols - The array of symbols to process and analyze
15+
* @returns An array of ReferenceValue objects representing extracted references with their paths
16+
*
17+
* @example
18+
* // Extracts all references from symbols prefixed with node_, p_, or flow_
19+
* const references = getReferences(checker, variableDecl, expectedType, [symbol1, symbol2]);
20+
*/
21+
export const getReferences = (
22+
checker: ts.TypeChecker,
23+
node: ts.VariableDeclaration,
24+
paramType: ts.Type,
25+
symbols: ts.Symbol[]
26+
): ReferenceValue[] => {
27+
return symbols.flatMap((symbol) => {
28+
const name = symbol.getName();
29+
30+
// Filter symbols by required prefix
31+
if (!isValidSymbolPrefix(name)) {
32+
return [];
33+
}
34+
35+
// Validate symbol declaration exists and is declared before current node
36+
const symbolDeclaration = symbol.getDeclarations()?.[0];
37+
if (!symbolDeclaration || symbolDeclaration.getEnd() >= node.getEnd()!) {
38+
return [];
39+
}
40+
41+
const symbolType = checker.getTypeOfSymbolAtLocation(symbol, node);
42+
43+
// Process symbol based on its prefix
44+
if (name.startsWith("node_")) {
45+
return processNodeSymbol(name, symbolType, checker, paramType);
46+
} else if (name.startsWith("p_")) {
47+
return processParameterSymbol(name, symbolType, checker, paramType);
48+
} else if (name.startsWith("flow_")) {
49+
return processFlowSymbol(symbolType, checker, paramType);
50+
}
51+
52+
return [];
53+
});
54+
};
55+
56+
/**
57+
* Checks if a symbol name has a valid prefix for reference extraction.
58+
*
59+
* Valid prefixes are: node_, p_, flow_
60+
*
61+
* @param name - The symbol name to validate
62+
* @returns True if the name starts with a valid prefix, false otherwise
63+
*/
64+
const isValidSymbolPrefix = (name: string): boolean => {
65+
return name.startsWith("node_") || name.startsWith("p_") || name.startsWith("flow_");
66+
};
67+
68+
/**
69+
* Processes a node symbol and creates corresponding reference values.
70+
*
71+
* Node symbols represent references to node functions. The function extracts the node ID
72+
* from the symbol name by reversing the name encoding, then extracts all matching object
73+
* properties from the symbol type.
74+
*
75+
* @param name - The symbol name starting with "node_"
76+
* @param symbolType - The TypeScript type of the symbol
77+
* @param checker - TypeScript TypeChecker for type operations
78+
* @param paramType - The expected parameter type for property matching
79+
* @returns Array of ReferenceValue objects for this node symbol
80+
*/
81+
const processNodeSymbol = (
82+
name: string,
83+
symbolType: ts.Type,
84+
checker: ts.TypeChecker,
85+
paramType: ts.Type
86+
): ReferenceValue[] => {
87+
// Skip void types
88+
if ((symbolType.flags & ts.TypeFlags.Void) !== 0) {
89+
return [];
90+
}
91+
92+
// Decode the node function ID from the symbol name
93+
const nodeFunctionId = decodeIdentifier(name.replace("node_", ""));
94+
95+
// Extract all properties that match the expected parameter type
96+
const propertyPaths = extractObjectProperties(symbolType, checker, paramType);
97+
98+
return propertyPaths.flatMap(({ path }) => {
99+
const referenceValue: ReferenceValue = {
100+
__typename: "ReferenceValue",
101+
nodeFunctionId: nodeFunctionId as any,
102+
};
103+
104+
if (path.length > 0) {
105+
referenceValue.referencePath = path;
106+
}
107+
108+
return referenceValue;
109+
});
110+
};
111+
112+
/**
113+
* Processes a parameter symbol and creates corresponding reference values.
114+
*
115+
* Parameter symbols represent references to function parameters. They contain encoded
116+
* information about the node function ID, parameter index, and input index. If the symbol
117+
* type is a tuple, each tuple element is processed separately.
118+
*
119+
* @param name - The symbol name starting with "p_"
120+
* @param symbolType - The TypeScript type of the symbol
121+
* @param checker - TypeScript TypeChecker for type operations
122+
* @param paramType - The expected parameter type for property matching
123+
* @returns Array of ReferenceValue objects for this parameter symbol
124+
*/
125+
const processParameterSymbol = (
126+
name: string,
127+
symbolType: ts.Type,
128+
checker: ts.TypeChecker,
129+
paramType: ts.Type
130+
): ReferenceValue[] => {
131+
// Decode parameter information from symbol name
132+
const { nodeFunctionId, paramIndexFromName } = decodeParameterName(name);
133+
134+
// If the symbol type is not a tuple, return empty array
135+
if (!checker.isTupleType(symbolType)) {
136+
return [];
137+
}
138+
139+
const typeReference = symbolType as ts.TypeReference;
140+
const typeArguments = checker.getTypeArguments(typeReference);
141+
142+
return typeArguments.flatMap((tupleElementType, tupleIndex) => {
143+
const propertyPaths = extractObjectProperties(tupleElementType, checker, paramType);
144+
145+
return propertyPaths.flatMap(({ path }) => {
146+
const referenceValue: ReferenceValue = {
147+
__typename: "ReferenceValue",
148+
nodeFunctionId: nodeFunctionId as any,
149+
parameterIndex: isNaN(paramIndexFromName) ? 0 : paramIndexFromName,
150+
inputIndex: tupleIndex,
151+
inputTypeIdentifier: (typeReference.target as any).labeledElementDeclarations?.[
152+
tupleIndex
153+
].name.getText(),
154+
};
155+
156+
if (path.length > 0) {
157+
referenceValue.referencePath = path;
158+
}
159+
160+
return referenceValue;
161+
});
162+
});
163+
};
164+
165+
/**
166+
* Processes a flow symbol and creates corresponding reference values.
167+
*
168+
* Flow symbols represent references within data flows. They do not have an associated
169+
* node function ID (it is null) and extract properties that match the expected type.
170+
*
171+
* @param symbolType - The TypeScript type of the symbol
172+
* @param checker - TypeScript TypeChecker for type operations
173+
* @param paramType - The expected parameter type for property matching
174+
* @returns Array of ReferenceValue objects for this flow symbol
175+
*/
176+
const processFlowSymbol = (
177+
symbolType: ts.Type,
178+
checker: ts.TypeChecker,
179+
paramType: ts.Type
180+
): ReferenceValue[] => {
181+
const propertyPaths = extractObjectProperties(symbolType, checker, paramType);
182+
183+
return propertyPaths.flatMap(({ path }) => {
184+
const referenceValue: ReferenceValue = {
185+
__typename: "ReferenceValue",
186+
nodeFunctionId: null,
187+
};
188+
189+
if (path.length > 0) {
190+
referenceValue.referencePath = path;
191+
}
192+
193+
return referenceValue;
194+
});
195+
};
196+
197+
/**
198+
* Decodes an encoded identifier string back to its original form.
199+
*
200+
* The encoding scheme is:
201+
* - ___ → ://
202+
* - __ → /
203+
* - _ → /
204+
*
205+
* @param encoded - The encoded identifier string
206+
* @returns The decoded identifier
207+
*/
208+
const decodeIdentifier = (encoded: string): string => {
209+
return encoded.replace(/___/g, "://").replace(/__/g, "/").replace(/_/g, "/");
210+
};
211+
212+
/**
213+
* Decodes parameter name to extract node function ID and parameter index.
214+
*
215+
* The parameter name format is: p_<rawId>_<paramIndex>
216+
* The rawId is then decoded using decodeIdentifier.
217+
*
218+
* @param name - The parameter symbol name
219+
* @returns Object containing decoded nodeFunctionId and paramIndexFromName
220+
*/
221+
const decodeParameterName = (name: string): { nodeFunctionId: string; paramIndexFromName: number } => {
222+
const idPart = name.replace("p_", "");
223+
const lastUnderscoreIndex = idPart.lastIndexOf("_");
224+
const rawId = idPart.substring(0, lastUnderscoreIndex);
225+
const paramIndexFromName = parseInt(idPart.substring(lastUnderscoreIndex + 1), 10);
226+
227+
const nodeFunctionId = decodeIdentifier(rawId);
228+
229+
return { nodeFunctionId, paramIndexFromName };
230+
};
231+
232+
/**
233+
* Recursively extracts object properties from a type that match an expected type.
234+
*
235+
* This function performs a depth-first traversal of an object's property tree. It collects
236+
* all paths (property chains) where the type at that path is assignable to the expected type.
237+
* For example, if type is {a: {b: string}} and expectedType is string, it returns the path [a, b].
238+
*
239+
* The recursion continues into nested object properties, building up the path as it traverses.
240+
* Primitive types (string, number, boolean, etc.) are treated as leaf nodes.
241+
*
242+
* @param type - The type to extract properties from
243+
* @param checker - TypeScript TypeChecker for type comparison operations
244+
* @param expectedType - The target type to match when extracting properties
245+
* @param currentPath - The current property path being built during recursion (default: empty array)
246+
* @returns An array of objects containing the property path and the type at that path
247+
*
248+
* @example
249+
* // For type {user: {name: string, age: number}} with expectedType = string
250+
* // Returns: [{path: [{path: 'user'}, {path: 'name'}], type: stringType}]
251+
*/
252+
const extractObjectProperties = (
253+
type: ts.Type,
254+
checker: ts.TypeChecker,
255+
expectedType: ts.Type,
256+
currentPath: ReferencePath[] = []
257+
): Array<{ path: ReferencePath[]; type: ts.Type }> => {
258+
const results: Array<{ path: ReferencePath[]; type: ts.Type }> = [];
259+
260+
// Check if the current type matches the expected type
261+
if (checker.isTypeAssignableTo(type, expectedType)) {
262+
results.push({ path: currentPath, type });
263+
}
264+
265+
// Recursively traverse into object properties
266+
if (isRealObjectType(type)) {
267+
const properties = type.getProperties();
268+
if (properties && properties.length > 0) {
269+
properties.forEach((property) => {
270+
const propType = checker.getTypeOfSymbolAtLocation(property, property.valueDeclaration!);
271+
const propName = property.getName();
272+
const newPath = [...currentPath, { path: propName }];
273+
274+
// Recurse into nested properties
275+
results.push(...extractObjectProperties(propType, checker, expectedType, newPath));
276+
});
277+
}
278+
}
279+
280+
return results;
281+
};
282+
283+
/**
284+
* Determines whether a type is a real object type or a primitive.
285+
*
286+
* This function checks if a type is NOT one of the primitive types (string, number, boolean,
287+
* undefined, null, bigint, symbol). Any type that is not a primitive is considered a real object type.
288+
*
289+
* @param type - The type to check
290+
* @returns True if the type is a real object type, false if it's a primitive type
291+
*
292+
* @example
293+
* isRealObjectType(stringType) // false
294+
* isRealObjectType(objectType) // true
295+
* isRealObjectType(numberType) // false
296+
*/
297+
const isRealObjectType = (type: ts.Type): boolean => {
298+
const primitiveFlags =
299+
ts.TypeFlags.String |
300+
ts.TypeFlags.Number |
301+
ts.TypeFlags.Boolean |
302+
ts.TypeFlags.Undefined |
303+
ts.TypeFlags.Null |
304+
ts.TypeFlags.BigInt |
305+
ts.TypeFlags.ESSymbol;
306+
307+
return (type.flags & primitiveFlags) === 0;
308+
};

0 commit comments

Comments
 (0)