refactor: remove extractMethodSignature — MethodExtractor is sole source of method metadata

Remove extractMethodSignature (270-line generic AST heuristic) and its
MethodSignature type. All 12 languages now use provider.methodExtractor
exclusively for parameterCount, parameterTypes, and returnType.

Key changes:
- Enhance PHP/Ruby/TS MethodExtractor configs with doc-comment return
  type parsing (PHPDoc @return, YARD @return, JSDoc @returns), moved
  from TypeConfig.extractReturnType post-hoc fallback
- Add function_definition/function_declaration/function_signature to
  PHP/TS/C# methodNodeTypes so extractFromNode covers top-level functions
- Remove extractMethodSignature fallback from definition phase in both
  parse-worker.ts and parsing-processor.ts
- Remove TypeConfig.extractReturnType post-hoc upgrade block
- Replace extractMethodSignature with lightweight countMethodParameters
  for findEnclosingFunctionId arity fallback
- Delete method-signature.test.ts (tested the removed function)
This commit is contained in:
Gergo Magyar 2026-04-04 16:21:25 +01:00
parent 7751af16bd
commit f309328372
11 changed files with 243 additions and 814 deletions

View file

@ -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 #<arity> 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 #<arity> 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}`);
}

View file

@ -187,6 +187,7 @@ export const csharpMethodConfig: MethodExtractionConfig = {
'destructor_declaration',
'operator_declaration',
'conversion_operator_declaration',
'local_function_statement',
],
bodyNodeTypes: ['declaration_list'],

View file

@ -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<string> = 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*</.test(type)) return type;
return undefined;
}
/**
* Walk backwards through preceding siblings of `node` to find a PHPDoc
* `@return Type` annotation. Skips `attribute_list` nodes (PHP 8 attributes).
*/
function extractPhpDocReturnType(node: SyntaxNode): string | undefined {
let sibling = node.previousSibling;
while (sibling) {
if (sibling.type === 'comment') {
const match = PHPDOC_RETURN_RE.exec(sibling.text);
if (match) return normalizePhpReturnType(match[1]);
} else if (sibling.isNamed && !PHPDOC_SKIP_NODE_TYPES.has(sibling.type)) {
break;
}
sibling = sibling.previousSibling;
}
return undefined;
}
const PHP_VIS = new Set<MethodVisibility>(['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) {

View file

@ -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<User>"
* -> "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<Symbol, User>
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<User>" -> "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;
},

View file

@ -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<User>`) 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<MethodExtractionConfig, 'language'> = {
// 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) {

View file

@ -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<string, unknown> = {};
@ -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 #<paramCount> to Method/Constructor IDs to disambiguate overloads.

View file

@ -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;

View file

@ -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, =, <literal> — 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;
};
// ============================================================================

View file

@ -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 #<arity> 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 #<arity> 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 #<paramCount> to Method/Constructor IDs to disambiguate overloads.

View file

@ -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');

View file

@ -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();
});
});
});