|
1 | 1 | import ts from "typescript"; |
2 | | -import { |
3 | | - DataType, |
4 | | - Flow, |
5 | | - FunctionDefinition, |
6 | | - NodeFunction, |
7 | | - NodeFunctionIdWrapper, |
8 | | - ReferenceValue |
9 | | -} from "@code0-tech/sagittarius-graphql-types"; |
10 | | -import {createCompilerHost, getSharedTypeDeclarations, getInferredTypesFromFlow, sanitizeId} from "../utils"; |
| 2 | +import {DataType, Flow, FunctionDefinition, NodeFunction, ReferenceValue} from "@code0-tech/sagittarius-graphql-types"; |
| 3 | +import {createCompilerHost, generateFlowSourceCode} from "../utils"; |
| 4 | + |
11 | 5 | /** |
12 | 6 | * Calculates all available reference suggestions for a specific target node in a flow |
13 | 7 | * and filters them by a required type. |
14 | 8 | */ |
15 | 9 | export const getReferenceSuggestions = ( |
16 | 10 | flow?: Flow, |
17 | 11 | nodeId?: NodeFunction['id'], |
18 | | - type: string = "any", |
| 12 | + targetIndex?: number, |
19 | 13 | functions?: FunctionDefinition[], |
20 | 14 | dataTypes?: DataType[] |
21 | 15 | ): ReferenceValue[] => { |
22 | | - if (!flow || !nodeId) return []; |
23 | | - const suggestions: ReferenceValue[] = []; |
24 | | - const nodes = flow?.nodes?.nodes || []; |
25 | | - const targetNode = nodes.find(n => n?.id === nodeId); |
26 | | - if (!targetNode) return []; |
27 | | - const typeDefs = getSharedTypeDeclarations(dataTypes); |
28 | | - const inferred = getInferredTypesFromFlow(flow, functions, dataTypes); |
29 | | - |
30 | | - console.log(inferred) |
31 | | - |
32 | | - // Helper to check if a type is assignable to the required type |
33 | | - const isAssignable = (inferredType: string, path?: string): boolean => { |
34 | | - const fileName = `index.ts`; |
35 | | - const sourceCode = ` |
36 | | - ${typeDefs} |
37 | | - declare const val: ${inferredType}; |
38 | | - const test: ${type} = val${path ? `.${path}` : ""}; |
39 | | - `; |
40 | | - const host = createCompilerHost(fileName, sourceCode); |
41 | | - const program = host.languageService.getProgram()!; |
42 | | - const sourceFile = program.getSourceFile(fileName)!; |
43 | | - const diagnostics = program.getSemanticDiagnostics(sourceFile); |
44 | | - return !diagnostics.some(d => d.category === ts.DiagnosticCategory.Error); |
45 | | - }; |
46 | | - |
47 | | - // Helper to get sub-properties and their types by probing with TypeScript |
48 | | - const getValidPaths = (inferredType: string, baseValue: ReferenceValue): ReferenceValue[] => { |
49 | | - const validRefs: ReferenceValue[] = []; |
50 | | - |
51 | | - // 1. Check base type |
52 | | - if (isAssignable(inferredType)) { |
53 | | - validRefs.push({...baseValue, referencePath: []}); |
54 | | - } |
55 | 16 |
|
56 | | - const fileName = `probe.ts`; |
57 | | - const sourceCode = ` |
58 | | - ${typeDefs} |
59 | | - declare const val: ${inferredType}; |
60 | | - `; |
61 | | - const host = createCompilerHost(fileName, sourceCode); |
62 | | - const program = host.languageService.getProgram()!; |
63 | | - const checker = program.getTypeChecker(); |
64 | | - const sourceFile = program.getSourceFile(fileName)!; |
65 | | - |
66 | | - const findProperties = (typeObj: ts.Type, currentPath: string[] = []) => { |
67 | | - if (currentPath.length >= 3) return; // Limit depth |
68 | | - |
69 | | - const properties = typeObj.getProperties(); |
70 | | - for (const prop of properties) { |
71 | | - const propName = prop.getName(); |
72 | | - if (propName.startsWith("__")) continue; |
73 | | - |
74 | | - const fullPath = [...currentPath, propName]; |
75 | | - const pathString = fullPath.join("."); |
76 | | - |
77 | | - if (isAssignable(inferredType, pathString)) { |
78 | | - validRefs.push({ |
79 | | - ...baseValue, |
80 | | - referencePath: fullPath.map(p => ({ |
81 | | - __typename: "ReferencePath", |
82 | | - path: p |
83 | | - })) as any |
84 | | - }); |
85 | | - } |
86 | | - |
87 | | - // Recursively check sub-properties |
88 | | - const propType = checker.getTypeOfSymbolAtLocation(prop, sourceFile); |
89 | | - if (propType && (propType.getFlags() & ts.TypeFlags.Object)) { |
90 | | - findProperties(propType, fullPath); |
91 | | - } |
92 | | - } |
93 | | - }; |
94 | | - |
95 | | - const lastStatement = sourceFile.statements[sourceFile.statements.length - 1]; |
96 | | - if (lastStatement && ts.isVariableStatement(lastStatement)) { |
97 | | - const decl = lastStatement.declarationList.declarations[0]; |
98 | | - if (decl) { |
99 | | - const typeObj = checker.getTypeAtLocation(decl); |
100 | | - if (typeObj) findProperties(typeObj); |
| 17 | + const sourceCode = generateFlowSourceCode(flow, functions, dataTypes, true); |
| 18 | + const fileName = "index.ts"; |
| 19 | + const host = createCompilerHost(fileName, sourceCode); |
| 20 | + const sourceFile = host.getSourceFile(fileName)!; |
| 21 | + const program = host.languageService.getProgram()!; |
| 22 | + const checker = program.getTypeChecker(); |
| 23 | + |
| 24 | + // 2. Suche die exakte Text-Position des Kommentars |
| 25 | + const fullText = sourceFile.getFullText(); |
| 26 | + const commentPattern = `/* @pos ${nodeId} ${targetIndex} */`; |
| 27 | + const commentIndex = fullText.indexOf(commentPattern); |
| 28 | + |
| 29 | + // Die Position des eigentlichen Nodes ist direkt nach dem Kommentar |
| 30 | + const targetPos = commentIndex + commentPattern.length; |
| 31 | + |
| 32 | + // 3. Finde den kleinsten AST-Node an dieser Position |
| 33 | + function findNodeAtPosition(node: ts.Node, pos: number): ts.Node { |
| 34 | + let found = node; |
| 35 | + ts.forEachChild(node, child => { |
| 36 | + if (child.getStart(sourceFile, true) <= pos && child.getEnd() >= pos) { |
| 37 | + found = findNodeAtPosition(child, pos); |
101 | 38 | } |
102 | | - } |
103 | | - |
104 | | - return validRefs; |
105 | | - }; |
106 | | - |
107 | | - // 1. Flow Input |
108 | | - if (flow.inputType) { |
109 | | - suggestions.push(...getValidPaths(flow.inputType, { __typename: "ReferenceValue" } as ReferenceValue)); |
| 39 | + }); |
| 40 | + return found; |
110 | 41 | } |
111 | 42 |
|
112 | | - // 2. Previously executed nodes and scope inputs |
113 | | - nodes.forEach(node => { |
114 | | - if (!node || !node.id) return; |
115 | | - const sId = sanitizeId(node.id); |
116 | | - |
117 | | - // Suggestions from node return values |
118 | | - if (node.id !== nodeId) { |
119 | | - const nodeType = inferred.nodes.get(sId); |
120 | | - if (nodeType !== "void" && nodeType && isNodeBefore(flow, node.id, nodeId)) { |
121 | | - suggestions.push(...getValidPaths(nodeType, { |
122 | | - __typename: "ReferenceValue", |
123 | | - nodeFunctionId: node.id, |
124 | | - referencePath: [] |
125 | | - } as ReferenceValue)); |
126 | | - } |
127 | | - } |
| 43 | + let targetNode = findNodeAtPosition(sourceFile, targetPos); |
| 44 | + const targetExpression = targetNode as ts.Expression; |
128 | 45 |
|
129 | | - // Suggestions from scope inputs (parameters of lambda) |
130 | | - const pTypes = inferred.parameters.get(sId); |
131 | | - if (pTypes) { |
132 | | - pTypes.forEach((pType, idx) => { |
133 | | - if (isParentScope(flow, node.id!, nodeId!)) { |
134 | | - |
135 | | - // Extract T from CONSUMER<T> or similar if needed |
136 | | - let actualPType = pType; |
137 | | - if (pType.startsWith("CONSUMER<") && pType.endsWith(">")) { |
138 | | - actualPType = pType.substring(9, pType.length - 1); |
139 | | - } |
140 | | - |
141 | | - suggestions.push(...getValidPaths(actualPType, { |
142 | | - __typename: "ReferenceValue", |
143 | | - nodeFunctionId: node.id, |
144 | | - parameterIndex: idx, |
145 | | - inputIndex: 0, |
146 | | - referencePath: [] |
147 | | - } as ReferenceValue)); |
148 | | - } |
149 | | - }); |
150 | | - } |
151 | | - }); |
| 46 | + // 4. Umschließenden Funktionsaufruf finden (identisch zu deinem Code) |
| 47 | + let parentCall: ts.CallExpression | undefined; |
152 | 48 |
|
153 | | - return suggestions; |
154 | | -}; |
| 49 | + if (ts.isCallExpression(targetExpression)) { |
| 50 | + parentCall = targetExpression; |
| 51 | + } |
155 | 52 |
|
156 | | -function isNodeBefore(flow: Flow, startId: string, targetId: string): boolean { |
157 | | - const nodes = flow.nodes?.nodes || []; |
158 | | - const visited = new Set<string>(); |
| 53 | + if (!parentCall) { |
| 54 | + return [] |
| 55 | + } |
159 | 56 |
|
160 | | - // Check if there's a path from startId to targetId |
161 | | - const checkForward = (currentId: string): boolean => { |
162 | | - if (currentId === targetId) return true; |
163 | | - if (visited.has(currentId)) return false; |
164 | | - visited.add(currentId); |
| 57 | + // 5. Typ-Check und Variablen-Extraktion |
| 58 | + const signature = checker.getResolvedSignature(parentCall); |
| 59 | + if (!signature) return []; |
165 | 60 |
|
166 | | - const node = nodes.find(n => n?.id === currentId); |
167 | | - if (!node) return false; |
| 61 | + const params = signature.getParameters(); |
| 62 | + const paramSymbol = params[targetIndex!] || params[params.length - 1]; |
| 63 | + const expectedType = checker.getTypeOfSymbolAtLocation(paramSymbol, targetExpression); |
168 | 64 |
|
169 | | - if (node.nextNodeId && checkForward(node.nextNodeId)) return true; |
170 | | - return node.parameters?.nodes?.some(p => { |
171 | | - if (p?.value?.__typename === "NodeFunctionIdWrapper") { |
172 | | - return checkForward((p.value as NodeFunctionIdWrapper).id!); |
173 | | - } |
174 | | - return false; |
175 | | - }) || false; |
176 | | - }; |
| 65 | + const allSymbols = checker.getSymbolsInScope(targetExpression, ts.SymbolFlags.Variable); |
177 | 66 |
|
178 | | - return checkForward(startId); |
179 | | -} |
| 67 | + const referenceValues: ReferenceValue[] = []; |
180 | 68 |
|
181 | | -function isParentScope(flow: Flow, parentId: string, childId: string): boolean { |
182 | | - const nodes = flow.nodes?.nodes || []; |
183 | | - const parent = nodes.find(n => n?.id === parentId); |
184 | | - if (!parent) return false; |
| 69 | + allSymbols.forEach(symbol => { |
| 70 | + const name = symbol.getName(); |
| 71 | + if (!name.startsWith("node_") && !name.startsWith("p_")) return; |
185 | 72 |
|
186 | | - return parent.parameters?.nodes?.some(p => { |
187 | | - const val = p?.value; |
188 | | - if (val?.__typename === "NodeFunctionIdWrapper") { |
189 | | - const wrapper = val as NodeFunctionIdWrapper; |
190 | | - return wrapper.id === childId || isNodeInSubtree(flow, wrapper.id!, childId); |
191 | | - } |
192 | | - return false; |
193 | | - }) || false; |
194 | | -} |
| 73 | + // 1. Erhalte die Deklaration der Variable |
| 74 | + const declaration = symbol.valueDeclaration || symbol.declarations?.[0]; |
| 75 | + if (!declaration) return; |
195 | 76 |
|
196 | | -function isNodeInSubtree(flow: Flow, rootId: string, targetId: string): boolean { |
197 | | - if (rootId === targetId) return true; |
198 | | - const nodes = flow.nodes?.nodes || []; |
199 | | - const root = nodes.find(n => n?.id === rootId); |
200 | | - if (!root) return false; |
| 77 | + // 2. Reachability-Check: |
| 78 | + // Die Deklaration muss VOR der targetPos enden. |
| 79 | + // (Damit schließen wir die aktuelle Zeile und alles danach aus) |
| 80 | + if (declaration.getEnd() >= targetPos) { |
| 81 | + return; |
| 82 | + } |
201 | 83 |
|
202 | | - if (root.nextNodeId && isNodeInSubtree(flow, root.nextNodeId, targetId)) return true; |
| 84 | + const symbolType = checker.getTypeOfSymbolAtLocation(symbol, targetExpression!); |
| 85 | + |
| 86 | + // FALL 1: Node-Ergebnis (node_) |
| 87 | + 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)) { |
| 90 | + const nodeFunctionId = name |
| 91 | + .replace("node_", "") |
| 92 | + .replace(/___/g, "://") |
| 93 | + .replace(/__/g, "/") |
| 94 | + .replace(/_/g, "/"); |
| 95 | + |
| 96 | + referenceValues.push({ |
| 97 | + __typename: 'ReferenceValue', |
| 98 | + nodeFunctionId: nodeFunctionId as any |
| 99 | + // Bei node_ keine Indizes laut Vorgabe |
| 100 | + }); |
| 101 | + } |
| 102 | + } |
203 | 103 |
|
204 | | - return root.parameters?.nodes?.some(p => { |
205 | | - const val = p?.value; |
206 | | - if (val?.__typename === "NodeFunctionIdWrapper") { |
207 | | - const wrapper = val as NodeFunctionIdWrapper; |
208 | | - if (!wrapper.id) return false; |
209 | | - return isNodeInSubtree(flow, wrapper.id, targetId); |
| 104 | + // FALL 2: Parameter / Input (p_) |
| 105 | + else if (name.startsWith("p_")) { |
| 106 | + const idPart = name.replace("p_", ""); |
| 107 | + const lastUnderscoreIndex = idPart.lastIndexOf("_"); |
| 108 | + const rawId = idPart.substring(0, lastUnderscoreIndex); |
| 109 | + const paramIndexFromName = parseInt(idPart.substring(lastUnderscoreIndex + 1), 10); |
| 110 | + |
| 111 | + const nodeFunctionId = rawId |
| 112 | + .replace("p_", "") |
| 113 | + .replace(/___/g, "://") |
| 114 | + .replace(/__/g, "/") |
| 115 | + .replace(/_/g, "/"); |
| 116 | + |
| 117 | + // Da p_ oft ein Rest-Parameter (...p) ist, prüfen wir auf Array/Tupel |
| 118 | + if (checker.isTupleType(symbolType)) { |
| 119 | + // Bei einem Tupel (z.B. [item, index]) extrahieren wir die Element-Typen |
| 120 | + const typeReference = symbolType as ts.TypeReference; |
| 121 | + const typeArguments = checker.getTypeArguments(typeReference); |
| 122 | + |
| 123 | + typeArguments.forEach((tupleElementType, tupleIndex) => { |
| 124 | + if (checker.isTypeAssignableTo(tupleElementType, expectedType)) { |
| 125 | + referenceValues.push({ |
| 126 | + __typename: 'ReferenceValue', |
| 127 | + nodeFunctionId: nodeFunctionId as any, |
| 128 | + parameterIndex: isNaN(paramIndexFromName) ? 0 : paramIndexFromName, |
| 129 | + inputIndex: tupleIndex, |
| 130 | + //@ts-ignore |
| 131 | + inputTypeIdentifier: (typeReference.target as any).labeledElementDeclarations?.[tupleIndex].name.getText() |
| 132 | + }); |
| 133 | + } |
| 134 | + }); |
| 135 | + } |
210 | 136 | } |
211 | | - return false; |
212 | | - }) || false; |
| 137 | + }); |
| 138 | + |
| 139 | + return referenceValues; |
213 | 140 | } |
0 commit comments