From 0b2d40a107ab5e42c53b0c39fbdf97aad66fb675 Mon Sep 17 00:00:00 2001 From: nicosammito Date: Thu, 2 Jul 2026 22:54:00 +0200 Subject: [PATCH 1/3] feat: add widened type declarations for generic parameters in suggestions --- src/utils.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 55b2790..d5ff282 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -90,7 +90,15 @@ export function getSharedTypeDeclarations(dataTypes?: DataType[], genericType: s `type ${dt.identifier}${(dt.genericKeys?.length ?? 0) > 0 ? `<${dt.genericKeys?.join(",")}>` : ""} = ${dt.type};` ).join("\n"); - return `${useGenericDeclarations ? genericDeclarations : ""}\n${typeAliasDeclarations}`; + // Pre-instantiate every generic type with `any` for each type parameter. + // These are used by widenForSuggestions to produce the widened type for + // suggestion-scope checks when a parameter type has free TypeParameters. + const widenedDeclarations = dataTypes + ?.filter(dt => (dt.genericKeys?.length ?? 0) > 0) + .map(dt => `declare const __widen_${dt.identifier}: ${dt.identifier}<${dt.genericKeys!.map(() => "any").join(", ")}>;`) + .join("\n") ?? ""; + + return `${useGenericDeclarations ? genericDeclarations : ""}\n${typeAliasDeclarations}\n${widenedDeclarations}`; } /** From e50960335d3f194e68e52ca4e62c2f44b604fc58 Mon Sep 17 00:00:00 2001 From: nicosammito Date: Thu, 2 Jul 2026 22:54:06 +0200 Subject: [PATCH 2/3] feat: add test case for LIST reference suggestion in std::list::at --- test/schema/schema.test.ts | 62 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/test/schema/schema.test.ts b/test/schema/schema.test.ts index d69a838..0a2a0e6 100644 --- a/test/schema/schema.test.ts +++ b/test/schema/schema.test.ts @@ -525,6 +525,68 @@ describe("Schema", () => { expect(new Set(numberSet)).toEqual(new Set(objectSet)); }); + it('std::control::value with array literal is offered as a LIST reference in std::list::at', () => { + // Node 1: std::control::value(value: T): T receives a literal array [1, 2, 3]. + // TypeScript infers T = number[], so the node's return type is number[] (LIST). + // + // Node 2: std::list::at(list: LIST, index: NUMBER): T + // The `list` parameter expects LIST (any array). number[] is assignable to + // LIST (T unconstrained → upper bound unknown → number[] ⊆ unknown[]), so + // node 1 must appear as a ReferenceValue suggestion for the `list` parameter. + const flow: Flow = { + id: "gid://sagittarius/Flow/1", + startingNodeId: "gid://sagittarius/NodeFunction/1", + signature: "(): void", + nodes: { + nodes: [ + { + id: "gid://sagittarius/NodeFunction/1", + functionDefinition: {identifier: "std::control::value"}, + nextNodeId: "gid://sagittarius/NodeFunction/2", + parameters: { + nodes: [ + {value: {__typename: "LiteralValue", value: [1, 2, 3]}}, + ], + }, + }, + { + id: "gid://sagittarius/NodeFunction/2", + functionDefinition: {identifier: "std::list::at"}, + parameters: { + nodes: [ + {value: null}, + {value: {__typename: "LiteralValue", value: 0}}, + ], + }, + }, + ], + }, + }; + + const [listSchema] = getSignatureSchema( + flow, + DATA_TYPES, + FUNCTION_SIGNATURES, + "gid://sagittarius/NodeFunction/2", + ); + + // The `list` parameter is of type LIST → must render as a list input. + expect(listSchema.schema.input).toBe("list"); + + // Node 1 returns LIST, which is assignable to LIST. + // Its return value is a direct reference (no property path). + const suggestions = (listSchema.schema.suggestions ?? []) as any[]; + expect(suggestions.length).toBeGreaterThan(0); + expect( + suggestions.some( + (s) => + s.__typename === "ReferenceValue" && + s.nodeFunctionId === "gid://sagittarius/NodeFunction/1" && + !s.referencePath, + ), + ).toBe(true); + }); + it('demotes select to free-form when function parameter is generic', () => { // std::control::return(value: T): T — function declares T, node sets a // string literal. Result must be text (free-form), NOT select with one option. From e35d399b475e1a0354ba6271527a9f1b0d11d432 Mon Sep 17 00:00:00 2001 From: nicosammito Date: Thu, 2 Jul 2026 22:54:18 +0200 Subject: [PATCH 3/3] feat: enhance type widening for suggestions in generic parameters --- src/schema/getSignatureSchema.ts | 54 +++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/src/schema/getSignatureSchema.ts b/src/schema/getSignatureSchema.ts index bcd0ac3..c5963ef 100644 --- a/src/schema/getSignatureSchema.ts +++ b/src/schema/getSignatureSchema.ts @@ -335,7 +335,7 @@ const generateNodeSchemas = ( // setting a boolean literal in a generic slot would silently hide all // other suggestions. const suggestionType = functionParameterType - ? widenForSuggestions(checker, functionParameterType) + ? widenForSuggestions(checker, functionParameterType, node!) : undefined const nodeSchema = getSchema( @@ -375,13 +375,51 @@ const generateNodeSchemas = ( // 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 -// parameter is replaced by its constraint. Everything else is used as-is. -const widenForSuggestions = (checker: ts.TypeChecker, type: ts.Type): ts.Type => { - if ((type.flags & ts.TypeFlags.TypeParameter) === 0) return type - const decl = type.symbol?.declarations?.[0] - if (decl && ts.isTypeParameterDeclaration(decl) && decl.constraint) { - return checker.getTypeFromTypeNode(decl.constraint) +// parameter is replaced by its constraint. Any generic type with free type parameters +// (e.g. LIST, OBJECT) is widened by substituting `any` for each free +// TypeParameter, because TypeScript's isTypeAssignableTo returns false for concrete +// types against generics with free T even though they are structurally valid candidates. +// +// For array types (TypeReference) the public createArrayType API rebuilds the widened +// type directly. For type-alias types (e.g. mapped types like OBJECT) the source +// code is pre-seeded with `declare const __widen_: ` declarations +// so the widened type can be looked up in the checker's scope via node. +const widenForSuggestions = (checker: ts.TypeChecker, type: ts.Type, node: ts.VariableDeclaration): ts.Type => { + if ((type.flags & ts.TypeFlags.TypeParameter) !== 0) { + const decl = type.symbol?.declarations?.[0] + if (decl && ts.isTypeParameterDeclaration(decl) && decl.constraint) { + return checker.getTypeFromTypeNode(decl.constraint) + } + return checker.getAnyType() + } + + if ((type.flags & ts.TypeFlags.Object) !== 0 && hasFreeTypeParam(type, checker, new Set())) { + const aliasName: string | undefined = (type as any).aliasSymbol?.getName() + if (aliasName) { + const widenedSym = checker + .getSymbolsInScope(node, ts.SymbolFlags.Variable) + .find(s => s.getName() === `__widen_${aliasName}`) + if (widenedSym) { + return checker.getTypeOfSymbolAtLocation(widenedSym, node) + } + } + return checker.getAnyType() + } + + return type +} + +// Returns true if type itself or any of its type arguments (direct or alias) is a +// free TypeParameter, recursing into nested generic types. +const hasFreeTypeParam = (type: ts.Type, checker: ts.TypeChecker, visited: Set): boolean => { + if (visited.has(type)) return false + visited.add(type) + if ((type.flags & ts.TypeFlags.TypeParameter) !== 0) return true + if ((type.flags & ts.TypeFlags.Object) !== 0) { + if (checker.getTypeArguments(type as ts.TypeReference).some(arg => hasFreeTypeParam(arg, checker, visited))) return true + const aliasArgs = (type as any).aliasTypeArguments as ts.Type[] | undefined + if (aliasArgs?.some(arg => hasFreeTypeParam(arg, checker, visited))) return true } - return checker.getAnyType() + return false }