mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
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:
parent
739de2f645
commit
c009678acd
12 changed files with 156 additions and 393 deletions
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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']> = (
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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++) {
|
||||
|
|
|
|||
38
gitnexus/src/core/ingestion/utils/method-props.ts
Normal file
38
gitnexus/src/core/ingestion/utils/method-props.ts
Normal 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 } : {}),
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue