mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
refactor: eliminate extractFunctionName from ast-helpers — MethodExtractor owns all extraction
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.
This commit is contained in:
parent
16f752413a
commit
739de2f645
15 changed files with 213 additions and 142 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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<string, ReadonlyMap<string, string>>,
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue