diff --git a/gitnexus/src/core/ingestion/call-processor.ts b/gitnexus/src/core/ingestion/call-processor.ts index dfb274fc8..a478f3855 100644 --- a/gitnexus/src/core/ingestion/call-processor.ts +++ b/gitnexus/src/core/ingestion/call-processor.ts @@ -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 # suffix to match definition-phase Method/Constructor IDs - const needsArity = finalLabel === 'Method' || finalLabel === 'Constructor'; - const arity = needsArity ? countMethodParameters(current) : undefined; + // Include # 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 # suffix to match definition-phase Method/Constructor IDs + // Include # 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; diff --git a/gitnexus/src/core/ingestion/language-provider.ts b/gitnexus/src/core/ingestion/language-provider.ts index 070a4acb0..5b2744d64 100644 --- a/gitnexus/src/core/ingestion/language-provider.ts +++ b/gitnexus/src/core/ingestion/language-provider.ts @@ -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 diff --git a/gitnexus/src/core/ingestion/languages/c-cpp.ts b/gitnexus/src/core/ingestion/languages/c-cpp.ts index 87bd3a7bf..7badd296a 100644 --- a/gitnexus/src/core/ingestion/languages/c-cpp.ts +++ b/gitnexus/src/core/ingestion/languages/c-cpp.ts @@ -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 = 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 = ( @@ -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, }); diff --git a/gitnexus/src/core/ingestion/languages/ruby.ts b/gitnexus/src/core/ingestion/languages/ruby.ts index cff82fc1d..c17f3a819 100644 --- a/gitnexus/src/core/ingestion/languages/ruby.ts +++ b/gitnexus/src/core/ingestion/languages/ruby.ts @@ -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 = 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, }); diff --git a/gitnexus/src/core/ingestion/languages/rust.ts b/gitnexus/src/core/ingestion/languages/rust.ts index 8dc4ea30b..e2764c5d8 100644 --- a/gitnexus/src/core/ingestion/languages/rust.ts +++ b/gitnexus/src/core/ingestion/languages/rust.ts @@ -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 = 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, }); diff --git a/gitnexus/src/core/ingestion/languages/swift.ts b/gitnexus/src/core/ingestion/languages/swift.ts index d9fb77e8f..2c0301c08 100644 --- a/gitnexus/src/core/ingestion/languages/swift.ts +++ b/gitnexus/src/core/ingestion/languages/swift.ts @@ -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 = 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, }); diff --git a/gitnexus/src/core/ingestion/languages/typescript.ts b/gitnexus/src/core/ingestion/languages/typescript.ts index 80cf1ad8d..0e6c872c0 100644 --- a/gitnexus/src/core/ingestion/languages/typescript.ts +++ b/gitnexus/src/core/ingestion/languages/typescript.ts @@ -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 = 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, }); diff --git a/gitnexus/src/core/ingestion/utils/ast-helpers.ts b/gitnexus/src/core/ingestion/utils/ast-helpers.ts index e0fc221ec..707cc3ddc 100644 --- a/gitnexus/src/core/ingestion/utils/ast-helpers.ts +++ b/gitnexus/src/core/ingestion/utils/ast-helpers.ts @@ -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) // ============================================================================ diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index ea6b3732f..ec9bd7121 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -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 # suffix to match definition-phase Method/Constructor IDs - const needsArity = finalLabel === 'Method' || finalLabel === 'Constructor'; - const arity = needsArity ? countMethodParameters(current) : undefined; + // Include # 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 # suffix to match definition-phase Method/Constructor IDs + // Include # 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); diff --git a/gitnexus/test/unit/ingestion-utils.test.ts b/gitnexus/test/unit/ingestion-utils.test.ts index a2f2c9e82..9dfcbd80e 100644 --- a/gitnexus/test/unit/ingestion-utils.test.ts +++ b/gitnexus/test/unit/ingestion-utils.test.ts @@ -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');