Skip to content

Commit 2e062d3

Browse files
committed
feat: enhance getReferenceSuggestions to support nested property extraction and improve type inference
1 parent dee2d33 commit 2e062d3

2 files changed

Lines changed: 126 additions & 35 deletions

File tree

src/suggestion/getReferenceSuggestions.ts

Lines changed: 121 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,84 @@ import {DataType, Flow, FunctionDefinition, NodeFunction, ReferenceValue} from "
33
import {createCompilerHost, generateFlowSourceCode} from "../utils";
44

55
/**
6-
* Calculates all available reference suggestions for a specific target node in a flow
7-
* and filters them by a required type.
6+
* Determines whether a type is a real object type (not a primitive type).
7+
* Excludes built-in primitive types like string, number, boolean, etc.
8+
* to ensure only actual object properties are extracted.
9+
*
10+
* @param type The TypeScript type to check
11+
* @returns true if the type is an object, false if it's a primitive
12+
*/
13+
const isRealObjectType = (type: ts.Type): boolean => {
14+
const primitiveFlags =
15+
ts.TypeFlags.String |
16+
ts.TypeFlags.Number |
17+
ts.TypeFlags.Boolean |
18+
ts.TypeFlags.Undefined |
19+
ts.TypeFlags.Null |
20+
ts.TypeFlags.BigInt |
21+
ts.TypeFlags.ESSymbol;
22+
23+
return (type.flags & primitiveFlags) === 0;
24+
};
25+
26+
/**
27+
* Recursively extracts all nested properties of an object type that are assignable
28+
* to the expected type, along with their access paths.
29+
*
30+
* For example, given an object type {user: {id: number, name: string}} and
31+
* an expected type of 'number', this function returns:
32+
* - { path: ['user', 'id'], type: number }
33+
*
34+
* @param type The type to extract properties from
35+
* @param checker The TypeScript type checker
36+
* @param expectedType The expected type to match against
37+
* @param currentPath The current property path (used for recursion)
38+
* @returns Array of matching properties with their paths
39+
*/
40+
const extractObjectProperties = (
41+
type: ts.Type,
42+
checker: ts.TypeChecker,
43+
expectedType: ts.Type,
44+
currentPath: string[] = []
45+
): Array<{ path: string[]; type: ts.Type }> => {
46+
const results: Array<{ path: string[]; type: ts.Type }> = [];
47+
48+
// Add the current type if it matches the expected type
49+
if (checker.isTypeAssignableTo(type, expectedType)) {
50+
results.push({ path: currentPath, type });
51+
}
52+
53+
// Only extract properties from real object types, not primitives
54+
if (isRealObjectType(type)) {
55+
const properties = type.getProperties();
56+
if (properties && properties.length > 0) {
57+
properties.forEach(property => {
58+
const propType = checker.getTypeOfSymbolAtLocation(property, property.valueDeclaration!);
59+
const propName = property.getName();
60+
const newPath = [...currentPath, propName];
61+
62+
// Recursively extract nested properties
63+
results.push(...extractObjectProperties(propType, checker, expectedType, newPath));
64+
});
65+
}
66+
}
67+
68+
return results;
69+
};
70+
71+
/**
72+
* Calculates all available reference suggestions for a specific target parameter in a flow.
73+
*
74+
* This function analyzes the flow's generated source code to find all variables
75+
* (node_ and p_ prefixed) that are in scope and compatible with the target parameter's
76+
* expected type. For object types, it also extracts nested properties and their access paths.
77+
*
78+
* @param flow The flow configuration
79+
* @param nodeId The ID of the node containing the target parameter
80+
* @param targetIndex The index of the target parameter
81+
* @param functions Available function definitions for type resolution
82+
* @param dataTypes Available data type definitions
83+
* @returns Array of ReferenceValue objects representing available suggestions
884
*/
985
export const getReferenceSuggestions = (
1086
flow?: Flow,
@@ -21,15 +97,15 @@ export const getReferenceSuggestions = (
2197
const program = host.languageService.getProgram()!;
2298
const checker = program.getTypeChecker();
2399

24-
// 2. Suche die exakte Text-Position des Kommentars
100+
// Find the exact position of the target node using a marker comment
25101
const fullText = sourceFile.getFullText();
26102
const commentPattern = `/* @pos ${nodeId} ${targetIndex} */`;
27103
const commentIndex = fullText.indexOf(commentPattern);
28-
29-
// Die Position des eigentlichen Nodes ist direkt nach dem Kommentar
30104
const targetPos = commentIndex + commentPattern.length;
31105

32-
// 3. Finde den kleinsten AST-Node an dieser Position
106+
/**
107+
* Recursively finds the smallest AST node at the given position
108+
*/
33109
function findNodeAtPosition(node: ts.Node, pos: number): ts.Node {
34110
let found = node;
35111
ts.forEachChild(node, child => {
@@ -43,65 +119,71 @@ export const getReferenceSuggestions = (
43119
let targetNode = findNodeAtPosition(sourceFile, targetPos);
44120
const targetExpression = targetNode as ts.Expression;
45121

46-
// 4. Umschließenden Funktionsaufruf finden (identisch zu deinem Code)
122+
// Find the enclosing function call
47123
let parentCall: ts.CallExpression | undefined;
48-
49124
if (ts.isCallExpression(targetExpression)) {
50125
parentCall = targetExpression;
51126
}
52127

53128
if (!parentCall) {
54-
return []
129+
return [];
55130
}
56131

57-
// 5. Typ-Check und Variablen-Extraktion
132+
// Get the signature and expected type of the target parameter
58133
const signature = checker.getResolvedSignature(parentCall);
59134
if (!signature) return [];
60135

61136
const params = signature.getParameters();
62137
const paramSymbol = params[targetIndex!] || params[params.length - 1];
63138
const expectedType = checker.getTypeOfSymbolAtLocation(paramSymbol, targetExpression);
64139

140+
// Collect all variables in scope (node_ and p_ prefixed)
65141
const allSymbols = checker.getSymbolsInScope(targetExpression, ts.SymbolFlags.Variable);
66-
67142
const referenceValues: ReferenceValue[] = [];
68143

69144
allSymbols.forEach(symbol => {
70145
const name = symbol.getName();
71146
if (!name.startsWith("node_") && !name.startsWith("p_")) return;
72147

73-
// 1. Erhalte die Deklaration der Variable
148+
// Get the variable declaration
74149
const declaration = symbol.valueDeclaration || symbol.declarations?.[0];
75150
if (!declaration) return;
76151

77-
// 2. Reachability-Check:
78-
// Die Deklaration muss VOR der targetPos enden.
79-
// (Damit schließen wir die aktuelle Zeile und alles danach aus)
152+
// Skip variables declared after the target position
80153
if (declaration.getEnd() >= targetPos) {
81154
return;
82155
}
83156

84157
const symbolType = checker.getTypeOfSymbolAtLocation(symbol, targetExpression!);
85158

86-
// FALL 1: Node-Ergebnis (node_)
159+
// Handle node_ variables (node function results)
87160
if (name.startsWith("node_")) {
88-
// Nur hinzufügen, wenn der Typ grundsätzlich auf den Slot passt
89-
if (!((symbolType.flags & ts.TypeFlags.Void) !== 0) && checker.isTypeAssignableTo(symbolType, expectedType)) {
161+
if (!((symbolType.flags & ts.TypeFlags.Void) !== 0)) {
90162
const nodeFunctionId = name
91163
.replace("node_", "")
92164
.replace(/___/g, "://")
93165
.replace(/__/g, "/")
94166
.replace(/_/g, "/");
95167

96-
referenceValues.push({
97-
__typename: 'ReferenceValue',
98-
nodeFunctionId: nodeFunctionId as any
99-
// Bei node_ keine Indizes laut Vorgabe
168+
// Extract all compatible properties including nested ones
169+
const propertyPaths = extractObjectProperties(symbolType, checker, expectedType);
170+
171+
propertyPaths.forEach(({ path }) => {
172+
const referenceValue: ReferenceValue = {
173+
__typename: 'ReferenceValue',
174+
nodeFunctionId: nodeFunctionId as any
175+
};
176+
177+
if (path.length > 0) {
178+
//@ts-ignore
179+
referenceValue.referencePath = path;
180+
}
181+
182+
referenceValues.push(referenceValue);
100183
});
101184
}
102185
}
103-
104-
// FALL 2: Parameter / Input (p_)
186+
// Handle p_ variables (parameter/input values)
105187
else if (name.startsWith("p_")) {
106188
const idPart = name.replace("p_", "");
107189
const lastUnderscoreIndex = idPart.lastIndexOf("_");
@@ -114,23 +196,32 @@ export const getReferenceSuggestions = (
114196
.replace(/__/g, "/")
115197
.replace(/_/g, "/");
116198

117-
// Da p_ oft ein Rest-Parameter (...p) ist, prüfen wir auf Array/Tupel
199+
// Handle tuple types (e.g., destructured parameters like [item, index])
118200
if (checker.isTupleType(symbolType)) {
119-
// Bei einem Tupel (z.B. [item, index]) extrahieren wir die Element-Typen
120201
const typeReference = symbolType as ts.TypeReference;
121202
const typeArguments = checker.getTypeArguments(typeReference);
122203

123204
typeArguments.forEach((tupleElementType, tupleIndex) => {
124-
if (checker.isTypeAssignableTo(tupleElementType, expectedType)) {
125-
referenceValues.push({
205+
// Extract all compatible properties for this tuple element
206+
const propertyPaths = extractObjectProperties(tupleElementType, checker, expectedType);
207+
208+
propertyPaths.forEach(({ path }) => {
209+
const referenceValue: ReferenceValue = {
126210
__typename: 'ReferenceValue',
127211
nodeFunctionId: nodeFunctionId as any,
128212
parameterIndex: isNaN(paramIndexFromName) ? 0 : paramIndexFromName,
129213
inputIndex: tupleIndex,
130214
//@ts-ignore
131215
inputTypeIdentifier: (typeReference.target as any).labeledElementDeclarations?.[tupleIndex].name.getText()
132-
});
133-
}
216+
};
217+
218+
if (path.length > 0) {
219+
//@ts-ignore
220+
referenceValue.referencePath = path;
221+
}
222+
223+
referenceValues.push(referenceValue);
224+
});
134225
});
135226
}
136227
}

test/getReferenceSuggestions.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,9 @@ describe('getReferenceSuggestions', () => {
3636
"value": {
3737
"__typename": "LiteralValue",
3838
"value": [
39-
1,
40-
2,
41-
3,
42-
4,
43-
5
39+
{
40+
"test": "test"
41+
}
4442
]
4543
}
4644
},
@@ -128,6 +126,8 @@ describe('getReferenceSuggestions', () => {
128126

129127
const suggestions = getReferenceSuggestions(flow, "gid://sagittarius/NodeFunction/4", 0, FUNCTION_SIGNATURES, DATA_TYPES);
130128

129+
console.log(suggestions)
130+
131131
//expect(suggestions.some(s => !s.nodeFunctionId)).toBe(true);
132132
});
133133

0 commit comments

Comments
 (0)