Skip to content
Merged
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
29 changes: 15 additions & 14 deletions src/schema/getSignatureSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
Schema,
withSuggestions,
} from "../util/schema.util"
import {SuggestionCandidate, suggestionCandidates} from "../util/suggestion.util"

/**
* Represents the schema information for a node parameter.
Expand Down Expand Up @@ -428,14 +429,14 @@ const generateNodeSchemas = (
// 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(
const anySuggestions = suggestionCandidates(getSchema(
checker,
node,
checker.getAnyType(),
Array.from(declaredFunctionsMap.values()),
functions,
true,
).suggestions
))

return nodeParameterTypes.map((parameterType, index) => {
const functionParameterType = functionParameterTypes?.[index]
Expand Down Expand Up @@ -511,15 +512,15 @@ const generateNodeSchemas = (
(nodeTypeIsObject ||
(declaredMember != null && isPlainObjectType(checker, declaredMember)))))
) {
const wholeSuggestions = getSchema(
const wholeSuggestions = suggestionCandidates(getSchema(
checker,
node,
parameterType,
functionDeclarations,
functions,
true,
suggestionType,
).suggestions
))
return {
schema: ts.isArrayLiteralExpression(argExpr)
? buildValueDrivenListSchema(
Expand Down Expand Up @@ -723,8 +724,8 @@ const buildValueDrivenListSchema = (
arrayExpr: ts.ArrayLiteralExpression,
functionDeclarations: ts.FunctionDeclaration[],
functions: FunctionDefinition[],
suggestions?: Schema["suggestions"],
anySuggestions?: Schema["suggestions"],
suggestions?: SuggestionCandidate[],
anySuggestions?: SuggestionCandidate[],
): Schema => {
const funcSchema = funcListType
? getSchema(checker, node, funcListType, functionDeclarations, functions, true)
Expand Down Expand Up @@ -778,11 +779,11 @@ const buildValueDrivenListSchema = (
*/
const ownSuggestions = (
funcSchema: Schema | undefined,
anySuggestions?: Schema["suggestions"],
): Schema["suggestions"] | undefined =>
anySuggestions?: SuggestionCandidate[],
): SuggestionCandidate[] | undefined =>
!funcSchema || funcSchema.input === "generic"
? anySuggestions
: funcSchema.suggestions
: suggestionCandidates(funcSchema)

/**
* Builds a single list item schema for one array-literal element.
Expand All @@ -803,7 +804,7 @@ const buildValueDrivenItem = (
element: ts.Expression,
functionDeclarations: ts.FunctionDeclaration[],
functions: FunctionDefinition[],
anySuggestions?: Schema["suggestions"],
anySuggestions?: SuggestionCandidate[],
): Schema => {
// A union declared element (or property) type lists the options of this
// position; the entered value picks one, and that member is what describes
Expand Down Expand Up @@ -852,7 +853,7 @@ const buildValueDrivenItem = (
? {}
: {type: checker.typeToString(valueType)}),
} as Schema,
funcElementSchema!.suggestions,
suggestionCandidates(funcElementSchema!),
)
}

Expand Down Expand Up @@ -883,8 +884,8 @@ const buildValueDrivenObjectSchema = (
objectExpr: ts.ObjectLiteralExpression,
functionDeclarations: ts.FunctionDeclaration[],
functions: FunctionDefinition[],
suggestions?: Schema["suggestions"],
anySuggestions?: Schema["suggestions"],
suggestions?: SuggestionCandidate[],
anySuggestions?: SuggestionCandidate[],
): Schema => {
const funcSchema = funcObjectType
? getSchema(checker, node, funcObjectType, functionDeclarations, functions, true)
Expand Down Expand Up @@ -912,7 +913,7 @@ const buildValueDrivenObjectSchema = (
checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(objectExpr)),
),
} as Schema,
suggestions ?? funcSchema.suggestions,
suggestions ?? suggestionCandidates(funcSchema),
)
}

Expand Down
62 changes: 41 additions & 21 deletions src/util/nodes.util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import ts from "typescript";
import {FunctionDefinition, NodeFunction} from "@code0-tech/sagittarius-graphql-types";
import {
possibleMatch,
SuggestionCandidate,
typeMatchCertainty,
weakestCertainty,
} from "./suggestion.util";

/**
* Filters and transforms function declarations into a collection of compatible node functions.
Expand All @@ -25,9 +31,10 @@ import {FunctionDefinition, NodeFunction} from "@code0-tech/sagittarius-graphql-
* @param {ts.Type} paramType - The target parameter type used to filter compatible
* functions. Only functions with return types assignable to this type are included
*
* @returns {NodeFunction[]} Array of node functions that are compatible with the
* specified parameter type, or an empty array if no compatible
* functions are found
* @returns {SuggestionCandidate[]} Array of node-function candidates compatible with
* the specified parameter type, each carrying how certainly its return
* type produces that type, or an empty array if no compatible functions
* are found
*
* @example
* const compatibleNodes = getNodes(checker, funcDecls, funcDefs, stringType);
Expand All @@ -37,11 +44,11 @@ export const getNodes = (
functionDeclarations: ts.FunctionDeclaration[],
functions: FunctionDefinition[],
paramType: ts.Type
): NodeFunction[] => {
): SuggestionCandidate[] => {
// Transform each function declaration into a node function if it matches the parameter type
return functionDeclarations.flatMap((func) => {
const nodeFunction = createNodeFunctionIfCompatible(checker, func, functions, paramType);
return nodeFunction ? [nodeFunction] : [];
const candidate = createNodeFunctionIfCompatible(checker, func, functions, paramType);
return candidate ? [candidate] : [];
});
};

Expand All @@ -53,13 +60,20 @@ export const getNodes = (
* It extracts the function signature, resolves type parameters, verifies type compatibility,
* and builds a complete node function object with parameter definitions.
*
* The match is not only a yes/no: a return type that had to be weakened to fit —
* a type parameter matched through its constraint, an `any` return, a return
* that may be nullish where the slot is not — yields a `possible` candidate
* rather than an `exact` one. The function is still offered, but a consumer can
* tell a dedicated producer of the slot's type from one that merely cannot be
* ruled out (see {@link SuggestionCertainty}).
*
* @param {ts.TypeChecker} checker - The TypeScript type checker for type analysis
* @param {ts.FunctionDeclaration} func - The function declaration to process
* @param {FunctionDefinition[]} functions - Array of function definitions for metadata lookup
* @param {ts.Type} paramType - The target parameter type for compatibility check
*
* @returns {NodeFunction | null} A node function object if the function is compatible
* with the parameter type, otherwise null
* @returns {SuggestionCandidate | null} A node-function candidate with its certainty if
* the function is compatible with the parameter type, otherwise null
*
* @private
*/
Expand All @@ -68,7 +82,7 @@ const createNodeFunctionIfCompatible = (
func: ts.FunctionDeclaration,
functions: FunctionDefinition[],
paramType: ts.Type
): NodeFunction | null => {
): SuggestionCandidate | null => {

// Extract the function signature and its return type
const signature = checker.getSignatureFromDeclaration(func);
Expand All @@ -77,25 +91,27 @@ const createNodeFunctionIfCompatible = (
// Simplify the return type by resolving type parameters
const simplifiedReturnType = resolveReturnType(checker, returnType);

// Only proceed if the return type is assignable to the target parameter type.
// The nullish part of the return type is ignored: a function returning
// `string | null` is still a valid suggestion for a plain `string` parameter.
// A purely nullish return type strips down to `never` (assignable to anything),
// so it must be excluded explicitly.
const nonNullableReturnType = checker.getNonNullableType(simplifiedReturnType);
if (
(nonNullableReturnType.flags & ts.TypeFlags.Never) !== 0 ||
!checker.isTypeAssignableTo(nonNullableReturnType, paramType)
) {
// Only proceed if the return type fits the target parameter type, under
// {@link typeMatchCertainty}'s rule: a function returning `string | null` is
// still offered for a plain `string` parameter, as a `nullable` possible
// match rather than being dropped.
const matched = typeMatchCertainty(simplifiedReturnType, checker, paramType);
if (!matched) {
return null;
}

// Resolving a type parameter to its constraint is a weakening of its own: the
// concrete instantiation may be narrower than what matched here.
const certainty = returnType.isTypeParameter()
? weakestCertainty(matched, possibleMatch("generic"))
: matched;

// Extract and normalize the function name
const functionName = normalizeFunctionName(func.name?.getText());
const functionDefinition = functions.find((f) => f.identifier === functionName);

// Build and return the node function object
return buildNodeFunction(functionDefinition);
// Build and return the node function candidate
return {value: buildNodeFunction(functionDefinition), certainty};
};

/**
Expand All @@ -105,6 +121,10 @@ const createNodeFunctionIfCompatible = (
* If no base constraint exists, it falls back to the `any` type. Otherwise, it returns
* the type as-is.
*
* Both outcomes are a weakening the caller records as a `possible` match: the
* constraint only bounds the instantiation, and the `any` fallback matches every
* slot at all.
*
* @param {ts.TypeChecker} checker - The TypeScript type checker
* @param {ts.Type} returnType - The return type to resolve
*
Expand Down
Loading
Loading