@@ -124,39 +124,46 @@ export const getSchema = (
124124 parameterType : ts . Type ,
125125 functionDeclarations : FunctionDeclaration [ ] ,
126126 functions : FunctionDefinition [ ] ,
127- suggestions : boolean = true
127+ suggestions : boolean = true ,
128+ suggestionType ?: ts . Type ,
128129) : Schema => {
129130
130131 if ( ( parameterType . flags & ts . TypeFlags . TypeParameter ) !== 0 ) {
131132 const decl = parameterType . symbol ?. declarations ?. [ 0 ]
132133 if ( decl && ts . isTypeParameterDeclaration ( decl ) && decl . constraint ) {
133134 // getTypeFromTypeNode statt getBaseConstraintOfType → aliasSymbol bleibt erhalten
134135 const constraintType = checker . getTypeFromTypeNode ( decl . constraint )
135- return getSchema ( checker , node , constraintType , functionDeclarations , functions , suggestions )
136+ return getSchema ( checker , node , constraintType , functionDeclarations , functions , suggestions , suggestionType )
136137 }
137138 }
138139
140+ // Suggestions are filtered by what the surrounding function accepts, not by
141+ // the narrower type a current value happens to narrow the node-side to.
142+ // Example: `<T>(value: T)` with a current boolean literal must still surface
143+ // every reference in scope, because the function takes anything.
144+ const typeForSuggestions = suggestionType ?? parameterType ;
145+
139146 // Collect all available suggestions for this parameter
140147 const combinedSuggestions = suggestions ? {
141148 suggestions : [
142- ...getValues ( parameterType , checker ) ,
149+ ...getValues ( typeForSuggestions , checker ) ,
143150 ...( node ? getReferences (
144151 checker ,
145152 node ,
146- parameterType ,
153+ typeForSuggestions ,
147154 checker . getSymbolsInScope ( node , ts . SymbolFlags . Variable )
148155 ) : [ ] ) ,
149156 ...getNodes (
150157 checker ,
151158 functionDeclarations ,
152159 functions ,
153- parameterType
160+ typeForSuggestions
154161 ) ,
155162 ...getSubFlows (
156163 checker ,
157164 functionDeclarations ,
158165 functions ,
159- parameterType
166+ typeForSuggestions
160167 ) ,
161168 ] ,
162169 } : { } ;
@@ -274,9 +281,161 @@ export const getSchema = (
274281 } ;
275282 }
276283
277- // Fallback for unknown or generic types
284+ // Fallback for unknown or generic types — still surface any collected
285+ // suggestions (e.g. references in scope) so the UI is never silently empty.
278286 return {
279287 input : "generic" ,
288+ ...combinedSuggestions ,
289+ } ;
290+ } ;
291+
292+ /**
293+ * Merges a function-declared parameter schema with the schema derived from the
294+ * concrete node value. The function schema is treated as the source of truth for
295+ * the structural shape (input kind, properties, items); the node schema only
296+ * contributes additional suggestions and, when the function schema is generic,
297+ * a fallback shape.
298+ *
299+ * Rules:
300+ * - If the function schema is generic, follow the node schema — but never as a
301+ * select. A single literal value (e.g. "Test") narrowing a generic T must not
302+ * collapse the input into a select with one option; it should remain free-form
303+ * text/number/boolean matching the literal kind.
304+ * - Otherwise use the function schema's input kind and merge suggestions from both.
305+ * Recurse into `properties` (for data) and `items` (for list) so nested generics
306+ * inside concrete containers are handled the same way.
307+ *
308+ * Suggestions are concatenated and de-duplicated by structural equality.
309+ *
310+ * @param functionSchema - The schema derived from the declared function parameter type
311+ * @param nodeSchema - The schema derived from the node's concrete (narrowed) parameter type
312+ * @returns A single merged schema
313+ */
314+ export const mergeSchemas = (
315+ functionSchema : Schema | undefined ,
316+ nodeSchema : Schema ,
317+ valueProvided : boolean = false ,
318+ ) : Schema => {
319+ if ( ! functionSchema ) {
320+ return liftGenericIfValued ( demoteSelect ( nodeSchema ) , valueProvided ) ;
321+ }
322+
323+ if ( functionSchema . input === "generic" ) {
324+ return liftGenericIfValued ( demoteSelect ( nodeSchema ) , valueProvided ) ;
325+ }
326+
327+ const suggestions = mergeSuggestions (
328+ functionSchema . suggestions ,
329+ nodeSchema . suggestions ,
330+ ) ;
331+
332+ if ( functionSchema . input === "data" ) {
333+ const fProps = functionSchema . properties ?? { } ;
334+ const nProps =
335+ nodeSchema . input === "data" ? ( nodeSchema . properties ?? { } ) : { } ;
336+ const properties : Record < string , Schema | Schema [ ] > = { } ;
337+ const keys = new Set ( [ ...Object . keys ( fProps ) , ...Object . keys ( nProps ) ] ) ;
338+ for ( const key of keys ) {
339+ const f = fProps [ key ] ;
340+ const n = nProps [ key ] ;
341+ properties [ key ] = mergeProperty ( f , n ) ;
342+ }
343+ return {
344+ ...functionSchema ,
345+ properties,
346+ ...( suggestions ? { suggestions} : { } ) ,
347+ } ;
348+ }
349+
350+ if ( functionSchema . input === "list" ) {
351+ const fItems = functionSchema . items ?? [ ] ;
352+ const nItems =
353+ nodeSchema . input === "list" ? ( nodeSchema . items ?? [ ] ) : [ ] ;
354+ const items =
355+ fItems . length === nItems . length && fItems . length > 0
356+ ? fItems . map ( ( f , i ) => mergeSchemas ( f , nItems [ i ] ) )
357+ : fItems ;
358+ return {
359+ ...functionSchema ,
360+ items,
361+ ...( suggestions ? { suggestions} : { } ) ,
362+ } ;
363+ }
364+
365+ return {
366+ ...functionSchema ,
367+ ...( suggestions ? { suggestions} : { } ) ,
368+ } ;
369+ } ;
370+
371+ const mergeProperty = (
372+ f : Schema | Schema [ ] | undefined ,
373+ n : Schema | Schema [ ] | undefined ,
374+ ) : Schema | Schema [ ] => {
375+ if ( f && ! Array . isArray ( f ) && n && ! Array . isArray ( n ) ) {
376+ return mergeSchemas ( f , n ) ;
377+ }
378+ return ( f ?? n ) ! ;
379+ } ;
380+
381+ const mergeSuggestions = (
382+ a : Input [ "suggestions" ] ,
383+ b : Input [ "suggestions" ] ,
384+ ) : Input [ "suggestions" ] | undefined => {
385+ const all = [ ...( a ?? [ ] ) , ...( b ?? [ ] ) ] ;
386+ if ( all . length === 0 ) return undefined ;
387+ const seen = new Set < string > ( ) ;
388+ const result : NonNullable < Input [ "suggestions" ] > = [ ] ;
389+ for ( const item of all ) {
390+ const key = JSON . stringify ( item ) ;
391+ if ( seen . has ( key ) ) continue ;
392+ seen . add ( key ) ;
393+ result . push ( item ) ;
394+ }
395+ return result ;
396+ } ;
397+
398+ // Generic means "we extracted nothing structural from either side". That is the
399+ // right answer for an empty parameter slot, but if the user has provided a value
400+ // then the merge already had the node-side schema to draw from — if both sides
401+ // still came out generic (e.g. the value resolved to `any`/`unknown`), the most
402+ // useful open shape is `data`. Primitive values never reach this branch: their
403+ // node schema is text / number / boolean / select-then-demoted, so the merge
404+ // produces a concrete kind before this lift runs.
405+ const liftGenericIfValued = ( schema : Schema , valueProvided : boolean ) : Schema => {
406+ if ( ! valueProvided || schema . input !== "generic" ) return schema ;
407+ return {
408+ input : "data" ,
409+ properties : { } ,
410+ required : [ ] ,
411+ ...( schema . suggestions ? { suggestions : schema . suggestions } : { } ) ,
412+ } ;
413+ } ;
414+
415+ const demoteSelect = ( schema : Schema ) : Schema => {
416+ if ( schema . input !== "select" ) return schema ;
417+ const suggestions = schema . suggestions ?? [ ] ;
418+ const literalKinds = new Set < string > ( ) ;
419+ for ( const s of suggestions ) {
420+ const value = ( s as LiteralValue ) . value ;
421+ const kind = typeof value ;
422+ if ( kind === "string" || kind === "number" || kind === "boolean" ) {
423+ literalKinds . add ( kind ) ;
424+ }
425+ }
426+ const target : PrimitiveInput [ "input" ] =
427+ literalKinds . size === 1
428+ ? (
429+ {
430+ string : "text" ,
431+ number : "number" ,
432+ boolean : "boolean" ,
433+ } as const
434+ ) [ [ ...literalKinds ] [ 0 ] as "string" | "number" | "boolean" ]
435+ : "text" ;
436+ return {
437+ input : target ,
438+ ...( suggestions . length > 0 ? { suggestions} : { } ) ,
280439 } ;
281440} ;
282441
0 commit comments