@@ -3,8 +3,84 @@ import {DataType, Flow, FunctionDefinition, NodeFunction, ReferenceValue} from "
33import { 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 */
985export 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 }
0 commit comments