mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
fix: consolidate C/C++/C#/Rust language support from 6 overlapping PRs (#237)
* fix: consolidate C/C++/C#/Rust language support from 6 overlapping PRs Merges fixes from PRs #163, #170, #178, #216, #227, #234 into a single coherent changeset with shared modules and deduplication. Phase 0 — Pre-merge consolidation: - Extract isNodeExported to shared export-detection.ts module - Extract TREE_SITTER_BUFFER_SIZE to shared constants.ts with adaptive sizing - Consolidate FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise from duplicated call-processor.ts and parse-worker.ts into shared utils.ts - Add query compilation smoke tests for all 12 languages Language fixes: - fix(c/cpp): isExported checks static linkage instead of returning false - fix(c/cpp): .h files parsed as C++ (tree-sitter-cpp is superset of C) - fix(c/cpp): expanded entry point patterns (~30 new for C, ~18 for C++) - fix(cpp): add typedef, union, macro, prototype, inline method queries - fix(c#): isExported scans sibling modifiers instead of parent walk - fix(c#): heritage queries use correct base_list AST structure - fix(c#): add framework detection, import resolution, entry point scoring - fix(rust): isExported scans sibling visibility_modifier in declaration - fix(builtins): remove open/read/write/close (real C POSIX syscalls) - fix(buffer): adaptive bufferSize (2x fileSize, 512KB-32MB range) - feat(ts/js): add call_expression query patterns for const assignments Deduplication: - call-processor.ts: -226 lines (uses shared utils) - parse-worker.ts: -320 lines (uses shared utils) - parsing-processor.ts: -156 lines (uses shared export-detection) * perf: fix review findings — hoist Sets, deduplicate DEFINITION_CAPTURE_KEYS - Hoist CSHARP_DECL_TYPES and RUST_DECL_TYPES to module-level constants in export-detection.ts (was allocating new Set on every isNodeExported call) - Extract DEFINITION_CAPTURE_KEYS and getDefinitionNodeFromCaptures to shared utils.ts (was duplicated in parsing-processor.ts and parse-worker.ts) - Pre-compute merged entry point patterns to avoid per-call array spread in calculateEntryPointScore * test: add C, C++, and Tree-sitter buffer size tests * fix: C/C++/Rust review findings + comprehensive test coverage (+72 tests) Source fixes: - Add Rust built-in noise (unwrap, clone, into, collect, panic, etc.) - C++ anonymous namespace → internal linkage (not exported) - Replace .text regex with storage_class_specifier child scan (perf) - Raise file skip threshold from 512KB to 32MB (TREE_SITTER_MAX_BUFFER) - Export TREE_SITTER_MAX_BUFFER from constants.ts - Add C++ double pointer query patterns to CPP_QUERIES - Add C#: record_struct, record_class, file_scoped_namespace to decl types - Add Rust: union_item to visibility scanning set Tests (214 → 286): - ingestion-utils: +24 (Rust/C# noise, pointer/ref/destructor extraction, buffer) - parsing: +36 (real AST C/C++ static/namespace, Rust/C#/Java/PHP/Swift edge cases) - tree-sitter-languages: +12 (query accuracy for C/C++/C#/Rust captures)
This commit is contained in:
parent
1be910f54a
commit
7376e92063
17 changed files with 2271 additions and 809 deletions
|
|
@ -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.
|
||||
|
|
|
|||
19
gitnexus/src/core/ingestion/constants.ts
Normal file
19
gitnexus/src/core/ingestion/constants.ts
Normal file
|
|
@ -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);
|
||||
|
|
@ -63,10 +63,18 @@ const ENTRY_POINT_PATTERNS: Record<string, RegExp[]> = {
|
|||
|
||||
// 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<string, RegExp[]> = {
|
|||
/^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<string, RegExp[]> = {
|
|||
],
|
||||
};
|
||||
|
||||
/** Pre-computed merged patterns (universal + language-specific) to avoid per-call array allocation. */
|
||||
const MERGED_ENTRY_POINT_PATTERNS: Record<string, RegExp[]> = {};
|
||||
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') ||
|
||||
|
|
|
|||
198
gitnexus/src/core/ingestion/export-detection.ts
Normal file
198
gitnexus/src/core/ingestion/export-detection.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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<string, AstFrameworkPatternConf
|
|||
],
|
||||
csharp: [
|
||||
{ framework: 'aspnet', entryPointMultiplier: 3.2, reason: 'aspnet-attribute', patterns: FRAMEWORK_AST_PATTERNS.aspnet },
|
||||
{ framework: 'signalr', entryPointMultiplier: 2.8, reason: 'signalr-attribute', patterns: FRAMEWORK_AST_PATTERNS.signalr },
|
||||
{ framework: 'blazor', entryPointMultiplier: 2.5, reason: 'blazor-attribute', patterns: FRAMEWORK_AST_PATTERNS.blazor },
|
||||
{ framework: 'efcore', entryPointMultiplier: 2.0, reason: 'efcore-pattern', patterns: FRAMEWORK_AST_PATTERNS.efcore },
|
||||
],
|
||||
php: [
|
||||
{ framework: 'laravel', entryPointMultiplier: 3.0, reason: 'php-route-attribute', patterns: FRAMEWORK_AST_PATTERNS.laravel },
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/pa
|
|||
import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
|
||||
import { generateId } from '../../lib/utils.js';
|
||||
import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop } from './utils.js';
|
||||
import { getTreeSitterBufferSize } from './constants.js';
|
||||
import type { ExtractedHeritage } from './workers/parse-worker.js';
|
||||
|
||||
export const processHeritage = async (
|
||||
|
|
@ -55,7 +56,7 @@ export const processHeritage = async (
|
|||
if (!tree) {
|
||||
// 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;
|
||||
|
|
|
|||
|
|
@ -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<ComposerConfig | nu
|
|||
}
|
||||
}
|
||||
|
||||
/** C# project config parsed from .csproj files */
|
||||
interface CSharpProjectConfig {
|
||||
/** Root namespace from <RootNamespace> 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<CSharpProjectConfig[]> {
|
||||
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(/<RootNamespace>\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);
|
||||
|
|
|
|||
|
|
@ -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<string, any>): 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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<string, any>): 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
|
||||
|
|
|
|||
|
|
@ -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, any>): 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<string, any>): 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) {
|
||||
|
|
|
|||
43
gitnexus/test/fixtures/sample-code/simple.cs
vendored
43
gitnexus/test/fixtures/sample-code/simple.cs
vendored
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, any>): 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', () => {
|
||||
|
|
|
|||
61
gitnexus/test/integration/query-compilation.test.ts
Normal file
61
gitnexus/test/integration/query-compilation.test.ts
Normal file
|
|
@ -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<string, string> = {
|
||||
[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<string>([]);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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<typename T> 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<T> { data: Vec<T> }\nimpl<T> Vec<T> { 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue