From c009678acdf4e39d57e780edeadacc5d7df6f7c7 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Sat, 4 Apr 2026 17:45:50 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20post-extraction=20cleanup=20?= =?UTF-8?q?=E2=80=94=20deduplicate=20helpers,=20remove=20dead=20code,=20co?= =?UTF-8?q?mplete=20provider=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- gitnexus/src/core/ingestion/call-processor.ts | 36 +--- .../src/core/ingestion/languages/c-cpp.ts | 14 +- .../src/core/ingestion/languages/kotlin.ts | 16 +- .../src/core/ingestion/parsing-processor.ts | 38 +--- gitnexus/src/core/ingestion/type-env.ts | 19 +- .../src/core/ingestion/type-extractors/php.ts | 56 ------ .../core/ingestion/type-extractors/ruby.ts | 34 ---- .../core/ingestion/type-extractors/types.ts | 8 - .../ingestion/type-extractors/typescript.ts | 40 ---- .../src/core/ingestion/utils/ast-helpers.ts | 74 +++++--- .../src/core/ingestion/utils/method-props.ts | 38 ++++ .../core/ingestion/workers/parse-worker.ts | 176 ++++-------------- 12 files changed, 156 insertions(+), 393 deletions(-) create mode 100644 gitnexus/src/core/ingestion/utils/method-props.ts diff --git a/gitnexus/src/core/ingestion/call-processor.ts b/gitnexus/src/core/ingestion/call-processor.ts index 479031f19..ffa607ed8 100644 --- a/gitnexus/src/core/ingestion/call-processor.ts +++ b/gitnexus/src/core/ingestion/call-processor.ts @@ -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). diff --git a/gitnexus/src/core/ingestion/languages/c-cpp.ts b/gitnexus/src/core/ingestion/languages/c-cpp.ts index a19b54234..8edd1c2b8 100644 --- a/gitnexus/src/core/ingestion/languages/c-cpp.ts +++ b/gitnexus/src/core/ingestion/languages/c-cpp.ts @@ -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 = ( diff --git a/gitnexus/src/core/ingestion/languages/kotlin.ts b/gitnexus/src/core/ingestion/languages/kotlin.ts index 6e93912f0..639713c1a 100644 --- a/gitnexus/src/core/ingestion/languages/kotlin.ts +++ b/gitnexus/src/core/ingestion/languages/kotlin.ts @@ -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 = new Set([ 'println', 'print', diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index 68237ad1e..8511ce06b 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -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 { - 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); } } diff --git a/gitnexus/src/core/ingestion/type-env.ts b/gitnexus/src/core/ingestion/type-env.ts index ce2d50474..6bd469bee 100644 --- a/gitnexus/src/core/ingestion/type-env.ts +++ b/gitnexus/src/core/ingestion/type-env.ts @@ -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. diff --git a/gitnexus/src/core/ingestion/type-extractors/php.ts b/gitnexus/src/core/ingestion/type-extractors/php.ts index c33c9b9b2..ca517ee90 100644 --- a/gitnexus/src/core/ingestion/type-extractors/php.ts +++ b/gitnexus/src/core/ingestion/type-extractors/php.ts @@ -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 → Collection (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* { - 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, }; diff --git a/gitnexus/src/core/ingestion/type-extractors/ruby.ts b/gitnexus/src/core/ingestion/type-extractors/ruby.ts index 4cef12d53..b6fe6ecdc 100644 --- a/gitnexus/src/core/ingestion/type-extractors/ruby.ts +++ b/gitnexus/src/core/ingestion/type-extractors/ruby.ts @@ -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, }; diff --git a/gitnexus/src/core/ingestion/type-extractors/types.ts b/gitnexus/src/core/ingestion/type-extractors/types.ts index c68a22bda..85da9d21a 100644 --- a/gitnexus/src/core/ingestion/type-extractors/types.ts +++ b/gitnexus/src/core/ingestion/type-extractors/types.ts @@ -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. diff --git a/gitnexus/src/core/ingestion/type-extractors/typescript.ts b/gitnexus/src/core/ingestion/type-extractors/typescript.ts index a2d17c5fc..fcc58cff4 100644 --- a/gitnexus/src/core/ingestion/type-extractors/typescript.ts +++ b/gitnexus/src/core/ingestion/type-extractors/typescript.ts @@ -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`) 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 = 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, diff --git a/gitnexus/src/core/ingestion/utils/ast-helpers.ts b/gitnexus/src/core/ingestion/utils/ast-helpers.ts index 63845adbf..825600ddd 100644 --- a/gitnexus/src/core/ingestion/utils/ast-helpers.ts +++ b/gitnexus/src/core/ingestion/utils/ast-helpers.ts @@ -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 = { 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++) { diff --git a/gitnexus/src/core/ingestion/utils/method-props.ts b/gitnexus/src/core/ingestion/utils/method-props.ts new file mode 100644 index 000000000..7eb98004d --- /dev/null +++ b/gitnexus/src/core/ingestion/utils/method-props.ts @@ -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 { + 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 } : {}), + }; +} diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index d7f6af567..8698c4201 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -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 # 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 = {}; 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);