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:
Gergő Magyar 2026-03-10 23:03:32 +00:00 committed by GitHub
parent 1be910f54a
commit 7376e92063
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 2271 additions and 809 deletions

View file

@ -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.

View 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);

View file

@ -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') ||

View 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;
}
};

View file

@ -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 },

View file

@ -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;

View file

@ -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);

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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) {

View file

@ -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; }
}
}

View file

@ -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', () => {

View 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);
});
}
});

View file

@ -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);
}
});
});

View file

@ -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', () => {

View file

@ -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);
}
});
});