From 739de2f64584b453722cd6b091bcb1d2ddee48f4 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Sat, 4 Apr 2026 17:06:04 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20eliminate=20extractFunctionName=20f?= =?UTF-8?q?rom=20ast-helpers=20=E2=80=94=20MethodExtractor=20owns=20all=20?= =?UTF-8?q?extraction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete removal of extractFunctionName from ast-helpers.ts. Function name extraction is now exclusively owned by provider.methodExtractor: - Add extractFunctionName to MethodExtractor interface and config - Move from LanguageProvider to MethodExtractor for all 6 providers (C++, Swift, Rust, TS/JS, Ruby; generic fallback for others) - Thread extractFunctionName through buildTypeEnv options so type-env.ts can access provider-specific extraction without importing from ast-helpers - Inline FUNCTION_DECLARATION_TYPES into C++ provider (sole consumer) - Delete extractFunctionName function and FUNCTION_DECLARATION_TYPES from ast-helpers.ts ast-helpers.ts: 518 → 420 lines. Now contains only shared constants (FUNCTION_NODE_TYPES, CALL_ARGUMENT_LIST_TYPES, CLASS_CONTAINER_TYPES) and generic traversal helpers — zero language-specific extraction logic. --- gitnexus/src/core/ingestion/call-processor.ts | 43 +++++++- .../src/core/ingestion/language-provider.ts | 11 --- .../src/core/ingestion/languages/c-cpp.ts | 26 ++++- gitnexus/src/core/ingestion/languages/dart.ts | 22 ++++- gitnexus/src/core/ingestion/languages/ruby.ts | 6 +- gitnexus/src/core/ingestion/languages/rust.ts | 6 +- .../src/core/ingestion/languages/swift.ts | 6 +- .../core/ingestion/languages/typescript.ts | 12 ++- .../ingestion/method-extractors/generic.ts | 2 + gitnexus/src/core/ingestion/method-types.ts | 13 +++ .../src/core/ingestion/parsing-processor.ts | 5 +- gitnexus/src/core/ingestion/type-env.ts | 46 ++++++++- .../src/core/ingestion/utils/ast-helpers.ts | 99 ------------------- .../core/ingestion/workers/parse-worker.ts | 40 +++++++- gitnexus/test/unit/ingestion-utils.test.ts | 18 +++- 15 files changed, 213 insertions(+), 142 deletions(-) diff --git a/gitnexus/src/core/ingestion/call-processor.ts b/gitnexus/src/core/ingestion/call-processor.ts index a478f3855..479031f19 100644 --- a/gitnexus/src/core/ingestion/call-processor.ts +++ b/gitnexus/src/core/ingestion/call-processor.ts @@ -12,7 +12,6 @@ import { isVerboseIngestionEnabled } from './utils/verbose.js'; import { yieldToEventLoop } from './utils/event-loop.js'; import { FUNCTION_NODE_TYPES, - extractFunctionName, findEnclosingClassId, findEnclosingClassInfo, } from './utils/ast-helpers.js'; @@ -220,6 +219,40 @@ const TYPE_PRESERVING_METHODS = new Set([ 'orElseThrow', // Java Optional ]); +/** Generic name extraction from a function-like AST node. */ +const genericFuncName = (node: SyntaxNode): string | null => { + const nameField = node.childForFieldName?.('name'); + if (nameField) return nameField.text; + 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' + ) + return c.text; + } + return null; +}; + +/** Infer node label from AST node type for function-like nodes without a provider hook. */ +const METHOD_NODE_TYPES = new Set([ + 'method_definition', + 'method_declaration', + 'method', + 'singleton_method', +]); +const CONSTRUCTOR_NODE_TYPES = new Set([ + 'constructor_declaration', + 'compact_constructor_declaration', +]); +const inferFunctionLabel = (nodeType: string): import('gitnexus-shared').NodeLabel => + METHOD_NODE_TYPES.has(nodeType) + ? 'Method' + : CONSTRUCTOR_NODE_TYPES.has(nodeType) + ? 'Constructor' + : 'Function'; + /** * Walk up the AST from a node to find the enclosing function/method. * Returns null if the call is at module/file level (top-level code). @@ -234,7 +267,9 @@ const findEnclosingFunction = ( while (current) { if (FUNCTION_NODE_TYPES.has(current.type)) { - const { funcName, label } = extractFunctionName(current, provider); + const efnResult = provider.methodExtractor?.extractFunctionName?.(current); + const funcName = efnResult?.funcName ?? genericFuncName(current); + const label = efnResult?.label ?? inferFunctionLabel(current.type); if (funcName) { const resolved = ctx.resolve(funcName, filePath); @@ -626,6 +661,7 @@ export const processCalls = async ( importedReturnTypes, importedRawReturnTypes, enclosingFunctionFinder: provider?.enclosingFunctionFinder, + extractFunctionName: provider?.methodExtractor?.extractFunctionName, }); if (typeEnv && exportedTypeMap) { const fileExports = collectExportedBindings(typeEnv, file.path, ctx.symbols, graph); @@ -866,7 +902,8 @@ export const processCalls = async ( let p = callNode.parent; while (p) { if (FUNCTION_NODE_TYPES.has(p.type)) { - const { funcName } = extractFunctionName(p, provider); + const funcName = + provider.methodExtractor?.extractFunctionName?.(p)?.funcName ?? genericFuncName(p); 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 5b2744d64..070a4acb0 100644 --- a/gitnexus/src/core/ingestion/language-provider.ts +++ b/gitnexus/src/core/ingestion/language-provider.ts @@ -104,17 +104,6 @@ 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 7badd296a..a19b54234 100644 --- a/gitnexus/src/core/ingestion/languages/c-cpp.ts +++ b/gitnexus/src/core/ingestion/languages/c-cpp.ts @@ -15,7 +15,19 @@ 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, FUNCTION_DECLARATION_TYPES } from '../utils/ast-helpers.js'; +import { isCppInsideClassOrStruct } from '../utils/ast-helpers.js'; + +/** + * Node types for standard function declarations that need C/C++ declarator handling. + * Used by cCppExtractFunctionName to determine how to extract the function name. + */ +const FUNCTION_DECLARATION_TYPES = new Set([ + 'function_declaration', + 'function_definition', + 'async_function_declaration', + 'generator_function_declaration', + 'function_item', +]); import type { SyntaxNode } from '../utils/ast-helpers.js'; import type { NodeLabel } from 'gitnexus-shared'; import type { LanguageProvider } from '../language-provider.js'; @@ -289,8 +301,10 @@ export const cProvider = defineLanguage({ importResolver: resolveCImport, importSemantics: 'wildcard', fieldExtractor: createFieldExtractor(cFieldConfig), - methodExtractor: createMethodExtractor(cMethodConfig), - extractFunctionName: cCppExtractFunctionName, + methodExtractor: createMethodExtractor({ + ...cMethodConfig, + extractFunctionName: cCppExtractFunctionName, + }), labelOverride: cppLabelOverride, builtInNames: C_BUILT_INS, }); @@ -305,8 +319,10 @@ export const cppProvider = defineLanguage({ importSemantics: 'wildcard', mroStrategy: 'leftmost-base', fieldExtractor: createFieldExtractor(cppFieldConfig), - methodExtractor: createMethodExtractor(cppMethodConfig), - extractFunctionName: cCppExtractFunctionName, + methodExtractor: createMethodExtractor({ + ...cppMethodConfig, + extractFunctionName: cCppExtractFunctionName, + }), labelOverride: cppLabelOverride, builtInNames: C_BUILT_INS, }); diff --git a/gitnexus/src/core/ingestion/languages/dart.ts b/gitnexus/src/core/ingestion/languages/dart.ts index 808f4f157..cdd53c8d8 100644 --- a/gitnexus/src/core/ingestion/languages/dart.ts +++ b/gitnexus/src/core/ingestion/languages/dart.ts @@ -12,7 +12,7 @@ import type { SyntaxNode } from '../utils/ast-helpers.js'; import type { NodeLabel } from 'gitnexus-shared'; -import { FUNCTION_NODE_TYPES, extractFunctionName } from '../utils/ast-helpers.js'; +import { FUNCTION_NODE_TYPES } from '../utils/ast-helpers.js'; import { SupportedLanguages } from 'gitnexus-shared'; import { defineLanguage } from '../language-provider.js'; import { typeConfig as dartConfig } from '../type-extractors/dart.js'; @@ -30,8 +30,8 @@ import { dartMethodConfig } from '../method-extractors/configs/dart.js'; * function_body are siblings under program or class_body, unlike most languages * where the function declaration wraps both. * - * Delegates name extraction to the shared `extractFunctionName` which already - * handles Dart's function_signature and method_signature node types. + * Extracts the function name inline — Dart uses function_signature and + * method_signature (which wraps function_signature) as its FUNCTION_NODE_TYPES. */ const dartEnclosingFunctionFinder = ( node: SyntaxNode, @@ -39,7 +39,21 @@ const dartEnclosingFunctionFinder = ( if (node.type !== 'function_body') return null; const prev = node.previousSibling; if (!prev || !FUNCTION_NODE_TYPES.has(prev.type)) return null; - const { funcName, label } = extractFunctionName(prev); + + // method_signature wraps function_signature — unwrap to reach the name + let target = prev; + let label: NodeLabel = 'Function'; + if (prev.type === 'method_signature') { + label = 'Method'; + for (let i = 0; i < prev.childCount; i++) { + const c = prev.child(i); + if (c?.type === 'function_signature') { + target = c; + break; + } + } + } + const funcName = target.childForFieldName?.('name')?.text ?? null; return funcName ? { funcName, label } : null; }; diff --git a/gitnexus/src/core/ingestion/languages/ruby.ts b/gitnexus/src/core/ingestion/languages/ruby.ts index c17f3a819..8b488fafb 100644 --- a/gitnexus/src/core/ingestion/languages/ruby.ts +++ b/gitnexus/src/core/ingestion/languages/ruby.ts @@ -108,7 +108,9 @@ export const rubyProvider = defineLanguage({ callRouter: routeRubyCall, importSemantics: 'wildcard', fieldExtractor: createFieldExtractor(rubyFieldConfig), - methodExtractor: createMethodExtractor(rubyMethodConfig), - extractFunctionName: rubyExtractFunctionName, + 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 e2764c5d8..db8e2ad5b 100644 --- a/gitnexus/src/core/ingestion/languages/rust.ts +++ b/gitnexus/src/core/ingestion/languages/rust.ts @@ -120,7 +120,9 @@ export const rustProvider = defineLanguage({ namedBindingExtractor: extractRustNamedBindings, mroStrategy: 'qualified-syntax', fieldExtractor: createFieldExtractor(rustFieldConfig), - methodExtractor: createMethodExtractor(rustMethodConfig), - extractFunctionName: rustExtractFunctionName, + 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 2c0301c08..f6753c397 100644 --- a/gitnexus/src/core/ingestion/languages/swift.ts +++ b/gitnexus/src/core/ingestion/languages/swift.ts @@ -240,8 +240,10 @@ export const swiftProvider = defineLanguage({ importSemantics: 'wildcard', heritageDefaultEdge: 'IMPLEMENTS', fieldExtractor: createFieldExtractor(swiftFieldConfig), - methodExtractor: createMethodExtractor(swiftMethodConfig), - extractFunctionName: swiftExtractFunctionName, + 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 0e6c872c0..7563704e3 100644 --- a/gitnexus/src/core/ingestion/languages/typescript.ts +++ b/gitnexus/src/core/ingestion/languages/typescript.ts @@ -156,8 +156,10 @@ export const typescriptProvider = defineLanguage({ importResolver: resolveTypescriptImport, namedBindingExtractor: extractTsNamedBindings, fieldExtractor: typescriptFieldExtractor, - methodExtractor: createMethodExtractor(typescriptMethodConfig), - extractFunctionName: tsExtractFunctionName, + methodExtractor: createMethodExtractor({ + ...typescriptMethodConfig, + extractFunctionName: tsExtractFunctionName, + }), builtInNames: BUILT_INS, }); @@ -170,7 +172,9 @@ export const javascriptProvider = defineLanguage({ importResolver: resolveJavascriptImport, namedBindingExtractor: extractTsNamedBindings, fieldExtractor: createFieldExtractor(javascriptConfig), - methodExtractor: createMethodExtractor(javascriptMethodConfig), - extractFunctionName: tsExtractFunctionName, + methodExtractor: createMethodExtractor({ + ...javascriptMethodConfig, + extractFunctionName: tsExtractFunctionName, + }), builtInNames: BUILT_INS, }); diff --git a/gitnexus/src/core/ingestion/method-extractors/generic.ts b/gitnexus/src/core/ingestion/method-extractors/generic.ts index d4df84feb..054aa3f58 100644 --- a/gitnexus/src/core/ingestion/method-extractors/generic.ts +++ b/gitnexus/src/core/ingestion/method-extractors/generic.ts @@ -86,6 +86,8 @@ export function createMethodExtractor(config: MethodExtractionConfig): MethodExt if (!methodNodeSet.has(node.type)) return null; return buildMethod(node, node, context, config); }, + + ...(config.extractFunctionName ? { extractFunctionName: config.extractFunctionName } : {}), }; } diff --git a/gitnexus/src/core/ingestion/method-types.ts b/gitnexus/src/core/ingestion/method-types.ts index 30b34474a..11a613804 100644 --- a/gitnexus/src/core/ingestion/method-types.ts +++ b/gitnexus/src/core/ingestion/method-types.ts @@ -48,6 +48,14 @@ export interface MethodExtractor { isTypeDeclaration(node: SyntaxNode): boolean; /** Extract method info from a standalone method node (e.g. Go top-level method_declaration). */ extractFromNode?(node: SyntaxNode, context: MethodExtractorContext): MethodInfo | null; + /** 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. */ + extractFunctionName?( + node: SyntaxNode, + ): { funcName: string | null; label: import('gitnexus-shared').NodeLabel } | null; } export interface MethodExtractionConfig { @@ -75,4 +83,9 @@ export interface MethodExtractionConfig { ownerNode: SyntaxNode, context: MethodExtractorContext, ) => MethodInfo | null; + /** Extract function name + label from an AST node during parent-walk. + * Passed through to the MethodExtractor by createMethodExtractor. */ + extractFunctionName?: ( + node: SyntaxNode, + ) => { funcName: string | null; label: import('gitnexus-shared').NodeLabel } | null; } diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index a9a22a648..68237ad1e 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -371,7 +371,10 @@ const processParsingSequential = async ( // Build per-file type environment for FieldExtractor context (lightweight — skipped if no fieldExtractor) const typeEnv = provider.fieldExtractor - ? buildTypeEnv(tree, language, { enclosingFunctionFinder: provider.enclosingFunctionFinder }) + ? buildTypeEnv(tree, language, { + enclosingFunctionFinder: provider.enclosingFunctionFinder, + extractFunctionName: provider.methodExtractor?.extractFunctionName, + }) : null; matches.forEach((match) => { diff --git a/gitnexus/src/core/ingestion/type-env.ts b/gitnexus/src/core/ingestion/type-env.ts index f058dec0c..ce2d50474 100644 --- a/gitnexus/src/core/ingestion/type-env.ts +++ b/gitnexus/src/core/ingestion/type-env.ts @@ -1,7 +1,6 @@ import { type SyntaxNode, FUNCTION_NODE_TYPES, - extractFunctionName, CLASS_CONTAINER_TYPES, } from './utils/ast-helpers.js'; import { CALL_EXPRESSION_TYPES } from './utils/call-analysis.js'; @@ -132,6 +131,7 @@ const lookupInEnv = ( callNode: SyntaxNode, patternOverrides?: PatternOverrides, enclosingFunctionFinder?: (n: SyntaxNode) => { funcName: string; label: NodeLabel } | null, + extractFunctionNameHook?: (n: SyntaxNode) => { funcName: string | null; label: NodeLabel } | null, ): string | undefined => { // Self/this receiver: resolve to enclosing class name via AST walk if (varName === 'self' || varName === 'this' || varName === '$this') { @@ -145,7 +145,11 @@ const lookupInEnv = ( } // Determine the enclosing function scope for the call - const scopeKey = findEnclosingScopeKey(callNode, enclosingFunctionFinder); + const scopeKey = findEnclosingScopeKey( + callNode, + enclosingFunctionFinder, + extractFunctionNameHook, + ); // Check position-indexed pattern overrides first (e.g., Kotlin when/is smart casts). // These take priority over flat scopeEnv because they represent per-branch narrowing. @@ -353,6 +357,24 @@ const extractParentClassFromNode = (classNode: SyntaxNode): string | undefined = return undefined; }; +/** Generic name extraction from a function-like AST node: tries 'name' field, + * then scans children for common identifier types. */ +const genericFuncName = (node: SyntaxNode): string | null => { + const nameField = node.childForFieldName?.('name'); + if (nameField) return nameField.text; + 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' + ) { + return c.text; + } + } + return null; +}; + /** Find the enclosing function name for scope lookup. * When an `enclosingFunctionFinder` hook is provided (from the language provider), * it is consulted for each ancestor before the default FUNCTION_NODE_TYPES check. @@ -361,11 +383,12 @@ const extractParentClassFromNode = (classNode: SyntaxNode): string | undefined = const findEnclosingScopeKey = ( node: SyntaxNode, enclosingFunctionFinder?: (n: SyntaxNode) => { funcName: string; label: NodeLabel } | null, + extractFunctionNameHook?: (n: SyntaxNode) => { funcName: string | null; label: NodeLabel } | null, ): string | undefined => { let current = node.parent; while (current) { if (FUNCTION_NODE_TYPES.has(current.type)) { - const { funcName } = extractFunctionName(current); + const funcName = extractFunctionNameHook?.(current)?.funcName ?? genericFuncName(current); if (funcName) return `${funcName}@${current.startIndex}`; } // Language-specific hook (e.g., Dart function_body → sibling function_signature) @@ -773,6 +796,11 @@ export interface BuildTypeEnvOptions { enclosingFunctionFinder?: ( ancestorNode: SyntaxNode, ) => { funcName: string; label: NodeLabel } | null; + /** Language-specific function name extraction from an AST node. + * Replaces the generic name-field lookup for languages with non-standard + * AST structures (C/C++ declarator unwrapping, Swift init/deinit, etc.). + * When null is returned or not provided, falls back to node.childForFieldName('name')?.text. */ + extractFunctionName?: (node: SyntaxNode) => { funcName: string | null; label: NodeLabel } | null; } /** Seed cross-file type bindings into the file scope. @@ -802,6 +830,7 @@ export const buildTypeEnv = ( const symbolTable = options?.symbolTable; const parentMap = options?.parentMap; + const extractFuncNameHook = options?.extractFunctionName; const env: TypeEnv = new Map(); const patternOverrides: PatternOverrides = new Map(); // Phase P: maps `scope\0varName` → constructor type when a declaration has BOTH @@ -1065,7 +1094,7 @@ export const buildTypeEnv = ( // Detect scope boundaries (function/method definitions) let scope = currentScope; if (FUNCTION_NODE_TYPES.has(node.type)) { - const { funcName } = extractFunctionName(node); + const funcName = extractFuncNameHook?.(node)?.funcName ?? genericFuncName(node); if (funcName) scope = `${funcName}@${node.startIndex}`; } @@ -1214,7 +1243,14 @@ export const buildTypeEnv = ( return { lookup: (varName, callNode) => - lookupInEnv(env, varName, callNode, patternOverrides, options?.enclosingFunctionFinder), + lookupInEnv( + env, + varName, + callNode, + patternOverrides, + options?.enclosingFunctionFinder, + extractFuncNameHook, + ), constructorBindings: bindings, fileScope: () => env.get(FILE_SCOPE) ?? EMPTY_FILE_SCOPE, allScopes: () => env as ReadonlyMap>, diff --git a/gitnexus/src/core/ingestion/utils/ast-helpers.ts b/gitnexus/src/core/ingestion/utils/ast-helpers.ts index 707cc3ddc..63845adbf 100644 --- a/gitnexus/src/core/ingestion/utils/ast-helpers.ts +++ b/gitnexus/src/core/ingestion/utils/ast-helpers.ts @@ -90,18 +90,6 @@ export const FUNCTION_NODE_TYPES = new Set([ 'method_signature', ]); -/** - * Node types for standard function declarations that need C/C++ declarator handling. - * Used by extractFunctionName to determine how to extract the function name. - */ -export const FUNCTION_DECLARATION_TYPES = new Set([ - 'function_declaration', - 'function_definition', - 'async_function_declaration', - 'generator_function_declaration', - 'function_item', -]); - /** * AST node types that represent a class-like container (for HAS_METHOD edge extraction). * @@ -384,93 +372,6 @@ export const findSiblingChild = ( return null; }; -/** - * Extract function name and label from a function_definition or similar AST node. - * - * 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'; - - 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'; - } - - // Dart: method_signature wraps function_signature — unwrap to reach the name - if (node.type === 'method_signature') { - label = 'Method'; - let funcSig: SyntaxNode | null = null; - for (let i = 0; i < node.childCount; i++) { - const c = node.child(i); - if (c?.type === 'function_signature') { - funcSig = c; - break; - } - } - if (funcSig) { - let nameNode = funcSig.childForFieldName?.('name'); - if (!nameNode) { - for (let i = 0; i < funcSig.childCount; i++) { - const c = funcSig.child(i); - if (c?.type === 'identifier') { - nameNode = c; - break; - } - } - } - funcName = nameNode?.text ?? null; - } - 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']); diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index ec9bd7121..d7f6af567 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -41,7 +41,6 @@ try { import { getLanguageFromFilename } from 'gitnexus-shared'; import { FUNCTION_NODE_TYPES, - extractFunctionName, getDefinitionNodeFromCaptures, findEnclosingClassInfo, type EnclosingClassInfo, @@ -512,6 +511,40 @@ function getMethodInfo( import type { LanguageProvider } from '../language-provider.js'; +/** Generic name extraction from a function-like AST node. */ +const genericFuncName = (node: SyntaxNode): string | null => { + const nameField = node.childForFieldName?.('name'); + if (nameField) return nameField.text; + 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' + ) + return c.text; + } + return null; +}; + +/** Infer node label from AST node type for function-like nodes without a provider hook. */ +const METHOD_NODE_TYPES = new Set([ + 'method_definition', + 'method_declaration', + 'method', + 'singleton_method', +]); +const CONSTRUCTOR_NODE_TYPES = new Set([ + 'constructor_declaration', + 'compact_constructor_declaration', +]); +const inferFunctionLabel = (nodeType: string): import('gitnexus-shared').NodeLabel => + METHOD_NODE_TYPES.has(nodeType) + ? 'Method' + : CONSTRUCTOR_NODE_TYPES.has(nodeType) + ? 'Constructor' + : 'Function'; + /** Walk up AST to find enclosing function, return its generateId or null for top-level. * Applies provider.labelOverride so the label matches the definition phase (single source of truth). */ const findEnclosingFunctionId = ( @@ -525,7 +558,9 @@ const findEnclosingFunctionId = ( let current = node.parent; while (current) { if (FUNCTION_NODE_TYPES.has(current.type)) { - const { funcName, label } = extractFunctionName(current, provider); + const efnResult = provider.methodExtractor?.extractFunctionName?.(current); + const funcName = efnResult?.funcName ?? genericFuncName(current); + const label = efnResult?.label ?? inferFunctionLabel(current.type); 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. @@ -1353,6 +1388,7 @@ const processFileGroup = ( const typeEnv = buildTypeEnv(tree, language, { parentMap, enclosingFunctionFinder: provider?.enclosingFunctionFinder, + extractFunctionName: provider?.methodExtractor?.extractFunctionName, }); const callRouter = provider.callRouter; diff --git a/gitnexus/test/unit/ingestion-utils.test.ts b/gitnexus/test/unit/ingestion-utils.test.ts index 9dfcbd80e..c41b2e43b 100644 --- a/gitnexus/test/unit/ingestion-utils.test.ts +++ b/gitnexus/test/unit/ingestion-utils.test.ts @@ -1,7 +1,9 @@ import { describe, it, expect } from 'vitest'; import { getLanguageFromFilename, SupportedLanguages } from 'gitnexus-shared'; import { getProvider } from '../../src/core/ingestion/languages/index.js'; -import { extractFunctionName } from '../../src/core/ingestion/utils/ast-helpers.js'; +import type { SyntaxNode } from '../../src/core/ingestion/utils/ast-helpers.js'; +import type { NodeLabel } from 'gitnexus-shared'; +import type { LanguageProvider } from '../../src/core/ingestion/language-provider.js'; import { getTreeSitterBufferSize, TREE_SITTER_BUFFER_SIZE, @@ -341,12 +343,24 @@ describe('isBuiltInOrNoise', () => { }); }); -describe('extractFunctionName', () => { +describe('extractFunctionName (via methodExtractor)', () => { const parser = new Parser(); const cProvider = getProvider(SupportedLanguages.C); const cppProvider = getProvider(SupportedLanguages.CPlusPlus); const tsProvider = getProvider(SupportedLanguages.TypeScript); + /** Test helper: extracts function name using methodExtractor hook with generic fallback. */ + const extractFunctionName = ( + node: SyntaxNode | null, + provider?: LanguageProvider, + ): { funcName: string | null; label: NodeLabel } => { + if (!node) return { funcName: null, label: 'Function' }; + const result = provider?.methodExtractor?.extractFunctionName?.(node); + if (result) return result; + const funcName = node.childForFieldName?.('name')?.text ?? null; + return { funcName, label: 'Function' }; + }; + describe('C', () => { it('extracts function name from C function definition', () => { parser.setLanguage(C);