Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 171 additions & 3 deletions src/schema/getSignatureSchema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {DataType, Flow, FunctionDefinition, NodeFunction} from "@code0-tech/sagittarius-graphql-types"
import {createCompilerHost, generateFlowSourceCode, sanitizeId} from "../utils"
import ts, {Type} from "typescript"
import {getSchema, mergeSchemas, Schema} from "../util/schema.util"
import {genericNodeSchema, getSchema, mergeSchemas, normalizeNodeSchema, Schema} from "../util/schema.util"

/**
* Represents the schema information for a node parameter.
Expand Down Expand Up @@ -398,8 +398,21 @@ const generateNodeSchemas = (
return []
}

// The suggestion set for a fully generic ("accepts anything") slot. Every
// position the declared type leaves unconstrained offers this same set,
// independent of the concrete value entered there — computed once against `any`.
const anySuggestions = getSchema(
checker,
node,
checker.getAnyType(),
Array.from(declaredFunctionsMap.values()),
functions,
true,
).suggestions

return nodeParameterTypes.map((parameterType, index) => {
const functionParameterType = functionParameterTypes?.[index]

// Suggestions are scoped by what the *function* parameter accepts (e.g.
// `T` widens to `any`, so anything in scope is a valid candidate), even
// when the node value has narrowed the actual parameter type — otherwise
Expand All @@ -409,15 +422,53 @@ const generateNodeSchemas = (
? widenForSuggestions(checker, functionParameterType, node!)
: undefined

const nodeSchema = getSchema(
// Value-driven list items: when the argument is an array literal, the
// list renders exactly one item per entered element (like an object's
// properties mirror its fields). The item's input kind and select
// options come from the declared element type; the item's `type` is the
// concrete value's base type (e.g. "string"). This overrides the
// type-driven, union-expanded items a plain type analysis would produce.
// The whole-list suggestions (references/nodes that produce a matching
// list) are still surfaced, scoped by what the function accepts.
const argExpr = getArgumentExpression(node, index)
if (argExpr && ts.isArrayLiteralExpression(argExpr)) {
const listSuggestions = getSchema(
checker,
node,
parameterType,
Array.from(declaredFunctionsMap.values()),
functions,
true,
suggestionType,
).suggestions
return {
schema: buildValueDrivenListSchema(
checker,
node,
functionParameterType,
argExpr,
Array.from(declaredFunctionsMap.values()),
functions,
listSuggestions,
anySuggestions,
),
blockedBy: funktionDependencies
.filter((dep) => dep.parameterIndex === index)
.map((dep) => dep.dependsOnIndex),
}
}

// Specialized list-* inputs are a declared-type concern; the node value
// only contributes concrete element types, so normalize what it produced.
const nodeSchema = normalizeNodeSchema(getSchema(
checker,
node,
parameterType,
Array.from(declaredFunctionsMap.values()),
functions,
true,
suggestionType,
)
))
const functionSchema = functionParameterType
? getSchema(
checker,
Expand All @@ -434,6 +485,7 @@ const generateNodeSchemas = (
functionSchema,
nodeSchema,
valueProvidedByIndex[index] ?? false,
anySuggestions,
),
blockedBy: funktionDependencies
.filter((dep) => dep.parameterIndex === index)
Expand All @@ -442,6 +494,122 @@ const generateNodeSchemas = (
})
}

/**
* Returns the argument expression at the given position of the node's call
* expression, or undefined when the node has no call initializer or fewer
* arguments.
*/
const getArgumentExpression = (
node: ts.VariableDeclaration,
index: number,
): ts.Expression | undefined => {
if (!node.initializer || !ts.isCallExpression(node.initializer)) return undefined
return node.initializer.arguments[index]
}

// Primitive item kinds whose schema is rebuilt value-first: the declared kind is
// kept, but the item's `type` comes from the concrete value while the declared
// element type's suggestions (options, references, nodes) are carried along.
const PRIMITIVE_ITEM_INPUTS = new Set(["select", "boolean", "number", "text"])

/**
* Builds a value-driven list schema from an array-literal argument.
*
* The list kind (e.g. `list`, `list-select`) comes from the declared function
* list type, while `items` has exactly one entry per entered element (like an
* object's properties mirror its fields). Nested array literals recurse, so the
* per-value cardinality holds at every level. Whole-list suggestions (what can
* produce the list) are attached when provided.
*/
const buildValueDrivenListSchema = (
checker: ts.TypeChecker,
node: ts.VariableDeclaration,
funcListType: Type | undefined,
arrayExpr: ts.ArrayLiteralExpression,
functionDeclarations: ts.FunctionDeclaration[],
functions: FunctionDefinition[],
suggestions?: Schema["suggestions"],
anySuggestions?: Schema["suggestions"],
): Schema => {
const funcSchema = funcListType
? getSchema(checker, node, funcListType, functionDeclarations, functions, false)
: undefined
const isListKind =
funcSchema != null &&
(funcSchema.input as string | undefined)?.startsWith("list") === true
const funcElementType =
funcListType && checker.isArrayType(funcListType)
? checker.getTypeArguments(funcListType as ts.TypeReference)[0]
: undefined

const items = arrayExpr.elements.map((element) =>
buildValueDrivenItem(checker, node, funcElementType, element, functionDeclarations, functions, anySuggestions),
)

return {
input: isListKind ? funcSchema!.input : "list",
type:
(isListKind ? funcSchema!.type : undefined) ??
checker.typeToString(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(arrayExpr))),
items,
...(suggestions?.length ? {suggestions} : {}),
} as Schema
}

/**
* Builds a single list item schema for one array-literal element.
*
* The item's input kind comes from the declared element type; its `type` is the
* concrete value's base type (literals widened, e.g. "GET" → string). Each item
* carries the full suggestions of its element slot — literal options (e.g. a
* select's members or true/false for a boolean), in-scope references, and
* compatible function nodes. A generic declared element lets the value drive kind
* and type; a nested array literal recurses into a value-driven list; a
* structured element (object, …) keeps its declared schema and only contributes
* to the item count.
*/
const buildValueDrivenItem = (
checker: ts.TypeChecker,
node: ts.VariableDeclaration,
funcElementType: Type | undefined,
element: ts.Expression,
functionDeclarations: ts.FunctionDeclaration[],
functions: FunctionDefinition[],
anySuggestions?: Schema["suggestions"],
): Schema => {
if (ts.isArrayLiteralExpression(element)) {
return buildValueDrivenListSchema(checker, node, funcElementType, element, functionDeclarations, functions, undefined, anySuggestions)
}

const funcElementSchema = funcElementType
? getSchema(checker, node, funcElementType, functionDeclarations, functions, true)
: undefined
const funcIsGeneric = !funcElementSchema || funcElementSchema.input === "generic"
const valueType = checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(element))

// Generic declared element: the value drives kind and type, but the slot
// accepts anything, so the suggestions are the constant `any` set — not the
// subset the concrete value would narrow to.
if (funcIsGeneric) {
return genericNodeSchema(
getSchema(checker, node, valueType, functionDeclarations, functions, true),
anySuggestions,
)
}

// Primitive/select element: keep the declared kind and suggestions, but take
// the concrete value's base type as the item type.
if (PRIMITIVE_ITEM_INPUTS.has(funcElementSchema!.input as string)) {
return {
...funcElementSchema!,
type: checker.typeToString(valueType),
} as Schema
}

// Structured element (object, …): keep the declared schema as-is.
return funcElementSchema!
}

// Widen a function parameter type so that suggestion collection asks "what could
// the function accept here", not "what does the current value narrow this to".
// An unconstrained type parameter accepts anything → `any`. A constrained type
Expand Down
3 changes: 0 additions & 3 deletions src/util/nodes.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,6 @@ const createNodeFunctionIfCompatible = (
paramType: ts.Type
): NodeFunction | null => {

if (func.parameters.length > 0)
return null;

// Extract the function signature and its return type
const signature = checker.getSignatureFromDeclaration(func);
const returnType = checker.getReturnTypeOfSignature(signature!);
Expand Down
Loading