GitNexus/gitnexus/test/unit/entry-point-scoring.test.ts
Gergő Magyar 7376e92063
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)
2026-03-10 23:03:32 +00:00

254 lines
9.5 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { calculateEntryPointScore, isTestFile, isUtilityFile } from '../../src/core/ingestion/entry-point-scoring.js';
describe('calculateEntryPointScore', () => {
describe('base scoring', () => {
it('returns 0 for functions with no outgoing calls', () => {
const result = calculateEntryPointScore('handler', 'typescript', true, 0, 0);
expect(result.score).toBe(0);
expect(result.reasons).toContain('no-outgoing-calls');
});
it('calculates base score as calleeCount / (callerCount + 1)', () => {
const result = calculateEntryPointScore('doStuff', 'typescript', false, 0, 5);
// base = 5 / (0 + 1) = 5, no export bonus, no name bonus
expect(result.score).toBe(5);
});
it('reduces score for functions with many callers', () => {
const few = calculateEntryPointScore('doStuff', 'typescript', false, 1, 5);
const many = calculateEntryPointScore('doStuff', 'typescript', false, 10, 5);
expect(few.score).toBeGreaterThan(many.score);
});
});
describe('export multiplier', () => {
it('applies 2.0 multiplier for exported functions', () => {
const exported = calculateEntryPointScore('doStuff', 'typescript', true, 0, 4);
const notExported = calculateEntryPointScore('doStuff', 'typescript', false, 0, 4);
expect(exported.score).toBe(notExported.score * 2);
expect(exported.reasons).toContain('exported');
});
it('does not add exported reason when not exported', () => {
const result = calculateEntryPointScore('doStuff', 'typescript', false, 0, 4);
expect(result.reasons).not.toContain('exported');
});
});
describe('universal name patterns', () => {
it.each([
'main', 'init', 'bootstrap', 'start', 'run', 'setup', 'configure',
])('recognizes "%s" as entry point pattern', (name) => {
const result = calculateEntryPointScore(name, 'typescript', false, 0, 3);
expect(result.reasons).toContain('entry-pattern');
});
it.each([
'handleLogin', 'handleSubmit', 'onClick', 'onSubmit',
'RequestHandler', 'UserController',
'processPayment', 'executeQuery', 'performAction',
'dispatchEvent', 'triggerAction', 'fireEvent', 'emitEvent',
])('recognizes "%s" as entry point pattern', (name) => {
const result = calculateEntryPointScore(name, 'typescript', false, 0, 3);
expect(result.reasons).toContain('entry-pattern');
});
it('applies 1.5x name multiplier for entry patterns', () => {
const matching = calculateEntryPointScore('handleLogin', 'typescript', false, 0, 4);
const plain = calculateEntryPointScore('doStuff', 'typescript', false, 0, 4);
// matching gets 1.5x, plain gets 1.0x
expect(matching.score).toBe(plain.score * 1.5);
});
});
describe('language-specific patterns', () => {
it('recognizes React hooks for TypeScript', () => {
const result = calculateEntryPointScore('useEffect', 'typescript', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes React hooks for JavaScript', () => {
const result = calculateEntryPointScore('useState', 'javascript', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes Python REST patterns', () => {
const result = calculateEntryPointScore('get_users', 'python', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes Java servlet patterns', () => {
const result = calculateEntryPointScore('doGet', 'java', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes Go handler patterns', () => {
const result = calculateEntryPointScore('NewServer', 'go', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes Rust entry patterns', () => {
const result = calculateEntryPointScore('handle_request', 'rust', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes Swift UIKit lifecycle', () => {
const result = calculateEntryPointScore('viewDidLoad', 'swift', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes Swift SwiftUI body', () => {
const result = calculateEntryPointScore('body', 'swift', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes PHP Laravel patterns', () => {
// __invoke starts with '_' which matches utility pattern first
const result = calculateEntryPointScore('handle', 'php', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes PHP RESTful resource methods', () => {
const result = calculateEntryPointScore('index', 'php', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes C# ASP.NET patterns', () => {
const result = calculateEntryPointScore('GetUsers', 'csharp', false, 0, 2);
expect(result.reasons).toContain('entry-pattern');
});
it('recognizes C main entry point', () => {
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', () => {
it.each([
'getUser', 'setName', 'isValid', 'hasPermission', 'canEdit',
'formatDate', 'parseJSON', 'validateInput',
'toString', 'fromJSON', 'encodeBase64', 'serializeData',
'cloneDeep', 'mergeObjects',
])('penalizes utility function "%s"', (name) => {
const result = calculateEntryPointScore(name, 'typescript', false, 0, 3);
expect(result.reasons).toContain('utility-pattern');
// 0.3 multiplier
const plain = calculateEntryPointScore('doStuff', 'typescript', false, 0, 3);
expect(result.score).toBeLessThan(plain.score);
});
it('penalizes private-by-convention functions', () => {
const result = calculateEntryPointScore('_internal', 'typescript', false, 0, 3);
expect(result.reasons).toContain('utility-pattern');
});
});
describe('framework detection from path', () => {
it('boosts Next.js page entry points', () => {
const result = calculateEntryPointScore('render', 'typescript', true, 0, 3, 'pages/users.tsx');
expect(result.reasons.some(r => r.includes('framework:'))).toBe(true);
expect(result.score).toBeGreaterThan(0);
});
it('does not apply framework bonus for non-framework paths', () => {
const result = calculateEntryPointScore('render', 'typescript', true, 0, 3, 'src/lib/utils.ts');
expect(result.reasons.every(r => !r.includes('framework:'))).toBe(true);
});
});
describe('combined scoring', () => {
it('multiplies all factors together', () => {
// handleLogin: entry pattern (1.5x) + exported (2.0x) + base
const result = calculateEntryPointScore('handleLogin', 'typescript', true, 0, 4, 'routes/auth.ts');
expect(result.score).toBeGreaterThan(0);
expect(result.reasons).toContain('exported');
expect(result.reasons).toContain('entry-pattern');
});
});
});
describe('isTestFile', () => {
it.each([
'src/utils.test.ts',
'src/utils.spec.ts',
'__tests__/utils.ts',
'__mocks__/api.ts',
'src/test/integration/db.ts',
'src/tests/unit/helper.ts',
'src/testing/setup.ts',
'lib/test_utils.py',
'pkg/handler_test.go',
'src/test/java/com/example/Test.java',
'MyViewTests.swift',
'MyViewTest.swift',
'UITests/LoginTest.swift',
'App.Tests/MyTest.cs',
'tests/Feature/UserTest.php',
'tests/Unit/AuthSpec.php',
])('returns true for test file "%s"', (filePath) => {
expect(isTestFile(filePath)).toBe(true);
});
it.each([
'src/utils.ts',
'src/controllers/auth.ts',
'src/main.py',
'cmd/server.go',
'src/main/java/App.java',
])('returns false for non-test file "%s"', (filePath) => {
expect(isTestFile(filePath)).toBe(false);
});
it('normalizes Windows backslashes', () => {
expect(isTestFile('src\\__tests__\\utils.ts')).toBe(true);
});
});
describe('isUtilityFile', () => {
it.each([
'src/utils/format.ts',
'src/util/helpers.ts',
'src/helpers/date.ts',
'src/helper/string.ts',
'src/common/types.ts',
'src/shared/constants.ts',
'src/lib/crypto.ts',
'src/utils.ts',
'src/utils.js',
'src/helpers.ts',
'lib/date_utils.py',
'lib/date_helpers.py',
])('returns true for utility file "%s"', (filePath) => {
expect(isUtilityFile(filePath)).toBe(true);
});
it.each([
'src/controllers/auth.ts',
'src/routes/api.ts',
'src/main.ts',
'src/app.ts',
])('returns false for non-utility file "%s"', (filePath) => {
expect(isUtilityFile(filePath)).toBe(false);
});
});