diff --git a/gitnexus/src/core/ingestion/call-processor.ts b/gitnexus/src/core/ingestion/call-processor.ts index 36a53e4ae..0651072ad 100644 --- a/gitnexus/src/core/ingestion/call-processor.ts +++ b/gitnexus/src/core/ingestion/call-processor.ts @@ -6,46 +6,10 @@ import Parser from 'tree-sitter'; import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js'; import { LANGUAGE_QUERIES } from './tree-sitter-queries.js'; import { generateId } from '../../lib/utils.js'; -import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop } from './utils.js'; +import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise } from './utils.js'; +import { getTreeSitterBufferSize } from './constants.js'; import type { ExtractedCall, ExtractedRoute } from './workers/parse-worker.js'; -/** - * Node types that represent function/method definitions across languages. - * Used to find the enclosing function for a call site. - */ -const FUNCTION_NODE_TYPES = new Set([ - // TypeScript/JavaScript - 'function_declaration', - 'arrow_function', - 'function_expression', - 'method_definition', - 'generator_function_declaration', - // Python - 'function_definition', - // Common async variants - 'async_function_declaration', - 'async_arrow_function', - // Java - 'method_declaration', - 'constructor_declaration', - // C/C++ - // 'function_definition' already included above - // Go - // 'method_declaration' already included from Java - // C# - 'local_function_statement', - // Rust - 'function_item', - 'impl_item', // Methods inside impl blocks - // Kotlin (function_declaration already included above via JS/TS) - 'anonymous_function', - 'lambda_literal', - // PHP — no additional node types needed - // Swift - 'init_declaration', - 'deinit_declaration', -]); - /** * 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). @@ -56,89 +20,22 @@ const findEnclosingFunction = ( symbolTable: SymbolTable ): string | null => { let current = node.parent; - + while (current) { if (FUNCTION_NODE_TYPES.has(current.type)) { - // Found enclosing function - try to get its name - let funcName: string | null = null; - let label = 'Function'; - - // Different node types have different name locations - // Swift init/deinit — handle before generic cases (more specific) - if (current.type === 'init_declaration' || current.type === 'deinit_declaration') { - const funcName = current.type === 'init_declaration' ? 'init' : 'deinit'; - return generateId('Constructor', `${filePath}:${funcName}`); - } + const { funcName, label } = extractFunctionName(current); - if (current.type === 'function_declaration' || - current.type === 'function_definition' || - current.type === 'async_function_declaration' || - current.type === 'generator_function_declaration' || - current.type === 'function_item') { // Rust function - // Named function: function foo() {} - const nameNode = current.childForFieldName?.('name') || - current.children?.find((c: any) => c.type === 'identifier' || c.type === 'property_identifier'); - funcName = nameNode?.text; - } else if (current.type === 'impl_item') { - // Rust method inside impl block: wrapper around function_item or const_item - // We need to look inside for the function_item - const funcItem = current.children?.find((c: any) => c.type === 'function_item'); - if (funcItem) { - const nameNode = funcItem.childForFieldName?.('name') || - funcItem.children?.find((c: any) => c.type === 'identifier'); - funcName = nameNode?.text; - label = 'Method'; - } - } else if (current.type === 'method_definition') { - // Method: foo() {} inside class (JS/TS) - const nameNode = current.childForFieldName?.('name') || - current.children?.find((c: any) => c.type === 'property_identifier'); - funcName = nameNode?.text; - label = 'Method'; - } else if (current.type === 'method_declaration') { - // Java method: public void foo() {} - const nameNode = current.childForFieldName?.('name') || - current.children?.find((c: any) => c.type === 'identifier'); - funcName = nameNode?.text; - label = 'Method'; - } else if (current.type === 'constructor_declaration') { - // Java constructor: public ClassName() {} - const nameNode = current.childForFieldName?.('name') || - current.children?.find((c: any) => c.type === 'identifier'); - funcName = nameNode?.text; - label = 'Method'; // Treat constructors as methods for process detection - } else if (current.type === 'arrow_function' || current.type === 'function_expression') { - // Arrow/expression: const foo = () => {} - check parent variable declarator - const parent = current.parent; - if (parent?.type === 'variable_declarator') { - const nameNode = parent.childForFieldName?.('name') || - parent.children?.find((c: any) => c.type === 'identifier'); - funcName = nameNode?.text; - } - } - if (funcName) { - // Look up the function in symbol table to get its node ID - // Try exact match first const nodeId = symbolTable.lookupExact(filePath, funcName); if (nodeId) return nodeId; - - // Try construct ID manually if lookup fails (common for non-exported internal functions) - // Format should match what parsing-processor generates: "Function:path/to/file:funcName" - // Check if we already have a node with this ID in the symbol table to be safe - const generatedId = generateId(label, `${filePath}:${funcName}`); - - // Ideally we should verify this ID exists, but strictly speaking if we are inside it, - // it SHOULD exist. Returning it is better than falling back to File. - return generatedId; + + return generateId(label, `${filePath}:${funcName}`); } - - // Couldn't determine function name - try parent (might be nested) } current = current.parent; } - - return null; // Top-level call (not inside any function) + + return null; }; export const processCalls = async ( @@ -182,7 +79,7 @@ export const processCalls = async ( // Cache Miss: Re-parse // Use larger bufferSize for files > 32KB try { - tree = parser.parse(file.content, undefined, { bufferSize: 1024 * 256 }); + tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) }); } catch (parseError) { // Skip files that can't be parsed continue; @@ -311,111 +208,6 @@ const resolveCallTarget = ( return null; }; -/** - * Filter out common built-in functions and noise - * that shouldn't be tracked as calls - */ -/** Pre-built set (module-level singleton) to avoid re-creating per call */ -const BUILT_IN_NAMES = new Set([ - // JavaScript/TypeScript built-ins - 'console', 'log', 'warn', 'error', 'info', 'debug', - 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', - 'parseInt', 'parseFloat', 'isNaN', 'isFinite', - 'encodeURI', 'decodeURI', 'encodeURIComponent', 'decodeURIComponent', - 'JSON', 'parse', 'stringify', - 'Object', 'Array', 'String', 'Number', 'Boolean', 'Symbol', 'BigInt', - 'Map', 'Set', 'WeakMap', 'WeakSet', - 'Promise', 'resolve', 'reject', 'then', 'catch', 'finally', - 'Math', 'Date', 'RegExp', 'Error', - 'require', 'import', 'export', - 'fetch', 'Response', 'Request', - // React hooks and common functions - 'useState', 'useEffect', 'useCallback', 'useMemo', 'useRef', 'useContext', - 'useReducer', 'useLayoutEffect', 'useImperativeHandle', 'useDebugValue', - 'createElement', 'createContext', 'createRef', 'forwardRef', 'memo', 'lazy', - // Common array/object methods - 'map', 'filter', 'reduce', 'forEach', 'find', 'findIndex', 'some', 'every', - 'includes', 'indexOf', 'slice', 'splice', 'concat', 'join', 'split', - 'push', 'pop', 'shift', 'unshift', 'sort', 'reverse', - 'keys', 'values', 'entries', 'assign', 'freeze', 'seal', - 'hasOwnProperty', 'toString', 'valueOf', - // Python built-ins - 'print', 'len', 'range', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple', - 'open', 'read', 'write', 'close', 'append', 'extend', 'update', - 'super', 'type', 'isinstance', 'issubclass', 'getattr', 'setattr', 'hasattr', - 'enumerate', 'zip', 'sorted', 'reversed', 'min', 'max', 'sum', 'abs', - // Kotlin stdlib (IMPORTANT: keep in sync with parse-worker.ts BUILT_IN_NAMES) - 'println', 'print', 'readLine', 'require', 'requireNotNull', 'check', 'assert', 'lazy', 'error', - 'listOf', 'mapOf', 'setOf', 'mutableListOf', 'mutableMapOf', 'mutableSetOf', - 'arrayOf', 'sequenceOf', 'also', 'apply', 'run', 'with', 'takeIf', 'takeUnless', - 'TODO', 'buildString', 'buildList', 'buildMap', 'buildSet', - 'repeat', 'synchronized', - // Kotlin coroutine builders & scope functions - 'launch', 'async', 'runBlocking', 'withContext', 'coroutineScope', - 'supervisorScope', 'delay', - // Kotlin Flow operators - 'flow', 'flowOf', 'collect', 'emit', 'onEach', 'catch', - 'buffer', 'conflate', 'distinctUntilChanged', - 'flatMapLatest', 'flatMapMerge', 'combine', - 'stateIn', 'shareIn', 'launchIn', - // Kotlin infix stdlib functions - 'to', 'until', 'downTo', 'step', - // C/C++ standard library and common kernel helpers - 'printf', 'fprintf', 'sprintf', 'snprintf', 'vprintf', 'vfprintf', 'vsprintf', 'vsnprintf', - 'scanf', 'fscanf', 'sscanf', - 'malloc', 'calloc', 'realloc', 'free', 'memcpy', 'memmove', 'memset', 'memcmp', - 'strlen', 'strcpy', 'strncpy', 'strcat', 'strncat', 'strcmp', 'strncmp', 'strstr', 'strchr', 'strrchr', - 'atoi', 'atol', 'atof', 'strtol', 'strtoul', 'strtoll', 'strtoull', 'strtod', - 'sizeof', 'offsetof', 'typeof', - 'assert', 'abort', 'exit', '_exit', - 'fopen', 'fclose', 'fread', 'fwrite', 'fseek', 'ftell', 'rewind', 'fflush', 'fgets', 'fputs', - // Linux kernel common macros/helpers (not real call targets) - 'likely', 'unlikely', 'BUG', 'BUG_ON', 'WARN', 'WARN_ON', 'WARN_ONCE', - 'IS_ERR', 'PTR_ERR', 'ERR_PTR', 'IS_ERR_OR_NULL', - 'ARRAY_SIZE', 'container_of', 'list_for_each_entry', 'list_for_each_entry_safe', - 'min', 'max', 'clamp', 'abs', 'swap', - 'pr_info', 'pr_warn', 'pr_err', 'pr_debug', 'pr_notice', 'pr_crit', 'pr_emerg', - 'printk', 'dev_info', 'dev_warn', 'dev_err', 'dev_dbg', - 'GFP_KERNEL', 'GFP_ATOMIC', - 'spin_lock', 'spin_unlock', 'spin_lock_irqsave', 'spin_unlock_irqrestore', - 'mutex_lock', 'mutex_unlock', 'mutex_init', - 'kfree', 'kmalloc', 'kzalloc', 'kcalloc', 'krealloc', 'kvmalloc', 'kvfree', - 'get', 'put', - // Swift/iOS built-ins and standard library - 'print', 'debugPrint', 'dump', 'fatalError', 'precondition', 'preconditionFailure', - 'assert', 'assertionFailure', 'NSLog', - 'abs', 'min', 'max', 'zip', 'stride', 'sequence', 'repeatElement', - 'swap', 'withUnsafePointer', 'withUnsafeMutablePointer', 'withUnsafeBytes', - 'autoreleasepool', 'unsafeBitCast', 'unsafeDowncast', 'numericCast', - 'type', 'MemoryLayout', - // Swift collection/string methods (common noise) - 'map', 'flatMap', 'compactMap', 'filter', 'reduce', 'forEach', 'contains', - 'first', 'last', 'prefix', 'suffix', 'dropFirst', 'dropLast', - 'sorted', 'reversed', 'enumerated', 'joined', 'split', - 'append', 'insert', 'remove', 'removeAll', 'removeFirst', 'removeLast', - 'isEmpty', 'count', 'index', 'startIndex', 'endIndex', - // UIKit/Foundation common methods (noise in call graph) - 'addSubview', 'removeFromSuperview', 'layoutSubviews', 'setNeedsLayout', - 'layoutIfNeeded', 'setNeedsDisplay', 'invalidateIntrinsicContentSize', - 'addTarget', 'removeTarget', 'addGestureRecognizer', - 'addConstraint', 'addConstraints', 'removeConstraint', 'removeConstraints', - 'NSLocalizedString', 'Bundle', - 'reloadData', 'reloadSections', 'reloadRows', 'performBatchUpdates', - 'register', 'dequeueReusableCell', 'dequeueReusableSupplementaryView', - 'beginUpdates', 'endUpdates', 'insertRows', 'deleteRows', 'insertSections', 'deleteSections', - 'present', 'dismiss', 'pushViewController', 'popViewController', 'popToRootViewController', - 'performSegue', 'prepare', - // GCD / async - 'DispatchQueue', 'async', 'sync', 'asyncAfter', - 'Task', 'withCheckedContinuation', 'withCheckedThrowingContinuation', - // Combine - 'sink', 'store', 'assign', 'receive', 'subscribe', - // Notification / KVO - 'addObserver', 'removeObserver', 'post', 'NotificationCenter', -]); - -const isBuiltInOrNoise = (name: string): boolean => BUILT_IN_NAMES.has(name); - /** * Fast path: resolve pre-extracted call sites from workers. * No AST parsing — workers already extracted calledName + sourceId. diff --git a/gitnexus/src/core/ingestion/constants.ts b/gitnexus/src/core/ingestion/constants.ts new file mode 100644 index 000000000..81f1107e1 --- /dev/null +++ b/gitnexus/src/core/ingestion/constants.ts @@ -0,0 +1,19 @@ +/** + * Default minimum buffer size for tree-sitter parsing (512 KB). + * tree-sitter requires bufferSize >= file size in bytes. + */ +export const TREE_SITTER_BUFFER_SIZE = 512 * 1024; + +/** + * Maximum buffer size cap (32 MB) to prevent OOM on huge files. + * Also used as the file-size skip threshold — files larger than this are not parsed. + */ +export const TREE_SITTER_MAX_BUFFER = 32 * 1024 * 1024; + +/** + * Compute adaptive buffer size for tree-sitter parsing. + * Uses 2× file size, clamped between 512 KB and 32 MB. + * Previous 256 KB fixed limit silently skipped files > ~200 KB (e.g., imgui.h at 411 KB). + */ +export const getTreeSitterBufferSize = (contentLength: number): number => + Math.min(Math.max(contentLength * 2, TREE_SITTER_BUFFER_SIZE), TREE_SITTER_MAX_BUFFER); diff --git a/gitnexus/src/core/ingestion/entry-point-scoring.ts b/gitnexus/src/core/ingestion/entry-point-scoring.ts index b7b9d457e..39dc8e40a 100644 --- a/gitnexus/src/core/ingestion/entry-point-scoring.ts +++ b/gitnexus/src/core/ingestion/entry-point-scoring.ts @@ -63,10 +63,18 @@ const ENTRY_POINT_PATTERNS: Record = { // C# 'csharp': [ - /^(Get|Post|Put|Delete)/, // ASP.NET conventions - /Action$/, // MVC actions - /^On[A-Z]/, // Event handlers - /Async$/, // Async entry points + /^(Get|Post|Put|Delete|Patch)/, // ASP.NET action methods + /Action$/, // MVC actions + /^On[A-Z]/, // Event handlers / Blazor lifecycle + /Async$/, // Async entry points + /^Configure$/, // Startup.Configure + /^ConfigureServices$/, // Startup.ConfigureServices + /^Handle$/, // MediatR / generic handler + /^Execute$/, // Command pattern + /^Invoke$/, // Middleware Invoke + /^Map[A-Z]/, // Minimal API MapGet, MapPost + /Service$/, // Service classes + /^Seed/, // Database seeding ], // Go @@ -86,21 +94,60 @@ const ENTRY_POINT_PATTERNS: Record = { /^spawn/, // Async spawn ], - // C - explicit main() boost (critical for C programs) + // C - explicit main() boost plus common C entry point conventions 'c': [ /^main$/, // THE entry point - /^init_/, // Initialization functions - /^start_/, // Start functions - /^run_/, // Run functions + /^init_/, // init_server, init_client + /_init$/, // module_init, server_init + /^start_/, // start_server + /_start$/, // thread_start + /^run_/, // run_loop + /_run$/, // event_run + /^stop_/, // stop_server + /_stop$/, // service_stop + /^open_/, // open_connection + /_open$/, // file_open + /^close_/, // close_connection + /_close$/, // socket_close + /^create_/, // create_session + /_create$/, // object_create + /^destroy_/, // destroy_session + /_destroy$/, // object_destroy + /^handle_/, // handle_request + /_handler$/, // signal_handler + /_callback$/, // event_callback + /^cmd_/, // tmux: cmd_new_window, cmd_attach_session + /^server_/, // server_start, server_loop + /^client_/, // client_connect + /^session_/, // session_create + /^window_/, // window_resize (tmux) + /^key_/, // key_press + /^input_/, // input_parse + /^output_/, // output_write + /^notify_/, // notify_client + /^control_/, // control_start ], - - // C++ - same as C plus class patterns + + // C++ - same as C plus OOP/template patterns 'cpp': [ /^main$/, // THE entry point /^init_/, + /_init$/, /^Create[A-Z]/, // Factory patterns + /^create_/, /^Run$/, // Run methods + /^run$/, /^Start$/, // Start methods + /^start$/, + /^handle_/, + /_handler$/, + /_callback$/, + /^OnEvent/, // Event callbacks + /^on_/, + /::Run$/, // Class::Run + /::Start$/, // Class::Start + /::Init$/, // Class::Init + /::Execute$/, // Class::Execute ], // Swift / iOS @@ -145,6 +192,14 @@ const ENTRY_POINT_PATTERNS: Record = { ], }; +/** Pre-computed merged patterns (universal + language-specific) to avoid per-call array allocation. */ +const MERGED_ENTRY_POINT_PATTERNS: Record = {}; +const UNIVERSAL_PATTERNS = ENTRY_POINT_PATTERNS['*'] || []; +for (const [lang, patterns] of Object.entries(ENTRY_POINT_PATTERNS)) { + if (lang === '*') continue; + MERGED_ENTRY_POINT_PATTERNS[lang] = [...UNIVERSAL_PATTERNS, ...patterns]; +} + // ============================================================================ // UTILITY PATTERNS - Functions that should be penalized // ============================================================================ @@ -232,9 +287,7 @@ export function calculateEntryPointScore( reasons.push('utility-pattern'); } else { // Check positive patterns - const universalPatterns = ENTRY_POINT_PATTERNS['*'] || []; - const langPatterns = ENTRY_POINT_PATTERNS[language] || []; - const allPatterns = [...universalPatterns, ...langPatterns]; + const allPatterns = MERGED_ENTRY_POINT_PATTERNS[language] || UNIVERSAL_PATTERNS; if (allPatterns.some(p => p.test(name))) { nameMultiplier = 1.5; // Bonus for matching entry point pattern @@ -296,8 +349,13 @@ export function isTestFile(filePath: string): boolean { p.endsWith('test.swift') || p.includes('uitests/') || // C# test patterns + p.endsWith('tests.cs') || + p.endsWith('test.cs') || p.includes('.tests/') || - p.includes('tests.cs') || + p.includes('.test/') || + p.includes('.integrationtests/') || + p.includes('.unittests/') || + p.includes('/testproject/') || // PHP/Laravel test patterns p.endsWith('test.php') || p.endsWith('spec.php') || diff --git a/gitnexus/src/core/ingestion/export-detection.ts b/gitnexus/src/core/ingestion/export-detection.ts new file mode 100644 index 000000000..21729ddb5 --- /dev/null +++ b/gitnexus/src/core/ingestion/export-detection.ts @@ -0,0 +1,198 @@ +/** + * Export Detection + * + * Determines whether a symbol (function, class, etc.) is exported/public + * in its language. This is a pure function — safe for use in worker threads. + * + * Shared between parse-worker.ts (worker pool) and parsing-processor.ts (sequential fallback). + */ + +import { findSiblingChild } from './utils.js'; + +/** C# declaration node types for sibling modifier scanning. */ +const CSHARP_DECL_TYPES = new Set([ + 'method_declaration', 'local_function_statement', 'constructor_declaration', + 'class_declaration', 'interface_declaration', 'struct_declaration', + 'enum_declaration', 'record_declaration', 'record_struct_declaration', + 'record_class_declaration', 'delegate_declaration', + 'property_declaration', 'field_declaration', 'event_declaration', + 'namespace_declaration', 'file_scoped_namespace_declaration', +]); + +/** Rust declaration node types for sibling visibility_modifier scanning. */ +const RUST_DECL_TYPES = new Set([ + 'function_item', 'struct_item', 'enum_item', 'trait_item', 'impl_item', + 'union_item', 'type_item', 'const_item', 'static_item', 'mod_item', + 'use_declaration', 'associated_type', 'function_signature_item', +]); + +/** + * Check if a tree-sitter node is exported/public in its language. + * @param node - The tree-sitter AST node + * @param name - The symbol name + * @param language - The programming language + * @returns true if the symbol is exported/public + */ +export const isNodeExported = (node: any, name: string, language: string): boolean => { + let current = node; + + switch (language) { + // JavaScript/TypeScript: Check for export keyword in ancestors + case 'javascript': + case 'typescript': + while (current) { + const type = current.type; + if (type === 'export_statement' || + type === 'export_specifier' || + (type === 'lexical_declaration' && current.parent?.type === 'export_statement')) { + return true; + } + // Fallback: check if node text starts with 'export ' for edge cases + if (current.text?.startsWith('export ')) { + return true; + } + current = current.parent; + } + return false; + + // Python: Public if no leading underscore (convention) + case 'python': + return !name.startsWith('_'); + + // Java: Check for 'public' modifier + // In tree-sitter Java, modifiers are siblings of the name node, not parents + case 'java': + while (current) { + if (current.parent) { + const parent = current.parent; + for (let i = 0; i < parent.childCount; i++) { + const child = parent.child(i); + if (child?.type === 'modifiers' && child.text?.includes('public')) { + return true; + } + } + if (parent.type === 'method_declaration' || parent.type === 'constructor_declaration') { + if (parent.text?.trimStart().startsWith('public')) { + return true; + } + } + } + current = current.parent; + } + return false; + + // C#: modifier nodes are SIBLINGS of the name node inside the declaration. + // Walk up to the declaration node, then scan its direct children. + case 'csharp': { + while (current) { + if (CSHARP_DECL_TYPES.has(current.type)) { + for (let i = 0; i < current.childCount; i++) { + const child = current.child(i); + if (child?.type === 'modifier' && child.text === 'public') return true; + } + return false; + } + current = current.parent; + } + return false; + } + + // Go: Uppercase first letter = exported + case 'go': + if (name.length === 0) return false; + const first = name[0]; + return first === first.toUpperCase() && first !== first.toLowerCase(); + + // Rust: visibility_modifier is a SIBLING of the name node within the + // declaration node (function_item, struct_item, etc.), not a parent. + // Walk up to the declaration node, then scan its direct children. + case 'rust': { + while (current) { + if (RUST_DECL_TYPES.has(current.type)) { + for (let i = 0; i < current.childCount; i++) { + const child = current.child(i); + if (child?.type === 'visibility_modifier' && child.text?.startsWith('pub')) return true; + } + return false; + } + current = current.parent; + } + return false; + } + + // Kotlin: Default visibility is public (unlike Java) + // visibility_modifier is inside modifiers, a sibling of the name node within the declaration + case 'kotlin': + while (current) { + if (current.parent) { + const visMod = findSiblingChild(current.parent, 'modifiers', 'visibility_modifier'); + if (visMod) { + const text = visMod.text; + if (text === 'private' || text === 'internal' || text === 'protected') return false; + if (text === 'public') return true; + } + } + current = current.parent; + } + // No visibility modifier = public (Kotlin default) + return true; + + // C/C++: Functions without 'static' storage class have external linkage + // by default, making them globally accessible (equivalent to exported). + // Only functions explicitly marked 'static' are file-scoped (not exported). + // C++ anonymous namespaces (namespace { ... }) also give internal linkage. + case 'c': + case 'cpp': { + // Walk up to the function_definition/declaration and check for 'static' + let cur = node; + while (cur) { + if (cur.type === 'function_definition' || cur.type === 'declaration') { + // Check for 'static' storage class specifier as a direct child node. + // This avoids reading the full function text (which can be very large). + for (let i = 0; i < cur.childCount; i++) { + const child = cur.child(i); + if (child?.type === 'storage_class_specifier' && child.text === 'static') return false; + } + } + // C++ anonymous namespace: namespace_definition with no name child = internal linkage + if (cur.type === 'namespace_definition') { + const hasName = cur.childForFieldName?.('name'); + if (!hasName) return false; + } + cur = cur.parent; + } + return true; // Top-level C/C++ functions default to external linkage + } + + // PHP: Check for visibility modifier or top-level scope + case 'php': + while (current) { + if (current.type === 'class_declaration' || + current.type === 'interface_declaration' || + current.type === 'trait_declaration' || + current.type === 'enum_declaration') { + return true; + } + if (current.type === 'visibility_modifier') { + return current.text === 'public'; + } + current = current.parent; + } + // Top-level functions are globally accessible + return true; + + // Swift: Check for 'public' or 'open' access modifiers + case 'swift': + while (current) { + if (current.type === 'modifiers' || current.type === 'visibility_modifier') { + const text = current.text || ''; + if (text.includes('public') || text.includes('open')) return true; + } + current = current.parent; + } + return false; + + default: + return false; + } +}; diff --git a/gitnexus/src/core/ingestion/framework-detection.ts b/gitnexus/src/core/ingestion/framework-detection.ts index aecff1126..8729c5c89 100644 --- a/gitnexus/src/core/ingestion/framework-detection.ts +++ b/gitnexus/src/core/ingestion/framework-detection.ts @@ -183,7 +183,35 @@ export function detectFrameworkFromPath(filePath: string): FrameworkHint | null if (p.endsWith('controller.cs')) { return { framework: 'aspnet', entryPointMultiplier: 3.0, reason: 'aspnet-controller-file' }; } - + + // ASP.NET Services + if ((p.includes('/services/') || p.includes('/service/')) && p.endsWith('.cs')) { + return { framework: 'aspnet', entryPointMultiplier: 1.8, reason: 'aspnet-service' }; + } + + // ASP.NET Middleware + if (p.includes('/middleware/') && p.endsWith('.cs')) { + return { framework: 'aspnet', entryPointMultiplier: 2.5, reason: 'aspnet-middleware' }; + } + + // SignalR Hubs + if (p.includes('/hubs/') && p.endsWith('.cs')) { + return { framework: 'signalr', entryPointMultiplier: 2.5, reason: 'signalr-hub' }; + } + if (p.endsWith('hub.cs')) { + return { framework: 'signalr', entryPointMultiplier: 2.5, reason: 'signalr-hub-file' }; + } + + // Minimal API / Program.cs / Startup.cs + if (p.endsWith('/program.cs') || p.endsWith('/startup.cs')) { + return { framework: 'aspnet', entryPointMultiplier: 3.0, reason: 'aspnet-entry' }; + } + + // Background services / Hosted services + if ((p.includes('/backgroundservices/') || p.includes('/hostedservices/')) && p.endsWith('.cs')) { + return { framework: 'aspnet', entryPointMultiplier: 2.0, reason: 'aspnet-background-service' }; + } + // Blazor pages if (p.includes('/pages/') && p.endsWith('.razor')) { return { framework: 'blazor', entryPointMultiplier: 2.5, reason: 'blazor-page' }; @@ -385,7 +413,11 @@ export const FRAMEWORK_AST_PATTERNS = { 'jaxrs': ['@Path', '@GET', '@POST', '@PUT', '@DELETE'], // C# attributes - 'aspnet': ['[ApiController]', '[HttpGet]', '[HttpPost]', '[Route]'], + 'aspnet': ['[ApiController]', '[HttpGet]', '[HttpPost]', '[HttpPut]', '[HttpDelete]', + '[Route]', '[Authorize]', '[AllowAnonymous]'], + 'signalr': ['[HubMethodName]', ': Hub', ': Hub<'], + 'blazor': ['@page', '[Parameter]', '@inject'], + 'efcore': ['DbContext', 'DbSet<', 'OnModelCreating'], // Go patterns (function signatures) 'go-http': ['http.Handler', 'http.HandlerFunc', 'ServeHTTP'], @@ -435,6 +467,9 @@ const AST_FRAMEWORK_PATTERNS_BY_LANGUAGE: Record 32KB try { - tree = parser.parse(file.content, undefined, { bufferSize: 1024 * 256 }); + tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) }); } catch (parseError) { // Skip files that can't be parsed continue; diff --git a/gitnexus/src/core/ingestion/import-processor.ts b/gitnexus/src/core/ingestion/import-processor.ts index 7aaf4a199..fc1914c65 100644 --- a/gitnexus/src/core/ingestion/import-processor.ts +++ b/gitnexus/src/core/ingestion/import-processor.ts @@ -9,6 +9,7 @@ import { generateId } from '../../lib/utils.js'; import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop } from './utils.js'; import { SupportedLanguages } from '../../config/supported-languages.js'; import type { ExtractedImport } from './workers/parse-worker.js'; +import { getTreeSitterBufferSize } from './constants.js'; const isDev = process.env.NODE_ENV === 'development'; @@ -153,6 +154,152 @@ async function loadComposerConfig(repoRoot: string): Promise or assembly name (default: project directory name) */ + rootNamespace: string; + /** Directory containing the .csproj file */ + projectDir: string; +} + +/** + * Parse .csproj files to extract RootNamespace. + * Scans the repo root for .csproj files and returns configs for each. + */ +async function loadCSharpProjectConfig(repoRoot: string): Promise { + const configs: CSharpProjectConfig[] = []; + // BFS scan for .csproj files up to 5 levels deep, cap at 100 dirs to avoid runaway scanning + const scanQueue: { dir: string; depth: number }[] = [{ dir: repoRoot, depth: 0 }]; + const maxDepth = 5; + const maxDirs = 100; + let dirsScanned = 0; + + while (scanQueue.length > 0 && dirsScanned < maxDirs) { + const { dir, depth } = scanQueue.shift()!; + dirsScanned++; + try { + const entries = await fs.readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory() && depth < maxDepth) { + // Skip common non-project directories + if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'bin' || entry.name === 'obj') continue; + scanQueue.push({ dir: path.join(dir, entry.name), depth: depth + 1 }); + } + if (entry.isFile() && entry.name.endsWith('.csproj')) { + try { + const csprojPath = path.join(dir, entry.name); + const content = await fs.readFile(csprojPath, 'utf-8'); + const nsMatch = content.match(/\s*([^<]+)\s*<\/RootNamespace>/); + const rootNamespace = nsMatch + ? nsMatch[1].trim() + : entry.name.replace(/\.csproj$/, ''); + const projectDir = path.relative(repoRoot, dir).replace(/\\/g, '/'); + configs.push({ rootNamespace, projectDir }); + if (isDev) { + console.log(`📦 Loaded C# project: ${entry.name} (namespace: ${rootNamespace}, dir: ${projectDir})`); + } + } catch { + // Can't read .csproj + } + } + } + } catch { + // Can't read directory + } + } + return configs; +} + +/** + * Resolve a C# using directive to file paths. + * C# `using` directives import namespaces (not files), so one using can resolve + * to multiple .cs files in a directory — similar to Go package imports. + * + * e.g. "MyApp.Services" -> all .cs files in "src/Services/" + * e.g. "MyApp.Services.UserService" -> "src/Services/UserService.cs" (single file) + * + * Strategy: + * 1. Strip root namespace prefix from each known .csproj project + * 2. Convert remaining namespace to path: Dots -> / + * 3. Try as single file first (ClassName import), then as directory (namespace import) + */ +function resolveCSharpImport( + importPath: string, + csharpConfigs: CSharpProjectConfig[], + normalizedFileList: string[], + allFileList: string[], + index?: SuffixIndex, +): string[] { + const namespacePath = importPath.replace(/\./g, '/'); + const results: string[] = []; + + for (const config of csharpConfigs) { + const nsPath = config.rootNamespace.replace(/\./g, '/'); + let relative: string; + if (namespacePath.startsWith(nsPath + '/')) { + relative = namespacePath.slice(nsPath.length + 1); + } else if (namespacePath === nsPath) { + // The import IS the root namespace — resolve to all .cs files in project root + relative = ''; + } else { + continue; + } + + const dirPrefix = config.projectDir + ? (relative ? config.projectDir + '/' + relative : config.projectDir) + : relative; + + // 1. Try as single file: relative.cs (e.g., "Models/DlqMessage.cs") + if (relative) { + const candidate = dirPrefix + '.cs'; + if (index) { + const result = index.get(candidate) || index.getInsensitive(candidate); + if (result) return [result]; + } + // Also try suffix match + const suffixResult = index?.get(relative + '.cs') || index?.getInsensitive(relative + '.cs'); + if (suffixResult) return [suffixResult]; + } + + // 2. Try as directory: all .cs files directly inside (namespace import) + if (index) { + const dirFiles = index.getFilesInDir(dirPrefix, '.cs'); + for (const f of dirFiles) { + const normalized = f.replace(/\\/g, '/'); + // Check it's a direct child by finding the dirPrefix and ensuring no deeper slashes + const prefixIdx = normalized.indexOf(dirPrefix + '/'); + if (prefixIdx < 0) continue; + const afterDir = normalized.substring(prefixIdx + dirPrefix.length + 1); + if (!afterDir.includes('/')) { + results.push(f); + } + } + if (results.length > 0) return results; + } + + // 3. Linear scan fallback for directory matching + if (results.length === 0) { + const dirTrail = dirPrefix + '/'; + for (let i = 0; i < normalizedFileList.length; i++) { + const normalized = normalizedFileList[i]; + if (!normalized.endsWith('.cs')) continue; + const prefixIdx = normalized.indexOf(dirTrail); + if (prefixIdx < 0) continue; + const afterDir = normalized.substring(prefixIdx + dirTrail.length); + if (!afterDir.includes('/')) { + results.push(allFileList[i]); + } + } + if (results.length > 0) return results; + } + } + + // Fallback: suffix matching without namespace stripping (single file) + const pathParts = namespacePath.split('/').filter(Boolean); + const fallback = suffixResolve(pathParts, normalizedFileList, allFileList, index); + return fallback ? [fallback] : []; +} + /** Swift Package Manager module config */ interface SwiftPackageConfig { /** Map of target name -> source directory path (e.g., "SiuperModel" -> "Package/Sources/SiuperModel") */ @@ -751,6 +898,7 @@ export const processImports = async ( const goModule = await loadGoModulePath(effectiveRoot); const composerConfig = await loadComposerConfig(effectiveRoot); const swiftPackageConfig = await loadSwiftPackageConfig(effectiveRoot); + const csharpConfigs = await loadCSharpProjectConfig(effectiveRoot); // Helper: add an IMPORTS edge + update import map const addImportEdge = (filePath: string, resolvedPath: string) => { @@ -802,7 +950,7 @@ export const processImports = async ( if (!tree) { try { - tree = parser.parse(file.content, undefined, { bufferSize: 1024 * 256 }); + tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) }); } catch (parseError) { continue; } @@ -897,6 +1045,15 @@ export const processImports = async ( // Fall through if no files found (package might be external) } + // ---- C#: handle namespace-based imports (using directives) ---- + if (language === SupportedLanguages.CSharp && csharpConfigs.length > 0) { + const resolvedFiles = resolveCSharpImport(rawImportPath, csharpConfigs, normalizedFileList, allFileList, index); + for (const resolvedFile of resolvedFiles) { + addImportEdge(file.path, resolvedFile); + } + return; + } + // ---- PHP: handle namespace-based imports (use statements) ---- if (language === SupportedLanguages.PHP) { const resolved = resolvePhpImport(rawImportPath, composerConfig, allFilePaths, normalizedFileList, allFileList, index); @@ -984,6 +1141,7 @@ export const processImportsFromExtracted = async ( const goModule = await loadGoModulePath(effectiveRoot); const composerConfig = await loadComposerConfig(effectiveRoot); const swiftPackageConfig = await loadSwiftPackageConfig(effectiveRoot); + const csharpConfigs = await loadCSharpProjectConfig(effectiveRoot); const addImportEdge = (filePath: string, resolvedPath: string) => { const sourceId = generateId('File', filePath); @@ -1097,6 +1255,15 @@ export const processImportsFromExtracted = async ( } } + // C#: handle namespace-based imports (using directives) + if (language === SupportedLanguages.CSharp && csharpConfigs.length > 0) { + const resolvedFiles = resolveCSharpImport(rawImportPath, csharpConfigs, normalizedFileList, allFileList, index); + for (const resolvedFile of resolvedFiles) { + addImportEdge(filePath, resolvedFile); + } + continue; + } + // PHP: handle namespace-based imports (use statements) if (language === SupportedLanguages.PHP) { const resolved = resolvePhpImport(rawImportPath, composerConfig, allFilePaths, normalizedFileList, allFileList, index); diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index ae0e7026f..be5317fed 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -5,10 +5,12 @@ import { LANGUAGE_QUERIES } from './tree-sitter-queries.js'; import { generateId } from '../../lib/utils.js'; import { SymbolTable } from './symbol-table.js'; import { ASTCache } from './ast-cache.js'; -import { findSiblingChild, getLanguageFromFilename, yieldToEventLoop } from './utils.js'; +import { getLanguageFromFilename, yieldToEventLoop, DEFINITION_CAPTURE_KEYS, getDefinitionNodeFromCaptures } from './utils.js'; +import { isNodeExported } from './export-detection.js'; import { detectFrameworkFromAST } from './framework-detection.js'; import { WorkerPool } from './workers/worker-pool.js'; import type { ParseWorkerResult, ParseWorkerInput, ExtractedImport, ExtractedCall, ExtractedHeritage, ExtractedRoute } from './workers/parse-worker.js'; +import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from './constants.js'; export type FileProgressCallback = (current: number, total: number, filePath: string) => void; @@ -19,183 +21,9 @@ export interface WorkerExtractedData { routes: ExtractedRoute[]; } -const DEFINITION_CAPTURE_KEYS = [ - 'definition.function', - 'definition.class', - 'definition.interface', - 'definition.method', - 'definition.struct', - 'definition.enum', - 'definition.namespace', - 'definition.module', - 'definition.trait', - 'definition.impl', - 'definition.type', - 'definition.const', - 'definition.static', - 'definition.typedef', - 'definition.macro', - 'definition.union', - 'definition.property', - 'definition.record', - 'definition.delegate', - 'definition.annotation', - 'definition.constructor', - 'definition.template', -] as const; - -const getDefinitionNodeFromCaptures = (captureMap: Record): any | null => { - for (const key of DEFINITION_CAPTURE_KEYS) { - if (captureMap[key]) return captureMap[key]; - } - return null; -}; - -// ============================================================================ -// EXPORT DETECTION - Language-specific visibility detection -// ============================================================================ - -/** - * Check if a symbol (function, class, etc.) is exported/public - * Handles all 9 supported languages with explicit logic - * - * @param node - The AST node for the symbol name - * @param name - The symbol name - * @param language - The programming language - * @returns true if the symbol is exported/public - */ -export const isNodeExported = (node: any, name: string, language: string): boolean => { - let current = node; - - switch (language) { - // JavaScript/TypeScript: Check for export keyword in ancestors - case 'javascript': - case 'typescript': - while (current) { - const type = current.type; - if (type === 'export_statement' || - type === 'export_specifier' || - type === 'lexical_declaration' && current.parent?.type === 'export_statement') { - return true; - } - // Also check if text starts with 'export ' - if (current.text?.startsWith('export ')) { - return true; - } - current = current.parent; - } - return false; - - // Python: Public if no leading underscore (convention) - case 'python': - return !name.startsWith('_'); - - // Java: Check for 'public' modifier - // In tree-sitter Java, modifiers are siblings of the name node, not parents - case 'java': - while (current) { - // Check if this node or any sibling is a 'modifiers' node containing 'public' - if (current.parent) { - const parent = current.parent; - // Check all children of the parent for modifiers - for (let i = 0; i < parent.childCount; i++) { - const child = parent.child(i); - if (child?.type === 'modifiers' && child.text?.includes('public')) { - return true; - } - } - // Also check if the parent's text starts with 'public' (fallback) - if (parent.type === 'method_declaration' || parent.type === 'constructor_declaration') { - if (parent.text?.trimStart().startsWith('public')) { - return true; - } - } - } - current = current.parent; - } - return false; - - // C#: Check for 'public' modifier in ancestors - case 'csharp': - while (current) { - if (current.type === 'modifier' || current.type === 'modifiers') { - if (current.text?.includes('public')) return true; - } - current = current.parent; - } - return false; - - // Go: Uppercase first letter = exported - case 'go': - if (name.length === 0) return false; - const first = name[0]; - // Must be uppercase letter (not a number or symbol) - return first === first.toUpperCase() && first !== first.toLowerCase(); - - // Rust: Check for 'pub' visibility modifier - case 'rust': - while (current) { - if (current.type === 'visibility_modifier') { - if (current.text?.includes('pub')) return true; - } - current = current.parent; - } - return false; - - // Kotlin: Default visibility is public (unlike Java) - // visibility_modifier is inside modifiers, a sibling of the name node within the declaration - case 'kotlin': - while (current) { - if (current.parent) { - const visMod = findSiblingChild(current.parent, 'modifiers', 'visibility_modifier'); - if (visMod) { - const text = visMod.text; - if (text === 'private' || text === 'internal' || text === 'protected') return false; - if (text === 'public') return true; - } - } - current = current.parent; - } - // No visibility modifier = public (Kotlin default) - return true; - - // C/C++: No native export concept at language level - // Entry points will be detected via name patterns (main, etc.) - case 'c': - case 'cpp': - return false; - - // Swift: Check for 'public' or 'open' access modifiers - case 'swift': - while (current) { - if (current.type === 'modifiers' || current.type === 'visibility_modifier') { - const text = current.text || ''; - if (text.includes('public') || text.includes('open')) return true; - } - current = current.parent; - } - return false; - - // PHP: Check for visibility modifier or top-level scope - case 'php': - while (current) { - if (current.type === 'class_declaration' || - current.type === 'interface_declaration' || - current.type === 'trait_declaration' || - current.type === 'enum_declaration') { - return true; - } - if (current.type === 'visibility_modifier') { - return current.text === 'public'; - } - current = current.parent; - } - return true; // Top-level functions are globally accessible - - default: - return false; - } -}; +// isNodeExported imported from ./export-detection.js (shared module) +// Re-export for backward compatibility with any external consumers +export { isNodeExported } from './export-detection.js'; // ============================================================================ // Worker-based parallel parsing @@ -286,8 +114,8 @@ const processParsingSequential = async ( if (!language) continue; - // Skip very large files — they can crash tree-sitter or cause OOM - if (file.content.length > 512 * 1024) continue; + // Skip files larger than the max tree-sitter buffer (32 MB) + if (file.content.length > TREE_SITTER_MAX_BUFFER) continue; try { await loadLanguage(language, file.path); @@ -297,7 +125,7 @@ const processParsingSequential = async ( let tree; try { - tree = parser.parse(file.content, undefined, { bufferSize: 1024 * 256 }); + tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) }); } catch (parseError) { console.warn(`Skipping unparseable file: ${file.path}`); continue; @@ -368,7 +196,7 @@ const processParsingSequential = async ( const definitionNodeForRange = getDefinitionNodeFromCaptures(captureMap); const startLine = definitionNodeForRange ? definitionNodeForRange.startPosition.row : (nameNode ? nameNode.startPosition.row : 0); - const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}:${startLine}`); + const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}`); const definitionNode = getDefinitionNodeFromCaptures(captureMap); const frameworkHint = definitionNode diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index 7eeeb73e0..e5300b149 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -69,7 +69,7 @@ export const TYPESCRIPT_QUERIES = ` (type_identifier) @heritage.implements))) @heritage.impl `; -// JavaScript queries - works with tree-sitter-javascript +// JavaScript queries - works with tree-sitter-javascript export const JAVASCRIPT_QUERIES = ` (class_declaration name: (identifier) @name) @definition.class @@ -178,10 +178,17 @@ export const JAVA_QUERIES = ` // C queries - works with tree-sitter-c export const C_QUERIES = ` -; Functions +; Functions (direct declarator) (function_definition declarator: (function_declarator declarator: (identifier) @name)) @definition.function (declaration declarator: (function_declarator declarator: (identifier) @name)) @definition.function +; Functions returning pointers (pointer_declarator wraps function_declarator) +(function_definition declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name))) @definition.function +(declaration declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name))) @definition.function + +; Functions returning double pointers (nested pointer_declarator) +(function_definition declarator: (pointer_declarator declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name)))) @definition.function + ; Structs, Unions, Enums, Typedefs (struct_specifier name: (type_identifier) @name) @definition.struct (union_specifier name: (type_identifier) @name) @definition.union @@ -228,10 +235,46 @@ export const CPP_QUERIES = ` (namespace_definition name: (namespace_identifier) @name) @definition.namespace (enum_specifier name: (type_identifier) @name) @definition.enum -; Functions & Methods +; Typedefs and unions (common in C-style headers and mixed C/C++ code) +(type_definition declarator: (type_identifier) @name) @definition.typedef +(union_specifier name: (type_identifier) @name) @definition.union + +; Macros +(preproc_function_def name: (identifier) @name) @definition.macro +(preproc_def name: (identifier) @name) @definition.macro + +; Functions & Methods (direct declarator) (function_definition declarator: (function_declarator declarator: (identifier) @name)) @definition.function (function_definition declarator: (function_declarator declarator: (qualified_identifier name: (identifier) @name))) @definition.method +; Functions/methods returning pointers (pointer_declarator wraps function_declarator) +(function_definition declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name))) @definition.function +(function_definition declarator: (pointer_declarator declarator: (function_declarator declarator: (qualified_identifier name: (identifier) @name)))) @definition.method + +; Functions/methods returning double pointers (nested pointer_declarator) +(function_definition declarator: (pointer_declarator declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name)))) @definition.function +(function_definition declarator: (pointer_declarator declarator: (pointer_declarator declarator: (function_declarator declarator: (qualified_identifier name: (identifier) @name))))) @definition.method + +; Functions/methods returning references (reference_declarator wraps function_declarator) +(function_definition declarator: (reference_declarator (function_declarator declarator: (identifier) @name))) @definition.function +(function_definition declarator: (reference_declarator (function_declarator declarator: (qualified_identifier name: (identifier) @name)))) @definition.method + +; Destructors (destructor_name is distinct from identifier in tree-sitter-cpp) +(function_definition declarator: (function_declarator declarator: (qualified_identifier name: (destructor_name) @name))) @definition.method + +; Function declarations / prototypes (common in headers) +(declaration declarator: (function_declarator declarator: (identifier) @name)) @definition.function +(declaration declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name))) @definition.function + +; Inline class method declarations (inside class body, no body: void Foo();) +(field_declaration declarator: (function_declarator declarator: (identifier) @name)) @definition.method + +; Inline class method definitions (inside class body, with body: void Foo() { ... }) +(field_declaration_list + (function_definition + declarator: (function_declarator + declarator: [(field_identifier) (identifier) (operator_name) (destructor_name)] @name))) @definition.method + ; Templates (template_declaration (class_specifier name: (type_identifier) @name)) @definition.template (template_declaration (function_definition declarator: (function_declarator declarator: (identifier) @name))) @definition.template @@ -262,9 +305,11 @@ export const CSHARP_QUERIES = ` (record_declaration name: (identifier) @name) @definition.record (delegate_declaration name: (identifier) @name) @definition.delegate -; Namespaces +; Namespaces (block form and C# 10+ file-scoped form) (namespace_declaration name: (identifier) @name) @definition.namespace (namespace_declaration name: (qualified_name) @name) @definition.namespace +(file_scoped_namespace_declaration name: (identifier) @name) @definition.namespace +(file_scoped_namespace_declaration name: (qualified_name) @name) @definition.namespace ; Methods & Properties (method_declaration name: (identifier) @name) @definition.method @@ -282,9 +327,9 @@ export const CSHARP_QUERIES = ` ; Heritage (class_declaration name: (identifier) @heritage.class - (base_list (simple_base_type (identifier) @heritage.extends))) @heritage + (base_list (identifier) @heritage.extends)) @heritage (class_declaration name: (identifier) @heritage.class - (base_list (simple_base_type (generic_name (identifier) @heritage.extends)))) @heritage + (base_list (generic_name (identifier) @heritage.extends))) @heritage `; // Rust queries - works with tree-sitter-rust @@ -294,7 +339,8 @@ export const RUST_QUERIES = ` (struct_item name: (type_identifier) @name) @definition.struct (enum_item name: (type_identifier) @name) @definition.enum (trait_item name: (type_identifier) @name) @definition.trait -(impl_item type: (type_identifier) @name) @definition.impl +(impl_item type: (type_identifier) @name !trait) @definition.impl +(impl_item type: (generic_type type: (type_identifier) @name) !trait) @definition.impl (mod_item name: (identifier) @name) @definition.module ; Type aliases, const, static, macros @@ -312,9 +358,11 @@ export const RUST_QUERIES = ` (call_expression function: (scoped_identifier name: (identifier) @call.name)) @call (call_expression function: (generic_function function: (identifier) @call.name)) @call -; Heritage (trait implementation) +; Heritage (trait implementation) — all combinations of concrete/generic trait × concrete/generic type (impl_item trait: (type_identifier) @heritage.trait type: (type_identifier) @heritage.class) @heritage (impl_item trait: (generic_type type: (type_identifier) @heritage.trait) type: (type_identifier) @heritage.class) @heritage +(impl_item trait: (type_identifier) @heritage.trait type: (generic_type type: (type_identifier) @heritage.class)) @heritage +(impl_item trait: (generic_type type: (type_identifier) @heritage.trait) type: (generic_type type: (type_identifier) @heritage.class)) @heritage `; // PHP queries - works with tree-sitter-php (php_only grammar) diff --git a/gitnexus/src/core/ingestion/utils.ts b/gitnexus/src/core/ingestion/utils.ts index 7abe07626..3fb36e6e9 100644 --- a/gitnexus/src/core/ingestion/utils.ts +++ b/gitnexus/src/core/ingestion/utils.ts @@ -1,5 +1,333 @@ import { SupportedLanguages } from '../../config/supported-languages.js'; +/** + * Ordered list of definition capture keys for tree-sitter query matches. + * Used to extract the definition node from a capture map. + */ +export const DEFINITION_CAPTURE_KEYS = [ + 'definition.function', + 'definition.class', + 'definition.interface', + 'definition.method', + 'definition.struct', + 'definition.enum', + 'definition.namespace', + 'definition.module', + 'definition.trait', + 'definition.impl', + 'definition.type', + 'definition.const', + 'definition.static', + 'definition.typedef', + 'definition.macro', + 'definition.union', + 'definition.property', + 'definition.record', + 'definition.delegate', + 'definition.annotation', + 'definition.constructor', + 'definition.template', +] as const; + +/** Extract the definition node from a tree-sitter query capture map. */ +export const getDefinitionNodeFromCaptures = (captureMap: Record): any | null => { + for (const key of DEFINITION_CAPTURE_KEYS) { + if (captureMap[key]) return captureMap[key]; + } + return null; +}; + +/** + * Node types that represent function/method definitions across languages. + * Used to find the enclosing function for a call site. + */ +export const FUNCTION_NODE_TYPES = new Set([ + // TypeScript/JavaScript + 'function_declaration', + 'arrow_function', + 'function_expression', + 'method_definition', + 'generator_function_declaration', + // Python + 'function_definition', + // Common async variants + 'async_function_declaration', + 'async_arrow_function', + // Java + 'method_declaration', + 'constructor_declaration', + // C/C++ + // 'function_definition' already included above + // Go + // 'method_declaration' already included from Java + // C# + 'local_function_statement', + // Rust + 'function_item', + 'impl_item', // Methods inside impl blocks + // PHP + 'anonymous_function', + // Kotlin + 'lambda_literal', + // Swift + 'init_declaration', + 'deinit_declaration', +]); + +/** + * Node types for standard function declarations that need C/C++ declarator handling. + * Used by extractFunctionName to determine how to extract the function name. + */ +export const FUNCTION_DECLARATION_TYPES = new Set([ + 'function_declaration', + 'function_definition', + 'async_function_declaration', + 'generator_function_declaration', + 'function_item', +]); + +/** + * Built-in function/method names that should not be tracked as call targets. + * Covers JS/TS, Python, Kotlin, C/C++, PHP, Swift standard library functions. + */ +export const BUILT_IN_NAMES = new Set([ + // JavaScript/TypeScript + 'console', 'log', 'warn', 'error', 'info', 'debug', + 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', + 'parseInt', 'parseFloat', 'isNaN', 'isFinite', + 'encodeURI', 'decodeURI', 'encodeURIComponent', 'decodeURIComponent', + 'JSON', 'parse', 'stringify', + 'Object', 'Array', 'String', 'Number', 'Boolean', 'Symbol', 'BigInt', + 'Map', 'Set', 'WeakMap', 'WeakSet', + 'Promise', 'resolve', 'reject', 'then', 'catch', 'finally', + 'Math', 'Date', 'RegExp', 'Error', + 'require', 'import', 'export', 'fetch', 'Response', 'Request', + 'useState', 'useEffect', 'useCallback', 'useMemo', 'useRef', 'useContext', + 'useReducer', 'useLayoutEffect', 'useImperativeHandle', 'useDebugValue', + 'createElement', 'createContext', 'createRef', 'forwardRef', 'memo', 'lazy', + 'map', 'filter', 'reduce', 'forEach', 'find', 'findIndex', 'some', 'every', + 'includes', 'indexOf', 'slice', 'splice', 'concat', 'join', 'split', + 'push', 'pop', 'shift', 'unshift', 'sort', 'reverse', + 'keys', 'values', 'entries', 'assign', 'freeze', 'seal', + 'hasOwnProperty', 'toString', 'valueOf', + // Python + 'print', 'len', 'range', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple', + 'append', 'extend', 'update', + // NOTE: 'open', 'read', 'write', 'close' removed — these are real C POSIX syscalls + 'super', 'type', 'isinstance', 'issubclass', 'getattr', 'setattr', 'hasattr', + 'enumerate', 'zip', 'sorted', 'reversed', 'min', 'max', 'sum', 'abs', + // Kotlin stdlib + 'println', 'print', 'readLine', 'require', 'requireNotNull', 'check', 'assert', 'lazy', 'error', + 'listOf', 'mapOf', 'setOf', 'mutableListOf', 'mutableMapOf', 'mutableSetOf', + 'arrayOf', 'sequenceOf', 'also', 'apply', 'run', 'with', 'takeIf', 'takeUnless', + 'TODO', 'buildString', 'buildList', 'buildMap', 'buildSet', + 'repeat', 'synchronized', + // Kotlin coroutine builders & scope functions + 'launch', 'async', 'runBlocking', 'withContext', 'coroutineScope', + 'supervisorScope', 'delay', + // Kotlin Flow operators + 'flow', 'flowOf', 'collect', 'emit', 'onEach', 'catch', + 'buffer', 'conflate', 'distinctUntilChanged', + 'flatMapLatest', 'flatMapMerge', 'combine', + 'stateIn', 'shareIn', 'launchIn', + // Kotlin infix stdlib functions + 'to', 'until', 'downTo', 'step', + // C/C++ standard library + 'printf', 'fprintf', 'sprintf', 'snprintf', 'vprintf', 'vfprintf', 'vsprintf', 'vsnprintf', + 'scanf', 'fscanf', 'sscanf', + 'malloc', 'calloc', 'realloc', 'free', 'memcpy', 'memmove', 'memset', 'memcmp', + 'strlen', 'strcpy', 'strncpy', 'strcat', 'strncat', 'strcmp', 'strncmp', 'strstr', 'strchr', 'strrchr', + 'atoi', 'atol', 'atof', 'strtol', 'strtoul', 'strtoll', 'strtoull', 'strtod', + 'sizeof', 'offsetof', 'typeof', + 'assert', 'abort', 'exit', '_exit', + 'fopen', 'fclose', 'fread', 'fwrite', 'fseek', 'ftell', 'rewind', 'fflush', 'fgets', 'fputs', + // Linux kernel common macros/helpers (not real call targets) + 'likely', 'unlikely', 'BUG', 'BUG_ON', 'WARN', 'WARN_ON', 'WARN_ONCE', + 'IS_ERR', 'PTR_ERR', 'ERR_PTR', 'IS_ERR_OR_NULL', + 'ARRAY_SIZE', 'container_of', 'list_for_each_entry', 'list_for_each_entry_safe', + 'min', 'max', 'clamp', 'abs', 'swap', + 'pr_info', 'pr_warn', 'pr_err', 'pr_debug', 'pr_notice', 'pr_crit', 'pr_emerg', + 'printk', 'dev_info', 'dev_warn', 'dev_err', 'dev_dbg', + 'GFP_KERNEL', 'GFP_ATOMIC', + 'spin_lock', 'spin_unlock', 'spin_lock_irqsave', 'spin_unlock_irqrestore', + 'mutex_lock', 'mutex_unlock', 'mutex_init', + 'kfree', 'kmalloc', 'kzalloc', 'kcalloc', 'krealloc', 'kvmalloc', 'kvfree', + 'get', 'put', + // C# / .NET built-ins + 'Console', 'WriteLine', 'ReadLine', 'Write', + 'Task', 'Run', 'Wait', 'WhenAll', 'WhenAny', 'FromResult', 'Delay', 'ContinueWith', + 'ConfigureAwait', 'GetAwaiter', 'GetResult', + 'ToString', 'GetType', 'Equals', 'GetHashCode', 'ReferenceEquals', + 'Add', 'Remove', 'Contains', 'Clear', 'Count', 'Any', 'All', + 'Where', 'Select', 'SelectMany', 'OrderBy', 'OrderByDescending', 'GroupBy', + 'First', 'FirstOrDefault', 'Single', 'SingleOrDefault', 'Last', 'LastOrDefault', + 'ToList', 'ToArray', 'ToDictionary', 'AsEnumerable', 'AsQueryable', + 'Aggregate', 'Sum', 'Average', 'Min', 'Max', 'Distinct', 'Skip', 'Take', + 'String', 'Format', 'IsNullOrEmpty', 'IsNullOrWhiteSpace', 'Concat', 'Join', + 'Trim', 'TrimStart', 'TrimEnd', 'Split', 'Replace', 'StartsWith', 'EndsWith', + 'Convert', 'ToInt32', 'ToDouble', 'ToBoolean', 'ToByte', + 'Math', 'Abs', 'Ceiling', 'Floor', 'Round', 'Pow', 'Sqrt', + 'Dispose', 'Close', + 'TryParse', 'Parse', + 'AddRange', 'RemoveAt', 'RemoveAll', 'FindAll', 'Exists', 'TrueForAll', + 'ContainsKey', 'TryGetValue', 'AddOrUpdate', + 'Throw', 'ThrowIfNull', + // PHP built-ins + 'echo', 'isset', 'empty', 'unset', 'list', 'array', 'compact', 'extract', + 'count', 'strlen', 'strpos', 'strrpos', 'substr', 'strtolower', 'strtoupper', 'trim', + 'ltrim', 'rtrim', 'str_replace', 'str_contains', 'str_starts_with', 'str_ends_with', + 'sprintf', 'vsprintf', 'printf', 'number_format', + 'array_map', 'array_filter', 'array_reduce', 'array_push', 'array_pop', 'array_shift', + 'array_unshift', 'array_slice', 'array_splice', 'array_merge', 'array_keys', 'array_values', + 'array_key_exists', 'in_array', 'array_search', 'array_unique', 'usort', 'rsort', + 'json_encode', 'json_decode', 'serialize', 'unserialize', + 'intval', 'floatval', 'strval', 'boolval', 'is_null', 'is_string', 'is_int', 'is_array', + 'is_object', 'is_numeric', 'is_bool', 'is_float', + 'var_dump', 'print_r', 'var_export', + 'date', 'time', 'strtotime', 'mktime', 'microtime', + 'file_exists', 'file_get_contents', 'file_put_contents', 'is_file', 'is_dir', + 'preg_match', 'preg_match_all', 'preg_replace', 'preg_split', + 'header', 'session_start', 'session_destroy', 'ob_start', 'ob_end_clean', 'ob_get_clean', + 'dd', 'dump', + // Swift/iOS built-ins and standard library + 'print', 'debugPrint', 'dump', 'fatalError', 'precondition', 'preconditionFailure', + 'assert', 'assertionFailure', 'NSLog', + 'abs', 'min', 'max', 'zip', 'stride', 'sequence', 'repeatElement', + 'swap', 'withUnsafePointer', 'withUnsafeMutablePointer', 'withUnsafeBytes', + 'autoreleasepool', 'unsafeBitCast', 'unsafeDowncast', 'numericCast', + 'type', 'MemoryLayout', + // Swift collection/string methods (common noise) + 'map', 'flatMap', 'compactMap', 'filter', 'reduce', 'forEach', 'contains', + 'first', 'last', 'prefix', 'suffix', 'dropFirst', 'dropLast', + 'sorted', 'reversed', 'enumerated', 'joined', 'split', + 'append', 'insert', 'remove', 'removeAll', 'removeFirst', 'removeLast', + 'isEmpty', 'count', 'index', 'startIndex', 'endIndex', + // UIKit/Foundation common methods (noise in call graph) + 'addSubview', 'removeFromSuperview', 'layoutSubviews', 'setNeedsLayout', + 'layoutIfNeeded', 'setNeedsDisplay', 'invalidateIntrinsicContentSize', + 'addTarget', 'removeTarget', 'addGestureRecognizer', + 'addConstraint', 'addConstraints', 'removeConstraint', 'removeConstraints', + 'NSLocalizedString', 'Bundle', + 'reloadData', 'reloadSections', 'reloadRows', 'performBatchUpdates', + 'register', 'dequeueReusableCell', 'dequeueReusableSupplementaryView', + 'beginUpdates', 'endUpdates', 'insertRows', 'deleteRows', 'insertSections', 'deleteSections', + 'present', 'dismiss', 'pushViewController', 'popViewController', 'popToRootViewController', + 'performSegue', 'prepare', + // GCD / async + 'DispatchQueue', 'async', 'sync', 'asyncAfter', + 'Task', 'withCheckedContinuation', 'withCheckedThrowingContinuation', + // Combine + 'sink', 'store', 'assign', 'receive', 'subscribe', + // Notification / KVO + 'addObserver', 'removeObserver', 'post', 'NotificationCenter', + // Rust standard library (common noise in call graphs) + 'unwrap', 'expect', 'unwrap_or', 'unwrap_or_else', 'unwrap_or_default', + 'ok', 'err', 'is_ok', 'is_err', 'map', 'map_err', 'and_then', 'or_else', + 'clone', 'to_string', 'to_owned', 'into', 'from', 'as_ref', 'as_mut', + 'iter', 'into_iter', 'collect', 'map', 'filter', 'fold', 'for_each', + 'len', 'is_empty', 'push', 'pop', 'insert', 'remove', 'contains', + 'format', 'write', 'writeln', 'panic', 'unreachable', 'todo', 'unimplemented', + 'vec', 'println', 'eprintln', 'dbg', + 'lock', 'read', 'write', 'try_lock', + 'spawn', 'join', 'sleep', + 'Some', 'None', 'Ok', 'Err', +]); + +/** Check if a name is a built-in function or common noise that should be filtered out */ +export const isBuiltInOrNoise = (name: string): boolean => BUILT_IN_NAMES.has(name); + +/** + * Extract function name and label from a function_definition or similar AST node. + * Handles C/C++ qualified_identifier (ClassName::MethodName) and other language patterns. + */ +export const extractFunctionName = (node: any): { funcName: string | null; label: string } => { + let funcName: string | null = null; + let label = 'Function'; + + // Swift init/deinit + if (node.type === 'init_declaration' || node.type === 'deinit_declaration') { + return { + funcName: node.type === 'init_declaration' ? 'init' : 'deinit', + label: 'Constructor', + }; + } + + if (FUNCTION_DECLARATION_TYPES.has(node.type)) { + // C/C++: function_definition -> [pointer_declarator ->] function_declarator -> qualified_identifier/identifier + // Unwrap pointer_declarator / reference_declarator wrappers to reach function_declarator + let declarator = node.childForFieldName?.('declarator') || + node.children?.find((c: any) => c.type === 'function_declarator'); + while (declarator && (declarator.type === 'pointer_declarator' || declarator.type === 'reference_declarator')) { + declarator = declarator.childForFieldName?.('declarator') || + declarator.children?.find((c: any) => + c.type === 'function_declarator' || c.type === 'pointer_declarator' || c.type === 'reference_declarator'); + } + if (declarator) { + const innerDeclarator = declarator.childForFieldName?.('declarator') || + declarator.children?.find((c: any) => + c.type === 'qualified_identifier' || c.type === 'identifier' || c.type === 'parenthesized_declarator'); + + if (innerDeclarator?.type === 'qualified_identifier') { + const nameNode = innerDeclarator.childForFieldName?.('name') || + innerDeclarator.children?.find((c: any) => c.type === 'identifier'); + if (nameNode?.text) { + funcName = nameNode.text; + label = 'Method'; + } + } else if (innerDeclarator?.type === 'identifier') { + funcName = innerDeclarator.text; + } else if (innerDeclarator?.type === 'parenthesized_declarator') { + const nestedId = innerDeclarator.children?.find((c: any) => + c.type === 'qualified_identifier' || c.type === 'identifier'); + if (nestedId?.type === 'qualified_identifier') { + const nameNode = nestedId.childForFieldName?.('name') || + nestedId.children?.find((c: any) => c.type === 'identifier'); + if (nameNode?.text) { + funcName = nameNode.text; + label = 'Method'; + } + } else if (nestedId?.type === 'identifier') { + funcName = nestedId.text; + } + } + } + + // Fallback for other languages + if (!funcName) { + const nameNode = node.childForFieldName?.('name') || + node.children?.find((c: any) => c.type === 'identifier' || c.type === 'property_identifier'); + funcName = nameNode?.text; + } + } else if (node.type === 'impl_item') { + const funcItem = node.children?.find((c: any) => c.type === 'function_item'); + if (funcItem) { + const nameNode = funcItem.childForFieldName?.('name') || + funcItem.children?.find((c: any) => c.type === 'identifier'); + funcName = nameNode?.text; + label = 'Method'; + } + } else if (node.type === 'method_definition') { + const nameNode = node.childForFieldName?.('name') || + node.children?.find((c: any) => c.type === 'property_identifier'); + funcName = nameNode?.text; + label = 'Method'; + } else if (node.type === 'method_declaration' || node.type === 'constructor_declaration') { + const nameNode = node.childForFieldName?.('name') || + node.children?.find((c: any) => c.type === 'identifier'); + funcName = nameNode?.text; + label = 'Method'; + } else if (node.type === 'arrow_function' || node.type === 'function_expression') { + const parent = node.parent; + if (parent?.type === 'variable_declarator') { + const nameNode = parent.childForFieldName?.('name') || + parent.children?.find((c: any) => c.type === 'identifier'); + funcName = nameNode?.text; + } + } + + return { funcName, label }; +}; + /** * Yield control to the event loop so spinners/progress can render. * Call periodically in hot loops to prevent UI freezes. @@ -37,11 +365,13 @@ export const getLanguageFromFilename = (filename: string): SupportedLanguages | if (filename.endsWith('.py')) return SupportedLanguages.Python; // Java if (filename.endsWith('.java')) return SupportedLanguages.Java; - // C (source and headers) - if (filename.endsWith('.c') || filename.endsWith('.h')) return SupportedLanguages.C; - // C++ (all common extensions) + // C source files + if (filename.endsWith('.c')) return SupportedLanguages.C; + // C++ (all common extensions, including .h) + // .h is parsed as C++ because tree-sitter-cpp is a strict superset of C, so pure-C + // headers parse correctly, and C++ headers (classes, templates) are handled properly. if (filename.endsWith('.cpp') || filename.endsWith('.cc') || filename.endsWith('.cxx') || - filename.endsWith('.hpp') || filename.endsWith('.hxx') || filename.endsWith('.hh')) return SupportedLanguages.CPlusPlus; + filename.endsWith('.h') || filename.endsWith('.hpp') || filename.endsWith('.hxx') || filename.endsWith('.hh')) return SupportedLanguages.CPlusPlus; // C# if (filename.endsWith('.cs')) return SupportedLanguages.CSharp; // Go diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index b9d0b2bdb..f95c0eb34 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -14,12 +14,14 @@ import PHP from 'tree-sitter-php'; import { createRequire } from 'node:module'; import { SupportedLanguages } from '../../../config/supported-languages.js'; import { LANGUAGE_QUERIES } from '../tree-sitter-queries.js'; +import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from '../constants.js'; // tree-sitter-swift is an optionalDependency — may not be installed const _require = createRequire(import.meta.url); let Swift: any = null; try { Swift = _require('tree-sitter-swift'); } catch {} -import { findSiblingChild, getLanguageFromFilename } from '../utils.js'; +import { findSiblingChild, getLanguageFromFilename, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, DEFINITION_CAPTURE_KEYS, getDefinitionNodeFromCaptures } from '../utils.js'; +import { isNodeExported } from '../export-detection.js'; import { detectFrameworkFromAST } from '../framework-detection.js'; import { generateId } from '../../../lib/utils.js'; @@ -138,198 +140,20 @@ const setLanguage = (language: SupportedLanguages, filePath: string): void => { parser.setLanguage(lang); }; -// ============================================================================ -// Export detection (copied — needs AST parent traversal, can't cross threads) -// ============================================================================ - -const isNodeExported = (node: any, name: string, language: string): boolean => { - let current = node; - - switch (language) { - case 'javascript': - case 'typescript': - while (current) { - const type = current.type; - if (type === 'export_statement' || - type === 'export_specifier' || - type === 'lexical_declaration' && current.parent?.type === 'export_statement') { - return true; - } - if (current.text?.startsWith('export ')) { - return true; - } - current = current.parent; - } - return false; - - case 'python': - return !name.startsWith('_'); - - case 'java': - while (current) { - if (current.parent) { - const parent = current.parent; - for (let i = 0; i < parent.childCount; i++) { - const child = parent.child(i); - if (child?.type === 'modifiers' && child.text?.includes('public')) { - return true; - } - } - if (parent.type === 'method_declaration' || parent.type === 'constructor_declaration') { - if (parent.text?.trimStart().startsWith('public')) { - return true; - } - } - } - current = current.parent; - } - return false; - - case 'csharp': - while (current) { - if (current.type === 'modifier' || current.type === 'modifiers') { - if (current.text?.includes('public')) return true; - } - current = current.parent; - } - return false; - - case 'go': - if (name.length === 0) return false; - const first = name[0]; - return first === first.toUpperCase() && first !== first.toLowerCase(); - - case 'rust': - while (current) { - if (current.type === 'visibility_modifier') { - if (current.text?.includes('pub')) return true; - } - current = current.parent; - } - return false; - - // Kotlin: Default visibility is public (unlike Java) - // visibility_modifier is inside modifiers, a sibling of the name node within the declaration - case 'kotlin': - while (current) { - if (current.parent) { - const visMod = findSiblingChild(current.parent, 'modifiers', 'visibility_modifier'); - if (visMod) { - const text = visMod.text; - if (text === 'private' || text === 'internal' || text === 'protected') return false; - if (text === 'public') return true; - } - } - current = current.parent; - } - // No visibility modifier = public (Kotlin default) - return true; - - case 'c': - case 'cpp': - return false; - - case 'php': - // Top-level classes/interfaces/traits are always accessible - // Methods/properties are exported only if they have 'public' modifier - while (current) { - if (current.type === 'class_declaration' || - current.type === 'interface_declaration' || - current.type === 'trait_declaration' || - current.type === 'enum_declaration') { - return true; - } - if (current.type === 'visibility_modifier') { - return current.text === 'public'; - } - current = current.parent; - } - // Top-level functions (no parent class) are globally accessible - return true; - - case 'swift': - while (current) { - if (current.type === 'modifiers' || current.type === 'visibility_modifier') { - const text = current.text || ''; - if (text.includes('public') || text.includes('open')) return true; - } - current = current.parent; - } - return false; - - default: - return false; - } -}; +// isNodeExported imported from ../export-detection.js (shared module) // ============================================================================ // Enclosing function detection (for call extraction) // ============================================================================ -const FUNCTION_NODE_TYPES = new Set([ - 'function_declaration', 'arrow_function', 'function_expression', - 'method_definition', 'generator_function_declaration', - 'function_definition', 'async_function_declaration', 'async_arrow_function', - 'method_declaration', 'constructor_declaration', - 'local_function_statement', 'function_item', 'impl_item', - // Kotlin - 'lambda_literal', - // PHP - 'anonymous_function', - // Swift initializers/deinitializers - 'init_declaration', 'deinit_declaration', -]); - /** Walk up AST to find enclosing function, return its generateId or null for top-level */ const findEnclosingFunctionId = (node: any, filePath: string): string | null => { let current = node.parent; while (current) { if (FUNCTION_NODE_TYPES.has(current.type)) { - let funcName: string | null = null; - let label = 'Function'; - - if (current.type === 'init_declaration' || current.type === 'deinit_declaration') { - const funcName = current.type === 'init_declaration' ? 'init' : 'deinit'; - const label = 'Constructor'; - const startLine = current.startPosition?.row ?? 0; - return generateId(label, `${filePath}:${funcName}:${startLine}`); - } - - if (['function_declaration', 'function_definition', 'async_function_declaration', - 'generator_function_declaration', 'function_item'].includes(current.type)) { - const nameNode = current.childForFieldName?.('name') || - current.children?.find((c: any) => c.type === 'identifier' || c.type === 'property_identifier'); - funcName = nameNode?.text; - } else if (current.type === 'impl_item') { - const funcItem = current.children?.find((c: any) => c.type === 'function_item'); - if (funcItem) { - const nameNode = funcItem.childForFieldName?.('name') || - funcItem.children?.find((c: any) => c.type === 'identifier'); - funcName = nameNode?.text; - label = 'Method'; - } - } else if (current.type === 'method_definition') { - const nameNode = current.childForFieldName?.('name') || - current.children?.find((c: any) => c.type === 'property_identifier'); - funcName = nameNode?.text; - label = 'Method'; - } else if (current.type === 'method_declaration' || current.type === 'constructor_declaration') { - const nameNode = current.childForFieldName?.('name') || - current.children?.find((c: any) => c.type === 'identifier'); - funcName = nameNode?.text; - label = 'Method'; - } else if (current.type === 'arrow_function' || current.type === 'function_expression') { - const parent = current.parent; - if (parent?.type === 'variable_declarator') { - const nameNode = parent.childForFieldName?.('name') || - parent.children?.find((c: any) => c.type === 'identifier'); - funcName = nameNode?.text; - } - } - + const { funcName, label } = extractFunctionName(current); if (funcName) { - const startLine = current.startPosition?.row ?? 0; - return generateId(label, `${filePath}:${funcName}:${startLine}`); + return generateId(label, `${filePath}:${funcName}`); } } current = current.parent; @@ -337,118 +161,6 @@ const findEnclosingFunctionId = (node: any, filePath: string): string | null => return null; }; -const BUILT_INS = new Set([ - // JavaScript/TypeScript - 'console', 'log', 'warn', 'error', 'info', 'debug', - 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', - 'parseInt', 'parseFloat', 'isNaN', 'isFinite', - 'encodeURI', 'decodeURI', 'encodeURIComponent', 'decodeURIComponent', - 'JSON', 'parse', 'stringify', - 'Object', 'Array', 'String', 'Number', 'Boolean', 'Symbol', 'BigInt', - 'Map', 'Set', 'WeakMap', 'WeakSet', - 'Promise', 'resolve', 'reject', 'then', 'catch', 'finally', - 'Math', 'Date', 'RegExp', 'Error', - 'require', 'import', 'export', 'fetch', 'Response', 'Request', - 'useState', 'useEffect', 'useCallback', 'useMemo', 'useRef', 'useContext', - 'useReducer', 'useLayoutEffect', 'useImperativeHandle', 'useDebugValue', - 'createElement', 'createContext', 'createRef', 'forwardRef', 'memo', 'lazy', - 'map', 'filter', 'reduce', 'forEach', 'find', 'findIndex', 'some', 'every', - 'includes', 'indexOf', 'slice', 'splice', 'concat', 'join', 'split', - 'push', 'pop', 'shift', 'unshift', 'sort', 'reverse', - 'keys', 'values', 'entries', 'assign', 'freeze', 'seal', - 'hasOwnProperty', 'toString', 'valueOf', - // Python - 'print', 'len', 'range', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple', - 'open', 'read', 'write', 'close', 'append', 'extend', 'update', - 'super', 'type', 'isinstance', 'issubclass', 'getattr', 'setattr', 'hasattr', - 'enumerate', 'zip', 'sorted', 'reversed', 'min', 'max', 'sum', 'abs', - // Kotlin stdlib (IMPORTANT: keep in sync with call-processor.ts BUILT_IN_NAMES) - 'println', 'print', 'readLine', 'require', 'requireNotNull', 'check', 'assert', 'lazy', 'error', - 'listOf', 'mapOf', 'setOf', 'mutableListOf', 'mutableMapOf', 'mutableSetOf', - 'arrayOf', 'sequenceOf', 'also', 'apply', 'run', 'with', 'takeIf', 'takeUnless', - 'TODO', 'buildString', 'buildList', 'buildMap', 'buildSet', - 'repeat', 'synchronized', - // Kotlin coroutine builders & scope functions - 'launch', 'async', 'runBlocking', 'withContext', 'coroutineScope', - 'supervisorScope', 'delay', - // Kotlin Flow operators - 'flow', 'flowOf', 'collect', 'emit', 'onEach', 'catch', - 'buffer', 'conflate', 'distinctUntilChanged', - 'flatMapLatest', 'flatMapMerge', 'combine', - 'stateIn', 'shareIn', 'launchIn', - // Kotlin infix stdlib functions - 'to', 'until', 'downTo', 'step', - // C/C++ standard library - 'printf', 'fprintf', 'sprintf', 'snprintf', 'vprintf', 'vfprintf', 'vsprintf', 'vsnprintf', - 'scanf', 'fscanf', 'sscanf', - 'malloc', 'calloc', 'realloc', 'free', 'memcpy', 'memmove', 'memset', 'memcmp', - 'strlen', 'strcpy', 'strncpy', 'strcat', 'strncat', 'strcmp', 'strncmp', 'strstr', 'strchr', 'strrchr', - 'atoi', 'atol', 'atof', 'strtol', 'strtoul', 'strtoll', 'strtoull', 'strtod', - 'sizeof', 'offsetof', 'typeof', - 'assert', 'abort', 'exit', '_exit', - 'fopen', 'fclose', 'fread', 'fwrite', 'fseek', 'ftell', 'rewind', 'fflush', 'fgets', 'fputs', - // Linux kernel common macros/helpers (not real call targets) - 'likely', 'unlikely', 'BUG', 'BUG_ON', 'WARN', 'WARN_ON', 'WARN_ONCE', - 'IS_ERR', 'PTR_ERR', 'ERR_PTR', 'IS_ERR_OR_NULL', - 'ARRAY_SIZE', 'container_of', 'list_for_each_entry', 'list_for_each_entry_safe', - 'min', 'max', 'clamp', 'abs', 'swap', - 'pr_info', 'pr_warn', 'pr_err', 'pr_debug', 'pr_notice', 'pr_crit', 'pr_emerg', - 'printk', 'dev_info', 'dev_warn', 'dev_err', 'dev_dbg', - 'GFP_KERNEL', 'GFP_ATOMIC', - 'spin_lock', 'spin_unlock', 'spin_lock_irqsave', 'spin_unlock_irqrestore', - 'mutex_lock', 'mutex_unlock', 'mutex_init', - 'kfree', 'kmalloc', 'kzalloc', 'kcalloc', 'krealloc', 'kvmalloc', 'kvfree', - 'get', 'put', - // PHP built-ins - 'echo', 'isset', 'empty', 'unset', 'list', 'array', 'compact', 'extract', - 'count', 'strlen', 'strpos', 'strrpos', 'substr', 'strtolower', 'strtoupper', 'trim', - 'ltrim', 'rtrim', 'str_replace', 'str_contains', 'str_starts_with', 'str_ends_with', - 'sprintf', 'vsprintf', 'printf', 'number_format', - 'array_map', 'array_filter', 'array_reduce', 'array_push', 'array_pop', 'array_shift', - 'array_unshift', 'array_slice', 'array_splice', 'array_merge', 'array_keys', 'array_values', - 'array_key_exists', 'in_array', 'array_search', 'array_unique', 'usort', 'rsort', - 'json_encode', 'json_decode', 'serialize', 'unserialize', - 'intval', 'floatval', 'strval', 'boolval', 'is_null', 'is_string', 'is_int', 'is_array', - 'is_object', 'is_numeric', 'is_bool', 'is_float', - 'var_dump', 'print_r', 'var_export', - 'date', 'time', 'strtotime', 'mktime', 'microtime', - 'file_exists', 'file_get_contents', 'file_put_contents', 'is_file', 'is_dir', - 'preg_match', 'preg_match_all', 'preg_replace', 'preg_split', - 'header', 'session_start', 'session_destroy', 'ob_start', 'ob_end_clean', 'ob_get_clean', - 'dd', 'dump', - // Swift/iOS built-ins and standard library - 'print', 'debugPrint', 'dump', 'fatalError', 'precondition', 'preconditionFailure', - 'assert', 'assertionFailure', 'NSLog', - 'abs', 'min', 'max', 'zip', 'stride', 'sequence', 'repeatElement', - 'swap', 'withUnsafePointer', 'withUnsafeMutablePointer', 'withUnsafeBytes', - 'autoreleasepool', 'unsafeBitCast', 'unsafeDowncast', 'numericCast', - 'type', 'MemoryLayout', - // Swift collection/string methods (common noise) - 'map', 'flatMap', 'compactMap', 'filter', 'reduce', 'forEach', 'contains', - 'first', 'last', 'prefix', 'suffix', 'dropFirst', 'dropLast', - 'sorted', 'reversed', 'enumerated', 'joined', 'split', - 'append', 'insert', 'remove', 'removeAll', 'removeFirst', 'removeLast', - 'isEmpty', 'count', 'index', 'startIndex', 'endIndex', - // UIKit/Foundation common methods (noise in call graph) - 'addSubview', 'removeFromSuperview', 'layoutSubviews', 'setNeedsLayout', - 'layoutIfNeeded', 'setNeedsDisplay', 'invalidateIntrinsicContentSize', - 'addTarget', 'removeTarget', 'addGestureRecognizer', - 'addConstraint', 'addConstraints', 'removeConstraint', 'removeConstraints', - 'NSLocalizedString', 'Bundle', - 'reloadData', 'reloadSections', 'reloadRows', 'performBatchUpdates', - 'register', 'dequeueReusableCell', 'dequeueReusableSupplementaryView', - 'beginUpdates', 'endUpdates', 'insertRows', 'deleteRows', 'insertSections', 'deleteSections', - 'present', 'dismiss', 'pushViewController', 'popViewController', 'popToRootViewController', - 'performSegue', 'prepare', - // GCD / async - 'DispatchQueue', 'async', 'sync', 'asyncAfter', - 'Task', 'withCheckedContinuation', 'withCheckedThrowingContinuation', - // Combine - 'sink', 'store', 'assign', 'receive', 'subscribe', - // Notification / KVO - 'addObserver', 'removeObserver', 'post', 'NotificationCenter', -]); - // ============================================================================ // Label detection from capture map // ============================================================================ @@ -483,37 +195,7 @@ const getLabelFromCaptures = (captureMap: Record): string | null => return 'CodeElement'; }; -const DEFINITION_CAPTURE_KEYS = [ - 'definition.function', - 'definition.class', - 'definition.interface', - 'definition.method', - 'definition.struct', - 'definition.enum', - 'definition.namespace', - 'definition.module', - 'definition.trait', - 'definition.impl', - 'definition.type', - 'definition.const', - 'definition.static', - 'definition.typedef', - 'definition.macro', - 'definition.union', - 'definition.property', - 'definition.record', - 'definition.delegate', - 'definition.annotation', - 'definition.constructor', - 'definition.template', -] as const; - -const getDefinitionNodeFromCaptures = (captureMap: Record): any | null => { - for (const key of DEFINITION_CAPTURE_KEYS) { - if (captureMap[key]) return captureMap[key]; - } - return null; -}; +// DEFINITION_CAPTURE_KEYS and getDefinitionNodeFromCaptures imported from ../utils.js /** * Append .* to a Kotlin import path if the AST has a wildcard_import sibling node. @@ -1099,18 +781,25 @@ const processFileGroup = ( try { const lang = parser.getLanguage(); query = new Parser.Query(lang, queryString); - } catch { + } catch (err) { + const message = `Query compilation failed for ${language}: ${err instanceof Error ? err.message : String(err)}`; + if (parentPort) { + parentPort.postMessage({ type: 'warning', message }); + } else { + console.warn(message); + } return; } for (const file of files) { - // Skip very large files — they can crash tree-sitter or cause OOM - if (file.content.length > 512 * 1024) continue; + // Skip files larger than the max tree-sitter buffer (32 MB) + if (file.content.length > TREE_SITTER_MAX_BUFFER) continue; let tree; try { - tree = parser.parse(file.content, undefined, { bufferSize: 1024 * 256 }); - } catch { + tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) }); + } catch (err) { + console.warn(`Failed to parse file ${file.path}: ${err instanceof Error ? err.message : String(err)}`); continue; } @@ -1120,7 +809,8 @@ const processFileGroup = ( let matches; try { matches = query.matches(tree.rootNode); - } catch { + } catch (err) { + console.warn(`Query execution failed for ${file.path}: ${err instanceof Error ? err.message : String(err)}`); continue; } @@ -1148,7 +838,7 @@ const processFileGroup = ( const callNameNode = captureMap['call.name']; if (callNameNode) { const calledName = callNameNode.text; - if (!BUILT_INS.has(calledName)) { + if (!isBuiltInOrNoise(calledName)) { const callNode = captureMap['call']; const sourceId = findEnclosingFunctionId(callNode, file.path) || generateId('File', file.path); @@ -1198,7 +888,7 @@ const processFileGroup = ( const nodeName = nameNode ? nameNode.text : 'init'; const definitionNode = getDefinitionNodeFromCaptures(captureMap); const startLine = definitionNode ? definitionNode.startPosition.row : (nameNode ? nameNode.startPosition.row : 0); - const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}:${startLine}`); + const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}`); let description: string | undefined; if (language === SupportedLanguages.PHP) { diff --git a/gitnexus/test/fixtures/sample-code/simple.cs b/gitnexus/test/fixtures/sample-code/simple.cs index b7c15edc9..cfb2b971e 100644 --- a/gitnexus/test/fixtures/sample-code/simple.cs +++ b/gitnexus/test/fixtures/sample-code/simple.cs @@ -1,22 +1,55 @@ using System; +using System.Collections.Generic; namespace SampleApp { - public class Calculator + public interface ICalculator { + int Add(int a, int b); + } + + public class Calculator : ICalculator + { + public int Result { get; private set; } + + public Calculator() { Result = 0; } + public int Add(int a, int b) { - return a + b; + Result = a + b; + LogResult(Result); + return Result; } - private int Multiply(int a, int b) + private void LogResult(int value) { - return a * b; + Console.WriteLine(value); } + + private int Multiply(int a, int b) { return a * b; } } internal class Helper { - public void DoWork() { } + public void DoWork() + { + var calc = new Calculator(); + calc.Add(1, 2); + } + } + + public enum Operation + { + Add, + Subtract, + Multiply + } + + public record CalculationResult(int Value, Operation Op); + + public struct Point + { + public int X { get; set; } + public int Y { get; set; } } } diff --git a/gitnexus/test/integration/parsing.test.ts b/gitnexus/test/integration/parsing.test.ts index 741f3919c..fb96eb252 100644 --- a/gitnexus/test/integration/parsing.test.ts +++ b/gitnexus/test/integration/parsing.test.ts @@ -23,14 +23,22 @@ const FIXTURES_DIR = path.join(process.cwd(), 'test', 'fixtures', 'sample-code') /** * Minimal mock of a tree-sitter AST node. */ -function mockNode(type: string, text: string = '', parent?: any): any { - return { +function mockNode(type: string, text: string = '', parent?: any, children?: any[], fields?: Record): any { + const node: any = { type, text, parent: parent || null, - childCount: 0, - child: () => null, + childCount: children?.length ?? 0, + child: (i: number) => children?.[i] ?? null, + childForFieldName: (name: string) => fields?.[name] ?? null, }; + // Set parent references on children + if (children) { + for (const child of children) { + child.parent = node; + } + } + return node; } // ─── isNodeExported per-language ───────────────────────────────────── @@ -99,16 +107,15 @@ describe('parsing', () => { describe('rust', () => { it('pub function is exported', () => { const visMod = mockNode('visibility_modifier', 'pub'); - const fnDecl = mockNode('function_item', 'pub fn foo() {}', visMod); - // For rust, isNodeExported walks up parents checking for visibility_modifier - // The visMod is a parent of the nameNode - const nameNode = mockNode('identifier', 'foo', visMod); + const nameNode = mockNode('identifier', 'foo'); + // visibility_modifier is a sibling of the name inside function_item + const fnDecl = mockNode('function_item', 'pub fn foo() {}', undefined, [visMod, nameNode]); expect(isNodeExported(nameNode, 'foo', 'rust')).toBe(true); }); it('non-pub function is not exported', () => { - const fnDecl = mockNode('function_item', 'fn foo() {}'); - const nameNode = mockNode('identifier', 'foo', fnDecl); + const nameNode = mockNode('identifier', 'foo'); + const fnDecl = mockNode('function_item', 'fn foo() {}', undefined, [nameNode]); expect(isNodeExported(nameNode, 'foo', 'rust')).toBe(false); }); }); @@ -165,14 +172,23 @@ describe('parsing', () => { // C/C++ describe('c/cpp', () => { - it('C functions are never exported', () => { - const node = mockNode('identifier', 'add'); - expect(isNodeExported(node, 'add', 'c')).toBe(false); + it('C functions without static are exported (external linkage)', () => { + const nameNode = mockNode('identifier', 'add'); + const fnDef = mockNode('function_definition', 'int add(int a, int b) {}', undefined, [nameNode]); + expect(isNodeExported(nameNode, 'add', 'c')).toBe(true); }); - it('C++ functions are never exported', () => { - const node = mockNode('identifier', 'helperFunction'); - expect(isNodeExported(node, 'helperFunction', 'cpp')).toBe(false); + it('C++ functions without static are exported', () => { + const nameNode = mockNode('identifier', 'helperFunction'); + const fnDef = mockNode('function_definition', 'void helperFunction() {}', undefined, [nameNode]); + expect(isNodeExported(nameNode, 'helperFunction', 'cpp')).toBe(true); + }); + + it('static C function is not exported', () => { + const nameNode = mockNode('identifier', 'internalHelper'); + const staticSpec = mockNode('storage_class_specifier', 'static'); + const fnDef = mockNode('function_definition', 'static void internalHelper() {}', undefined, [staticSpec, nameNode]); + expect(isNodeExported(nameNode, 'internalHelper', 'c')).toBe(false); }); }); @@ -180,17 +196,489 @@ describe('parsing', () => { describe('csharp', () => { it('public modifier means exported', () => { const modifier = mockNode('modifier', 'public'); - const nameNode = mockNode('identifier', 'Add', modifier); + const nameNode = mockNode('identifier', 'Add'); + // modifier is a sibling of nameNode inside method_declaration + const methodDecl = mockNode('method_declaration', 'public int Add() {}', undefined, [modifier, nameNode]); expect(isNodeExported(nameNode, 'Add', 'csharp')).toBe(true); }); it('no public modifier means not exported', () => { - const classDecl = mockNode('class_declaration', 'class Helper {}'); - const nameNode = mockNode('identifier', 'Helper', classDecl); + const nameNode = mockNode('identifier', 'Helper'); + const classDecl = mockNode('class_declaration', 'class Helper {}', undefined, [nameNode]); expect(isNodeExported(nameNode, 'Helper', 'csharp')).toBe(false); }); }); + // Java + describe('java', () => { + it('public method is exported', () => { + const modifiers = mockNode('modifiers', 'public'); + const nameNode = mockNode('identifier', 'getUser'); + const methodDecl = mockNode('method_declaration', 'public User getUser() {}', undefined, [modifiers, nameNode]); + expect(isNodeExported(nameNode, 'getUser', 'java')).toBe(true); + }); + + it('public class method via text check is exported', () => { + const nameNode = mockNode('identifier', 'doGet'); + const methodDecl = mockNode('method_declaration', 'public void doGet() {}', undefined, [nameNode]); + // text starts with 'public' so it should be detected + expect(isNodeExported(nameNode, 'doGet', 'java')).toBe(true); + }); + + it('private method is not exported', () => { + const modifiers = mockNode('modifiers', 'private'); + const nameNode = mockNode('identifier', 'helper'); + const methodDecl = mockNode('method_declaration', 'private void helper() {}', undefined, [modifiers, nameNode]); + expect(isNodeExported(nameNode, 'helper', 'java')).toBe(false); + }); + + it('package-private (no modifier) is not exported', () => { + const nameNode = mockNode('identifier', 'internal'); + const methodDecl = mockNode('method_declaration', 'void internal() {}', undefined, [nameNode]); + expect(isNodeExported(nameNode, 'internal', 'java')).toBe(false); + }); + }); + + // Kotlin + describe('kotlin', () => { + it('function without visibility modifier is public by default', () => { + const nameNode = mockNode('identifier', 'greet'); + const fnDecl = mockNode('function_declaration', 'fun greet() {}', undefined, [nameNode]); + expect(isNodeExported(nameNode, 'greet', 'kotlin')).toBe(true); + }); + + it('public function is exported', () => { + const visMod = mockNode('visibility_modifier', 'public'); + const modifiers = mockNode('modifiers', 'public', undefined, [visMod]); + const nameNode = mockNode('identifier', 'greet'); + const fnDecl = mockNode('function_declaration', 'public fun greet() {}', undefined, [modifiers, nameNode]); + expect(isNodeExported(nameNode, 'greet', 'kotlin')).toBe(true); + }); + + it('private function is not exported', () => { + const visMod = mockNode('visibility_modifier', 'private'); + const modifiers = mockNode('modifiers', 'private', undefined, [visMod]); + const nameNode = mockNode('identifier', 'secret'); + const fnDecl = mockNode('function_declaration', 'private fun secret() {}', undefined, [modifiers, nameNode]); + expect(isNodeExported(nameNode, 'secret', 'kotlin')).toBe(false); + }); + + it('internal function is not exported', () => { + const visMod = mockNode('visibility_modifier', 'internal'); + const modifiers = mockNode('modifiers', 'internal', undefined, [visMod]); + const nameNode = mockNode('identifier', 'moduleOnly'); + const fnDecl = mockNode('function_declaration', 'internal fun moduleOnly() {}', undefined, [modifiers, nameNode]); + expect(isNodeExported(nameNode, 'moduleOnly', 'kotlin')).toBe(false); + }); + }); + + // C# additional cases + describe('csharp additional', () => { + it('internal modifier is not exported', () => { + const modifier = mockNode('modifier', 'internal'); + const nameNode = mockNode('identifier', 'InternalService'); + const classDecl = mockNode('class_declaration', 'internal class InternalService {}', undefined, [modifier, nameNode]); + expect(isNodeExported(nameNode, 'InternalService', 'csharp')).toBe(false); + }); + + it('private modifier is not exported', () => { + const modifier = mockNode('modifier', 'private'); + const nameNode = mockNode('identifier', 'helper'); + const methodDecl = mockNode('method_declaration', 'private void helper() {}', undefined, [modifier, nameNode]); + expect(isNodeExported(nameNode, 'helper', 'csharp')).toBe(false); + }); + + it('struct with public modifier is exported', () => { + const modifier = mockNode('modifier', 'public'); + const nameNode = mockNode('identifier', 'Point'); + const structDecl = mockNode('struct_declaration', 'public struct Point {}', undefined, [modifier, nameNode]); + expect(isNodeExported(nameNode, 'Point', 'csharp')).toBe(true); + }); + + it('enum with public modifier is exported', () => { + const modifier = mockNode('modifier', 'public'); + const nameNode = mockNode('identifier', 'Status'); + const enumDecl = mockNode('enum_declaration', 'public enum Status {}', undefined, [modifier, nameNode]); + expect(isNodeExported(nameNode, 'Status', 'csharp')).toBe(true); + }); + + it('record with public modifier is exported', () => { + const modifier = mockNode('modifier', 'public'); + const nameNode = mockNode('identifier', 'UserDto'); + const recordDecl = mockNode('record_declaration', 'public record UserDto {}', undefined, [modifier, nameNode]); + expect(isNodeExported(nameNode, 'UserDto', 'csharp')).toBe(true); + }); + + it('interface with public modifier is exported', () => { + const modifier = mockNode('modifier', 'public'); + const nameNode = mockNode('identifier', 'IService'); + const ifaceDecl = mockNode('interface_declaration', 'public interface IService {}', undefined, [modifier, nameNode]); + expect(isNodeExported(nameNode, 'IService', 'csharp')).toBe(true); + }); + }); + + // Rust additional cases + describe('rust additional', () => { + it('pub(crate) is exported', () => { + const visMod = mockNode('visibility_modifier', 'pub(crate)'); + const nameNode = mockNode('identifier', 'internal_fn'); + const fnDecl = mockNode('function_item', 'pub(crate) fn internal_fn() {}', undefined, [visMod, nameNode]); + expect(isNodeExported(nameNode, 'internal_fn', 'rust')).toBe(true); + }); + + it('pub struct is exported', () => { + const visMod = mockNode('visibility_modifier', 'pub'); + const nameNode = mockNode('type_identifier', 'Config'); + const structDecl = mockNode('struct_item', 'pub struct Config {}', undefined, [visMod, nameNode]); + expect(isNodeExported(nameNode, 'Config', 'rust')).toBe(true); + }); + + it('private struct is not exported', () => { + const nameNode = mockNode('type_identifier', 'Inner'); + const structDecl = mockNode('struct_item', 'struct Inner {}', undefined, [nameNode]); + expect(isNodeExported(nameNode, 'Inner', 'rust')).toBe(false); + }); + + it('pub enum is exported', () => { + const visMod = mockNode('visibility_modifier', 'pub'); + const nameNode = mockNode('type_identifier', 'ErrorKind'); + const enumDecl = mockNode('enum_item', 'pub enum ErrorKind {}', undefined, [visMod, nameNode]); + expect(isNodeExported(nameNode, 'ErrorKind', 'rust')).toBe(true); + }); + + it('pub trait is exported', () => { + const visMod = mockNode('visibility_modifier', 'pub'); + const nameNode = mockNode('type_identifier', 'Handler'); + const traitDecl = mockNode('trait_item', 'pub trait Handler {}', undefined, [visMod, nameNode]); + expect(isNodeExported(nameNode, 'Handler', 'rust')).toBe(true); + }); + }); + + // C/C++ additional cases + describe('c/cpp additional', () => { + it('static C++ function is not exported', () => { + const nameNode = mockNode('identifier', 'localHelper'); + const staticSpec = mockNode('storage_class_specifier', 'static'); + const fnDef = mockNode('function_definition', 'static int localHelper() {}', undefined, [staticSpec, nameNode]); + expect(isNodeExported(nameNode, 'localHelper', 'cpp')).toBe(false); + }); + + it('declaration (not definition) without static is exported', () => { + const nameNode = mockNode('identifier', 'compute'); + const decl = mockNode('declaration', 'int compute(int x);', undefined, [nameNode]); + expect(isNodeExported(nameNode, 'compute', 'c')).toBe(true); + }); + + it('static declaration is not exported', () => { + const nameNode = mockNode('identifier', 'internalFn'); + const staticSpec = mockNode('storage_class_specifier', 'static'); + const decl = mockNode('declaration', 'static int internalFn(void);', undefined, [staticSpec, nameNode]); + expect(isNodeExported(nameNode, 'internalFn', 'c')).toBe(false); + }); + + it('detached node defaults to exported (external linkage)', () => { + const nameNode = mockNode('identifier', 'orphan'); + expect(isNodeExported(nameNode, 'orphan', 'c')).toBe(true); + }); + + it('C++ anonymous namespace function is not exported (internal linkage)', () => { + const nameNode = mockNode('identifier', 'anonHelper'); + const fnDef = mockNode('function_definition', 'void anonHelper() {}', undefined, [nameNode]); + // Anonymous namespace: namespace_definition with no name field + const anonNs = mockNode('namespace_definition', 'namespace { void anonHelper() {} }', undefined, [fnDef]); + expect(isNodeExported(nameNode, 'anonHelper', 'cpp')).toBe(false); + }); + + it('C++ named namespace function is still exported', () => { + const nameNode = mockNode('identifier', 'namedHelper'); + const fnDef = mockNode('function_definition', 'void namedHelper() {}', undefined, [nameNode]); + const nsName = mockNode('namespace_identifier', 'utils'); + const namedNs = mockNode('namespace_definition', 'namespace utils { void namedHelper() {} }', undefined, [fnDef], { name: nsName }); + expect(isNodeExported(nameNode, 'namedHelper', 'cpp')).toBe(true); + }); + }); + + // C/C++ with real tree-sitter (validates structural storage_class_specifier detection) + describe('c/cpp real tree-sitter', () => { + it('non-static function is exported using real AST', async () => { + const parser = await loadParser(); + await loadLanguage(SupportedLanguages.C); + const tree = parser.parse('int add(int a, int b) { return a + b; }'); + const funcDef = tree.rootNode.child(0)!; + // Find the identifier name node inside the function_definition + const declNode = funcDef.childForFieldName('declarator'); + const nameNode = declNode?.childForFieldName?.('declarator') || declNode; + expect(isNodeExported(nameNode, 'add', 'c')).toBe(true); + }); + + it('static function is not exported using real AST', async () => { + const parser = await loadParser(); + await loadLanguage(SupportedLanguages.C); + const tree = parser.parse('static int internal_helper(void) { return 0; }'); + const funcDef = tree.rootNode.child(0)!; + const declNode = funcDef.childForFieldName('declarator'); + const nameNode = declNode?.childForFieldName?.('declarator') || declNode; + expect(isNodeExported(nameNode, 'internal_helper', 'c')).toBe(false); + }); + + it('extern function is exported using real AST', async () => { + const parser = await loadParser(); + await loadLanguage(SupportedLanguages.C); + const tree = parser.parse('extern int shared_func(void);'); + const decl = tree.rootNode.child(0)!; + // Declaration nodes should not have storage_class_specifier 'static' + const nameNode = decl.descendantsOfType?.('identifier')?.[0] || decl; + expect(isNodeExported(nameNode, 'shared_func', 'c')).toBe(true); + }); + + it('C++ anonymous namespace detected via real AST', async () => { + const parser = await loadParser(); + await loadLanguage(SupportedLanguages.CPlusPlus); + const code = 'namespace { void hidden() {} }'; + const tree = parser.parse(code); + const nsDef = tree.rootNode.child(0)!; + // Find function_definition inside the namespace body + const body = nsDef.childForFieldName('body'); + const funcDef = body?.namedChild(0); + const declNode = funcDef?.childForFieldName?.('declarator'); + const nameNode = declNode?.childForFieldName?.('declarator') || declNode; + expect(isNodeExported(nameNode, 'hidden', 'cpp')).toBe(false); + }); + + it('C++ named namespace is still exported via real AST', async () => { + const parser = await loadParser(); + await loadLanguage(SupportedLanguages.CPlusPlus); + const code = 'namespace utils { void helper() {} }'; + const tree = parser.parse(code); + const nsDef = tree.rootNode.child(0)!; + const body = nsDef.childForFieldName('body'); + const funcDef = body?.namedChild(0); + const declNode = funcDef?.childForFieldName?.('declarator'); + const nameNode = declNode?.childForFieldName?.('declarator') || declNode; + expect(isNodeExported(nameNode, 'helper', 'cpp')).toBe(true); + }); + }); + + // C/C++ edge cases with mocks + describe('c/cpp edge cases', () => { + it('nested anonymous namespace (double nesting) is not exported', () => { + const nameNode = mockNode('identifier', 'deepHidden'); + const fnDef = mockNode('function_definition', 'void deepHidden() {}', undefined, [nameNode]); + const innerNs = mockNode('namespace_definition', 'namespace { }', undefined, [fnDef]); + const outerNs = mockNode('namespace_definition', 'namespace outer { }', undefined, [innerNs], { name: mockNode('namespace_identifier', 'outer') }); + expect(isNodeExported(nameNode, 'deepHidden', 'cpp')).toBe(false); + }); + + it('static function inside named namespace is not exported', () => { + const nameNode = mockNode('identifier', 'staticInNs'); + const staticSpec = mockNode('storage_class_specifier', 'static'); + const fnDef = mockNode('function_definition', 'static void staticInNs() {}', undefined, [staticSpec, nameNode]); + const ns = mockNode('namespace_definition', 'namespace foo { }', undefined, [fnDef], { name: mockNode('namespace_identifier', 'foo') }); + expect(isNodeExported(nameNode, 'staticInNs', 'cpp')).toBe(false); + }); + + it('extern storage class is not confused with static', () => { + const nameNode = mockNode('identifier', 'externFn'); + const externSpec = mockNode('storage_class_specifier', 'extern'); + const fnDef = mockNode('function_definition', 'extern void externFn() {}', undefined, [externSpec, nameNode]); + expect(isNodeExported(nameNode, 'externFn', 'c')).toBe(true); + }); + }); + + // Rust additional edge cases + describe('rust edge cases', () => { + it('pub(super) is treated as exported', () => { + const visMod = mockNode('visibility_modifier', 'pub(super)'); + const nameNode = mockNode('identifier', 'parent_fn'); + const fnDecl = mockNode('function_item', 'pub(super) fn parent_fn() {}', undefined, [visMod, nameNode]); + expect(isNodeExported(nameNode, 'parent_fn', 'rust')).toBe(true); + }); + + it('pub union is exported', () => { + const visMod = mockNode('visibility_modifier', 'pub'); + const nameNode = mockNode('type_identifier', 'MyUnion'); + const unionDecl = mockNode('union_item', 'pub union MyUnion {}', undefined, [visMod, nameNode]); + expect(isNodeExported(nameNode, 'MyUnion', 'rust')).toBe(true); + }); + + it('private union is not exported', () => { + const nameNode = mockNode('type_identifier', 'InternalUnion'); + const unionDecl = mockNode('union_item', 'union InternalUnion {}', undefined, [nameNode]); + expect(isNodeExported(nameNode, 'InternalUnion', 'rust')).toBe(false); + }); + + it('pub type alias is exported', () => { + const visMod = mockNode('visibility_modifier', 'pub'); + const nameNode = mockNode('type_identifier', 'Result'); + const typeDecl = mockNode('type_item', 'pub type Result = ...', undefined, [visMod, nameNode]); + expect(isNodeExported(nameNode, 'Result', 'rust')).toBe(true); + }); + + it('pub const is exported', () => { + const visMod = mockNode('visibility_modifier', 'pub'); + const nameNode = mockNode('identifier', 'MAX_SIZE'); + const constDecl = mockNode('const_item', 'pub const MAX_SIZE: usize = 100;', undefined, [visMod, nameNode]); + expect(isNodeExported(nameNode, 'MAX_SIZE', 'rust')).toBe(true); + }); + + it('private const is not exported', () => { + const nameNode = mockNode('identifier', 'INTERNAL_LIMIT'); + const constDecl = mockNode('const_item', 'const INTERNAL_LIMIT: usize = 50;', undefined, [nameNode]); + expect(isNodeExported(nameNode, 'INTERNAL_LIMIT', 'rust')).toBe(false); + }); + + it('pub static is exported', () => { + const visMod = mockNode('visibility_modifier', 'pub'); + const nameNode = mockNode('identifier', 'INSTANCE'); + const staticDecl = mockNode('static_item', 'pub static INSTANCE: ...', undefined, [visMod, nameNode]); + expect(isNodeExported(nameNode, 'INSTANCE', 'rust')).toBe(true); + }); + + it('associated_type without pub is not exported', () => { + const nameNode = mockNode('type_identifier', 'Item'); + const assocType = mockNode('associated_type', 'type Item;', undefined, [nameNode]); + expect(isNodeExported(nameNode, 'Item', 'rust')).toBe(false); + }); + }); + + // C# edge cases + describe('csharp edge cases', () => { + it('protected modifier is not exported', () => { + const modifier = mockNode('modifier', 'protected'); + const nameNode = mockNode('identifier', 'OnInit'); + const methodDecl = mockNode('method_declaration', 'protected void OnInit() {}', undefined, [modifier, nameNode]); + expect(isNodeExported(nameNode, 'OnInit', 'csharp')).toBe(false); + }); + + it('protected internal is not exported (first modifier wins)', () => { + const mod1 = mockNode('modifier', 'protected'); + const mod2 = mockNode('modifier', 'internal'); + const nameNode = mockNode('identifier', 'Setup'); + const methodDecl = mockNode('method_declaration', 'protected internal void Setup() {}', undefined, [mod1, mod2, nameNode]); + // Neither modifier is 'public', so not exported + expect(isNodeExported(nameNode, 'Setup', 'csharp')).toBe(false); + }); + + it('record_struct with public modifier is exported', () => { + const modifier = mockNode('modifier', 'public'); + const nameNode = mockNode('identifier', 'Coord'); + const recStruct = mockNode('record_struct_declaration', 'public record struct Coord {}', undefined, [modifier, nameNode]); + expect(isNodeExported(nameNode, 'Coord', 'csharp')).toBe(true); + }); + + it('record_class with public modifier is exported', () => { + const modifier = mockNode('modifier', 'public'); + const nameNode = mockNode('identifier', 'UserRecord'); + const recClass = mockNode('record_class_declaration', 'public record class UserRecord {}', undefined, [modifier, nameNode]); + expect(isNodeExported(nameNode, 'UserRecord', 'csharp')).toBe(true); + }); + + it('file_scoped_namespace_declaration is a valid context', () => { + const modifier = mockNode('modifier', 'public'); + const nameNode = mockNode('identifier', 'MyClass'); + const classDecl = mockNode('class_declaration', 'public class MyClass {}', undefined, [modifier, nameNode]); + // class_declaration is found before namespace, so public is detected + expect(isNodeExported(nameNode, 'MyClass', 'csharp')).toBe(true); + }); + + it('delegate with public modifier is exported', () => { + const modifier = mockNode('modifier', 'public'); + const nameNode = mockNode('identifier', 'OnChange'); + const delegateDecl = mockNode('delegate_declaration', 'public delegate void OnChange();', undefined, [modifier, nameNode]); + expect(isNodeExported(nameNode, 'OnChange', 'csharp')).toBe(true); + }); + + it('event with public modifier is exported', () => { + const modifier = mockNode('modifier', 'public'); + const nameNode = mockNode('identifier', 'Changed'); + const eventDecl = mockNode('event_declaration', 'public event EventHandler Changed;', undefined, [modifier, nameNode]); + expect(isNodeExported(nameNode, 'Changed', 'csharp')).toBe(true); + }); + + it('property with public modifier is exported', () => { + const modifier = mockNode('modifier', 'public'); + const nameNode = mockNode('identifier', 'Name'); + const propDecl = mockNode('property_declaration', 'public string Name { get; set; }', undefined, [modifier, nameNode]); + expect(isNodeExported(nameNode, 'Name', 'csharp')).toBe(true); + }); + }); + + // Kotlin edge cases + describe('kotlin edge cases', () => { + it('protected function is not exported', () => { + const visMod = mockNode('visibility_modifier', 'protected'); + const modifiers = mockNode('modifiers', 'protected', undefined, [visMod]); + const nameNode = mockNode('identifier', 'onInit'); + const fnDecl = mockNode('function_declaration', 'protected fun onInit() {}', undefined, [modifiers, nameNode]); + expect(isNodeExported(nameNode, 'onInit', 'kotlin')).toBe(false); + }); + }); + + // Java edge cases + describe('java edge cases', () => { + it('protected method is not exported', () => { + const modifiers = mockNode('modifiers', 'protected'); + const nameNode = mockNode('identifier', 'onInit'); + const methodDecl = mockNode('method_declaration', 'protected void onInit() {}', undefined, [modifiers, nameNode]); + expect(isNodeExported(nameNode, 'onInit', 'java')).toBe(false); + }); + + it('static public method is exported', () => { + const modifiers = mockNode('modifiers', 'public static'); + const nameNode = mockNode('identifier', 'main'); + const methodDecl = mockNode('method_declaration', 'public static void main(String[] args) {}', undefined, [modifiers, nameNode]); + expect(isNodeExported(nameNode, 'main', 'java')).toBe(true); + }); + }); + + // PHP edge cases + describe('php edge cases', () => { + it('protected method is not exported', () => { + const visMod = mockNode('visibility_modifier', 'protected'); + const nameNode = mockNode('name', 'init', visMod); + expect(isNodeExported(nameNode, 'init', 'php')).toBe(false); + }); + + it('interface declaration is exported', () => { + const ifaceDecl = mockNode('interface_declaration', 'interface Loggable {}'); + const nameNode = mockNode('name', 'Loggable', ifaceDecl); + expect(isNodeExported(nameNode, 'Loggable', 'php')).toBe(true); + }); + + it('trait declaration is exported', () => { + const traitDecl = mockNode('trait_declaration', 'trait Cacheable {}'); + const nameNode = mockNode('name', 'Cacheable', traitDecl); + expect(isNodeExported(nameNode, 'Cacheable', 'php')).toBe(true); + }); + + it('enum declaration is exported', () => { + const enumDecl = mockNode('enum_declaration', 'enum Status {}'); + const nameNode = mockNode('name', 'Status', enumDecl); + expect(isNodeExported(nameNode, 'Status', 'php')).toBe(true); + }); + }); + + // Swift edge cases + describe('swift edge cases', () => { + it('internal function is not exported (Swift default)', () => { + const visMod = mockNode('visibility_modifier', 'internal'); + const nameNode = mockNode('identifier', 'setup', visMod); + expect(isNodeExported(nameNode, 'setup', 'swift')).toBe(false); + }); + + it('private function is not exported', () => { + const visMod = mockNode('visibility_modifier', 'private'); + const nameNode = mockNode('identifier', 'helper', visMod); + expect(isNodeExported(nameNode, 'helper', 'swift')).toBe(false); + }); + + it('fileprivate function is not exported', () => { + const visMod = mockNode('visibility_modifier', 'fileprivate'); + const nameNode = mockNode('identifier', 'localHelper', visMod); + expect(isNodeExported(nameNode, 'localHelper', 'swift')).toBe(false); + }); + }); + // Unknown language describe('unknown language', () => { it('returns false for unknown language', () => { diff --git a/gitnexus/test/integration/query-compilation.test.ts b/gitnexus/test/integration/query-compilation.test.ts new file mode 100644 index 000000000..ac7453b97 --- /dev/null +++ b/gitnexus/test/integration/query-compilation.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect, beforeAll } from 'vitest'; +import { loadParser, loadLanguage, isLanguageAvailable } from '../../src/core/tree-sitter/parser-loader.js'; +import { LANGUAGE_QUERIES } from '../../src/core/ingestion/tree-sitter-queries.js'; +import { SupportedLanguages } from '../../src/config/supported-languages.js'; +import Parser from 'tree-sitter'; + +/** + * Smoke test: verify that every LANGUAGE_QUERIES entry compiles against + * its tree-sitter grammar without throwing. A silent Query compilation + * failure is the #1 cause of "0 nodes extracted for language X" bugs. + */ +describe('Query compilation smoke tests', () => { + let parser: Parser; + + beforeAll(async () => { + parser = await loadParser(); + }); + + const languageFiles: Record = { + [SupportedLanguages.TypeScript]: 'test.ts', + [SupportedLanguages.JavaScript]: 'test.js', + [SupportedLanguages.Python]: 'test.py', + [SupportedLanguages.Java]: 'Test.java', + [SupportedLanguages.C]: 'test.c', + [SupportedLanguages.CPlusPlus]: 'test.cpp', + [SupportedLanguages.CSharp]: 'Test.cs', + [SupportedLanguages.Go]: 'test.go', + [SupportedLanguages.Rust]: 'test.rs', + [SupportedLanguages.PHP]: 'test.php', + [SupportedLanguages.Kotlin]: 'Test.kt', + [SupportedLanguages.Swift]: 'test.swift', + }; + + // Known query compilation failures — remove from this set as PRs fix them + const knownFailures = new Set([]); + + for (const [lang, filename] of Object.entries(languageFiles)) { + const testFn = knownFailures.has(lang) ? it.fails : it; + + testFn(`compiles query for ${lang}`, async () => { + if (!isLanguageAvailable(lang as SupportedLanguages)) { + return; // parser binary not available in this environment + } + + await loadLanguage(lang as SupportedLanguages, filename); + const queryStr = LANGUAGE_QUERIES[lang as SupportedLanguages]; + expect(queryStr).toBeTruthy(); + + const grammar = parser.getLanguage(); + // This is the line that silently fails in production when queries + // use node types that don't exist in the grammar. + const query = new Parser.Query(grammar, queryStr); + expect(query).toBeDefined(); + + // Verify it can actually run against a minimal tree + const tree = parser.parse(''); + const matches = query.matches(tree.rootNode); + expect(Array.isArray(matches)).toBe(true); + }); + } +}); diff --git a/gitnexus/test/integration/tree-sitter-languages.test.ts b/gitnexus/test/integration/tree-sitter-languages.test.ts index 4fa225d91..6faa8eecd 100644 --- a/gitnexus/test/integration/tree-sitter-languages.test.ts +++ b/gitnexus/test/integration/tree-sitter-languages.test.ts @@ -133,6 +133,31 @@ describe('Tree-sitter multi-language parsing', () => { expect(defs.length).toBeGreaterThan(0); const defTypes = defs.map(d => d.type); expect(defTypes).toContain('definition.function'); + const names = defs.map(d => d.name); + expect(names).toContain('add'); + expect(names).toContain('internal_helper'); + expect(names).toContain('print_message'); + }); + + it('captures pointer-returning function definitions', async () => { + await loadLanguage(SupportedLanguages.C); + const code = `int* get_ptr() { return 0; }\nchar** get_strs() { return 0; }`; + const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.C]); + const defs = extractDefinitions(matches); + const names = defs.map(d => d.name); + expect(names).toContain('get_ptr'); + expect(names).toContain('get_strs'); + }); + + it('captures macros and typedefs', async () => { + await loadLanguage(SupportedLanguages.C); + const code = `#define MAX_SIZE 100\ntypedef unsigned int uint;\nstruct Point { int x; int y; };`; + const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.C]); + const defs = extractDefinitions(matches); + const names = defs.map(d => d.name); + expect(names).toContain('MAX_SIZE'); + expect(names).toContain('uint'); + expect(names).toContain('Point'); }); }); @@ -146,21 +171,106 @@ describe('Tree-sitter multi-language parsing', () => { expect(defs.length).toBeGreaterThan(0); const defTypes = defs.map(d => d.type); expect(defTypes).toContain('definition.class'); + const names = defs.map(d => d.name); + expect(names).toContain('UserManager'); + expect(names).toContain('helperFunction'); + }); + + it('captures pointer-returning methods and functions', async () => { + await loadLanguage(SupportedLanguages.CPlusPlus); + const code = `int* Factory::create() { return nullptr; }\nchar** getNames() { return 0; }`; + const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.CPlusPlus]); + const defs = extractDefinitions(matches); + const names = defs.map(d => d.name); + expect(names).toContain('create'); + expect(names).toContain('getNames'); + }); + + it('captures reference-returning functions', async () => { + await loadLanguage(SupportedLanguages.CPlusPlus); + const code = `int& Container::at(int i) { static int x; return x; }`; + const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.CPlusPlus]); + const defs = extractDefinitions(matches); + const names = defs.map(d => d.name); + expect(names).toContain('at'); + }); + + it('captures destructor definitions', async () => { + await loadLanguage(SupportedLanguages.CPlusPlus); + const code = `MyClass::~MyClass() { cleanup(); }`; + const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.CPlusPlus]); + const defs = extractDefinitions(matches); + const names = defs.map(d => d.name); + expect(names).toContain('~MyClass'); + }); + + it('captures template declarations', async () => { + await loadLanguage(SupportedLanguages.CPlusPlus); + const code = `template class Container { T value; };`; + const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.CPlusPlus]); + const defs = extractDefinitions(matches); + const names = defs.map(d => d.name); + expect(names).toContain('Container'); + }); + + it('captures namespace definitions', async () => { + await loadLanguage(SupportedLanguages.CPlusPlus); + const code = `namespace utils { void helper() {} }`; + const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.CPlusPlus]); + const defs = extractDefinitions(matches); + const names = defs.map(d => d.name); + expect(names).toContain('utils'); + expect(names).toContain('helper'); }); }); describe('C#', () => { - it('parses class, method, and property declarations', async () => { + it('parses class, method, and namespace declarations', async () => { await loadLanguage(SupportedLanguages.CSharp); const content = readFixture('simple.cs'); - try { - const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.CSharp]); - const defs = extractDefinitions(matches); - expect(defs.length).toBeGreaterThan(0); - } catch (e: any) { - // Some tree-sitter-c-sharp versions don't support all query node types - expect(e.message).toContain('TSQueryError'); - } + const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.CSharp]); + const defs = extractDefinitions(matches); + + expect(defs.length).toBeGreaterThan(0); + const defTypes = defs.map(d => d.type); + expect(defTypes).toContain('definition.class'); + expect(defTypes).toContain('definition.method'); + expect(defTypes).toContain('definition.namespace'); + const names = defs.map(d => d.name); + expect(names).toContain('Calculator'); + expect(names).toContain('Add'); + }); + + it('captures interfaces, enums, records, structs', async () => { + await loadLanguage(SupportedLanguages.CSharp); + const content = readFixture('simple.cs'); + const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.CSharp]); + const defs = extractDefinitions(matches); + const names = defs.map(d => d.name); + expect(names).toContain('ICalculator'); + expect(names).toContain('Operation'); + expect(names).toContain('CalculationResult'); + expect(names).toContain('Point'); + }); + + it('captures file-scoped namespace declarations', async () => { + await loadLanguage(SupportedLanguages.CSharp); + const code = `namespace MyApp;\npublic class Program { }`; + const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.CSharp]); + const defs = extractDefinitions(matches); + const names = defs.map(d => d.name); + expect(names).toContain('MyApp'); + expect(names).toContain('Program'); + }); + + it('captures constructors and properties', async () => { + await loadLanguage(SupportedLanguages.CSharp); + const content = readFixture('simple.cs'); + const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.CSharp]); + const defs = extractDefinitions(matches); + const defTypes = defs.map(d => d.type); + expect(defTypes).toContain('definition.constructor'); + expect(defTypes).toContain('definition.property'); }); }); @@ -174,6 +284,58 @@ describe('Tree-sitter multi-language parsing', () => { expect(defs.length).toBeGreaterThan(0); const defTypes = defs.map(d => d.type); expect(defTypes).toContain('definition.function'); + const names = defs.map(d => d.name); + expect(names).toContain('public_function'); + expect(names).toContain('private_function'); + expect(names).toContain('Config'); + }); + + it('captures impl blocks and methods', async () => { + await loadLanguage(SupportedLanguages.Rust); + const content = readFixture('simple.rs'); + const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.Rust]); + const defs = extractDefinitions(matches); + const defTypes = defs.map(d => d.type); + expect(defTypes).toContain('definition.impl'); + const names = defs.map(d => d.name); + expect(names).toContain('new'); + }); + + it('captures generic impl blocks', async () => { + await loadLanguage(SupportedLanguages.Rust); + const code = `struct Vec { data: Vec }\nimpl Vec { fn len(&self) -> usize { 0 } }`; + const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.Rust]); + const defs = extractDefinitions(matches); + const names = defs.map(d => d.name); + expect(names).toContain('Vec'); + }); + + it('captures trait impl heritage', async () => { + await loadLanguage(SupportedLanguages.Rust); + const code = `trait Display { fn fmt(&self); }\nstruct Foo;\nimpl Display for Foo { fn fmt(&self) {} }`; + const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.Rust]); + // Look for heritage captures + const heritageCaptures: string[] = []; + for (const match of matches) { + for (const capture of match.captures) { + if (capture.name.startsWith('heritage.')) { + heritageCaptures.push(`${capture.name}:${capture.node.text}`); + } + } + } + expect(heritageCaptures).toContain('heritage.trait:Display'); + expect(heritageCaptures).toContain('heritage.class:Foo'); + }); + + it('captures modules, consts, and statics', async () => { + await loadLanguage(SupportedLanguages.Rust); + const code = `mod utils { pub fn helper() {} }\npub const MAX: usize = 100;\nstatic INSTANCE: i32 = 0;`; + const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.Rust]); + const defs = extractDefinitions(matches); + const names = defs.map(d => d.name); + expect(names).toContain('utils'); + expect(names).toContain('MAX'); + expect(names).toContain('INSTANCE'); }); }); @@ -252,14 +414,9 @@ describe('Tree-sitter multi-language parsing', () => { for (const [lang, fixture, filePath] of langFixtures) { await loadLanguage(lang, filePath || fixture); const content = readFixture(fixture); - try { - const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[lang]); - const defs = extractDefinitions(matches); - expect(defs.length, `${lang} (${fixture}) should have definitions`).toBeGreaterThan(0); - } catch (e: any) { - // Some grammars may have query compatibility issues - if (!e.message?.includes('TSQueryError')) throw e; - } + const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[lang]); + const defs = extractDefinitions(matches); + expect(defs.length, `${lang} (${fixture}) should have definitions`).toBeGreaterThan(0); } }); }); diff --git a/gitnexus/test/unit/entry-point-scoring.test.ts b/gitnexus/test/unit/entry-point-scoring.test.ts index 953394a09..b53f1f8e5 100644 --- a/gitnexus/test/unit/entry-point-scoring.test.ts +++ b/gitnexus/test/unit/entry-point-scoring.test.ts @@ -123,6 +123,25 @@ describe('calculateEntryPointScore', () => { const result = calculateEntryPointScore('main', 'c', false, 0, 2); expect(result.reasons).toContain('entry-pattern'); }); + + // C-specific patterns + it.each([ + 'init_server', 'server_init', 'start_server', 'handle_request', + 'signal_handler', 'event_callback', 'cmd_new_window', 'server_start', + 'client_connect', 'session_create', 'window_resize', + ])('recognizes C pattern "%s"', (name) => { + const result = calculateEntryPointScore(name, 'c', false, 0, 2); + expect(result.reasons).toContain('entry-pattern'); + }); + + // C++-specific patterns + it.each([ + 'CreateInstance', 'create_session', 'Run', 'run', 'Start', 'start', + 'OnEventReceived', 'on_click', + ])('recognizes C++ pattern "%s"', (name) => { + const result = calculateEntryPointScore(name, 'cpp', false, 0, 2); + expect(result.reasons).toContain('entry-pattern'); + }); }); describe('utility pattern penalty', () => { diff --git a/gitnexus/test/unit/ingestion-utils.test.ts b/gitnexus/test/unit/ingestion-utils.test.ts index f0de36ba3..de0507c90 100644 --- a/gitnexus/test/unit/ingestion-utils.test.ts +++ b/gitnexus/test/unit/ingestion-utils.test.ts @@ -1,6 +1,12 @@ import { describe, it, expect } from 'vitest'; -import { getLanguageFromFilename } from '../../src/core/ingestion/utils.js'; +import { getLanguageFromFilename, isBuiltInOrNoise, extractFunctionName } from '../../src/core/ingestion/utils.js'; +import { getTreeSitterBufferSize, TREE_SITTER_BUFFER_SIZE, TREE_SITTER_MAX_BUFFER } from '../../src/core/ingestion/constants.js'; import { SupportedLanguages } from '../../src/config/supported-languages.js'; +import Parser from 'tree-sitter'; +import C from 'tree-sitter-c'; +import CPP from 'tree-sitter-cpp'; +import Python from 'tree-sitter-python'; +import TypeScript from 'tree-sitter-typescript'; describe('getLanguageFromFilename', () => { describe('TypeScript', () => { @@ -43,14 +49,10 @@ describe('getLanguageFromFilename', () => { it('detects .c files', () => { expect(getLanguageFromFilename('main.c')).toBe(SupportedLanguages.C); }); - - it('detects .h header files', () => { - expect(getLanguageFromFilename('header.h')).toBe(SupportedLanguages.C); - }); }); describe('C++', () => { - it.each(['.cpp', '.cc', '.cxx', '.hpp', '.hxx', '.hh'])( + it.each(['.cpp', '.cc', '.cxx', '.h', '.hpp', '.hxx', '.hh'])( 'detects %s files', (ext) => { expect(getLanguageFromFilename(`file${ext}`)).toBe(SupportedLanguages.CPlusPlus); @@ -117,3 +119,539 @@ describe('getLanguageFromFilename', () => { }); }); }); + +describe('isBuiltInOrNoise', () => { + describe('JavaScript/TypeScript', () => { + it('filters console methods', () => { + expect(isBuiltInOrNoise('console')).toBe(true); + expect(isBuiltInOrNoise('log')).toBe(true); + expect(isBuiltInOrNoise('warn')).toBe(true); + }); + + it('filters React hooks', () => { + expect(isBuiltInOrNoise('useState')).toBe(true); + expect(isBuiltInOrNoise('useEffect')).toBe(true); + expect(isBuiltInOrNoise('useCallback')).toBe(true); + }); + + it('filters array methods', () => { + expect(isBuiltInOrNoise('map')).toBe(true); + expect(isBuiltInOrNoise('filter')).toBe(true); + expect(isBuiltInOrNoise('reduce')).toBe(true); + }); + }); + + describe('Python', () => { + it('filters built-in functions', () => { + expect(isBuiltInOrNoise('print')).toBe(true); + expect(isBuiltInOrNoise('len')).toBe(true); + expect(isBuiltInOrNoise('range')).toBe(true); + }); + }); + + describe('PHP', () => { + it('filters PHP built-in functions', () => { + expect(isBuiltInOrNoise('echo')).toBe(true); + expect(isBuiltInOrNoise('isset')).toBe(true); + expect(isBuiltInOrNoise('date')).toBe(true); + expect(isBuiltInOrNoise('json_encode')).toBe(true); + expect(isBuiltInOrNoise('array_map')).toBe(true); + }); + + it('filters PHP string functions', () => { + expect(isBuiltInOrNoise('strlen')).toBe(true); + expect(isBuiltInOrNoise('substr')).toBe(true); + expect(isBuiltInOrNoise('str_replace')).toBe(true); + }); + }); + + describe('C/C++', () => { + it('filters standard library functions', () => { + expect(isBuiltInOrNoise('printf')).toBe(true); + expect(isBuiltInOrNoise('malloc')).toBe(true); + expect(isBuiltInOrNoise('free')).toBe(true); + }); + + it('filters Linux kernel macros', () => { + expect(isBuiltInOrNoise('container_of')).toBe(true); + expect(isBuiltInOrNoise('ARRAY_SIZE')).toBe(true); + expect(isBuiltInOrNoise('pr_info')).toBe(true); + }); + }); + + describe('Kotlin', () => { + it('filters stdlib functions', () => { + expect(isBuiltInOrNoise('println')).toBe(true); + expect(isBuiltInOrNoise('listOf')).toBe(true); + expect(isBuiltInOrNoise('TODO')).toBe(true); + }); + + it('filters coroutine functions', () => { + expect(isBuiltInOrNoise('launch')).toBe(true); + expect(isBuiltInOrNoise('async')).toBe(true); + }); + }); + + describe('Swift', () => { + it('filters built-in functions', () => { + expect(isBuiltInOrNoise('print')).toBe(true); + expect(isBuiltInOrNoise('fatalError')).toBe(true); + }); + + it('filters UIKit methods', () => { + expect(isBuiltInOrNoise('addSubview')).toBe(true); + expect(isBuiltInOrNoise('reloadData')).toBe(true); + }); + }); + + describe('Rust', () => { + it('filters Result/Option methods', () => { + expect(isBuiltInOrNoise('unwrap')).toBe(true); + expect(isBuiltInOrNoise('expect')).toBe(true); + expect(isBuiltInOrNoise('unwrap_or')).toBe(true); + expect(isBuiltInOrNoise('unwrap_or_else')).toBe(true); + expect(isBuiltInOrNoise('unwrap_or_default')).toBe(true); + expect(isBuiltInOrNoise('ok')).toBe(true); + expect(isBuiltInOrNoise('err')).toBe(true); + expect(isBuiltInOrNoise('is_ok')).toBe(true); + expect(isBuiltInOrNoise('is_err')).toBe(true); + expect(isBuiltInOrNoise('map_err')).toBe(true); + expect(isBuiltInOrNoise('and_then')).toBe(true); + expect(isBuiltInOrNoise('or_else')).toBe(true); + }); + + it('filters trait conversion methods', () => { + expect(isBuiltInOrNoise('clone')).toBe(true); + expect(isBuiltInOrNoise('to_string')).toBe(true); + expect(isBuiltInOrNoise('to_owned')).toBe(true); + expect(isBuiltInOrNoise('into')).toBe(true); + expect(isBuiltInOrNoise('from')).toBe(true); + expect(isBuiltInOrNoise('as_ref')).toBe(true); + expect(isBuiltInOrNoise('as_mut')).toBe(true); + }); + + it('filters iterator methods', () => { + expect(isBuiltInOrNoise('iter')).toBe(true); + expect(isBuiltInOrNoise('into_iter')).toBe(true); + expect(isBuiltInOrNoise('collect')).toBe(true); + expect(isBuiltInOrNoise('fold')).toBe(true); + expect(isBuiltInOrNoise('for_each')).toBe(true); + }); + + it('filters collection methods', () => { + expect(isBuiltInOrNoise('len')).toBe(true); + expect(isBuiltInOrNoise('is_empty')).toBe(true); + expect(isBuiltInOrNoise('push')).toBe(true); + expect(isBuiltInOrNoise('pop')).toBe(true); + expect(isBuiltInOrNoise('insert')).toBe(true); + expect(isBuiltInOrNoise('remove')).toBe(true); + expect(isBuiltInOrNoise('contains')).toBe(true); + }); + + it('filters macro-like and panic functions', () => { + expect(isBuiltInOrNoise('format')).toBe(true); + expect(isBuiltInOrNoise('panic')).toBe(true); + expect(isBuiltInOrNoise('unreachable')).toBe(true); + expect(isBuiltInOrNoise('todo')).toBe(true); + expect(isBuiltInOrNoise('unimplemented')).toBe(true); + expect(isBuiltInOrNoise('vec')).toBe(true); + expect(isBuiltInOrNoise('println')).toBe(true); + expect(isBuiltInOrNoise('eprintln')).toBe(true); + expect(isBuiltInOrNoise('dbg')).toBe(true); + }); + + it('filters sync primitives', () => { + expect(isBuiltInOrNoise('lock')).toBe(true); + expect(isBuiltInOrNoise('try_lock')).toBe(true); + expect(isBuiltInOrNoise('spawn')).toBe(true); + expect(isBuiltInOrNoise('join')).toBe(true); + expect(isBuiltInOrNoise('sleep')).toBe(true); + }); + + it('filters enum constructors', () => { + expect(isBuiltInOrNoise('Some')).toBe(true); + expect(isBuiltInOrNoise('None')).toBe(true); + expect(isBuiltInOrNoise('Ok')).toBe(true); + expect(isBuiltInOrNoise('Err')).toBe(true); + }); + + it('does not filter user-defined Rust functions', () => { + expect(isBuiltInOrNoise('process_request')).toBe(false); + expect(isBuiltInOrNoise('handle_connection')).toBe(false); + expect(isBuiltInOrNoise('build_response')).toBe(false); + }); + }); + + describe('C#/.NET', () => { + it('filters Console I/O', () => { + expect(isBuiltInOrNoise('Console')).toBe(true); + expect(isBuiltInOrNoise('WriteLine')).toBe(true); + expect(isBuiltInOrNoise('ReadLine')).toBe(true); + }); + + it('filters LINQ methods', () => { + expect(isBuiltInOrNoise('Where')).toBe(true); + expect(isBuiltInOrNoise('Select')).toBe(true); + expect(isBuiltInOrNoise('GroupBy')).toBe(true); + expect(isBuiltInOrNoise('OrderBy')).toBe(true); + expect(isBuiltInOrNoise('FirstOrDefault')).toBe(true); + expect(isBuiltInOrNoise('ToList')).toBe(true); + }); + + it('filters Task async methods', () => { + expect(isBuiltInOrNoise('Task')).toBe(true); + expect(isBuiltInOrNoise('Run')).toBe(true); + expect(isBuiltInOrNoise('WhenAll')).toBe(true); + expect(isBuiltInOrNoise('ConfigureAwait')).toBe(true); + }); + + it('filters Object base methods', () => { + expect(isBuiltInOrNoise('ToString')).toBe(true); + expect(isBuiltInOrNoise('GetType')).toBe(true); + expect(isBuiltInOrNoise('Equals')).toBe(true); + expect(isBuiltInOrNoise('GetHashCode')).toBe(true); + }); + }); + + describe('user-defined functions', () => { + it('does not filter custom function names', () => { + expect(isBuiltInOrNoise('myCustomFunction')).toBe(false); + expect(isBuiltInOrNoise('processData')).toBe(false); + expect(isBuiltInOrNoise('handleUserRequest')).toBe(false); + }); + }); +}); + +describe('extractFunctionName', () => { + const parser = new Parser(); + + describe('C', () => { + it('extracts function name from C function definition', () => { + parser.setLanguage(C); + const code = `int main() { return 0; }`; + const tree = parser.parse(code); + const funcNode = tree.rootNode.child(0); + + const result = extractFunctionName(funcNode); + + expect(result.funcName).toBe('main'); + expect(result.label).toBe('Function'); + }); + + it('extracts function name with parameters', () => { + parser.setLanguage(C); + const code = `void helper(int a, char* b) {}`; + const tree = parser.parse(code); + const funcNode = tree.rootNode.child(0); + + const result = extractFunctionName(funcNode); + + expect(result.funcName).toBe('helper'); + expect(result.label).toBe('Function'); + }); + }); + + describe('C++', () => { + it('extracts method name from C++ class method definition', () => { + parser.setLanguage(CPP); + const code = `int MyClass::OnEncryptData() { return 0; }`; + const tree = parser.parse(code); + const funcNode = tree.rootNode.child(0); + + const result = extractFunctionName(funcNode); + + expect(result.funcName).toBe('OnEncryptData'); + expect(result.label).toBe('Method'); + }); + + it('extracts method name with namespace', () => { + parser.setLanguage(CPP); + const code = `void HuksListener::OnDataOprEvent(int type, DataInfo& info) {}`; + const tree = parser.parse(code); + const funcNode = tree.rootNode.child(0); + + const result = extractFunctionName(funcNode); + + expect(result.funcName).toBe('OnDataOprEvent'); + expect(result.label).toBe('Method'); + }); + + it('extracts C function (not method)', () => { + parser.setLanguage(CPP); + const code = `void standalone_function() {}`; + const tree = parser.parse(code); + const funcNode = tree.rootNode.child(0); + + const result = extractFunctionName(funcNode); + + expect(result.funcName).toBe('standalone_function'); + expect(result.label).toBe('Function'); + }); + + it('extracts method with parenthesized declarator', () => { + parser.setLanguage(CPP); + const code = `void (MyClass::handler)() {}`; + const tree = parser.parse(code); + const funcNode = tree.rootNode.child(0); + + const result = extractFunctionName(funcNode); + + expect(result.funcName).toBe('handler'); + expect(result.label).toBe('Method'); + }); + }); + + describe('C pointer returns', () => { + it('extracts name from function returning pointer', () => { + parser.setLanguage(C); + const code = `int* get_data() { return 0; }`; + const tree = parser.parse(code); + const funcNode = tree.rootNode.child(0); + + const result = extractFunctionName(funcNode); + + expect(result.funcName).toBe('get_data'); + expect(result.label).toBe('Function'); + }); + + it('extracts name from function returning double pointer', () => { + parser.setLanguage(C); + const code = `char** get_strings() { return 0; }`; + const tree = parser.parse(code); + const funcNode = tree.rootNode.child(0); + + const result = extractFunctionName(funcNode); + + expect(result.funcName).toBe('get_strings'); + expect(result.label).toBe('Function'); + }); + + it('extracts name from struct pointer return', () => { + parser.setLanguage(C); + const code = `struct Node* create_node(int val) { return 0; }`; + const tree = parser.parse(code); + const funcNode = tree.rootNode.child(0); + + const result = extractFunctionName(funcNode); + + expect(result.funcName).toBe('create_node'); + expect(result.label).toBe('Function'); + }); + }); + + describe('C++ pointer/reference returns', () => { + it('extracts name from method returning pointer', () => { + parser.setLanguage(CPP); + const code = `int* MyClass::getData() { return nullptr; }`; + const tree = parser.parse(code); + const funcNode = tree.rootNode.child(0); + + const result = extractFunctionName(funcNode); + + expect(result.funcName).toBe('getData'); + expect(result.label).toBe('Method'); + }); + + it('extracts name from function returning reference', () => { + parser.setLanguage(CPP); + const code = `std::string& get_name() { static std::string s; return s; }`; + const tree = parser.parse(code); + const funcNode = tree.rootNode.child(0); + + const result = extractFunctionName(funcNode); + + expect(result.funcName).toBe('get_name'); + expect(result.label).toBe('Function'); + }); + + it('extracts name from method returning reference', () => { + parser.setLanguage(CPP); + const code = `int& Container::at(int i) { return data[i]; }`; + const tree = parser.parse(code); + const funcNode = tree.rootNode.child(0); + + const result = extractFunctionName(funcNode); + + expect(result.funcName).toBe('at'); + expect(result.label).toBe('Method'); + }); + + it('extracts name from method returning const reference', () => { + parser.setLanguage(CPP); + const code = `const std::string& Config::getName() const { return name_; }`; + const tree = parser.parse(code); + const funcNode = tree.rootNode.child(0); + + const result = extractFunctionName(funcNode); + + expect(result.funcName).toBe('getName'); + expect(result.label).toBe('Method'); + }); + }); + + describe('C++ destructors', () => { + it('extracts destructor name from out-of-line definition', () => { + parser.setLanguage(CPP); + const code = `MyClass::~MyClass() { cleanup(); }`; + const tree = parser.parse(code); + const funcNode = tree.rootNode.child(0); + + const result = extractFunctionName(funcNode); + + // destructor_name includes the ~ prefix + expect(result.funcName).toBe('~MyClass'); + expect(result.label).toBe('Method'); + }); + }); + + describe('TypeScript', () => { + it('extracts arrow function name from variable declarator', () => { + parser.setLanguage(TypeScript.typescript); + const code = `const myHandler = () => { return 1; }`; + const tree = parser.parse(code); + const program = tree.rootNode; + const varDecl = program.child(0); + const declarator = varDecl!.namedChild(0); + const arrowFunc = declarator!.namedChild(1); + + const result = extractFunctionName(arrowFunc); + + expect(result.funcName).toBe('myHandler'); + expect(result.label).toBe('Function'); + }); + + it('extracts function expression name from variable declarator', () => { + parser.setLanguage(TypeScript.typescript); + const code = `const processItem = function() { }`; + const tree = parser.parse(code); + const program = tree.rootNode; + const varDecl = program.child(0); + const declarator = varDecl!.namedChild(0); + const funcExpr = declarator!.namedChild(1); + + const result = extractFunctionName(funcExpr); + + expect(result.funcName).toBe('processItem'); + expect(result.label).toBe('Function'); + }); + }); + + describe('Python', () => { + it('extracts function name from Python function definition', () => { + parser.setLanguage(Python); + const code = `def hello_world():\n pass`; + const tree = parser.parse(code); + const funcNode = tree.rootNode.child(0); + + const result = extractFunctionName(funcNode); + + expect(result.funcName).toBe('hello_world'); + expect(result.label).toBe('Function'); + }); + + it('extracts function name with parameters', () => { + parser.setLanguage(Python); + const code = `def calculate_sum(a, b):\n return a + b`; + const tree = parser.parse(code); + const funcNode = tree.rootNode.child(0); + + const result = extractFunctionName(funcNode); + + expect(result.funcName).toBe('calculate_sum'); + expect(result.label).toBe('Function'); + }); + + it('extracts async function name', () => { + parser.setLanguage(Python); + const code = `async def fetch_data():\n pass`; + const tree = parser.parse(code); + const funcNode = tree.rootNode.child(0); + + const result = extractFunctionName(funcNode); + + expect(result.funcName).toBe('fetch_data'); + expect(result.label).toBe('Function'); + }); + + it('extracts function name with type hints', () => { + parser.setLanguage(Python); + const code = `def process_data(items: list[int]) -> bool:\n return True`; + const tree = parser.parse(code); + const funcNode = tree.rootNode.child(0); + + const result = extractFunctionName(funcNode); + + expect(result.funcName).toBe('process_data'); + expect(result.label).toBe('Function'); + }); + + it('extracts nested function name', () => { + parser.setLanguage(Python); + const code = `def outer():\n def inner():\n pass`; + const tree = parser.parse(code); + const outerFunc = tree.rootNode.child(0); + const block = outerFunc!.child(4); + const innerFunc = block!.namedChild(0); + + const result = extractFunctionName(innerFunc); + + expect(result.funcName).toBe('inner'); + expect(result.label).toBe('Function'); + }); + }); +}); + +describe('getTreeSitterBufferSize', () => { + it('returns minimum 512KB for small files', () => { + expect(getTreeSitterBufferSize(100)).toBe(TREE_SITTER_BUFFER_SIZE); + expect(getTreeSitterBufferSize(0)).toBe(TREE_SITTER_BUFFER_SIZE); + expect(getTreeSitterBufferSize(1000)).toBe(TREE_SITTER_BUFFER_SIZE); + }); + + it('returns 2x content length when larger than minimum', () => { + const size = 400 * 1024; // 400 KB — 2x = 800 KB > 512 KB min + expect(getTreeSitterBufferSize(size)).toBe(size * 2); + }); + + it('caps at 32MB for very large files', () => { + const huge = 20 * 1024 * 1024; // 20 MB — 2x = 40 MB > 32 MB cap + expect(getTreeSitterBufferSize(huge)).toBe(32 * 1024 * 1024); + }); + + it('returns exactly 512KB at the boundary', () => { + // 256KB * 2 = 512KB = minimum, so should return minimum + expect(getTreeSitterBufferSize(256 * 1024)).toBe(TREE_SITTER_BUFFER_SIZE); + }); + + it('scales linearly between min and max', () => { + const small = getTreeSitterBufferSize(300 * 1024); + const medium = getTreeSitterBufferSize(1 * 1024 * 1024); + const large = getTreeSitterBufferSize(5 * 1024 * 1024); + expect(small).toBeLessThan(medium); + expect(medium).toBeLessThan(large); + }); + + it('TREE_SITTER_MAX_BUFFER is 32MB', () => { + expect(TREE_SITTER_MAX_BUFFER).toBe(32 * 1024 * 1024); + }); + + it('returns max buffer at exact boundary (16MB input)', () => { + // 16MB * 2 = 32MB = max + expect(getTreeSitterBufferSize(16 * 1024 * 1024)).toBe(TREE_SITTER_MAX_BUFFER); + }); + + it('file just over max returns max buffer', () => { + // 17MB * 2 = 34MB > 32MB cap + expect(getTreeSitterBufferSize(17 * 1024 * 1024)).toBe(TREE_SITTER_MAX_BUFFER); + }); + + it('handles files between old 512KB limit and new 32MB limit', () => { + // This is the range that was previously silently skipped + const sizes = [600 * 1024, 1024 * 1024, 5 * 1024 * 1024, 10 * 1024 * 1024]; + for (const size of sizes) { + const bufSize = getTreeSitterBufferSize(size); + expect(bufSize).toBeGreaterThanOrEqual(TREE_SITTER_BUFFER_SIZE); + expect(bufSize).toBeLessThanOrEqual(TREE_SITTER_MAX_BUFFER); + } + }); +});