mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
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:
parent
7751af16bd
commit
f309328372
11 changed files with 243 additions and 814 deletions
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,6 +187,7 @@ export const csharpMethodConfig: MethodExtractionConfig = {
|
|||
'destructor_declaration',
|
||||
'operator_declaration',
|
||||
'conversion_operator_declaration',
|
||||
'local_function_statement',
|
||||
],
|
||||
bodyNodeTypes: ['declaration_list'],
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue