Resolve merge conflicts with main
- extraction/index.ts: use picomatch with static import (replacing dynamic require) and keep normalizePath for other call sites - utils.ts: keep normalizePath from main, take PR's PID-based FileLock
This commit is contained in:
+190
-23
@@ -9,8 +9,10 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { CodeGraph } from '../src';
|
||||
import { extractFromSource } from '../src/extraction';
|
||||
import { extractFromSource, scanDirectory, shouldIncludeFile } from '../src/extraction';
|
||||
import { detectLanguage, isLanguageSupported, getSupportedLanguages } from '../src/extraction/grammars';
|
||||
import { normalizePath } from '../src/utils';
|
||||
import { DEFAULT_CONFIG } from '../src/types';
|
||||
|
||||
// Create a temporary directory for each test
|
||||
function createTempDir(): string {
|
||||
@@ -127,14 +129,19 @@ export function processPayment(amount: number): Promise<Receipt> {
|
||||
`;
|
||||
const result = extractFromSource('payment.ts', code);
|
||||
|
||||
expect(result.nodes).toHaveLength(1);
|
||||
expect(result.nodes[0]).toMatchObject({
|
||||
// File node + function node
|
||||
const fileNode = result.nodes.find((n) => n.kind === 'file');
|
||||
expect(fileNode).toBeDefined();
|
||||
expect(fileNode?.name).toBe('payment.ts');
|
||||
|
||||
const funcNode = result.nodes.find((n) => n.kind === 'function');
|
||||
expect(funcNode).toMatchObject({
|
||||
kind: 'function',
|
||||
name: 'processPayment',
|
||||
language: 'typescript',
|
||||
isExported: true,
|
||||
});
|
||||
expect(result.nodes[0]?.signature).toContain('amount: number');
|
||||
expect(funcNode?.signature).toContain('amount: number');
|
||||
});
|
||||
|
||||
it('should extract class declarations', () => {
|
||||
@@ -175,8 +182,11 @@ export interface User {
|
||||
`;
|
||||
const result = extractFromSource('types.ts', code);
|
||||
|
||||
expect(result.nodes).toHaveLength(1);
|
||||
expect(result.nodes[0]).toMatchObject({
|
||||
const fileNode = result.nodes.find((n) => n.kind === 'file');
|
||||
expect(fileNode).toBeDefined();
|
||||
|
||||
const ifaceNode = result.nodes.find((n) => n.kind === 'interface');
|
||||
expect(ifaceNode).toMatchObject({
|
||||
kind: 'interface',
|
||||
name: 'User',
|
||||
isExported: true,
|
||||
@@ -207,8 +217,9 @@ export const useAuth = (): AuthContextValue => {
|
||||
`;
|
||||
const result = extractFromSource('hooks.ts', code);
|
||||
|
||||
expect(result.nodes).toHaveLength(1);
|
||||
expect(result.nodes[0]).toMatchObject({
|
||||
const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'useAuth');
|
||||
expect(funcNode).toBeDefined();
|
||||
expect(funcNode).toMatchObject({
|
||||
kind: 'function',
|
||||
name: 'useAuth',
|
||||
isExported: true,
|
||||
@@ -223,8 +234,9 @@ export const processData = function(input: string): string {
|
||||
`;
|
||||
const result = extractFromSource('utils.ts', code);
|
||||
|
||||
expect(result.nodes).toHaveLength(1);
|
||||
expect(result.nodes[0]).toMatchObject({
|
||||
const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'processData');
|
||||
expect(funcNode).toBeDefined();
|
||||
expect(funcNode).toMatchObject({
|
||||
kind: 'function',
|
||||
name: 'processData',
|
||||
isExported: true,
|
||||
@@ -286,8 +298,9 @@ export const fetchData = async () => {
|
||||
`;
|
||||
const result = extractFromSource('api.js', code);
|
||||
|
||||
expect(result.nodes).toHaveLength(1);
|
||||
expect(result.nodes[0]).toMatchObject({
|
||||
const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'fetchData');
|
||||
expect(funcNode).toBeDefined();
|
||||
expect(funcNode).toMatchObject({
|
||||
kind: 'function',
|
||||
name: 'fetchData',
|
||||
isExported: true,
|
||||
@@ -306,8 +319,8 @@ export type AuthContextValue = {
|
||||
`;
|
||||
const result = extractFromSource('types.ts', code);
|
||||
|
||||
expect(result.nodes).toHaveLength(1);
|
||||
expect(result.nodes[0]).toMatchObject({
|
||||
const typeNode = result.nodes.find((n) => n.kind === 'type_alias');
|
||||
expect(typeNode).toMatchObject({
|
||||
kind: 'type_alias',
|
||||
name: 'AuthContextValue',
|
||||
isExported: true,
|
||||
@@ -323,8 +336,8 @@ type InternalState = {
|
||||
`;
|
||||
const result = extractFromSource('internal.ts', code);
|
||||
|
||||
expect(result.nodes).toHaveLength(1);
|
||||
expect(result.nodes[0]).toMatchObject({
|
||||
const typeNode = result.nodes.find((n) => n.kind === 'type_alias');
|
||||
expect(typeNode).toMatchObject({
|
||||
kind: 'type_alias',
|
||||
name: 'InternalState',
|
||||
isExported: false,
|
||||
@@ -415,7 +428,7 @@ export const useAuth = () => {
|
||||
expect(varNodes).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not extract non-exported const as exported variable', () => {
|
||||
it('should extract non-exported const as non-exported variable', () => {
|
||||
const code = `
|
||||
const internalConfig = {
|
||||
debug: true,
|
||||
@@ -423,10 +436,10 @@ const internalConfig = {
|
||||
`;
|
||||
const result = extractFromSource('internal.ts', code);
|
||||
|
||||
// Non-exported const should NOT create a variable node
|
||||
// (only export_statement triggers extractExportedVariables)
|
||||
const varNodes = result.nodes.filter((n) => n.kind === 'variable' && n.name === 'internalConfig');
|
||||
expect(varNodes).toHaveLength(0);
|
||||
// Non-exported const at file level should be extracted as a constant (not exported)
|
||||
const varNodes = result.nodes.filter((n) => (n.kind === 'variable' || n.kind === 'constant') && n.name === 'internalConfig');
|
||||
expect(varNodes).toHaveLength(1);
|
||||
expect(varNodes[0]?.isExported).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should extract Zod schema exports', () => {
|
||||
@@ -463,6 +476,54 @@ export const authMachine = createMachine({
|
||||
});
|
||||
});
|
||||
|
||||
describe('File Node Extraction', () => {
|
||||
it('should create a file-kind node for each parsed file', () => {
|
||||
const code = `
|
||||
export function greet(name: string): string {
|
||||
return "Hello " + name;
|
||||
}
|
||||
`;
|
||||
const result = extractFromSource('greeter.ts', code);
|
||||
|
||||
const fileNode = result.nodes.find((n) => n.kind === 'file');
|
||||
expect(fileNode).toBeDefined();
|
||||
expect(fileNode?.name).toBe('greeter.ts');
|
||||
expect(fileNode?.filePath).toBe('greeter.ts');
|
||||
expect(fileNode?.language).toBe('typescript');
|
||||
expect(fileNode?.startLine).toBe(1);
|
||||
});
|
||||
|
||||
it('should create file nodes for Python files', () => {
|
||||
const code = `
|
||||
def main():
|
||||
pass
|
||||
`;
|
||||
const result = extractFromSource('main.py', code);
|
||||
|
||||
const fileNode = result.nodes.find((n) => n.kind === 'file');
|
||||
expect(fileNode).toBeDefined();
|
||||
expect(fileNode?.name).toBe('main.py');
|
||||
expect(fileNode?.language).toBe('python');
|
||||
});
|
||||
|
||||
it('should create containment edges from file node to top-level declarations', () => {
|
||||
const code = `
|
||||
export function foo() {}
|
||||
export function bar() {}
|
||||
`;
|
||||
const result = extractFromSource('fns.ts', code);
|
||||
|
||||
const fileNode = result.nodes.find((n) => n.kind === 'file');
|
||||
expect(fileNode).toBeDefined();
|
||||
|
||||
// There should be contains edges from the file node to each function
|
||||
const containsEdges = result.edges.filter(
|
||||
(e) => e.source === fileNode?.id && e.kind === 'contains'
|
||||
);
|
||||
expect(containsEdges.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Python Extraction', () => {
|
||||
it('should extract function definitions', () => {
|
||||
const code = `
|
||||
@@ -473,8 +534,11 @@ def calculate_total(items: list, tax_rate: float) -> float:
|
||||
`;
|
||||
const result = extractFromSource('calc.py', code);
|
||||
|
||||
expect(result.nodes).toHaveLength(1);
|
||||
expect(result.nodes[0]).toMatchObject({
|
||||
const fileNode = result.nodes.find((n) => n.kind === 'file');
|
||||
expect(fileNode).toBeDefined();
|
||||
|
||||
const funcNode = result.nodes.find((n) => n.kind === 'function');
|
||||
expect(funcNode).toMatchObject({
|
||||
kind: 'function',
|
||||
name: 'calculate_total',
|
||||
language: 'python',
|
||||
@@ -1880,3 +1944,106 @@ export function multiply(a: number, b: number): number {
|
||||
cg.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Path Normalization', () => {
|
||||
it('should convert backslashes to forward slashes', () => {
|
||||
expect(normalizePath('gui\\node_modules\\foo')).toBe('gui/node_modules/foo');
|
||||
expect(normalizePath('src\\components\\Button.tsx')).toBe('src/components/Button.tsx');
|
||||
});
|
||||
|
||||
it('should leave forward-slash paths unchanged', () => {
|
||||
expect(normalizePath('src/components/Button.tsx')).toBe('src/components/Button.tsx');
|
||||
});
|
||||
|
||||
it('should handle empty string', () => {
|
||||
expect(normalizePath('')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Directory Exclusion', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = createTempDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanupTempDir(tempDir);
|
||||
});
|
||||
|
||||
it('should exclude node_modules directories', () => {
|
||||
// Create structure: src/index.ts + node_modules/pkg/index.js
|
||||
const srcDir = path.join(tempDir, 'src');
|
||||
const nmDir = path.join(tempDir, 'node_modules', 'pkg');
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
fs.mkdirSync(nmDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
|
||||
fs.writeFileSync(path.join(nmDir, 'index.js'), 'module.exports = {};');
|
||||
|
||||
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
|
||||
const files = scanDirectory(tempDir, config);
|
||||
|
||||
expect(files).toContain('src/index.ts');
|
||||
expect(files.every((f) => !f.includes('node_modules'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should exclude nested node_modules directories', () => {
|
||||
// Create structure: packages/app/node_modules/pkg/index.js
|
||||
const srcDir = path.join(tempDir, 'packages', 'app', 'src');
|
||||
const nmDir = path.join(tempDir, 'packages', 'app', 'node_modules', 'pkg');
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
fs.mkdirSync(nmDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
|
||||
fs.writeFileSync(path.join(nmDir, 'index.js'), 'module.exports = {};');
|
||||
|
||||
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
|
||||
const files = scanDirectory(tempDir, config);
|
||||
|
||||
expect(files).toContain('packages/app/src/index.ts');
|
||||
expect(files.every((f) => !f.includes('node_modules'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should exclude .git directories', () => {
|
||||
const srcDir = path.join(tempDir, 'src');
|
||||
const gitDir = path.join(tempDir, '.git', 'objects');
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
fs.mkdirSync(gitDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
|
||||
fs.writeFileSync(path.join(gitDir, 'pack.ts'), 'export const y = 2;');
|
||||
|
||||
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
|
||||
const files = scanDirectory(tempDir, config);
|
||||
|
||||
expect(files).toContain('src/index.ts');
|
||||
expect(files.every((f) => !f.includes('.git'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return forward-slash paths on all platforms', () => {
|
||||
const srcDir = path.join(tempDir, 'src', 'components');
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(srcDir, 'Button.tsx'), 'export function Button() {}');
|
||||
|
||||
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
|
||||
const files = scanDirectory(tempDir, config);
|
||||
|
||||
expect(files.length).toBe(1);
|
||||
expect(files[0]).toBe('src/components/Button.tsx');
|
||||
expect(files[0]).not.toContain('\\');
|
||||
});
|
||||
|
||||
it('should respect .codegraphignore marker', () => {
|
||||
const srcDir = path.join(tempDir, 'src');
|
||||
const vendorDir = path.join(tempDir, 'vendor');
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
fs.mkdirSync(vendorDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
|
||||
fs.writeFileSync(path.join(vendorDir, 'lib.ts'), 'export const y = 2;');
|
||||
fs.writeFileSync(path.join(vendorDir, '.codegraphignore'), '');
|
||||
|
||||
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
|
||||
const files = scanDirectory(tempDir, config);
|
||||
|
||||
expect(files).toContain('src/index.ts');
|
||||
expect(files.every((f) => !f.includes('vendor'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Search Query Utilities Tests
|
||||
*
|
||||
* Tests multi-signal scoring, kind bonuses, and path relevance.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
extractSearchTerms,
|
||||
scorePathRelevance,
|
||||
kindBonus,
|
||||
STOP_WORDS,
|
||||
} from '../src/search/query-utils';
|
||||
|
||||
describe('Search Query Utilities', () => {
|
||||
describe('extractSearchTerms', () => {
|
||||
it('should extract meaningful terms from a query', () => {
|
||||
const terms = extractSearchTerms('find the login handler');
|
||||
expect(terms).toContain('login');
|
||||
expect(terms).toContain('handler');
|
||||
// 'find' and 'the' are stop words
|
||||
expect(terms).not.toContain('find');
|
||||
expect(terms).not.toContain('the');
|
||||
});
|
||||
|
||||
it('should filter stop words', () => {
|
||||
const terms = extractSearchTerms('how does the authentication work');
|
||||
expect(terms).not.toContain('how');
|
||||
expect(terms).not.toContain('does');
|
||||
expect(terms).not.toContain('the');
|
||||
expect(terms).toContain('authentication');
|
||||
expect(terms).toContain('work');
|
||||
});
|
||||
|
||||
it('should handle camelCase by lowercasing', () => {
|
||||
const terms = extractSearchTerms('UserService');
|
||||
expect(terms).toContain('userservice');
|
||||
});
|
||||
|
||||
it('should strip punctuation', () => {
|
||||
const terms = extractSearchTerms('payment.process()');
|
||||
expect(terms).toContain('payment');
|
||||
expect(terms).toContain('process');
|
||||
});
|
||||
|
||||
it('should return empty for all stop words', () => {
|
||||
const terms = extractSearchTerms('how do I get the');
|
||||
expect(terms).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should filter single-character terms', () => {
|
||||
const terms = extractSearchTerms('a b c auth');
|
||||
expect(terms).toEqual(['auth']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scorePathRelevance', () => {
|
||||
it('should score filename matches highest', () => {
|
||||
const score = scorePathRelevance('src/auth/login.ts', 'login');
|
||||
expect(score).toBeGreaterThanOrEqual(10);
|
||||
});
|
||||
|
||||
it('should score directory matches', () => {
|
||||
const score = scorePathRelevance('src/auth/index.ts', 'auth');
|
||||
expect(score).toBeGreaterThanOrEqual(5);
|
||||
});
|
||||
|
||||
it('should return 0 for unrelated paths', () => {
|
||||
const score = scorePathRelevance('src/utils/format.ts', 'payment');
|
||||
expect(score).toBe(0);
|
||||
});
|
||||
|
||||
it('should accumulate scores for multiple matching terms', () => {
|
||||
const score = scorePathRelevance('src/auth/login.ts', 'auth login');
|
||||
// Both 'auth' (dir match) and 'login' (filename match)
|
||||
expect(score).toBeGreaterThanOrEqual(15);
|
||||
});
|
||||
|
||||
it('should return 0 for empty query terms', () => {
|
||||
const score = scorePathRelevance('src/auth/login.ts', 'the a an');
|
||||
expect(score).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('kindBonus', () => {
|
||||
it('should give functions and methods highest bonus', () => {
|
||||
expect(kindBonus('function')).toBe(10);
|
||||
expect(kindBonus('method')).toBe(10);
|
||||
});
|
||||
|
||||
it('should rank functions > classes > variables > imports', () => {
|
||||
expect(kindBonus('function')).toBeGreaterThan(kindBonus('class'));
|
||||
expect(kindBonus('class')).toBeGreaterThan(kindBonus('variable'));
|
||||
expect(kindBonus('variable')).toBeGreaterThan(kindBonus('import'));
|
||||
});
|
||||
|
||||
it('should give routes high priority', () => {
|
||||
expect(kindBonus('route')).toBeGreaterThanOrEqual(9);
|
||||
});
|
||||
|
||||
it('should give components high priority', () => {
|
||||
expect(kindBonus('component')).toBeGreaterThanOrEqual(8);
|
||||
});
|
||||
|
||||
it('should return 0 for parameter and file kinds', () => {
|
||||
expect(kindBonus('parameter')).toBe(0);
|
||||
expect(kindBonus('file')).toBe(0);
|
||||
});
|
||||
|
||||
it('should return 0 for unknown kinds', () => {
|
||||
expect(kindBonus('unknown_kind' as any)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('STOP_WORDS', () => {
|
||||
it('should contain common English stop words', () => {
|
||||
expect(STOP_WORDS.has('the')).toBe(true);
|
||||
expect(STOP_WORDS.has('and')).toBe(true);
|
||||
expect(STOP_WORDS.has('or')).toBe(true);
|
||||
});
|
||||
|
||||
it('should contain action verbs used in queries', () => {
|
||||
expect(STOP_WORDS.has('find')).toBe(true);
|
||||
expect(STOP_WORDS.has('show')).toBe(true);
|
||||
expect(STOP_WORDS.has('get')).toBe(true);
|
||||
expect(STOP_WORDS.has('list')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not contain technical terms', () => {
|
||||
expect(STOP_WORDS.has('function')).toBe(false);
|
||||
expect(STOP_WORDS.has('class')).toBe(false);
|
||||
expect(STOP_WORDS.has('auth')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user