diff --git a/gitnexus/src/core/ingestion/call-processor.ts b/gitnexus/src/core/ingestion/call-processor.ts index dc264869c..dfb274fc8 100644 --- a/gitnexus/src/core/ingestion/call-processor.ts +++ b/gitnexus/src/core/ingestion/call-processor.ts @@ -13,7 +13,7 @@ import { yieldToEventLoop } from './utils/event-loop.js'; import { FUNCTION_NODE_TYPES, extractFunctionName, - extractMethodSignature, + countMethodParameters, findEnclosingClassId, findEnclosingClassInfo, } from './utils/ast-helpers.js'; @@ -267,7 +267,7 @@ const findEnclosingFunction = ( const qualifiedName = classInfo ? `${classInfo.className}.${funcName}` : funcName; // Include # suffix to match definition-phase Method/Constructor IDs const needsArity = finalLabel === 'Method' || finalLabel === 'Constructor'; - const arity = needsArity ? extractMethodSignature(current).parameterCount : undefined; + const arity = needsArity ? countMethodParameters(current) : undefined; const arityTag = arity !== undefined ? `#${arity}` : ''; return generateId(finalLabel, `${filePath}:${qualifiedName}${arityTag}`); } @@ -307,7 +307,7 @@ const findEnclosingFunction = ( // Include # suffix to match definition-phase Method/Constructor IDs const sigNode = current.previousSibling ?? current; const needsArity2 = finalLabel === 'Method' || finalLabel === 'Constructor'; - const arity2 = needsArity2 ? extractMethodSignature(sigNode).parameterCount : undefined; + const arity2 = needsArity2 ? countMethodParameters(sigNode) : undefined; const arityTag2 = arity2 !== undefined ? `#${arity2}` : ''; return generateId(finalLabel, `${filePath}:${qualifiedName}${arityTag2}`); } diff --git a/gitnexus/src/core/ingestion/method-extractors/configs/csharp.ts b/gitnexus/src/core/ingestion/method-extractors/configs/csharp.ts index 34d43f658..da7daca96 100644 --- a/gitnexus/src/core/ingestion/method-extractors/configs/csharp.ts +++ b/gitnexus/src/core/ingestion/method-extractors/configs/csharp.ts @@ -187,6 +187,7 @@ export const csharpMethodConfig: MethodExtractionConfig = { 'destructor_declaration', 'operator_declaration', 'conversion_operator_declaration', + 'local_function_statement', ], bodyNodeTypes: ['declaration_list'], diff --git a/gitnexus/src/core/ingestion/method-extractors/configs/php.ts b/gitnexus/src/core/ingestion/method-extractors/configs/php.ts index aee8cd512..9efc45a85 100644 --- a/gitnexus/src/core/ingestion/method-extractors/configs/php.ts +++ b/gitnexus/src/core/ingestion/method-extractors/configs/php.ts @@ -14,6 +14,57 @@ import type { SyntaxNode } from '../../utils/ast-helpers.js'; // PHP helpers // --------------------------------------------------------------------------- +/** Regex to extract PHPDoc @return annotations: `@return User` */ +const PHPDOC_RETURN_RE = /@return\s+(\S+)/; + +/** Node types to skip when walking backwards through siblings for PHPDoc. */ +const PHPDOC_SKIP_NODE_TYPES: ReadonlySet = new Set(['attribute_list', 'attribute']); + +/** + * Normalize a PHPDoc return type for the MethodExtractor. + * Strips nullable prefix, null/false/void unions, namespace prefixes, and + * rejects uninformative types (mixed, void, self, static, object, array). + */ +function normalizePhpReturnType(raw: string): string | undefined { + let type = raw.startsWith('?') ? raw.slice(1) : raw; + const parts = type + .split('|') + .filter((p) => p !== 'null' && p !== 'false' && p !== 'void' && p !== 'mixed'); + if (parts.length !== 1) return undefined; + type = parts[0]; + const segments = type.split('\\'); + type = segments[segments.length - 1]; + if ( + type === 'mixed' || + type === 'void' || + type === 'self' || + type === 'static' || + type === 'object' || + type === 'array' + ) + return undefined; + if (/^\w+(\[\])?$/.test(type) || /^\w+\s*(['public', 'private', 'protected']); /** @@ -52,6 +103,9 @@ function hasModifierNode(node: SyntaxNode, modifierType: string): boolean { * It appears as a type node (primitive_type, named_type, union_type, * optional_type, nullable_type, intersection_type) after the formal_parameters * and a `:` token separator. + * + * When the AST return type is missing or uninformative (`array` / `iterable`), + * falls back to parsing PHPDoc `@return Type` from preceding doc comments. */ function extractPhpReturnType(node: SyntaxNode): string | undefined { const TYPE_NODE_TYPES = new Set([ @@ -63,6 +117,7 @@ function extractPhpReturnType(node: SyntaxNode): string | undefined { 'intersection_type', ]); + let astType: string | undefined; let seenParams = false; for (let i = 0; i < node.childCount; i++) { const child = node.child(i); @@ -73,14 +128,22 @@ function extractPhpReturnType(node: SyntaxNode): string | undefined { } // After the parameters node, look for the colon and then the type if (seenParams && child.isNamed && TYPE_NODE_TYPES.has(child.type)) { - return child.text?.trim(); + astType = child.text?.trim(); + break; } // Stop at body or semicolon if (child.type === 'compound_statement' || (!child.isNamed && child.text === ';')) { break; } } - return undefined; + + // If AST type is missing or uninformative, try PHPDoc @return fallback + if (!astType || astType === 'array' || astType === 'iterable') { + const docType = extractPhpDocReturnType(node); + if (docType) return docType; + } + + return astType; } /** @@ -208,7 +271,7 @@ export const phpMethodConfig: MethodExtractionConfig = { 'trait_declaration', 'enum_declaration', ], - methodNodeTypes: ['method_declaration'], + methodNodeTypes: ['method_declaration', 'function_definition'], bodyNodeTypes: ['declaration_list'], extractName(node) { diff --git a/gitnexus/src/core/ingestion/method-extractors/configs/ruby.ts b/gitnexus/src/core/ingestion/method-extractors/configs/ruby.ts index f4ce41db5..097dea2f3 100644 --- a/gitnexus/src/core/ingestion/method-extractors/configs/ruby.ts +++ b/gitnexus/src/core/ingestion/method-extractors/configs/ruby.ts @@ -15,6 +15,50 @@ import type { SyntaxNode } from '../../utils/ast-helpers.js'; const VISIBILITY_MODIFIERS = new Set(['private', 'protected', 'public']); +/** Regex to extract YARD `@return [Type]` annotations from comments. */ +const YARD_RETURN_RE = /@return\s+\[([^\]]+)\]/; + +/** + * Extract the simple type name from a YARD type string. + * Handles qualified types ("Models::User" -> "User"), generics ("Array" + * -> "Array"), nullable ("String, nil" -> "String"), and rejects ambiguous + * unions ("String, Integer" -> undefined). + */ +function extractYardTypeName(yardType: string): string | undefined { + const trimmed = yardType.trim(); + + // Bracket-balanced split on commas to handle generics like Hash + const parts: string[] = []; + let depth = 0, + start = 0; + for (let i = 0; i < trimmed.length; i++) { + if (trimmed[i] === '<') depth++; + else if (trimmed[i] === '>') depth--; + else if (trimmed[i] === ',' && depth === 0) { + parts.push(trimmed.slice(start, i).trim()); + start = i + 1; + } + } + parts.push(trimmed.slice(start).trim()); + const filtered = parts.filter((p) => p !== '' && p !== 'nil'); + if (filtered.length !== 1) return undefined; // ambiguous union + + const typePart = filtered[0]; + + // Qualified: "Models::User" -> "User" + const segments = typePart.split('::'); + const last = segments[segments.length - 1]; + + // Generic: "Array" -> "Array" + const genericMatch = last.match(/^(\w+)\s*[<{(]/); + if (genericMatch) return genericMatch[1]; + + // Simple identifier + if (/^\w+$/.test(last)) return last; + + return undefined; +} + /** * Extract visibility for a Ruby method by walking backwards through the * parent body_statement's named children from the method node's position. @@ -166,8 +210,30 @@ export const rubyMethodConfig: MethodExtractionConfig = { return nameNode?.text; }, - extractReturnType(_node) { - // Ruby has no type annotations — return type is always null + extractReturnType(node) { + // Walk backwards through preceding siblings looking for YARD @return [Type]. + // Try direct siblings first, then fall back to parent (body_statement) siblings + // for class methods where the comment may be a sibling of the body_statement. + const search = (startNode: SyntaxNode): string | undefined => { + let sibling = startNode.previousSibling; + while (sibling) { + if (sibling.type === 'comment') { + const match = YARD_RETURN_RE.exec(sibling.text); + if (match) return extractYardTypeName(match[1]); + } else if (sibling.isNamed) { + break; + } + sibling = sibling.previousSibling; + } + return undefined; + }; + + const result = search(node); + if (result) return result; + + if (node.parent?.type === 'body_statement') { + return search(node.parent); + } return undefined; }, diff --git a/gitnexus/src/core/ingestion/method-extractors/configs/typescript-javascript.ts b/gitnexus/src/core/ingestion/method-extractors/configs/typescript-javascript.ts index 08bed31ea..1a18c87e0 100644 --- a/gitnexus/src/core/ingestion/method-extractors/configs/typescript-javascript.ts +++ b/gitnexus/src/core/ingestion/method-extractors/configs/typescript-javascript.ts @@ -125,8 +125,46 @@ function extractTsJsParameters(node: SyntaxNode): ParameterInfo[] { return params; } +/** Regex to extract @returns or @return from JSDoc comments: `@returns {Type}` */ +const JSDOC_RETURN_RE = /@returns?\s*\{([^}]+)\}/; + +/** + * Minimal sanitization for JSDoc return types — preserves generic wrappers + * (e.g. `Promise`) so that extractReturnTypeName in call-processor + * can apply WRAPPER_GENERICS unwrapping. Only strips JSDoc-specific syntax markers. + */ +function sanitizeJsDocReturnType(raw: string): string | undefined { + let type = raw.trim(); + // Strip JSDoc nullable/non-nullable prefixes: ?User → User, !User → User + if (type.startsWith('?') || type.startsWith('!')) type = type.slice(1); + // Strip module: prefix — module:models.User → models.User + if (type.startsWith('module:')) type = type.slice(7); + // Reject unions (ambiguous) + if (type.includes('|')) return undefined; + if (!type) return undefined; + return type; +} + +/** + * Walk backwards through preceding siblings looking for a JSDoc comment containing + * `@returns {Type}` or `@return {Type}`. Stops at the first non-comment named node + * (excluding decorators, which precede methods in TS/JS). + */ +function extractJsDocReturnType(node: SyntaxNode): string | undefined { + let sibling = node.previousSibling; + while (sibling) { + if (sibling.type === 'comment') { + const match = JSDOC_RETURN_RE.exec(sibling.text); + if (match) return sanitizeJsDocReturnType(match[1]); + } else if (sibling.isNamed && sibling.type !== 'decorator') break; + sibling = sibling.previousSibling; + } + return undefined; +} + /** * Extract return type from return_type field, unwrapping type_annotation. + * Falls back to JSDoc `@returns {Type}` when the AST has no return type annotation. * * tree-sitter-typescript uses `return_type` as the field name (not `type` like JVM). * The return_type field points to a type_annotation node that must be unwrapped. @@ -140,7 +178,8 @@ function extractTsJsReturnType(node: SyntaxNode): string | undefined { } return returnType.text?.trim(); } - return undefined; + // AST has no return type annotation — try JSDoc fallback + return extractJsDocReturnType(node); } /** @@ -227,7 +266,14 @@ const shared: Omit = { // are not discovered because class_expression is not in typeDeclarationNodes. // - declare module / declare global augmentations — methods inside ambient_module_declaration // wrappers are not surfaced because the top-level walker doesn't descend into them. - methodNodeTypes: ['method_definition', 'method_signature', 'abstract_method_signature'], + methodNodeTypes: [ + 'method_definition', + 'method_signature', + 'abstract_method_signature', + 'function_declaration', + 'generator_function_declaration', + 'function_signature', + ], bodyNodeTypes: ['class_body', 'interface_body'], extractName(node) { diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index cfcbb90f5..a9a22a648 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -12,7 +12,6 @@ import { yieldToEventLoop } from './utils/event-loop.js'; import { getDefinitionNodeFromCaptures, findEnclosingClassInfo, - extractMethodSignature, getLabelFromCaptures, CLASS_CONTAINER_TYPES, type SyntaxNode, @@ -417,9 +416,8 @@ const processParsingSequential = async ( // Extract method metadata for Function/Method/Constructor nodes BEFORE generating // the node ID — parameterCount is needed to disambiguate overloaded methods. - // Try the per-language methodExtractor first (provides isAbstract, isStatic, - // visibility, annotations, etc.). Fall back to extractMethodSignature for - // basic parameterCount/parameterTypes/returnType when no methodExtractor exists. + // Use the per-language MethodExtractor for method metadata (isAbstract, isStatic, + // visibility, annotations, parameterCount, parameterTypes, returnType, etc.). const isMethodLike = nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor'; let methodProps: Record = {}; @@ -473,31 +471,6 @@ const processParsingSequential = async ( } } } - - // Fallback to generic extractMethodSignature - if (!enriched) { - const sig = extractMethodSignature(definitionNode); - arityForId = sig.parameterCount; - methodProps = { - parameterCount: sig.parameterCount, - ...(sig.requiredParameterCount !== undefined - ? { requiredParameterCount: sig.requiredParameterCount } - : {}), - ...(sig.parameterTypes ? { parameterTypes: sig.parameterTypes } : {}), - returnType: sig.returnType, - }; - } - - // Language-specific return type fallback (e.g. Ruby YARD @return [Type]) - // Also upgrades uninformative AST types like PHP `array` with PHPDoc `@return User[]` - const rt = methodProps.returnType as string | undefined; - if (!rt || rt === 'array' || rt === 'iterable') { - const tc = provider.typeConfig; - if (tc?.extractReturnType) { - const docReturn = tc.extractReturnType(definitionNode); - if (docReturn) methodProps.returnType = docReturn; - } - } } // Append # to Method/Constructor IDs to disambiguate overloads. diff --git a/gitnexus/src/core/ingestion/type-extractors/types.ts b/gitnexus/src/core/ingestion/type-extractors/types.ts index d232ec448..c68a22bda 100644 --- a/gitnexus/src/core/ingestion/type-extractors/types.ts +++ b/gitnexus/src/core/ingestion/type-extractors/types.ts @@ -171,7 +171,7 @@ export interface LanguageTypeConfig { * The callee binding is unverified — the caller must confirm against the SymbolTable. */ scanConstructorBinding?: ConstructorBindingScanner; /** Extract return type from comment-based annotations (e.g. YARD @return [Type]). - * Called as fallback when extractMethodSignature finds no AST-based return type. */ + * Called as fallback when the MethodExtractor finds no AST-based return type. */ extractReturnType?: ReturnTypeExtractor; /** Extract loop variable → type binding from a for-each AST node. */ extractForLoopBinding?: ForLoopExtractor; diff --git a/gitnexus/src/core/ingestion/utils/ast-helpers.ts b/gitnexus/src/core/ingestion/utils/ast-helpers.ts index f4c721389..e0fc221ec 100644 --- a/gitnexus/src/core/ingestion/utils/ast-helpers.ts +++ b/gitnexus/src/core/ingestion/utils/ast-helpers.ts @@ -2,7 +2,6 @@ import type Parser from 'tree-sitter'; import type { NodeLabel } from 'gitnexus-shared'; import type { LanguageProvider } from '../language-provider.js'; import { generateId } from '../../../lib/utils.js'; -import { extractSimpleTypeName } from '../type-extractors/shared.js'; /** Tree-sitter AST node. Re-exported for use across ingestion modules. */ export type SyntaxNode = Parser.SyntaxNode; @@ -642,102 +641,56 @@ export const extractFunctionName = ( return { funcName, label }; }; -export interface MethodSignature { - parameterCount: number | undefined; - /** Number of required (non-optional, non-default) parameters. - * Only set when fewer than parameterCount — enables range-based arity filtering. - * undefined means all parameters are required (or metadata unavailable). */ - requiredParameterCount: number | undefined; - /** Per-parameter type names extracted via extractSimpleTypeName. - * Only populated for languages with method overloading (Java, Kotlin, C#, C++). - * undefined (not []) when no types are extractable — avoids empty array allocations. */ - parameterTypes: string[] | undefined; - returnType: string | undefined; -} - -/** Argument list node types shared between extractMethodSignature and countCallArguments. */ +/** Argument list node types shared between countCallArguments and call-resolution helpers. */ export const CALL_ARGUMENT_LIST_TYPES = new Set(['arguments', 'argument_list', 'value_arguments']); +/** Parameter list node types used for arity counting. */ +const PARAM_LIST_TYPES = new Set([ + 'formal_parameters', + 'parameters', + 'parameter_list', + 'function_parameters', + 'method_parameters', + 'function_value_parameters', + 'formal_parameter_list', // Dart +]); + +/** Node types that indicate variadic/rest parameters. */ +const VARIADIC_PARAM_TYPES = new Set([ + 'variadic_parameter_declaration', // Go + 'variadic_parameter', // Rust + 'spread_parameter', // Java + 'list_splat_pattern', // Python + 'dictionary_splat_pattern', // Python +]); + /** - * Extract parameter count and return type text from an AST method/function node. - * Works across languages by looking for common AST patterns. + * Count the number of parameters on a method/function AST node. + * Returns undefined for variadic signatures (same convention as MethodExtractor). + * Used by call-resolution to compute arity suffixes for overload disambiguation. */ -export const extractMethodSignature = (node: SyntaxNode | null | undefined): MethodSignature => { - let parameterCount: number | undefined = 0; - let requiredCount = 0; - let returnType: string | undefined; - let isVariadic = false; - const paramTypes: string[] = []; +export const countMethodParameters = (node: SyntaxNode | null | undefined): number | undefined => { + if (!node) return 0; - if (!node) - return { - parameterCount, - requiredParameterCount: undefined, - parameterTypes: undefined, - returnType, - }; - - const paramListTypes = new Set([ - 'formal_parameters', - 'parameters', - 'parameter_list', - 'function_parameters', - 'method_parameters', - 'function_value_parameters', - 'formal_parameter_list', // Dart - ]); - - // Node types that indicate variadic/rest parameters - const VARIADIC_PARAM_TYPES = new Set([ - 'variadic_parameter_declaration', // Go: ...string - 'variadic_parameter', // Rust: extern "C" fn(...) - 'spread_parameter', // Java: Object... args - 'list_splat_pattern', // Python: *args - 'dictionary_splat_pattern', // Python: **kwargs - ]); - - /** AST node types that represent parameters with default values. */ - const OPTIONAL_PARAM_TYPES = new Set([ - 'optional_parameter', // TypeScript, Ruby: (x?: number), (x: number = 5), def f(x = 5) - 'default_parameter', // Python: def f(x=5) - 'typed_default_parameter', // Python: def f(x: int = 5) - 'optional_parameter_declaration', // C++: void f(int x = 5) - ]); - - /** Check if a parameter node has a default value (handles Kotlin, C#, Swift, PHP - * where defaults are expressed as child nodes rather than distinct node types). */ - const hasDefaultValue = (paramNode: SyntaxNode): boolean => { - if (OPTIONAL_PARAM_TYPES.has(paramNode.type)) return true; - // C#, Swift, PHP: check for '=' token or equals_value_clause child - for (let i = 0; i < paramNode.childCount; i++) { - const c = paramNode.child(i); - if (!c) continue; - if (c.type === '=' || c.type === 'equals_value_clause') return true; - } - // Kotlin: default values are siblings of the parameter node, not children. - // The AST is: parameter, =, — all at function_value_parameters level. - // Check if the immediately following sibling is '=' (default value separator). - const sib = paramNode.nextSibling; - if (sib && sib.type === '=') return true; - return false; - }; - - const findParameterList = (current: SyntaxNode): SyntaxNode | null => { + const findParamList = (current: SyntaxNode): SyntaxNode | null => { for (const child of current.children) { - if (paramListTypes.has(child.type)) return child; + if (PARAM_LIST_TYPES.has(child.type)) return child; } for (const child of current.children) { - const nested = findParameterList(child); + const nested = findParamList(child); if (nested) return nested; } return null; }; - const parameterList = paramListTypes.has(node.type) - ? node // node itself IS the parameter list (e.g. C# primary constructors) - : (node.childForFieldName?.('parameters') ?? findParameterList(node)); + const parameterList = PARAM_LIST_TYPES.has(node.type) + ? node + : (node.childForFieldName?.('parameters') ?? findParamList(node)); - if (parameterList && paramListTypes.has(parameterList.type)) { + let count = 0; + let isVariadic = false; + + if (parameterList && PARAM_LIST_TYPES.has(parameterList.type)) { for (const param of parameterList.namedChildren) { if (param.type === 'comment') continue; if ( @@ -745,34 +698,27 @@ export const extractMethodSignature = (node: SyntaxNode | null | undefined): Met param.text === '&self' || param.text === '&mut self' || param.type === 'self_parameter' - ) { + ) continue; - } - // TypeScript: `this` parameter is a compile-time type constraint, not a real param - // e.g., handle(this: void, event: Event) — only count 'event' + // TypeScript: `this` parameter is a type constraint, not a real param if (param.type === 'required_parameter') { const patternNode = param.childForFieldName('pattern'); if (patternNode?.type === 'this') continue; } - // Kotlin: default values are siblings of the parameter node inside - // function_value_parameters, so they appear as named children (e.g. - // string_literal, integer_literal, boolean_literal, call_expression). - // Skip any named child that isn't a parameter-like or modifier node. + // Skip Kotlin default-value siblings that appear as named children if ( param.type.endsWith('_literal') || param.type === 'call_expression' || param.type === 'navigation_expression' || param.type === 'prefix_expression' || param.type === 'parenthesized_expression' - ) { + ) continue; - } - // Check for variadic parameter types if (VARIADIC_PARAM_TYPES.has(param.type)) { isVariadic = true; continue; } - // TypeScript/JavaScript: rest parameter — required_parameter containing rest_pattern + // TS/JS rest parameter if (param.type === 'required_parameter' || param.type === 'optional_parameter') { for (const child of param.children) { if (child.type === 'rest_pattern') { @@ -782,45 +728,16 @@ export const extractMethodSignature = (node: SyntaxNode | null | undefined): Met } if (isVariadic) continue; } - // Kotlin: vararg modifier on a regular parameter + // Kotlin vararg if (param.type === 'parameter' || param.type === 'formal_parameter') { const prev = param.previousSibling; if (prev?.type === 'parameter_modifiers' && prev.text.includes('vararg')) { isVariadic = true; } } - // Extract parameter type name for overload disambiguation. - // Works for Java (formal_parameter), Kotlin (parameter), C# (parameter), - // C++ (parameter_declaration). Uses childForFieldName('type') which is the - // standard tree-sitter field for typed parameters across these languages. - // Kotlin uses positional children instead of 'type' field — fall back to - // searching for user_type/nullable_type/predefined_type children. - const paramTypeNode = param.childForFieldName('type'); - if (paramTypeNode) { - const typeName = extractSimpleTypeName(paramTypeNode); - paramTypes.push(typeName ?? 'unknown'); - } else { - // Kotlin: parameter → [simple_identifier, user_type|nullable_type] - let found = false; - for (const child of param.namedChildren) { - if ( - child.type === 'user_type' || - child.type === 'nullable_type' || - child.type === 'type_identifier' || - child.type === 'predefined_type' - ) { - const typeName = extractSimpleTypeName(child); - paramTypes.push(typeName ?? 'unknown'); - found = true; - break; - } - } - if (!found) paramTypes.push('unknown'); - } - if (!hasDefaultValue(param)) requiredCount++; - parameterCount++; + count++; } - // C/C++: bare `...` token in parameter list (not a named child — check all children) + // C/C++: bare `...` token if (!isVariadic) { for (const child of parameterList.children) { if (!child.isNamed && child.text === '...') { @@ -831,105 +748,14 @@ export const extractMethodSignature = (node: SyntaxNode | null | undefined): Met } } - // Swift fallback: tree-sitter-swift places `parameter` nodes as direct children of - // function_declaration without a wrapping parameters/function_parameters list node. - // When no parameter list was found, count direct `parameter` children on the node. - if (!parameterList && parameterCount === 0) { + // Swift fallback: parameter nodes as direct children of function_declaration + if (!parameterList && count === 0) { for (const child of node.namedChildren) { - if (child.type === 'parameter') { - if (!hasDefaultValue(child)) requiredCount++; - parameterCount++; - } + if (child.type === 'parameter') count++; } } - // Return type extraction — language-specific field names - // Go: 'result' field is either a type_identifier or parameter_list (multi-return) - const goResult = node.childForFieldName?.('result'); - if (goResult) { - if (goResult.type === 'parameter_list') { - // Multi-return: extract first parameter's type only (e.g. (*User, error) → *User) - const firstParam = goResult.firstNamedChild; - if (firstParam?.type === 'parameter_declaration') { - const typeNode = firstParam.childForFieldName('type'); - if (typeNode) returnType = typeNode.text; - } else if (firstParam) { - // Unnamed return types: (string, error) — first child is a bare type node - returnType = firstParam.text; - } - } else { - returnType = goResult.text; - } - } - - // Rust: 'return_type' field — the value IS the type node (e.g. primitive_type, type_identifier). - // Skip if the node is a type_annotation (TS/Python), which is handled by the generic loop below. - if (!returnType) { - const rustReturn = node.childForFieldName?.('return_type'); - if (rustReturn && rustReturn.type !== 'type_annotation') { - returnType = rustReturn.text; - } - } - - // C/C++: 'type' field on function_definition - if (!returnType) { - const cppType = node.childForFieldName?.('type'); - if (cppType && cppType.text !== 'void') { - returnType = cppType.text; - } - } - - // C#: 'returns' field on method_declaration - if (!returnType) { - const csReturn = node.childForFieldName?.('returns'); - if (csReturn && csReturn.text !== 'void') { - returnType = csReturn.text; - } - } - - // TS/Rust/Python/C#/Kotlin: type_annotation or return_type child - if (!returnType) { - for (const child of node.children) { - if (child.type === 'type_annotation' || child.type === 'return_type') { - const typeNode = child.children.find((c) => c.isNamed); - if (typeNode) returnType = typeNode.text; - } - } - } - - // Kotlin: fun getUser(): User — return type is a bare user_type child of - // function_declaration. The Kotlin grammar does NOT wrap it in type_annotation - // or return_type; it appears as a direct child after function_value_parameters. - // Note: Kotlin uses function_value_parameters (not a field), so we find it by type. - if (!returnType) { - let paramsEnd = -1; - for (let i = 0; i < node.childCount; i++) { - const child = node.child(i); - if (!child) continue; - if (child.type === 'function_value_parameters' || child.type === 'value_parameters') { - paramsEnd = child.endIndex; - } - if (paramsEnd >= 0 && child.type === 'user_type' && child.startIndex > paramsEnd) { - returnType = child.text; - break; - } - } - } - - if (isVariadic) parameterCount = undefined; - - // Only include parameterTypes when at least one type was successfully extracted. - // Use undefined (not []) to avoid empty array allocations for untyped parameters. - const hasTypes = paramTypes.length > 0 && paramTypes.some((t) => t !== 'unknown'); - // Only set requiredParameterCount when it differs from total — saves memory on the common case. - const requiredParameterCount = - !isVariadic && requiredCount < (parameterCount ?? 0) ? requiredCount : undefined; - return { - parameterCount, - requiredParameterCount, - parameterTypes: hasTypes ? paramTypes : undefined, - returnType, - }; + return isVariadic ? undefined : count; }; // ============================================================================ diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index cded8f444..ea6b3732f 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -46,7 +46,7 @@ import { findEnclosingClassInfo, type EnclosingClassInfo, getLabelFromCaptures, - extractMethodSignature, + countMethodParameters, findDescendant, extractStringContent, type SyntaxNode, @@ -540,7 +540,7 @@ const findEnclosingFunctionId = ( const qualifiedName = classInfo ? `${classInfo.className}.${funcName}` : funcName; // Include # suffix to match definition-phase Method/Constructor IDs const needsArity = finalLabel === 'Method' || finalLabel === 'Constructor'; - const arity = needsArity ? extractMethodSignature(current).parameterCount : undefined; + const arity = needsArity ? countMethodParameters(current) : undefined; const arityTag = arity !== undefined ? `#${arity}` : ''; const result = generateId(finalLabel, `${filePath}:${qualifiedName}${arityTag}`); functionIdCache.set(node, result); @@ -569,7 +569,7 @@ const findEnclosingFunctionId = ( // Include # suffix to match definition-phase Method/Constructor IDs const sigNode = current.previousSibling ?? current; const needsArity2 = finalLabel === 'Method' || finalLabel === 'Constructor'; - const arity2 = needsArity2 ? extractMethodSignature(sigNode).parameterCount : undefined; + const arity2 = needsArity2 ? countMethodParameters(sigNode) : undefined; const arityTag2 = arity2 !== undefined ? `#${arity2}` : ''; const result = generateId(finalLabel, `${filePath}:${qualifiedName}${arityTag2}`); functionIdCache.set(node, result); @@ -1804,9 +1804,8 @@ const processFileGroup = ( let annotations: string[] | undefined; let arityForId: number | undefined; // raw param count for ID, even for variadic if (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor') { - // Try MethodExtractor first — it provides everything extractMethodSignature does, plus - // isAbstract/isFinal/annotations. Only fall back to extractMethodSignature when no - // MethodExtractor is available or the method isn't inside a class body. + // Use MethodExtractor for method metadata — provides parameterCount, parameterTypes, + // returnType, isAbstract/isFinal/annotations, visibility, and more. let enrichedByMethodExtractor = false; if (provider.methodExtractor && definitionNode) { const classNode = @@ -1886,28 +1885,6 @@ const processFileGroup = ( if (info.annotations.length > 0) annotations = info.annotations; } } - - if (!enrichedByMethodExtractor) { - const sig = extractMethodSignature(definitionNode); - arityForId = sig.parameterCount; - parameterCount = sig.parameterCount; - requiredParameterCount = sig.requiredParameterCount; - parameterTypes = sig.parameterTypes; - returnType = sig.returnType; - } - - // Language-specific return type fallback (e.g. Ruby YARD @return [Type]) - // Also upgrades uninformative AST types like PHP `array` with PHPDoc `@return User[]` - if ( - (!returnType || returnType === 'array' || returnType === 'iterable') && - definitionNode - ) { - const tc = provider.typeConfig; - if (tc?.extractReturnType) { - const docReturn = tc.extractReturnType(definitionNode); - if (docReturn) returnType = docReturn; - } - } } // Append # to Method/Constructor IDs to disambiguate overloads. diff --git a/gitnexus/test/integration/resolvers/typescript.test.ts b/gitnexus/test/integration/resolvers/typescript.test.ts index 33a28f604..cd55301d3 100644 --- a/gitnexus/test/integration/resolvers/typescript.test.ts +++ b/gitnexus/test/integration/resolvers/typescript.test.ts @@ -886,7 +886,7 @@ describe('TypeScript return type inference via explicit function return type', ( }); it('resolves user.save() to User#save via return type of getUser(): User', () => { - // TS has explicit return types in the source, so extractMethodSignature captures + // TS has explicit return types in the source, so the method extractor captures // the return type. The TS extractInitializer handles `const user = getUser()` // via the variable_declarator path, enabling save() to resolve to User#save. const calls = getRelationships(result, 'CALLS'); diff --git a/gitnexus/test/unit/method-signature.test.ts b/gitnexus/test/unit/method-signature.test.ts deleted file mode 100644 index af9e79e55..000000000 --- a/gitnexus/test/unit/method-signature.test.ts +++ /dev/null @@ -1,523 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { extractMethodSignature } from '../../src/core/ingestion/utils/ast-helpers.js'; -import Parser from 'tree-sitter'; -import TypeScript from 'tree-sitter-typescript'; -import Python from 'tree-sitter-python'; -import Java from 'tree-sitter-java'; -import CSharp from 'tree-sitter-c-sharp'; -import Kotlin from 'tree-sitter-kotlin'; -import CPP from 'tree-sitter-cpp'; -import Go from 'tree-sitter-go'; -import Rust from 'tree-sitter-rust'; - -describe('extractMethodSignature', () => { - const parser = new Parser(); - - it('returns zero params and no return type for null node', () => { - const sig = extractMethodSignature(null); - expect(sig.parameterCount).toBe(0); - expect(sig.returnType).toBeUndefined(); - }); - - describe('TypeScript', () => { - it('extracts params and return type from a typed method', () => { - parser.setLanguage(TypeScript.typescript); - const code = `class Foo { - greet(name: string, age: number): boolean { return true; } -}`; - const tree = parser.parse(code); - const classNode = tree.rootNode.child(0)!; - const classBody = classNode.childForFieldName('body')!; - const methodNode = classBody.namedChild(0)!; - - const sig = extractMethodSignature(methodNode); - expect(sig.parameterCount).toBe(2); - expect(sig.returnType).toBe('boolean'); - }); - - it('extracts zero params from a method with no parameters', () => { - parser.setLanguage(TypeScript.typescript); - const code = `class Foo { - run(): void {} -}`; - const tree = parser.parse(code); - const classNode = tree.rootNode.child(0)!; - const classBody = classNode.childForFieldName('body')!; - const methodNode = classBody.namedChild(0)!; - - const sig = extractMethodSignature(methodNode); - expect(sig.parameterCount).toBe(0); - expect(sig.returnType).toBe('void'); - }); - - it('extracts params without return type annotation', () => { - parser.setLanguage(TypeScript.typescript); - const code = `class Foo { - process(x: number) { return x + 1; } -}`; - const tree = parser.parse(code); - const classNode = tree.rootNode.child(0)!; - const classBody = classNode.childForFieldName('body')!; - const methodNode = classBody.namedChild(0)!; - - const sig = extractMethodSignature(methodNode); - expect(sig.parameterCount).toBe(1); - expect(sig.returnType).toBeUndefined(); - }); - - it('skips TypeScript this-parameter (compile-time constraint)', () => { - parser.setLanguage(TypeScript.typescript); - const code = `class Handler { - handle(this: void, event: Event): void {} -}`; - const tree = parser.parse(code); - const classNode = tree.rootNode.child(0)!; - const classBody = classNode.childForFieldName('body')!; - const methodNode = classBody.namedChild(0)!; - - const sig = extractMethodSignature(methodNode); - // 'this' is not a real parameter — only 'event' should be counted - expect(sig.parameterCount).toBe(1); - }); - - it('skips this-parameter in top-level function', () => { - parser.setLanguage(TypeScript.typescript); - const code = `function onClick(this: HTMLElement, ev: MouseEvent): void {}`; - const tree = parser.parse(code); - const funcNode = tree.rootNode.child(0)!; - - const sig = extractMethodSignature(funcNode); - expect(sig.parameterCount).toBe(1); - }); - }); - - describe('Python', () => { - it('skips self parameter', () => { - parser.setLanguage(Python); - const code = `class Foo: - def bar(self, x, y): - pass`; - const tree = parser.parse(code); - const classNode = tree.rootNode.child(0)!; - const classBody = classNode.childForFieldName('body')!; - const methodNode = classBody.namedChild(0)!; - - const sig = extractMethodSignature(methodNode); - expect(sig.parameterCount).toBe(2); - expect(sig.returnType).toBeUndefined(); - }); - - it('handles method with only self', () => { - parser.setLanguage(Python); - const code = `class Foo: - def noop(self): - pass`; - const tree = parser.parse(code); - const classNode = tree.rootNode.child(0)!; - const classBody = classNode.childForFieldName('body')!; - const methodNode = classBody.namedChild(0)!; - - const sig = extractMethodSignature(methodNode); - expect(sig.parameterCount).toBe(0); - }); - - it('handles Python return type annotation', () => { - parser.setLanguage(Python); - const code = `class Foo: - def bar(self, x: int) -> bool: - return True`; - const tree = parser.parse(code); - const classNode = tree.rootNode.child(0)!; - const classBody = classNode.childForFieldName('body')!; - const methodNode = classBody.namedChild(0)!; - - const sig = extractMethodSignature(methodNode); - expect(sig.parameterCount).toBe(1); - // The important thing is parameterCount is correct; returnType may vary. - }); - }); - - describe('Java', () => { - it('extracts params from a Java method', () => { - parser.setLanguage(Java); - const code = `class Foo { - public int add(int a, int b) { return a + b; } -}`; - const tree = parser.parse(code); - const classNode = tree.rootNode.child(0)!; - const classBody = classNode.childForFieldName('body')!; - const methodNode = classBody.namedChild(0)!; - - const sig = extractMethodSignature(methodNode); - expect(sig.parameterCount).toBe(2); - }); - - it('extracts zero params from no-arg Java method', () => { - parser.setLanguage(Java); - const code = `class Foo { - public void run() {} -}`; - const tree = parser.parse(code); - const classNode = tree.rootNode.child(0)!; - const classBody = classNode.childForFieldName('body')!; - const methodNode = classBody.namedChild(0)!; - - const sig = extractMethodSignature(methodNode); - expect(sig.parameterCount).toBe(0); - }); - - it('extracts parameterTypes for Java overloaded methods', () => { - parser.setLanguage(Java); - const code = `class Svc { - public User lookup(int id) { return null; } - public User lookup(String name) { return null; } - public void process(int code, String msg) {} -}`; - const tree = parser.parse(code); - const classBody = tree.rootNode.child(0)!.childForFieldName('body')!; - - const sig0 = extractMethodSignature(classBody.namedChild(0)!); - expect(sig0.parameterCount).toBe(1); - expect(sig0.parameterTypes).toEqual(['int']); - - const sig1 = extractMethodSignature(classBody.namedChild(1)!); - expect(sig1.parameterCount).toBe(1); - expect(sig1.parameterTypes).toEqual(['String']); - - const sig2 = extractMethodSignature(classBody.namedChild(2)!); - expect(sig2.parameterCount).toBe(2); - expect(sig2.parameterTypes).toEqual(['int', 'String']); - }); - }); - - describe('Kotlin', () => { - it('extracts params from a Kotlin function declaration', () => { - parser.setLanguage(Kotlin); - const code = `object OneArg { - fun writeAudit(message: String): String { - return message - } -}`; - const tree = parser.parse(code); - const objectNode = tree.rootNode.child(0)!; - const classBody = objectNode.namedChild(1)!; - const functionNode = classBody.namedChild(0)!; - - const sig = extractMethodSignature(functionNode); - expect(sig.parameterCount).toBe(1); - }); - - it('extracts zero params from a no-arg Kotlin function', () => { - parser.setLanguage(Kotlin); - const code = `object ZeroArg { - fun writeAudit(): String { - return "zero" - } -}`; - const tree = parser.parse(code); - const objectNode = tree.rootNode.child(0)!; - const classBody = objectNode.namedChild(1)!; - const functionNode = classBody.namedChild(0)!; - - const sig = extractMethodSignature(functionNode); - expect(sig.parameterCount).toBe(0); - }); - - it('extracts parameterTypes for Kotlin overloaded functions', () => { - parser.setLanguage(Kotlin); - const code = `class Svc { - fun lookup(id: Int): User? { return null } - fun lookup(name: String): User? { return null } -}`; - const tree = parser.parse(code); - const classBody = tree.rootNode.child(0)!.namedChild(1)!; - - const sig0 = extractMethodSignature(classBody.namedChild(0)!); - expect(sig0.parameterCount).toBe(1); - expect(sig0.parameterTypes).toEqual(['Int']); - - const sig1 = extractMethodSignature(classBody.namedChild(1)!); - expect(sig1.parameterCount).toBe(1); - expect(sig1.parameterTypes).toEqual(['String']); - }); - }); - - describe('C++', () => { - it('extracts params from a nested C++ declarator', () => { - parser.setLanguage(CPP); - const code = `inline const char* write_audit(const char* message) { - return message; -}`; - const tree = parser.parse(code); - const functionNode = tree.rootNode.namedChild(0)!; - - const sig = extractMethodSignature(functionNode); - expect(sig.parameterCount).toBe(1); - }); - - it('extracts zero params from a no-arg C++ function', () => { - parser.setLanguage(CPP); - const code = `inline const char* write_audit() { - return "zero"; -}`; - const tree = parser.parse(code); - const functionNode = tree.rootNode.namedChild(0)!; - - const sig = extractMethodSignature(functionNode); - expect(sig.parameterCount).toBe(0); - }); - - it('extracts parameterTypes for C++ overloaded functions', () => { - parser.setLanguage(CPP); - const code = `User* lookup(int id) { return nullptr; } -User* lookup(string name) { return nullptr; }`; - const tree = parser.parse(code); - - const sig0 = extractMethodSignature(tree.rootNode.namedChild(0)!); - expect(sig0.parameterCount).toBe(1); - expect(sig0.parameterTypes).toEqual(['int']); - - const sig1 = extractMethodSignature(tree.rootNode.namedChild(1)!); - expect(sig1.parameterCount).toBe(1); - expect(sig1.parameterTypes).toEqual(['string']); - }); - }); - - describe('C#', () => { - it('extracts params from a C# method', () => { - parser.setLanguage(CSharp); - const code = `class Foo { - public bool Check(string name, int count) { return true; } -}`; - const tree = parser.parse(code); - const classNode = tree.rootNode.child(0)!; - const classBody = classNode.childForFieldName('body')!; - const methodNode = classBody.namedChild(0)!; - - const sig = extractMethodSignature(methodNode); - expect(sig.parameterCount).toBe(2); - }); - - it('extracts parameterTypes for C# overloaded methods', () => { - parser.setLanguage(CSharp); - const code = `class Svc { - public User Lookup(int id) { return null; } - public User Lookup(string name) { return null; } -}`; - const tree = parser.parse(code); - const classBody = tree.rootNode.child(0)!.childForFieldName('body')!; - - const sig0 = extractMethodSignature(classBody.namedChild(0)!); - expect(sig0.parameterCount).toBe(1); - expect(sig0.parameterTypes).toEqual(['int']); - - const sig1 = extractMethodSignature(classBody.namedChild(1)!); - expect(sig1.parameterCount).toBe(1); - expect(sig1.parameterTypes).toEqual(['string']); - }); - - it('handles C# method with no params', () => { - parser.setLanguage(CSharp); - const code = `class Foo { - public void Execute() {} -}`; - const tree = parser.parse(code); - const classNode = tree.rootNode.child(0)!; - const classBody = classNode.childForFieldName('body')!; - const methodNode = classBody.namedChild(0)!; - - const sig = extractMethodSignature(methodNode); - expect(sig.parameterCount).toBe(0); - }); - - it('extracts return type from C# method', () => { - parser.setLanguage(CSharp); - const code = `class Svc { - public User GetUser(string name) { return null; } -}`; - const tree = parser.parse(code); - const classNode = tree.rootNode.child(0)!; - const classBody = classNode.childForFieldName('body')!; - const methodNode = classBody.namedChild(0)!; - - const sig = extractMethodSignature(methodNode); - expect(sig.returnType).toBe('User'); - }); - }); - - describe('Go', () => { - it('extracts params and single return type', () => { - parser.setLanguage(Go); - const code = `package main -func add(a int, b int) int { return a + b }`; - const tree = parser.parse(code); - const funcNode = tree.rootNode.namedChildren.find((c) => c.type === 'function_declaration')!; - - const sig = extractMethodSignature(funcNode); - expect(sig.parameterCount).toBe(2); - expect(sig.returnType).toBe('int'); - }); - - it('extracts multi-return type', () => { - parser.setLanguage(Go); - const code = `package main -func parse(s string) (string, error) { return s, nil }`; - const tree = parser.parse(code); - const funcNode = tree.rootNode.namedChildren.find((c) => c.type === 'function_declaration')!; - - const sig = extractMethodSignature(funcNode); - expect(sig.parameterCount).toBe(1); - expect(sig.returnType).toBe('string'); - }); - - it('handles no return type', () => { - parser.setLanguage(Go); - const code = `package main -func doSomething(x int) { }`; - const tree = parser.parse(code); - const funcNode = tree.rootNode.namedChildren.find((c) => c.type === 'function_declaration')!; - - const sig = extractMethodSignature(funcNode); - expect(sig.parameterCount).toBe(1); - expect(sig.returnType).toBeUndefined(); - }); - - it('marks variadic function with undefined parameterCount', () => { - parser.setLanguage(Go); - const code = `package main -func log(args ...string) int { return 0 }`; - const tree = parser.parse(code); - const funcNode = tree.rootNode.namedChildren.find((c) => c.type === 'function_declaration')!; - - const sig = extractMethodSignature(funcNode); - expect(sig.parameterCount).toBeUndefined(); - expect(sig.returnType).toBe('int'); - }); - }); - - describe('Rust', () => { - it('extracts return type from function', () => { - parser.setLanguage(Rust); - const code = `fn add(a: i32, b: i32) -> i32 { a + b }`; - const tree = parser.parse(code); - const funcNode = tree.rootNode.namedChild(0)!; - - const sig = extractMethodSignature(funcNode); - expect(sig.parameterCount).toBe(2); - expect(sig.returnType).toBe('i32'); - }); - }); - - describe('C++ return types', () => { - it('extracts primitive return type', () => { - parser.setLanguage(CPP); - const code = `int add(int a, int b) { return a + b; }`; - const tree = parser.parse(code); - const funcNode = tree.rootNode.namedChild(0)!; - - const sig = extractMethodSignature(funcNode); - expect(sig.parameterCount).toBe(2); - expect(sig.returnType).toBe('int'); - }); - - it('extracts qualified return type', () => { - parser.setLanguage(CPP); - const code = `std::string getName() { return ""; }`; - const tree = parser.parse(code); - const funcNode = tree.rootNode.namedChild(0)!; - - const sig = extractMethodSignature(funcNode); - expect(sig.parameterCount).toBe(0); - expect(sig.returnType).toBe('std::string'); - }); - - it('returns undefined returnType for void', () => { - parser.setLanguage(CPP); - const code = `void doNothing() { }`; - const tree = parser.parse(code); - const funcNode = tree.rootNode.namedChild(0)!; - - const sig = extractMethodSignature(funcNode); - expect(sig.returnType).toBeUndefined(); - }); - - it('marks variadic function with undefined parameterCount', () => { - parser.setLanguage(CPP); - const code = `int printf(const char* fmt, ...) { return 0; }`; - const tree = parser.parse(code); - const funcNode = tree.rootNode.namedChild(0)!; - - const sig = extractMethodSignature(funcNode); - expect(sig.parameterCount).toBeUndefined(); - expect(sig.returnType).toBe('int'); - }); - }); - - describe('variadic params', () => { - it('Java: marks varargs with undefined parameterCount', () => { - parser.setLanguage(Java); - const code = `class Foo { - public void log(String fmt, Object... args) {} -}`; - const tree = parser.parse(code); - const classNode = tree.rootNode.child(0)!; - const classBody = classNode.childForFieldName('body')!; - const methodNode = classBody.namedChild(0)!; - - const sig = extractMethodSignature(methodNode); - expect(sig.parameterCount).toBeUndefined(); - }); - - it('Python: marks *args with undefined parameterCount', () => { - parser.setLanguage(Python); - const code = `class Foo: - def log(self, fmt, *args): - pass`; - const tree = parser.parse(code); - const classNode = tree.rootNode.child(0)!; - const classBody = classNode.childForFieldName('body')!; - const methodNode = classBody.namedChild(0)!; - - const sig = extractMethodSignature(methodNode); - expect(sig.parameterCount).toBeUndefined(); - }); - - it('Python: marks **kwargs with undefined parameterCount', () => { - parser.setLanguage(Python); - const code = `class Foo: - def config(self, **kwargs): - pass`; - const tree = parser.parse(code); - const classNode = tree.rootNode.child(0)!; - const classBody = classNode.childForFieldName('body')!; - const methodNode = classBody.namedChild(0)!; - - const sig = extractMethodSignature(methodNode); - expect(sig.parameterCount).toBeUndefined(); - }); - - it('TypeScript: marks rest params with undefined parameterCount', () => { - parser.setLanguage(TypeScript.typescript); - const code = `function logEntry(...messages: string[]): void {}`; - const tree = parser.parse(code); - const funcNode = tree.rootNode.namedChild(0)!; - - const sig = extractMethodSignature(funcNode); - expect(sig.parameterCount).toBeUndefined(); - }); - - it('Kotlin: marks vararg with undefined parameterCount', () => { - parser.setLanguage(Kotlin); - const code = `object Foo { - fun log(vararg args: String) {} -}`; - const tree = parser.parse(code); - const objectNode = tree.rootNode.child(0)!; - const classBody = objectNode.namedChild(1)!; - const functionNode = classBody.namedChild(0)!; - - const sig = extractMethodSignature(functionNode); - expect(sig.parameterCount).toBeUndefined(); - }); - }); -});