refactor: post-extraction cleanup — deduplicate helpers, remove dead code, complete provider migration

Phase 1 — Deduplicate shared helpers:
- Move genericFuncName (was triplicated in 3 files) to ast-helpers.ts
- Move inferFunctionLabel + METHOD_LABEL/CONSTRUCTOR_LABEL node type
  Sets (was duplicated in 2 files) to ast-helpers.ts
- Extract buildMethodProps + arityForIdFromInfo to shared method-props.ts
- Replace 50-line inline MethodInfo conversion in parse-worker.ts with
  shared buildMethodProps call

Phase 2 — Remove dead code:
- Delete TypeConfig.extractReturnType from LanguageTypeConfig interface
  (zero callers since post-hoc fallback was removed)
- Delete 3 dead extractReturnType implementations in type-extractors
  (php.ts, ruby.ts, typescript.ts) — ~130 lines of unreachable code
- Delete ReturnTypeExtractor type alias

Phase 3 — Complete provider migration:
- Move isCppInsideClassOrStruct from ast-helpers.ts to c-cpp.ts
- Move isKotlinClassMethod from ast-helpers.ts to kotlin.ts

Phase 4 — Hygiene:
- Merge duplicate CLASS_CONTAINER_TYPES import in parse-worker.ts
- Move mid-file LanguageProvider import to top of parse-worker.ts
- Add FUNCTION_NODE_TYPES drift-prevention comment documenting the
  invariant with MethodExtractionConfig.methodNodeTypes

Net: -281 lines (391 removed, 110 added)
This commit is contained in:
Gergo Magyar 2026-04-04 17:45:50 +01:00
parent 739de2f645
commit c009678acd
12 changed files with 156 additions and 393 deletions

View file

@ -14,6 +14,8 @@ import {
FUNCTION_NODE_TYPES,
findEnclosingClassId,
findEnclosingClassInfo,
genericFuncName,
inferFunctionLabel,
} from './utils/ast-helpers.js';
import {
countCallArguments,
@ -219,40 +221,6 @@ 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).

View file

@ -15,8 +15,6 @@ 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';
/**
* Node types for standard function declarations that need C/C++ declarator handling.
* Used by cCppExtractFunctionName to determine how to extract the function name.
@ -282,6 +280,18 @@ const cCppExtractFunctionName = (
return { funcName, label };
};
/** Check if a C/C++ function_definition is inside a class or struct body.
* Used by cppLabelOverride to skip duplicate function captures
* that are already covered by definition.method queries. */
function isCppInsideClassOrStruct(functionNode: SyntaxNode): boolean {
let ancestor: SyntaxNode | null = functionNode?.parent ?? null;
while (ancestor) {
if (ancestor.type === 'class_specifier' || ancestor.type === 'struct_specifier') return true;
ancestor = ancestor.parent;
}
return false;
}
/** Label override shared by C and C++: skip function_definition captures inside class/struct
* bodies (they're duplicates of definition.method captures). */
const cppLabelOverride: NonNullable<LanguageProvider['labelOverride']> = (

View file

@ -15,12 +15,26 @@ import { resolveKotlinImport } from '../import-resolvers/jvm.js';
import { extractKotlinNamedBindings } from '../named-bindings/kotlin.js';
import { appendKotlinWildcard } from '../import-resolvers/jvm.js';
import { KOTLIN_QUERIES } from '../tree-sitter-queries.js';
import { isKotlinClassMethod } from '../utils/ast-helpers.js';
import type { SyntaxNode } from '../utils/ast-helpers.js';
import { createFieldExtractor } from '../field-extractors/generic.js';
import { kotlinConfig } from '../field-extractors/configs/jvm.js';
import { createMethodExtractor } from '../method-extractors/generic.js';
import { kotlinMethodConfig } from '../method-extractors/configs/jvm.js';
/** Check if a Kotlin function_declaration capture is inside a class_body (i.e., a method).
* Kotlin grammar uses function_declaration for both top-level functions and class methods.
* Returns true when the captured definition node has a class_body ancestor. */
function isKotlinClassMethod(
captureNode: { parent?: SyntaxNode | null } | null | undefined,
): boolean {
let ancestor = captureNode?.parent;
while (ancestor) {
if (ancestor.type === 'class_body') return true;
ancestor = ancestor.parent;
}
return false;
}
const BUILT_INS: ReadonlySet<string> = new Set([
'println',
'print',

View file

@ -21,6 +21,7 @@ import { detectFrameworkFromAST } from './framework-detection.js';
import { buildTypeEnv } from './type-env.js';
import type { FieldInfo, FieldExtractorContext } from './field-types.js';
import type { MethodInfo } from './method-types.js';
import { buildMethodProps, arityForIdFromInfo } from './utils/method-props.js';
import type { LanguageProvider } from './language-provider.js';
import { WorkerPool } from './workers/worker-pool.js';
import type {
@ -231,35 +232,6 @@ function seqFindEnclosingClassNode(node: SyntaxNode): SyntaxNode | null {
return null;
}
/** Convert MethodInfo from methodExtractor into flat properties for a graph node. */
function buildMethodProps(info: MethodInfo): Record<string, unknown> {
const types: string[] = [];
let optionalCount = 0;
let hasVariadic = false;
for (const p of info.parameters) {
if (p.type !== null) types.push(p.type);
if (p.isOptional) optionalCount++;
if (p.isVariadic) hasVariadic = true;
}
return {
parameterCount: hasVariadic ? undefined : info.parameters.length,
...(!hasVariadic && optionalCount > 0
? { requiredParameterCount: info.parameters.length - optionalCount }
: {}),
...(types.length > 0 ? { parameterTypes: types } : {}),
returnType: info.returnType ?? undefined,
visibility: info.visibility,
isStatic: info.isStatic,
isAbstract: info.isAbstract,
isFinal: info.isFinal,
...(info.isVirtual ? { isVirtual: info.isVirtual } : {}),
...(info.isOverride ? { isOverride: info.isOverride } : {}),
...(info.isAsync ? { isAsync: info.isAsync } : {}),
...(info.isPartial ? { isPartial: info.isPartial } : {}),
...(info.annotations.length > 0 ? { annotations: info.annotations } : {}),
};
}
/** Minimal no-op SymbolTable stub for FieldExtractorContext (sequential path has a real
* SymbolTable, but it's incomplete at this stage — use the stub for safety). */
const NOOP_SYMBOL_TABLE_SEQ = {
@ -451,9 +423,7 @@ const processParsingSequential = async (
const info = result.methods.find((m) => m.name === nodeName && m.line === defLine);
if (info) {
enriched = true;
arityForId = info.parameters.some((p) => p.isVariadic)
? undefined
: info.parameters.length;
arityForId = arityForIdFromInfo(info);
methodProps = buildMethodProps(info);
}
}
@ -467,9 +437,7 @@ const processParsingSequential = async (
});
if (info) {
enriched = true;
arityForId = info.parameters.some((p) => p.isVariadic)
? undefined
: info.parameters.length;
arityForId = arityForIdFromInfo(info);
methodProps = buildMethodProps(info);
}
}

View file

@ -2,6 +2,7 @@ import {
type SyntaxNode,
FUNCTION_NODE_TYPES,
CLASS_CONTAINER_TYPES,
genericFuncName,
} from './utils/ast-helpers.js';
import { CALL_EXPRESSION_TYPES } from './utils/call-analysis.js';
import { SupportedLanguages } from 'gitnexus-shared';
@ -357,24 +358,6 @@ 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.

View file

@ -6,7 +6,6 @@ import type {
InitializerExtractor,
ClassNameLookup,
ConstructorBindingScanner,
ReturnTypeExtractor,
PendingAssignmentExtractor,
ForLoopExtractor,
} from './types.js';
@ -337,60 +336,6 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
return undefined;
};
/** Regex to extract PHPDoc @return annotations: `@return User` */
const PHPDOC_RETURN_RE = /@return\s+(\S+)/;
/**
* Normalize a PHPDoc return type for storage in the SymbolTable.
* Unlike normalizePhpType (which strips User[] → User for scopeEnv), this preserves
* array notation so lookupRawReturnType can extract element types for for-loop resolution.
* \App\Models\User[] → User[]
* ?User → User
* Collection<User> → Collection<User> (preserved for extractElementTypeFromString)
*/
const normalizePhpReturnType = (raw: string): string | undefined => {
// Strip nullable prefix: ?User[] → User[]
let type = raw.startsWith('?') ? raw.slice(1) : raw;
// Strip union with null/false/void: User[]|null → User[]
const parts = type
.split('|')
.filter((p) => p !== 'null' && p !== 'false' && p !== 'void' && p !== 'mixed');
if (parts.length !== 1) return undefined;
type = parts[0];
// Strip namespace: \App\Models\User[] → User[]
const segments = type.split('\\');
type = segments[segments.length - 1];
// Skip uninformative types
if (
type === 'mixed' ||
type === 'void' ||
type === 'self' ||
type === 'static' ||
type === 'object' ||
type === 'array'
)
return undefined;
if (/^\w+(\[\])?$/.test(type) || /^\w+\s*</.test(type)) return type;
return undefined;
};
/**
* Extract return type from PHPDoc `@return Type` annotation preceding a method.
* Walks backwards through preceding siblings looking for comment nodes.
* Preserves array notation (e.g., User[]) for for-loop element type extraction.
*/
const extractReturnType: ReturnTypeExtractor = (node) => {
let sibling = node.previousSibling;
while (sibling) {
if (sibling.type === 'comment') {
const match = PHPDOC_RETURN_RE.exec(sibling.text);
if (match) return normalizePhpReturnType(match[1]);
} else if (sibling.isNamed && !SKIP_NODE_TYPES.has(sibling.type)) break;
sibling = sibling.previousSibling;
}
return undefined;
};
/** PHP: $alias = $user → assignment_expression with variable_name left/right.
* PHP TypeEnv stores variables WITH $ prefix ($user → User), so we keep $ in lhs/rhs. */
const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
@ -605,7 +550,6 @@ export const typeConfig: LanguageTypeConfig = {
extractParameter,
extractInitializer,
scanConstructorBinding,
extractReturnType,
extractForLoopBinding,
extractPendingAssignment,
};

View file

@ -4,7 +4,6 @@ import type {
TypeBindingExtractor,
InitializerExtractor,
ConstructorBindingScanner,
ReturnTypeExtractor,
PendingAssignmentExtractor,
ForLoopExtractor,
} from './types.js';
@ -43,9 +42,6 @@ const YARD_PARAM_RE = /@param\s+(\w+)\s+\[([^\]]+)\]/g;
/** Alternate YARD order: `@param [Type] name` */
const YARD_PARAM_ALT_RE = /@param\s+\[([^\]]+)\]\s+(\w+)/g;
/** Regex to extract @return annotations: `@return [Type]` */
const YARD_RETURN_RE = /@return\s+\[([^\]]+)\]/;
/**
* Extract the simple type name from a YARD type string.
* Handles:
@ -229,35 +225,6 @@ const extractInitializer: InitializerExtractor = (node, env, classNames): void =
}
};
/**
* Extract return type from YARD `@return [Type]` annotation preceding a method.
* Reuses the same comment-walking strategy as collectYardParams: try direct
* siblings first, fall back to parent (body_statement) siblings for class methods.
*/
const extractReturnType: ReturnTypeExtractor = (node) => {
const search = (startNode: SyntaxNode): string | undefined => {
let sibling = startNode.previousSibling;
while (sibling) {
if (sibling.type === 'comment') {
const match = YARD_RETURN_RE.exec(sibling.text);
if (match) return extractYardTypeName(match[1]);
} else if (sibling.isNamed) {
break;
}
sibling = sibling.previousSibling;
}
return undefined;
};
const result = search(node);
if (result) return result;
if (node.parent?.type === 'body_statement') {
return search(node.parent);
}
return undefined;
};
/**
* Ruby constructor binding scanner: captures both `user = User.new` and
* plain call assignments like `user = get_user()`.
@ -452,7 +419,6 @@ export const typeConfig: LanguageTypeConfig = {
extractParameter,
extractInitializer,
scanConstructorBinding,
extractReturnType,
extractForLoopBinding,
extractPendingAssignment,
};

View file

@ -30,11 +30,6 @@ export type ConstructorBindingScanner = (
node: SyntaxNode,
) => { varName: string; calleeName: string; receiverClassName?: string } | undefined;
/** Extracts a return type string from a method/function definition node.
* Used for languages where return types are expressed in comments (e.g. YARD @return [Type])
* rather than in AST fields. Returns undefined if no return type can be determined. */
export type ReturnTypeExtractor = (node: SyntaxNode) => string | undefined;
/** Infer the type name of a literal AST node for overload disambiguation.
* Returns the canonical type name (e.g. 'int', 'String', 'boolean') or undefined
* for non-literal nodes. Only used when resolveCallTarget has multiple candidates
@ -170,9 +165,6 @@ export interface LanguageTypeConfig {
* Called on every AST node during buildTypeEnv walk; returns undefined for non-matches.
* The callee binding is unverified — the caller must confirm against the SymbolTable. */
scanConstructorBinding?: ConstructorBindingScanner;
/** Extract return type from comment-based annotations (e.g. YARD @return [Type]).
* Called as fallback when the MethodExtractor finds no AST-based return type. */
extractReturnType?: ReturnTypeExtractor;
/** Extract loop variable → type binding from a for-each AST node. */
extractForLoopBinding?: ForLoopExtractor;
/** Extract pending assignment for Tier 2 propagation.

View file

@ -6,7 +6,6 @@ import type {
InitializerExtractor,
ClassNameLookup,
ConstructorBindingScanner,
ReturnTypeExtractor,
PendingAssignmentExtractor,
PendingAssignment,
ForLoopExtractor,
@ -198,44 +197,6 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
return { varName: nameNode.text, calleeName };
};
/** Regex to extract @returns or @return from JSDoc comments: `@returns {Type}` */
const JSDOC_RETURN_RE = /@returns?\s*\{([^}]+)\}/;
/**
* Minimal sanitization for JSDoc return types — preserves generic wrappers
* (e.g. `Promise<User>`) so that extractReturnTypeName in call-processor
* can apply WRAPPER_GENERICS unwrapping. Unlike normalizeJsDocType (which
* strips generics), this only strips JSDoc-specific syntax markers.
*/
const sanitizeReturnType = (raw: string): string | undefined => {
let type = raw.trim();
// Strip JSDoc nullable/non-nullable prefixes: ?User → User, !User → User
if (type.startsWith('?') || type.startsWith('!')) type = type.slice(1);
// Strip module: prefix — module:models.User → models.User
if (type.startsWith('module:')) type = type.slice(7);
// Reject unions (ambiguous)
if (type.includes('|')) return undefined;
if (!type) return undefined;
return type;
};
/**
* Extract return type from JSDoc `@returns {Type}` or `@return {Type}` annotation
* preceding a function/method definition. Walks backwards through preceding siblings
* looking for comment nodes containing the annotation.
*/
const extractReturnType: ReturnTypeExtractor = (node) => {
let sibling = node.previousSibling;
while (sibling) {
if (sibling.type === 'comment') {
const match = JSDOC_RETURN_RE.exec(sibling.text);
if (match) return sanitizeReturnType(match[1]);
} else if (sibling.isNamed && sibling.type !== 'decorator') break;
sibling = sibling.previousSibling;
}
return undefined;
};
const FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set(['for_in_statement']);
/** TS function/method node types that carry a parameters list. */
@ -742,7 +703,6 @@ export const typeConfig: LanguageTypeConfig = {
extractParameter,
extractInitializer,
scanConstructorBinding,
extractReturnType,
extractForLoopBinding,
extractPendingAssignment,
extractPatternBinding,

View file

@ -47,7 +47,13 @@ export const getDefinitionNodeFromCaptures = (
/**
* Node types that represent function/method definitions across languages.
* Used to find the enclosing function for a call site.
* Used by parent-walk in call-processor, parse-worker, and type-env to detect
* enclosing function scope boundaries.
*
* INVARIANT: This set MUST be a superset of every language's
* MethodExtractionConfig.methodNodeTypes. When adding a new node type to a
* MethodExtractor config, add it here too — otherwise enclosing-function
* resolution will silently miss that node type during parent-walks.
*/
export const FUNCTION_NODE_TYPES = new Set([
// TypeScript/JavaScript
@ -152,20 +158,6 @@ export const CONTAINER_TYPE_TO_LABEL: Record<string, string> = {
companion_object: 'Class',
};
/** Check if a Kotlin function_declaration capture is inside a class_body (i.e., a method).
* Kotlin grammar uses function_declaration for both top-level functions and class methods.
* Returns true when the captured definition node has a class_body ancestor. */
export function isKotlinClassMethod(
captureNode: { parent?: SyntaxNode | null } | null | undefined,
): boolean {
let ancestor = captureNode?.parent;
while (ancestor) {
if (ancestor.type === 'class_body') return true;
ancestor = ancestor.parent;
}
return false;
}
/**
* Determine the graph node label from a tree-sitter capture map.
* Handles language-specific reclassification via the provider's labelOverride hook
@ -372,6 +364,46 @@ export const findSiblingChild = (
return null;
};
/** Generic name extraction from a function-like AST node.
* Tries `node.childForFieldName('name')?.text`, then scans children for
* `identifier` / `property_identifier` / `simple_identifier`. */
export 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;
};
/** AST node types that represent a method definition (for `inferFunctionLabel`). */
export const METHOD_LABEL_NODE_TYPES = new Set([
'method_definition',
'method_declaration',
'method',
'singleton_method',
]);
/** AST node types that represent a constructor definition (for `inferFunctionLabel`). */
export const CONSTRUCTOR_LABEL_NODE_TYPES = new Set([
'constructor_declaration',
'compact_constructor_declaration',
]);
/** Infer node label from AST node type for function-like nodes without a provider hook. */
export const inferFunctionLabel = (nodeType: string): NodeLabel =>
METHOD_LABEL_NODE_TYPES.has(nodeType)
? 'Method'
: CONSTRUCTOR_LABEL_NODE_TYPES.has(nodeType)
? 'Constructor'
: 'Function';
/** Argument list node types shared between countCallArguments and call-resolution helpers. */
export const CALL_ARGUMENT_LIST_TYPES = new Set(['arguments', 'argument_list', 'value_arguments']);
@ -398,18 +430,6 @@ export function extractStringContent(node: SyntaxNode | null | undefined): strin
return null;
}
/** Check if a C/C++ function_definition is inside a class or struct body.
* Used by the C/C++ labelOverride to skip duplicate function captures
* that are already covered by definition.method queries. */
export function isCppInsideClassOrStruct(functionNode: SyntaxNode): boolean {
let ancestor: SyntaxNode | null = functionNode?.parent ?? null;
while (ancestor) {
if (ancestor.type === 'class_specifier' || ancestor.type === 'struct_specifier') return true;
ancestor = ancestor.parent;
}
return false;
}
/** Find the first direct named child of a tree-sitter node matching the given type. */
export function findChild(node: SyntaxNode, type: string): SyntaxNode | null {
for (let i = 0; i < node.namedChildCount; i++) {

View file

@ -0,0 +1,38 @@
import type { MethodInfo } from '../method-types.js';
/**
* Compute arity for ID-generation purposes.
* Returns `undefined` when any parameter is variadic (arity is indeterminate).
*/
export function arityForIdFromInfo(info: MethodInfo): number | undefined {
return info.parameters.some((p) => p.isVariadic) ? undefined : info.parameters.length;
}
/** Convert MethodInfo from methodExtractor into flat properties for a graph node. */
export function buildMethodProps(info: MethodInfo): Record<string, unknown> {
const types: string[] = [];
let optionalCount = 0;
let hasVariadic = false;
for (const p of info.parameters) {
if (p.type !== null) types.push(p.type);
if (p.isOptional) optionalCount++;
if (p.isVariadic) hasVariadic = true;
}
return {
parameterCount: hasVariadic ? undefined : info.parameters.length,
...(!hasVariadic && optionalCount > 0
? { requiredParameterCount: info.parameters.length - optionalCount }
: {}),
...(types.length > 0 ? { parameterTypes: types } : {}),
returnType: info.returnType ?? undefined,
visibility: info.visibility,
isStatic: info.isStatic,
isAbstract: info.isAbstract,
isFinal: info.isFinal,
...(info.isVirtual ? { isVirtual: info.isVirtual } : {}),
...(info.isOverride ? { isOverride: info.isOverride } : {}),
...(info.isAsync ? { isAsync: info.isAsync } : {}),
...(info.isPartial ? { isPartial: info.isPartial } : {}),
...(info.annotations.length > 0 ? { annotations: info.annotations } : {}),
};
}

View file

@ -47,6 +47,9 @@ import {
getLabelFromCaptures,
findDescendant,
extractStringContent,
genericFuncName,
inferFunctionLabel,
CLASS_CONTAINER_TYPES,
type SyntaxNode,
} from '../utils/ast-helpers.js';
import {
@ -73,7 +76,8 @@ import type { NamedBinding } from '../named-bindings/types.js';
import type { NodeLabel } from 'gitnexus-shared';
import type { FieldInfo, FieldExtractorContext } from '../field-types.js';
import type { MethodInfo, MethodExtractorContext } from '../method-types.js';
import { CLASS_CONTAINER_TYPES } from '../utils/ast-helpers.js';
import { buildMethodProps, arityForIdFromInfo } from '../utils/method-props.js';
import type { LanguageProvider } from '../language-provider.js';
// ============================================================================
// Types for serializable results
@ -92,14 +96,8 @@ interface ParsedNode {
astFrameworkMultiplier?: number;
astFrameworkReason?: string;
description?: string;
parameterCount?: number;
requiredParameterCount?: number;
returnType?: string;
// Field/property metadata (populated by FieldExtractor)
declaredType?: string;
visibility?: string;
isStatic?: boolean;
isReadonly?: boolean;
// Method/field metadata — extensible via buildMethodProps spread
[key: string]: unknown;
};
}
@ -509,42 +507,6 @@ function getMethodInfo(
// Enclosing function detection (for call extraction) — cached
// ============================================================================
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 = (
@ -1855,21 +1817,8 @@ const processFileGroup = (
// Extract method metadata BEFORE generating node ID — parameterCount is needed
// to disambiguate overloaded methods via #<arity> suffix in the ID.
let parameterCount: number | undefined;
let requiredParameterCount: number | undefined;
let parameterTypes: string[] | undefined;
let returnType: string | undefined;
let declaredType: string | undefined;
let visibility: string | undefined;
let isStatic: boolean | undefined;
let isReadonly: boolean | undefined;
let isAbstract: boolean | undefined;
let isFinal: boolean | undefined;
let isVirtual: boolean | undefined;
let isOverride: boolean | undefined;
let isAsync: boolean | undefined;
let isPartial: boolean | undefined;
let annotations: string[] | undefined;
let methodProps: Record<string, unknown> = {};
let arityForId: number | undefined; // raw param count for ID, even for variadic
if (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor') {
// Use MethodExtractor for method metadata — provides parameterCount, parameterTypes,
@ -1887,30 +1836,8 @@ const processFileGroup = (
const info = methodMap?.get(`${nodeName}:${defLine}`);
if (info) {
enrichedByMethodExtractor = true;
const hasVariadic = info.parameters.some((p) => p.isVariadic);
arityForId = hasVariadic ? undefined : info.parameters.length;
parameterCount = hasVariadic ? undefined : info.parameters.length;
const types: string[] = [];
let optionalCount = 0;
for (const p of info.parameters) {
if (p.type !== null) types.push(p.type);
if (p.isOptional) optionalCount++;
}
parameterTypes = types.length > 0 ? types : undefined;
requiredParameterCount =
!hasVariadic && optionalCount > 0
? info.parameters.length - optionalCount
: undefined;
returnType = info.returnType ?? undefined;
visibility = info.visibility;
isStatic = info.isStatic;
isAbstract = info.isAbstract;
isFinal = info.isFinal;
if (info.isVirtual) isVirtual = info.isVirtual;
if (info.isOverride) isOverride = info.isOverride;
if (info.isAsync) isAsync = info.isAsync;
if (info.isPartial) isPartial = info.isPartial;
if (info.annotations.length > 0) annotations = info.annotations;
arityForId = arityForIdFromInfo(info);
methodProps = buildMethodProps(info);
}
}
}
@ -1927,30 +1854,8 @@ const processFileGroup = (
});
if (info) {
enrichedByMethodExtractor = true;
const hasVariadic = info.parameters.some((p) => p.isVariadic);
arityForId = hasVariadic ? undefined : info.parameters.length;
parameterCount = hasVariadic ? undefined : info.parameters.length;
const types: string[] = [];
let optionalCount = 0;
for (const p of info.parameters) {
if (p.type !== null) types.push(p.type);
if (p.isOptional) optionalCount++;
}
parameterTypes = types.length > 0 ? types : undefined;
requiredParameterCount =
!hasVariadic && optionalCount > 0
? info.parameters.length - optionalCount
: undefined;
returnType = info.returnType ?? undefined;
visibility = info.visibility;
isStatic = info.isStatic;
isAbstract = info.isAbstract;
isFinal = info.isFinal;
if (info.isVirtual) isVirtual = info.isVirtual;
if (info.isOverride) isOverride = info.isOverride;
if (info.isAsync) isAsync = info.isAsync;
if (info.isPartial) isPartial = info.isPartial;
if (info.annotations.length > 0) annotations = info.annotations;
arityForId = arityForIdFromInfo(info);
methodProps = buildMethodProps(info);
}
}
}
@ -2016,9 +1921,9 @@ const processFileGroup = (
const info = fieldMap?.get(nodeName);
if (info) {
declaredType = info.type ?? undefined;
visibility = info.visibility;
isStatic = info.isStatic;
isReadonly = info.isReadonly;
methodProps.visibility = info.visibility;
methodProps.isStatic = info.isStatic;
methodProps.isReadonly = info.isReadonly;
}
}
}
@ -2044,21 +1949,8 @@ const processFileGroup = (
}
: {}),
...(description !== undefined ? { description } : {}),
...(parameterCount !== undefined ? { parameterCount } : {}),
...(requiredParameterCount !== undefined ? { requiredParameterCount } : {}),
...(parameterTypes !== undefined ? { parameterTypes } : {}),
...(returnType !== undefined ? { returnType } : {}),
...methodProps,
...(declaredType !== undefined ? { declaredType } : {}),
...(visibility !== undefined ? { visibility } : {}),
...(isStatic !== undefined ? { isStatic } : {}),
...(isReadonly !== undefined ? { isReadonly } : {}),
...(isAbstract !== undefined ? { isAbstract } : {}),
...(isFinal !== undefined ? { isFinal } : {}),
...(isVirtual !== undefined ? { isVirtual } : {}),
...(isOverride !== undefined ? { isOverride } : {}),
...(isAsync !== undefined ? { isAsync } : {}),
...(isPartial !== undefined ? { isPartial } : {}),
...(annotations !== undefined ? { annotations } : {}),
},
});
@ -2069,22 +1961,30 @@ const processFileGroup = (
name: nodeName,
nodeId,
type: nodeLabel,
...(parameterCount !== undefined ? { parameterCount } : {}),
...(requiredParameterCount !== undefined ? { requiredParameterCount } : {}),
...(parameterTypes !== undefined ? { parameterTypes } : {}),
...(returnType !== undefined ? { returnType } : {}),
parameterCount: methodProps.parameterCount as number | undefined,
requiredParameterCount: methodProps.requiredParameterCount as number | undefined,
parameterTypes: methodProps.parameterTypes as string[] | undefined,
returnType: methodProps.returnType as string | undefined,
...(declaredType !== undefined ? { declaredType } : {}),
...(enclosingClassId ? { ownerId: enclosingClassId } : {}),
...(visibility !== undefined ? { visibility } : {}),
...(isStatic !== undefined ? { isStatic } : {}),
...(isReadonly !== undefined ? { isReadonly } : {}),
...(isAbstract !== undefined ? { isAbstract } : {}),
...(isFinal !== undefined ? { isFinal } : {}),
...(isVirtual !== undefined ? { isVirtual } : {}),
...(isOverride !== undefined ? { isOverride } : {}),
...(isAsync !== undefined ? { isAsync } : {}),
...(isPartial !== undefined ? { isPartial } : {}),
...(annotations !== undefined ? { annotations } : {}),
visibility: methodProps.visibility as string | undefined,
isStatic: methodProps.isStatic as boolean | undefined,
isReadonly: methodProps.isReadonly as boolean | undefined,
isAbstract: methodProps.isAbstract as boolean | undefined,
isFinal: methodProps.isFinal as boolean | undefined,
...(methodProps.isVirtual !== undefined
? { isVirtual: methodProps.isVirtual as boolean }
: {}),
...(methodProps.isOverride !== undefined
? { isOverride: methodProps.isOverride as boolean }
: {}),
...(methodProps.isAsync !== undefined ? { isAsync: methodProps.isAsync as boolean } : {}),
...(methodProps.isPartial !== undefined
? { isPartial: methodProps.isPartial as boolean }
: {}),
...(methodProps.annotations !== undefined
? { annotations: methodProps.annotations as string[] }
: {}),
});
const fileId = generateId('File', file.path);