refactor: provider-routed function name extraction + remove countMethodParameters

Make ast-helpers.ts language-agnostic by moving all language-specific
logic into per-language providers:

extractFunctionName:
- Add optional extractFunctionName hook to LanguageProvider interface
- Move C/C++ declarator unwrapping to c-cpp.ts provider
- Move Swift init/deinit to swift.ts provider
- Move Rust impl_item to rust.ts provider
- Move TS arrow_function/function_expression to typescript.ts provider
- Move Ruby method/singleton_method to ruby.ts provider
- Replace 250-line monolithic switch with thin dispatcher + generic
  fallback (name field lookup + label inference from node type)
- Callers with provider available (parse-worker, call-processor) pass
  it for language-specific resolution; callers without (type-env) use
  generic fallback

countMethodParameters:
- Remove entirely from ast-helpers.ts (90 lines + PARAM_LIST_TYPES,
  VARIADIC_PARAM_TYPES constants)
- Replace 4 call sites with MethodExtractor-based arity via
  getMethodInfo (parse-worker) / extractFromNode (call-processor)
- When MethodExtractor doesn't produce arity, no #<N> suffix is
  appended (matches definition phase behavior)

ast-helpers.ts: 897 → 518 lines (-42%)
This commit is contained in:
Gergo Magyar 2026-04-04 16:45:47 +01:00
parent f309328372
commit 16f752413a
10 changed files with 380 additions and 367 deletions

View file

@ -13,7 +13,6 @@ import { yieldToEventLoop } from './utils/event-loop.js';
import {
FUNCTION_NODE_TYPES,
extractFunctionName,
countMethodParameters,
findEnclosingClassId,
findEnclosingClassInfo,
} from './utils/ast-helpers.js';
@ -235,7 +234,7 @@ const findEnclosingFunction = (
while (current) {
if (FUNCTION_NODE_TYPES.has(current.type)) {
const { funcName, label } = extractFunctionName(current);
const { funcName, label } = extractFunctionName(current, provider);
if (funcName) {
const resolved = ctx.resolve(funcName, filePath);
@ -265,9 +264,18 @@ const findEnclosingFunction = (
}
const classInfo = findEnclosingClassInfo(current, filePath);
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 ? countMethodParameters(current) : undefined;
// Include #<arity> suffix to match definition-phase Method/Constructor IDs.
// Use provider.methodExtractor.extractFromNode — same extractor as definition phase.
let arity: number | undefined;
if (finalLabel === 'Method' || finalLabel === 'Constructor') {
const language = getLanguageFromFilename(filePath);
const info = language
? provider.methodExtractor?.extractFromNode?.(current, { filePath, language })
: undefined;
if (info) {
arity = info.parameters.some((p) => p.isVariadic) ? undefined : info.parameters.length;
}
}
const arityTag = arity !== undefined ? `#${arity}` : '';
return generateId(finalLabel, `${filePath}:${qualifiedName}${arityTag}`);
}
@ -304,10 +312,18 @@ const findEnclosingFunction = (
const qualifiedName = classInfo
? `${classInfo.className}.${customResult.funcName}`
: customResult.funcName;
// Include #<arity> suffix to match definition-phase Method/Constructor IDs
// Include #<arity> suffix to match definition-phase Method/Constructor IDs.
const sigNode = current.previousSibling ?? current;
const needsArity2 = finalLabel === 'Method' || finalLabel === 'Constructor';
const arity2 = needsArity2 ? countMethodParameters(sigNode) : undefined;
let arity2: number | undefined;
if (finalLabel === 'Method' || finalLabel === 'Constructor') {
const language = getLanguageFromFilename(filePath);
const info = language
? provider.methodExtractor?.extractFromNode?.(sigNode, { filePath, language })
: undefined;
if (info) {
arity2 = info.parameters.some((p) => p.isVariadic) ? undefined : info.parameters.length;
}
}
const arityTag2 = arity2 !== undefined ? `#${arity2}` : '';
return generateId(finalLabel, `${filePath}:${qualifiedName}${arityTag2}`);
}
@ -850,7 +866,7 @@ export const processCalls = async (
let p = callNode.parent;
while (p) {
if (FUNCTION_NODE_TYPES.has(p.type)) {
const { funcName } = extractFunctionName(p);
const { funcName } = extractFunctionName(p, provider);
if (funcName) {
scope = `${funcName}@${p.startIndex}`;
break;

View file

@ -104,6 +104,17 @@ interface LanguageProviderConfig {
ancestorNode: SyntaxNode,
) => { funcName: string; label: NodeLabel } | null;
// ── Function name extraction ──────────────────────────────────────
/** Extract function name + label from an AST node during parent-walk.
* Languages with non-standard AST structures (e.g. C/C++ declarator
* unwrapping, Swift init/deinit, Rust impl_item) provide this hook
* to replace the generic name-field lookup.
* Return null to fall through to the generic extractor.
* Default: undefined (generic extraction from 'name' field). */
readonly extractFunctionName?: (
node: SyntaxNode,
) => { funcName: string | null; label: NodeLabel } | null;
// ── Labels ────────────────────────────────────────────────────────
/** Override the default node label for definition.function captures.
* Return null to skip (C/C++ duplicate), a different label to reclassify

View file

@ -15,7 +15,9 @@ import { cCppExportChecker } from '../export-detection.js';
import { resolveCImport, resolveCppImport } from '../import-resolvers/standard.js';
import { C_QUERIES, CPP_QUERIES } from '../tree-sitter-queries.js';
import { isCppInsideClassOrStruct } from '../utils/ast-helpers.js';
import { isCppInsideClassOrStruct, FUNCTION_DECLARATION_TYPES } from '../utils/ast-helpers.js';
import type { SyntaxNode } from '../utils/ast-helpers.js';
import type { NodeLabel } from 'gitnexus-shared';
import type { LanguageProvider } from '../language-provider.js';
import { createFieldExtractor } from '../field-extractors/generic.js';
import {
@ -132,6 +134,142 @@ const C_BUILT_INS: ReadonlySet<string> = new Set([
'put',
]);
/**
* C/C++ function name extraction — unwraps pointer_declarator / reference_declarator /
* function_declarator / qualified_identifier chains to find the actual function name.
* Handles field_identifier (method inside class body) and parenthesized_declarator.
*/
const cCppExtractFunctionName = (
node: SyntaxNode,
): { funcName: string | null; label: NodeLabel } | null => {
if (!FUNCTION_DECLARATION_TYPES.has(node.type)) return null;
let funcName: string | null = null;
let label: NodeLabel = 'Function';
// C/C++: function_definition -> [pointer_declarator ->] function_declarator -> qualified_identifier/identifier
// Unwrap pointer_declarator / reference_declarator wrappers to reach function_declarator
let declarator = node.childForFieldName?.('declarator');
if (!declarator) {
for (let i = 0; i < node.childCount; i++) {
const c = node.child(i);
if (c?.type === 'function_declarator') {
declarator = c;
break;
}
}
}
while (
declarator &&
(declarator.type === 'pointer_declarator' || declarator.type === 'reference_declarator')
) {
let nextDeclarator = declarator.childForFieldName?.('declarator');
if (!nextDeclarator) {
for (let i = 0; i < declarator.childCount; i++) {
const c = declarator.child(i);
if (
c?.type === 'function_declarator' ||
c?.type === 'pointer_declarator' ||
c?.type === 'reference_declarator'
) {
nextDeclarator = c;
break;
}
}
}
declarator = nextDeclarator;
}
if (declarator) {
let innerDeclarator = declarator.childForFieldName?.('declarator');
if (!innerDeclarator) {
for (let i = 0; i < declarator.childCount; i++) {
const c = declarator.child(i);
if (
c?.type === 'qualified_identifier' ||
c?.type === 'identifier' ||
c?.type === 'field_identifier' ||
c?.type === 'parenthesized_declarator'
) {
innerDeclarator = c;
break;
}
}
}
if (innerDeclarator?.type === 'qualified_identifier') {
let nameNode = innerDeclarator.childForFieldName?.('name');
if (!nameNode) {
for (let i = 0; i < innerDeclarator.childCount; i++) {
const c = innerDeclarator.child(i);
if (c?.type === 'identifier') {
nameNode = c;
break;
}
}
}
if (nameNode?.text) {
funcName = nameNode.text;
label = 'Method';
}
} else if (
innerDeclarator?.type === 'identifier' ||
innerDeclarator?.type === 'field_identifier'
) {
// field_identifier is used for method names inside C++ class bodies
funcName = innerDeclarator.text;
if (innerDeclarator.type === 'field_identifier') label = 'Method';
} else if (innerDeclarator?.type === 'parenthesized_declarator') {
let nestedId: SyntaxNode | null = null;
for (let i = 0; i < innerDeclarator.childCount; i++) {
const c = innerDeclarator.child(i);
if (c?.type === 'qualified_identifier' || c?.type === 'identifier') {
nestedId = c;
break;
}
}
if (nestedId?.type === 'qualified_identifier') {
let nameNode = nestedId.childForFieldName?.('name');
if (!nameNode) {
for (let i = 0; i < nestedId.childCount; i++) {
const c = nestedId.child(i);
if (c?.type === 'identifier') {
nameNode = c;
break;
}
}
}
if (nameNode?.text) {
funcName = nameNode.text;
label = 'Method';
}
} else if (nestedId?.type === 'identifier') {
funcName = nestedId.text;
}
}
}
// Fallback for other node types in FUNCTION_DECLARATION_TYPES (e.g. function_item for Rust in C++ tree)
if (!funcName) {
let nameNode = node.childForFieldName?.('name');
if (!nameNode) {
for (let i = 0; i < node.childCount; i++) {
const c = node.child(i);
if (
c?.type === 'identifier' ||
c?.type === 'property_identifier' ||
c?.type === 'simple_identifier'
) {
nameNode = c;
break;
}
}
}
funcName = nameNode?.text ?? null;
}
return { funcName, label };
};
/** Label override shared by C and C++: skip function_definition captures inside class/struct
* bodies (they're duplicates of definition.method captures). */
const cppLabelOverride: NonNullable<LanguageProvider['labelOverride']> = (
@ -152,6 +290,7 @@ export const cProvider = defineLanguage({
importSemantics: 'wildcard',
fieldExtractor: createFieldExtractor(cFieldConfig),
methodExtractor: createMethodExtractor(cMethodConfig),
extractFunctionName: cCppExtractFunctionName,
labelOverride: cppLabelOverride,
builtInNames: C_BUILT_INS,
});
@ -167,6 +306,7 @@ export const cppProvider = defineLanguage({
mroStrategy: 'leftmost-base',
fieldExtractor: createFieldExtractor(cppFieldConfig),
methodExtractor: createMethodExtractor(cppMethodConfig),
extractFunctionName: cCppExtractFunctionName,
labelOverride: cppLabelOverride,
builtInNames: C_BUILT_INS,
});

View file

@ -8,7 +8,9 @@
*/
import { SupportedLanguages } from 'gitnexus-shared';
import type { NodeLabel } from 'gitnexus-shared';
import { defineLanguage } from '../language-provider.js';
import type { SyntaxNode } from '../utils/ast-helpers.js';
import { typeConfig as rubyConfig } from '../type-extractors/ruby.js';
import { routeRubyCall } from '../call-routing.js';
import { rubyExportChecker } from '../export-detection.js';
@ -19,6 +21,25 @@ import { rubyConfig as rubyFieldConfig } from '../field-extractors/configs/ruby.
import { createMethodExtractor } from '../method-extractors/generic.js';
import { rubyMethodConfig } from '../method-extractors/configs/ruby.js';
/** Ruby method/singleton_method: extract name from 'name' field, label as Method. */
const rubyExtractFunctionName = (
node: SyntaxNode,
): { funcName: string | null; label: NodeLabel } | null => {
if (node.type !== 'method' && node.type !== 'singleton_method') return null;
let nameNode = node.childForFieldName?.('name');
if (!nameNode) {
for (let i = 0; i < node.childCount; i++) {
const c = node.child(i);
if (c?.type === 'identifier') {
nameNode = c;
break;
}
}
}
return { funcName: nameNode?.text ?? null, label: 'Method' };
};
const BUILT_INS: ReadonlySet<string> = new Set([
'puts',
'p',
@ -88,5 +109,6 @@ export const rubyProvider = defineLanguage({
importSemantics: 'wildcard',
fieldExtractor: createFieldExtractor(rubyFieldConfig),
methodExtractor: createMethodExtractor(rubyMethodConfig),
extractFunctionName: rubyExtractFunctionName,
builtInNames: BUILT_INS,
});

View file

@ -11,7 +11,9 @@
*/
import { SupportedLanguages } from 'gitnexus-shared';
import type { NodeLabel } from 'gitnexus-shared';
import { defineLanguage } from '../language-provider.js';
import type { SyntaxNode } from '../utils/ast-helpers.js';
import { typeConfig as rustConfig } from '../type-extractors/rust.js';
import { rustExportChecker } from '../export-detection.js';
import { resolveRustImport } from '../import-resolvers/rust.js';
@ -22,6 +24,35 @@ import { rustConfig as rustFieldConfig } from '../field-extractors/configs/rust.
import { createMethodExtractor } from '../method-extractors/generic.js';
import { rustMethodConfig } from '../method-extractors/configs/rust.js';
/** Rust impl_item: find the function_item child and extract its name as a Method. */
const rustExtractFunctionName = (
node: SyntaxNode,
): { funcName: string | null; label: NodeLabel } | null => {
if (node.type !== 'impl_item') return null;
let funcItem: SyntaxNode | null = null;
for (let i = 0; i < node.childCount; i++) {
const c = node.child(i);
if (c?.type === 'function_item') {
funcItem = c;
break;
}
}
if (!funcItem) return null;
let nameNode = funcItem.childForFieldName?.('name');
if (!nameNode) {
for (let i = 0; i < funcItem.childCount; i++) {
const c = funcItem.child(i);
if (c?.type === 'identifier') {
nameNode = c;
break;
}
}
}
return { funcName: nameNode?.text ?? null, label: 'Method' };
};
const BUILT_INS: ReadonlySet<string> = new Set([
'unwrap',
'expect',
@ -90,5 +121,6 @@ export const rustProvider = defineLanguage({
mroStrategy: 'qualified-syntax',
fieldExtractor: createFieldExtractor(rustFieldConfig),
methodExtractor: createMethodExtractor(rustMethodConfig),
extractFunctionName: rustExtractFunctionName,
builtInNames: BUILT_INS,
});

View file

@ -11,12 +11,14 @@
*/
import { SupportedLanguages } from 'gitnexus-shared';
import type { NodeLabel } from 'gitnexus-shared';
import { defineLanguage } from '../language-provider.js';
import { typeConfig as swiftConfig } from '../type-extractors/swift.js';
import { swiftExportChecker } from '../export-detection.js';
import { resolveSwiftImport } from '../import-resolvers/swift.js';
import { SWIFT_QUERIES } from '../tree-sitter-queries.js';
import type { SwiftPackageConfig } from '../language-config.js';
import type { SyntaxNode } from '../utils/ast-helpers.js';
import { createFieldExtractor } from '../field-extractors/generic.js';
import { swiftConfig as swiftFieldConfig } from '../field-extractors/configs/swift.js';
import { createMethodExtractor } from '../method-extractors/generic.js';
@ -109,6 +111,15 @@ function wireSwiftImplicitImports(
}
}
/** Swift init/deinit declarations have special names and Constructor label. */
const swiftExtractFunctionName = (
node: SyntaxNode,
): { funcName: string | null; label: NodeLabel } | null => {
if (node.type === 'init_declaration') return { funcName: 'init', label: 'Constructor' };
if (node.type === 'deinit_declaration') return { funcName: 'deinit', label: 'Constructor' };
return null; // fall through to generic
};
const BUILT_INS: ReadonlySet<string> = new Set([
'print',
'debugPrint',
@ -230,6 +241,7 @@ export const swiftProvider = defineLanguage({
heritageDefaultEdge: 'IMPLEMENTS',
fieldExtractor: createFieldExtractor(swiftFieldConfig),
methodExtractor: createMethodExtractor(swiftMethodConfig),
extractFunctionName: swiftExtractFunctionName,
implicitImportWirer: wireSwiftImplicitImports,
builtInNames: BUILT_INS,
});

View file

@ -8,7 +8,9 @@
*/
import { SupportedLanguages } from 'gitnexus-shared';
import type { NodeLabel } from 'gitnexus-shared';
import { defineLanguage } from '../language-provider.js';
import type { SyntaxNode } from '../utils/ast-helpers.js';
import { typeConfig as typescriptConfig } from '../type-extractors/typescript.js';
import { tsExportChecker } from '../export-detection.js';
import { resolveTypescriptImport, resolveJavascriptImport } from '../import-resolvers/standard.js';
@ -23,6 +25,31 @@ import {
javascriptMethodConfig,
} from '../method-extractors/configs/typescript-javascript.js';
/**
* TypeScript/JavaScript: arrow_function and function_expression get their name
* from the parent variable_declarator (e.g. `const foo = () => {}`).
*/
const tsExtractFunctionName = (
node: SyntaxNode,
): { funcName: string | null; label: NodeLabel } | null => {
if (node.type !== 'arrow_function' && node.type !== 'function_expression') return null;
const parent = node.parent;
if (parent?.type !== 'variable_declarator') return null;
let nameNode = parent.childForFieldName?.('name');
if (!nameNode) {
for (let i = 0; i < parent.childCount; i++) {
const c = parent.child(i);
if (c?.type === 'identifier') {
nameNode = c;
break;
}
}
}
return { funcName: nameNode?.text ?? null, label: 'Function' };
};
export const BUILT_INS: ReadonlySet<string> = new Set([
'console',
'log',
@ -130,6 +157,7 @@ export const typescriptProvider = defineLanguage({
namedBindingExtractor: extractTsNamedBindings,
fieldExtractor: typescriptFieldExtractor,
methodExtractor: createMethodExtractor(typescriptMethodConfig),
extractFunctionName: tsExtractFunctionName,
builtInNames: BUILT_INS,
});
@ -143,5 +171,6 @@ export const javascriptProvider = defineLanguage({
namedBindingExtractor: extractTsNamedBindings,
fieldExtractor: createFieldExtractor(javascriptConfig),
methodExtractor: createMethodExtractor(javascriptMethodConfig),
extractFunctionName: tsExtractFunctionName,
builtInNames: BUILT_INS,
});

View file

@ -386,234 +386,45 @@ export const findSiblingChild = (
/**
* Extract function name and label from a function_definition or similar AST node.
* Handles C/C++ qualified_identifier (ClassName::MethodName) and other language patterns.
*
* Thin dispatcher: delegates to provider.extractFunctionName when available,
* then falls back to generic name-field lookup for languages that don't need
* custom AST unwrapping.
*/
export const extractFunctionName = (
node: SyntaxNode,
provider?: {
extractFunctionName?: (
node: SyntaxNode,
) => { funcName: string | null; label: NodeLabel } | null;
},
): { funcName: string | null; label: NodeLabel } => {
// Provider hook — language-specific extraction (C/C++ declarator unwrapping,
// Swift init/deinit, Rust impl_item, TS arrow functions, Ruby methods)
if (provider?.extractFunctionName) {
const result = provider.extractFunctionName(node);
if (result) return result;
}
// Generic fallback: determine label from node type, then try 'name' field
let funcName: string | null = null;
let label: NodeLabel = 'Function';
// Swift init/deinit
if (node.type === 'init_declaration' || node.type === 'deinit_declaration') {
return {
funcName: node.type === 'init_declaration' ? 'init' : 'deinit',
label: 'Constructor',
};
if (
node.type === 'method_definition' ||
node.type === 'method_declaration' ||
node.type === 'method' ||
node.type === 'singleton_method'
) {
label = 'Method';
}
if (node.type === 'constructor_declaration' || node.type === 'compact_constructor_declaration') {
label = 'Constructor';
}
if (FUNCTION_DECLARATION_TYPES.has(node.type)) {
// C/C++: function_definition -> [pointer_declarator ->] function_declarator -> qualified_identifier/identifier
// Unwrap pointer_declarator / reference_declarator wrappers to reach function_declarator
let declarator = node.childForFieldName?.('declarator');
if (!declarator) {
for (let i = 0; i < node.childCount; i++) {
const c = node.child(i);
if (c?.type === 'function_declarator') {
declarator = c;
break;
}
}
}
while (
declarator &&
(declarator.type === 'pointer_declarator' || declarator.type === 'reference_declarator')
) {
let nextDeclarator = declarator.childForFieldName?.('declarator');
if (!nextDeclarator) {
for (let i = 0; i < declarator.childCount; i++) {
const c = declarator.child(i);
if (
c?.type === 'function_declarator' ||
c?.type === 'pointer_declarator' ||
c?.type === 'reference_declarator'
) {
nextDeclarator = c;
break;
}
}
}
declarator = nextDeclarator;
}
if (declarator) {
let innerDeclarator = declarator.childForFieldName?.('declarator');
if (!innerDeclarator) {
for (let i = 0; i < declarator.childCount; i++) {
const c = declarator.child(i);
if (
c?.type === 'qualified_identifier' ||
c?.type === 'identifier' ||
c?.type === 'field_identifier' ||
c?.type === 'parenthesized_declarator'
) {
innerDeclarator = c;
break;
}
}
}
if (innerDeclarator?.type === 'qualified_identifier') {
let nameNode = innerDeclarator.childForFieldName?.('name');
if (!nameNode) {
for (let i = 0; i < innerDeclarator.childCount; i++) {
const c = innerDeclarator.child(i);
if (c?.type === 'identifier') {
nameNode = c;
break;
}
}
}
if (nameNode?.text) {
funcName = nameNode.text;
label = 'Method';
}
} else if (
innerDeclarator?.type === 'identifier' ||
innerDeclarator?.type === 'field_identifier'
) {
// field_identifier is used for method names inside C++ class bodies
funcName = innerDeclarator.text;
if (innerDeclarator.type === 'field_identifier') label = 'Method';
} else if (innerDeclarator?.type === 'parenthesized_declarator') {
let nestedId: SyntaxNode | null = null;
for (let i = 0; i < innerDeclarator.childCount; i++) {
const c = innerDeclarator.child(i);
if (c?.type === 'qualified_identifier' || c?.type === 'identifier') {
nestedId = c;
break;
}
}
if (nestedId?.type === 'qualified_identifier') {
let nameNode = nestedId.childForFieldName?.('name');
if (!nameNode) {
for (let i = 0; i < nestedId.childCount; i++) {
const c = nestedId.child(i);
if (c?.type === 'identifier') {
nameNode = c;
break;
}
}
}
if (nameNode?.text) {
funcName = nameNode.text;
label = 'Method';
}
} else if (nestedId?.type === 'identifier') {
funcName = nestedId.text;
}
}
}
// Fallback for other languages (Kotlin uses simple_identifier, Swift uses simple_identifier)
if (!funcName) {
let nameNode = node.childForFieldName?.('name');
if (!nameNode) {
for (let i = 0; i < node.childCount; i++) {
const c = node.child(i);
if (
c?.type === 'identifier' ||
c?.type === 'property_identifier' ||
c?.type === 'simple_identifier'
) {
nameNode = c;
break;
}
}
}
funcName = nameNode?.text;
}
} else if (node.type === 'impl_item') {
let funcItem: SyntaxNode | null = null;
for (let i = 0; i < node.childCount; i++) {
const c = node.child(i);
if (c?.type === 'function_item') {
funcItem = c;
break;
}
}
if (funcItem) {
let nameNode = funcItem.childForFieldName?.('name');
if (!nameNode) {
for (let i = 0; i < funcItem.childCount; i++) {
const c = funcItem.child(i);
if (c?.type === 'identifier') {
nameNode = c;
break;
}
}
}
funcName = nameNode?.text;
label = 'Method';
}
} else if (node.type === 'method_definition') {
let nameNode = node.childForFieldName?.('name');
if (!nameNode) {
for (let i = 0; i < node.childCount; i++) {
const c = node.child(i);
if (c?.type === 'property_identifier') {
nameNode = c;
break;
}
}
}
funcName = nameNode?.text;
// Dart: method_signature wraps function_signature — unwrap to reach the name
if (node.type === 'method_signature') {
label = 'Method';
} else if (node.type === 'method_declaration' || node.type === 'constructor_declaration') {
let nameNode = node.childForFieldName?.('name');
if (!nameNode) {
for (let i = 0; i < node.childCount; i++) {
const c = node.child(i);
if (c?.type === 'identifier') {
nameNode = c;
break;
}
}
}
funcName = nameNode?.text;
label = 'Method';
} else if (node.type === 'arrow_function' || node.type === 'function_expression') {
const parent = node.parent;
if (parent?.type === 'variable_declarator') {
let nameNode = parent.childForFieldName?.('name');
if (!nameNode) {
for (let i = 0; i < parent.childCount; i++) {
const c = parent.child(i);
if (c?.type === 'identifier') {
nameNode = c;
break;
}
}
}
funcName = nameNode?.text;
}
} else if (node.type === 'method' || node.type === 'singleton_method') {
let nameNode = node.childForFieldName?.('name');
if (!nameNode) {
for (let i = 0; i < node.childCount; i++) {
const c = node.child(i);
if (c?.type === 'identifier') {
nameNode = c;
break;
}
}
}
funcName = nameNode?.text;
label = 'Method';
} else if (node.type === 'function_signature') {
// Dart: top-level function signatures
let nameNode = node.childForFieldName?.('name');
if (!nameNode) {
for (let i = 0; i < node.childCount; i++) {
const c = node.child(i);
if (c?.type === 'identifier') {
nameNode = c;
break;
}
}
}
funcName = nameNode?.text ?? null;
} else if (node.type === 'method_signature') {
// Dart: method_signature wraps function_signature
let funcSig: SyntaxNode | null = null;
for (let i = 0; i < node.childCount; i++) {
const c = node.child(i);
@ -635,129 +446,34 @@ export const extractFunctionName = (
}
funcName = nameNode?.text ?? null;
}
label = 'Method';
return { funcName, label };
}
// Try 'name' field first (works for most languages: Go, Python, PHP, Java, Kotlin,
// C#, Dart function_signature, Swift simple_identifier, generic function_declaration)
let nameNode = node.childForFieldName?.('name');
if (!nameNode) {
// Fallback: scan for common identifier child types
for (let i = 0; i < node.childCount; i++) {
const c = node.child(i);
if (
c?.type === 'identifier' ||
c?.type === 'property_identifier' ||
c?.type === 'simple_identifier'
) {
nameNode = c;
break;
}
}
}
funcName = nameNode?.text ?? null;
return { funcName, label };
};
/** 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
]);
/**
* 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 countMethodParameters = (node: SyntaxNode | null | undefined): number | undefined => {
if (!node) return 0;
const findParamList = (current: SyntaxNode): SyntaxNode | null => {
for (const child of current.children) {
if (PARAM_LIST_TYPES.has(child.type)) return child;
}
for (const child of current.children) {
const nested = findParamList(child);
if (nested) return nested;
}
return null;
};
const parameterList = PARAM_LIST_TYPES.has(node.type)
? node
: (node.childForFieldName?.('parameters') ?? findParamList(node));
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 (
param.text === 'self' ||
param.text === '&self' ||
param.text === '&mut self' ||
param.type === 'self_parameter'
)
continue;
// 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;
}
// 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;
if (VARIADIC_PARAM_TYPES.has(param.type)) {
isVariadic = true;
continue;
}
// TS/JS rest parameter
if (param.type === 'required_parameter' || param.type === 'optional_parameter') {
for (const child of param.children) {
if (child.type === 'rest_pattern') {
isVariadic = true;
break;
}
}
if (isVariadic) continue;
}
// 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;
}
}
count++;
}
// C/C++: bare `...` token
if (!isVariadic) {
for (const child of parameterList.children) {
if (!child.isNamed && child.text === '...') {
isVariadic = true;
break;
}
}
}
}
// Swift fallback: parameter nodes as direct children of function_declaration
if (!parameterList && count === 0) {
for (const child of node.namedChildren) {
if (child.type === 'parameter') count++;
}
}
return isVariadic ? undefined : count;
};
// ============================================================================
// Generic AST traversal helpers (shared by parse-worker + php-helpers)
// ============================================================================

View file

@ -46,7 +46,6 @@ import {
findEnclosingClassInfo,
type EnclosingClassInfo,
getLabelFromCaptures,
countMethodParameters,
findDescendant,
extractStringContent,
type SyntaxNode,
@ -526,7 +525,7 @@ const findEnclosingFunctionId = (
let current = node.parent;
while (current) {
if (FUNCTION_NODE_TYPES.has(current.type)) {
const { funcName, label } = extractFunctionName(current);
const { funcName, label } = extractFunctionName(current, provider);
if (funcName) {
// Apply labelOverride so label matches definition phase (e.g., Kotlin Function→Method).
// null means "skip as definition" — keep original label for scope identification.
@ -538,9 +537,26 @@ const findEnclosingFunctionId = (
// Qualify with enclosing class to match definition-phase node IDs
const classInfo = cachedFindEnclosingClassInfo(current, filePath);
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 ? countMethodParameters(current) : undefined;
// Include #<arity> suffix to match definition-phase Method/Constructor IDs.
// Use the same MethodExtractor (getMethodInfo) as the definition phase.
let arity: number | undefined;
if (finalLabel === 'Method' || finalLabel === 'Constructor') {
const classNode =
findEnclosingClassNode(current) ?? findClassNodeByQualifiedName(current);
if (classNode) {
const methodMap = getMethodInfo(classNode, provider, {
filePath,
language: getLanguageFromFilename(filePath),
});
const defLine = current.startPosition.row + 1;
const info = methodMap?.get(`${funcName}:${defLine}`);
if (info) {
arity = info.parameters.some((p) => p.isVariadic)
? undefined
: info.parameters.length;
}
}
}
const arityTag = arity !== undefined ? `#${arity}` : '';
const result = generateId(finalLabel, `${filePath}:${qualifiedName}${arityTag}`);
functionIdCache.set(node, result);
@ -566,10 +582,26 @@ const findEnclosingFunctionId = (
const qualifiedName = classInfo
? `${classInfo.className}.${customResult.funcName}`
: customResult.funcName;
// Include #<arity> suffix to match definition-phase Method/Constructor IDs
// Include #<arity> suffix to match definition-phase Method/Constructor IDs.
const sigNode = current.previousSibling ?? current;
const needsArity2 = finalLabel === 'Method' || finalLabel === 'Constructor';
const arity2 = needsArity2 ? countMethodParameters(sigNode) : undefined;
let arity2: number | undefined;
if (finalLabel === 'Method' || finalLabel === 'Constructor') {
const classNode2 =
findEnclosingClassNode(sigNode) ?? findClassNodeByQualifiedName(sigNode);
if (classNode2) {
const methodMap2 = getMethodInfo(classNode2, provider, {
filePath,
language: getLanguageFromFilename(filePath),
});
const defLine2 = sigNode.startPosition.row + 1;
const info2 = methodMap2?.get(`${customResult.funcName}:${defLine2}`);
if (info2) {
arity2 = info2.parameters.some((p) => p.isVariadic)
? undefined
: info2.parameters.length;
}
}
}
const arityTag2 = arity2 !== undefined ? `#${arity2}` : '';
const result = generateId(finalLabel, `${filePath}:${qualifiedName}${arityTag2}`);
functionIdCache.set(node, result);

View file

@ -343,6 +343,9 @@ describe('isBuiltInOrNoise', () => {
describe('extractFunctionName', () => {
const parser = new Parser();
const cProvider = getProvider(SupportedLanguages.C);
const cppProvider = getProvider(SupportedLanguages.CPlusPlus);
const tsProvider = getProvider(SupportedLanguages.TypeScript);
describe('C', () => {
it('extracts function name from C function definition', () => {
@ -351,7 +354,7 @@ describe('extractFunctionName', () => {
const tree = parser.parse(code);
const funcNode = tree.rootNode.child(0);
const result = extractFunctionName(funcNode);
const result = extractFunctionName(funcNode, cProvider);
expect(result.funcName).toBe('main');
expect(result.label).toBe('Function');
@ -363,7 +366,7 @@ describe('extractFunctionName', () => {
const tree = parser.parse(code);
const funcNode = tree.rootNode.child(0);
const result = extractFunctionName(funcNode);
const result = extractFunctionName(funcNode, cProvider);
expect(result.funcName).toBe('helper');
expect(result.label).toBe('Function');
@ -377,7 +380,7 @@ describe('extractFunctionName', () => {
const tree = parser.parse(code);
const funcNode = tree.rootNode.child(0);
const result = extractFunctionName(funcNode);
const result = extractFunctionName(funcNode, cppProvider);
expect(result.funcName).toBe('OnEncryptData');
expect(result.label).toBe('Method');
@ -389,7 +392,7 @@ describe('extractFunctionName', () => {
const tree = parser.parse(code);
const funcNode = tree.rootNode.child(0);
const result = extractFunctionName(funcNode);
const result = extractFunctionName(funcNode, cppProvider);
expect(result.funcName).toBe('OnDataOprEvent');
expect(result.label).toBe('Method');
@ -401,7 +404,7 @@ describe('extractFunctionName', () => {
const tree = parser.parse(code);
const funcNode = tree.rootNode.child(0);
const result = extractFunctionName(funcNode);
const result = extractFunctionName(funcNode, cppProvider);
expect(result.funcName).toBe('standalone_function');
expect(result.label).toBe('Function');
@ -413,7 +416,7 @@ describe('extractFunctionName', () => {
const tree = parser.parse(code);
const funcNode = tree.rootNode.child(0);
const result = extractFunctionName(funcNode);
const result = extractFunctionName(funcNode, cppProvider);
expect(result.funcName).toBe('handler');
expect(result.label).toBe('Method');
@ -427,7 +430,7 @@ describe('extractFunctionName', () => {
const tree = parser.parse(code);
const funcNode = tree.rootNode.child(0);
const result = extractFunctionName(funcNode);
const result = extractFunctionName(funcNode, cProvider);
expect(result.funcName).toBe('get_data');
expect(result.label).toBe('Function');
@ -439,7 +442,7 @@ describe('extractFunctionName', () => {
const tree = parser.parse(code);
const funcNode = tree.rootNode.child(0);
const result = extractFunctionName(funcNode);
const result = extractFunctionName(funcNode, cProvider);
expect(result.funcName).toBe('get_strings');
expect(result.label).toBe('Function');
@ -451,7 +454,7 @@ describe('extractFunctionName', () => {
const tree = parser.parse(code);
const funcNode = tree.rootNode.child(0);
const result = extractFunctionName(funcNode);
const result = extractFunctionName(funcNode, cProvider);
expect(result.funcName).toBe('create_node');
expect(result.label).toBe('Function');
@ -465,7 +468,7 @@ describe('extractFunctionName', () => {
const tree = parser.parse(code);
const funcNode = tree.rootNode.child(0);
const result = extractFunctionName(funcNode);
const result = extractFunctionName(funcNode, cppProvider);
expect(result.funcName).toBe('getData');
expect(result.label).toBe('Method');
@ -477,7 +480,7 @@ describe('extractFunctionName', () => {
const tree = parser.parse(code);
const funcNode = tree.rootNode.child(0);
const result = extractFunctionName(funcNode);
const result = extractFunctionName(funcNode, cppProvider);
expect(result.funcName).toBe('get_name');
expect(result.label).toBe('Function');
@ -489,7 +492,7 @@ describe('extractFunctionName', () => {
const tree = parser.parse(code);
const funcNode = tree.rootNode.child(0);
const result = extractFunctionName(funcNode);
const result = extractFunctionName(funcNode, cppProvider);
expect(result.funcName).toBe('at');
expect(result.label).toBe('Method');
@ -501,7 +504,7 @@ describe('extractFunctionName', () => {
const tree = parser.parse(code);
const funcNode = tree.rootNode.child(0);
const result = extractFunctionName(funcNode);
const result = extractFunctionName(funcNode, cppProvider);
expect(result.funcName).toBe('getName');
expect(result.label).toBe('Method');
@ -515,7 +518,7 @@ describe('extractFunctionName', () => {
const tree = parser.parse(code);
const funcNode = tree.rootNode.child(0);
const result = extractFunctionName(funcNode);
const result = extractFunctionName(funcNode, cppProvider);
// destructor_name includes the ~ prefix
expect(result.funcName).toBe('~MyClass');
@ -533,7 +536,7 @@ describe('extractFunctionName', () => {
const declarator = varDecl!.namedChild(0);
const arrowFunc = declarator!.namedChild(1);
const result = extractFunctionName(arrowFunc);
const result = extractFunctionName(arrowFunc, tsProvider);
expect(result.funcName).toBe('myHandler');
expect(result.label).toBe('Function');
@ -548,7 +551,7 @@ describe('extractFunctionName', () => {
const declarator = varDecl!.namedChild(0);
const funcExpr = declarator!.namedChild(1);
const result = extractFunctionName(funcExpr);
const result = extractFunctionName(funcExpr, tsProvider);
expect(result.funcName).toBe('processItem');
expect(result.label).toBe('Function');