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:
Gergo Magyar 2026-04-04 17:06:04 +01:00
parent 16f752413a
commit 739de2f645
15 changed files with 213 additions and 142 deletions

View file

@ -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;

View file

@ -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

View file

@ -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,
});

View file

@ -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;
};

View file

@ -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,
});

View file

@ -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,
});

View file

@ -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,
});

View file

@ -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,
});

View file

@ -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 } : {}),
};
}

View file

@ -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;
}

View file

@ -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) => {

View file

@ -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>>,

View file

@ -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']);

View file

@ -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;

View file

@ -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);