Skip to content

Commit 87f0a7b

Browse files
author
nicosammito
committed
feat: refactor getReferenceSuggestions and related tests for improved type handling and clarity
1 parent b2f8255 commit 87f0a7b

6 files changed

Lines changed: 542 additions & 398 deletions

File tree

Lines changed: 111 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -1,213 +1,140 @@
11
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+
115
/**
126
* Calculates all available reference suggestions for a specific target node in a flow
137
* and filters them by a required type.
148
*/
159
export const getReferenceSuggestions = (
1610
flow?: Flow,
1711
nodeId?: NodeFunction['id'],
18-
type: string = "any",
12+
targetIndex?: number,
1913
functions?: FunctionDefinition[],
2014
dataTypes?: DataType[]
2115
): 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-
}
5516

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);
10138
}
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;
11041
}
11142

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;
12845

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;
15248

153-
return suggestions;
154-
};
49+
if (ts.isCallExpression(targetExpression)) {
50+
parentCall = targetExpression;
51+
}
15552

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+
}
15956

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 [];
16560

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);
16864

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);
17766

178-
return checkForward(startId);
179-
}
67+
const referenceValues: ReferenceValue[] = [];
18068

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;
18572

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;
19576

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+
}
20183

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+
}
203103

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+
}
210136
}
211-
return false;
212-
}) || false;
137+
});
138+
139+
return referenceValues;
213140
}

src/suggestion/getValueSuggestions.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,20 @@ export const getValueSuggestions = (
1313

1414
const sourceCode = `
1515
${getSharedTypeDeclarations(dataTypes)}
16-
type T = ${type}; const val: T = {} as any;
16+
type VALUE = ${type}; const val: VALUE = {} as any;
1717
`;
18+
1819
const fileName = "index.ts";
1920
const host = createCompilerHost(fileName, sourceCode);
2021
const sourceFile = host.getSourceFile(fileName)!;
2122
const program = host.languageService.getProgram()!;
2223
const checker = program.getTypeChecker();
2324

24-
const typeAlias = sourceFile.statements.find(ts.isTypeAliasDeclaration);
25-
if (!typeAlias) return [];
25+
// Find the VALUE type alias (not the first one, but the one we defined)
26+
const typeAlias = sourceFile.statements.find(
27+
node => ts.isTypeAliasDeclaration(node) && node.name.text === 'VALUE'
28+
);
29+
if (!typeAlias || !ts.isTypeAliasDeclaration(typeAlias)) return [];
2630

2731
const typeFound = checker.getTypeAtLocation(typeAlias);
2832

0 commit comments

Comments
 (0)