Skip to content

Commit 93a1ac8

Browse files
committed
feat: enhance reference suggestion generation in getReferenceSuggestions and add node suggestion functionality
1 parent 9ffffa2 commit 93a1ac8

2 files changed

Lines changed: 247 additions & 119 deletions

File tree

src/suggestion/getSchema.ts

Lines changed: 210 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,12 @@ import {
33
Flow,
44
FunctionDefinition,
55
LiteralValue,
6-
NodeFunction,
6+
NodeFunction, ReferencePath,
77
ReferenceValue
88
} from "@code0-tech/sagittarius-graphql-types"
99
import {createCompilerHost, generateFlowSourceCode, sanitizeId} from "../utils"
1010
import ts, {NumberLiteralType, StringLiteralType, Type} from "typescript"
1111

12-
export interface TemporaryLiteralValue extends LiteralValue {
13-
references?: Record<string, ReferenceValue>
14-
}
15-
1612
interface Input {
1713
input?: string
1814
suggestions?: (NodeFunction | ReferenceValue | LiteralValue)[]
@@ -107,39 +103,41 @@ export const getSchema = (
107103

108104
const funktionParameterTypes: Type[] | undefined = funktion?.parameters?.map(p => {
109105
const symbol = checker.getSymbolAtLocation(p.name)
110-
return checker.getTypeOfSymbolAtLocation(symbol!, funktion)
106+
return checker.getTypeOfSymbolAtLocation(symbol!, node?.initializer as ts.CallExpression)
111107
})
112108

113109
const combinedParameterTypes: Type[] | undefined = funktionParameterTypes?.map((p, i) => {
114-
const nodeType = nodeParameterTypes?.[i];
115-
if (!nodeType) return p;
110+
const nodeType = nodeParameterTypes?.[i]
111+
if (!nodeType) return p
116112

117-
const pSymbol = p.getSymbol();
118-
const nodeSymbol = nodeType.getSymbol();
113+
const pSymbol = p.getSymbol()
114+
const nodeSymbol = nodeType.getSymbol()
119115

120116
if (pSymbol && nodeSymbol && pSymbol === nodeSymbol) {
121-
return nodeType;
117+
return nodeType
122118
}
123119

124120
if (p.isTypeParameter()) {
125-
const constraint = checker.getBaseConstraintOfType(p);
121+
const constraint = checker.getBaseConstraintOfType(p)
126122
if (!constraint || checker.isTypeAssignableTo(nodeType, constraint)) {
127-
return nodeType;
123+
return nodeType
128124
}
129125
}
130126

131127
if (checker.isTypeAssignableTo(nodeType, p)) {
132-
return nodeType;
128+
return nodeType
133129
}
134130

135-
return p;
131+
return p
136132
})
137133

138134
const generateSchema = (type: ts.Type): Schema => {
139135

140136
const literalValueSuggestions = getLiteralValueSuggestions(type)
137+
const referenceSuggestions = getReferenceSuggestions(checker, node!, type, checker.getSymbolsInScope(node!, ts.SymbolFlags.Variable))
138+
const nodeSuggestions = getNodeSuggestions(checker, Array.from(declaredFunctionsMap.values()), functions, type)
141139
const suggestions = {
142-
suggestions: [...literalValueSuggestions]
140+
suggestions: [...literalValueSuggestions, ...referenceSuggestions, ...nodeSuggestions],
143141
}
144142

145143
if (isPrimitiveLiteralUnion(type)) return {input: "select", ...suggestions}
@@ -249,6 +247,156 @@ function getLiteralValueSuggestions(type: ts.Type): LiteralValue[] {
249247
return []
250248
}
251249

250+
function getNodeSuggestions(checker: ts.TypeChecker, functionDeclarations: ts.FunctionDeclaration[], functions: FunctionDefinition[], paramType: ts.Type): NodeFunction[] {
251+
252+
//TODO: if paramType is callable than we should parse in all functions that match the parameters
253+
//TODO: otherwise we should only parse in functions that match the return type
254+
//TODO: we should differentiate between inline usable suggestions and not
255+
256+
return functionDeclarations.flatMap(func => {
257+
258+
const signature = checker.getSignatureFromDeclaration(func)
259+
const returnType = checker.getReturnTypeOfSignature(signature!)
260+
261+
const simplifiedReturnType = returnType.isTypeParameter()
262+
? (checker.getBaseConstraintOfType(returnType) || checker.getAnyType())
263+
: returnType
264+
265+
if (checker.isTypeAssignableTo(simplifiedReturnType, paramType)) {
266+
const functionName = func.name?.getText().replace("fn_", "").replace("_", "::").replace("_", "::")
267+
const funktion = functions.find(f => f.identifier === functionName)
268+
269+
const node: NodeFunction = {
270+
__typename: "NodeFunction",
271+
id: `gid://sagittarius/NodeFunction/1`,
272+
functionDefinition: {
273+
__typename: "FunctionDefinition",
274+
id: funktion?.id,
275+
identifier: funktion?.identifier,
276+
},
277+
parameters: {
278+
__typename: "NodeParameterConnection",
279+
nodes: (funktion?.parameterDefinitions?.nodes || []).map(p => ({
280+
__typename: "NodeParameter",
281+
parameterDefinition: {
282+
__typename: "ParameterDefinition",
283+
id: p?.id,
284+
identifier: p?.identifier
285+
},
286+
value: p?.defaultValue ? {
287+
__typename: "LiteralValue",
288+
value: p.defaultValue.value
289+
} : null
290+
}))
291+
}
292+
}
293+
294+
return node
295+
296+
}
297+
298+
return []
299+
300+
301+
})
302+
303+
}
304+
305+
function getReferenceSuggestions(checker: ts.TypeChecker, node: ts.VariableDeclaration, paramType: ts.Type, symbols: ts.Symbol[]): ReferenceValue[] {
306+
307+
return symbols.flatMap(symbol => {
308+
const name = symbol.getName()
309+
310+
if (!name.startsWith("node_") && !name.startsWith("p_") && !name.startsWith("flow_")) return []
311+
312+
const symbolDeclaration = symbol.getDeclarations()?.[0]
313+
if (!symbolDeclaration) return []
314+
if (symbolDeclaration.getEnd() >= node.getEnd()!) return []
315+
316+
const symbolType = checker.getTypeOfSymbolAtLocation(symbol, node)
317+
318+
if (name.startsWith("node_")) {
319+
if (!((symbolType.flags & ts.TypeFlags.Void) !== 0)) {
320+
321+
const nodeFunctionId = name
322+
.replace("node_", "")
323+
.replace(/___/g, "://")
324+
.replace(/__/g, "/")
325+
.replace(/_/g, "/")
326+
327+
const propertyPaths = extractObjectProperties(symbolType, checker, paramType)
328+
329+
return propertyPaths.flatMap(({path}) => {
330+
const referenceValue: ReferenceValue = {
331+
__typename: 'ReferenceValue',
332+
nodeFunctionId: nodeFunctionId as any
333+
}
334+
335+
if (path.length > 0) referenceValue.referencePath = path
336+
337+
return referenceValue
338+
})
339+
340+
}
341+
} else if (name.startsWith("p_")) {
342+
343+
const idPart = name.replace("p_", "")
344+
const lastUnderscoreIndex = idPart.lastIndexOf("_")
345+
const rawId = idPart.substring(0, lastUnderscoreIndex)
346+
const paramIndexFromName = parseInt(idPart.substring(lastUnderscoreIndex + 1), 10)
347+
348+
const nodeFunctionId = rawId
349+
.replace("p_", "")
350+
.replace(/___/g, "://")
351+
.replace(/__/g, "/")
352+
.replace(/_/g, "/")
353+
354+
if (checker.isTupleType(symbolType)) {
355+
const typeReference = symbolType as ts.TypeReference
356+
const typeArguments = checker.getTypeArguments(typeReference)
357+
358+
return typeArguments.flatMap((tupleElementType, tupleIndex) => {
359+
const propertyPaths = extractObjectProperties(tupleElementType, checker, paramType)
360+
361+
return propertyPaths.flatMap(({ path }) => {
362+
const referenceValue: ReferenceValue = {
363+
__typename: 'ReferenceValue',
364+
nodeFunctionId: nodeFunctionId as any,
365+
parameterIndex: isNaN(paramIndexFromName) ? 0 : paramIndexFromName,
366+
inputIndex: tupleIndex,
367+
inputTypeIdentifier: (typeReference.target as any).labeledElementDeclarations?.[tupleIndex].name.getText()
368+
}
369+
370+
if (path.length > 0) {
371+
referenceValue.referencePath = path
372+
}
373+
374+
return referenceValue
375+
})
376+
377+
})
378+
}
379+
380+
} else if (name.startsWith("flow_")) {
381+
const propertyPaths = extractObjectProperties(symbolType, checker, paramType)
382+
383+
return propertyPaths.flatMap(({ path }) => {
384+
const referenceValue: ReferenceValue = {
385+
__typename: 'ReferenceValue',
386+
nodeFunctionId: null
387+
}
388+
389+
if (path.length > 0) referenceValue.referencePath = path
390+
391+
return referenceValue
392+
})
393+
}
394+
395+
return []
396+
})
397+
398+
}
399+
252400
function isBoolean(type: ts.Type): boolean {
253401
return (
254402
(type.flags & ts.TypeFlags.Boolean) !== 0 ||
@@ -290,18 +438,57 @@ function isArrayType(checker: ts.TypeChecker, type: ts.Type): boolean {
290438
}
291439

292440
function getParameterDependencies(node: ts.FunctionDeclaration) {
293-
const typeParamNames = node.typeParameters?.map(tp => tp.name.getText()) || [];
294-
const usage: Record<string, number[]> = {};
441+
const typeParamNames = node.typeParameters?.map(tp => tp.name.getText()) || []
442+
const usage: Record<string, number[]> = {}
295443

296444
node.parameters.forEach((p, i) => {
297-
const text = p.type?.getText() || "";
445+
const text = p.type?.getText() || ""
298446
typeParamNames.forEach(t => {
299-
if (text.includes(t)) (usage[t] ??= []).push(i);
300-
});
301-
});
447+
if (text.includes(t)) (usage[t] ??= []).push(i)
448+
})
449+
})
302450

303451
return Object.values(usage)
304452
.filter(indices => indices.length > 1)
305453
.map(([first, ...rest]) => rest.map(idx => ({parameterIndex: idx, dependsOnIndex: first})))
306-
.flat();
454+
.flat()
455+
}
456+
457+
const extractObjectProperties = (
458+
type: ts.Type,
459+
checker: ts.TypeChecker,
460+
expectedType: ts.Type,
461+
currentPath: ReferencePath[] = []
462+
): Array<{ path: ReferencePath[], type: ts.Type }> => {
463+
const results: Array<{ path: ReferencePath[], type: ts.Type }> = []
464+
465+
if (checker.isTypeAssignableTo(type, expectedType)) results.push({ path: currentPath, type })
466+
467+
if (isRealObjectType(type)) {
468+
const properties = type.getProperties()
469+
if (properties && properties.length > 0) {
470+
properties.forEach(property => {
471+
const propType = checker.getTypeOfSymbolAtLocation(property, property.valueDeclaration!)
472+
const propName = property.getName()
473+
const newPath = [...currentPath, {path: propName}]
474+
475+
results.push(...extractObjectProperties(propType, checker, expectedType, newPath))
476+
})
477+
}
478+
}
479+
480+
return results
481+
}
482+
483+
const isRealObjectType = (type: ts.Type): boolean => {
484+
const primitiveFlags =
485+
ts.TypeFlags.String |
486+
ts.TypeFlags.Number |
487+
ts.TypeFlags.Boolean |
488+
ts.TypeFlags.Undefined |
489+
ts.TypeFlags.Null |
490+
ts.TypeFlags.BigInt |
491+
ts.TypeFlags.ESSymbol
492+
493+
return (type.flags & primitiveFlags) === 0
307494
}

0 commit comments

Comments
 (0)