GitNexus/gitnexus/test/integration/tree-sitter-languages.test.ts

288 lines
11 KiB
TypeScript

import { describe, it, expect, beforeAll } from 'vitest';
import fs from 'fs';
import path from 'path';
import { loadParser, loadLanguage } 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 { getLanguageFromFilename } from '../../src/core/ingestion/utils.js';
import Parser from 'tree-sitter';
const fixturesDir = path.resolve(__dirname, '..', 'fixtures', 'sample-code');
function readFixture(filename: string): string {
return fs.readFileSync(path.join(fixturesDir, filename), 'utf-8');
}
function parseAndQuery(parser: Parser, content: string, queryStr: string) {
const tree = parser.parse(content);
const lang = parser.getLanguage();
const query = new Parser.Query(lang, queryStr);
const matches = query.matches(tree.rootNode);
return { tree, matches };
}
function extractDefinitions(matches: any[]) {
const defs: { type: string; name: string }[] = [];
for (const match of matches) {
for (const capture of match.captures) {
if (capture.name === 'name' && match.captures.some((c: any) =>
c.name.startsWith('definition.'))) {
const defType = match.captures.find((c: any) => c.name.startsWith('definition.'))!.name;
defs.push({ type: defType, name: capture.node.text });
}
}
}
return defs;
}
describe('Tree-sitter multi-language parsing', () => {
let parser: Parser;
beforeAll(async () => {
parser = await loadParser();
});
describe('TypeScript', () => {
it('parses functions, classes, interfaces, methods, and arrow functions', async () => {
await loadLanguage(SupportedLanguages.TypeScript, 'simple.ts');
const content = readFixture('simple.ts');
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.TypeScript]);
const defs = extractDefinitions(matches);
const defTypes = defs.map(d => d.type);
expect(defTypes).toContain('definition.class');
expect(defTypes).toContain('definition.function');
});
});
describe('TSX', () => {
it('parses JSX components with tsx grammar', async () => {
await loadLanguage(SupportedLanguages.TypeScript, 'simple.tsx');
const content = readFixture('simple.tsx');
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.TypeScript]);
const defs = extractDefinitions(matches);
expect(defs.length).toBeGreaterThan(0);
// Should detect Counter class and Button/useCounter functions
const names = defs.map(d => d.name);
expect(names).toContain('Counter');
});
});
describe('JavaScript', () => {
it('parses class and function declarations', async () => {
await loadLanguage(SupportedLanguages.JavaScript);
const content = readFixture('simple.js');
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.JavaScript]);
const defs = extractDefinitions(matches);
expect(defs.length).toBeGreaterThan(0);
const names = defs.map(d => d.name);
expect(names).toContain('EventEmitter');
expect(names).toContain('createLogger');
});
});
describe('Python', () => {
it('parses class and function definitions', async () => {
await loadLanguage(SupportedLanguages.Python);
const content = readFixture('simple.py');
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.Python]);
const defs = extractDefinitions(matches);
const defTypes = defs.map(d => d.type);
expect(defTypes).toContain('definition.class');
expect(defTypes).toContain('definition.function');
});
});
describe('Java', () => {
it('parses class, method, and constructor declarations', async () => {
await loadLanguage(SupportedLanguages.Java);
const content = readFixture('simple.java');
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.Java]);
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');
});
});
describe('Go', () => {
it('parses function and type declarations', async () => {
await loadLanguage(SupportedLanguages.Go);
const content = readFixture('simple.go');
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.Go]);
const defs = extractDefinitions(matches);
expect(defs.length).toBeGreaterThan(0);
const defTypes = defs.map(d => d.type);
expect(defTypes).toContain('definition.function');
});
});
describe('C', () => {
it('parses function definitions and structs', async () => {
await loadLanguage(SupportedLanguages.C);
const content = readFixture('simple.c');
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.C]);
const defs = extractDefinitions(matches);
expect(defs.length).toBeGreaterThan(0);
const defTypes = defs.map(d => d.type);
expect(defTypes).toContain('definition.function');
});
});
describe('C++', () => {
it('parses class, function, and namespace declarations', async () => {
await loadLanguage(SupportedLanguages.CPlusPlus);
const content = readFixture('simple.cpp');
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.CPlusPlus]);
const defs = extractDefinitions(matches);
expect(defs.length).toBeGreaterThan(0);
const defTypes = defs.map(d => d.type);
expect(defTypes).toContain('definition.class');
});
});
describe('C#', () => {
it('parses class, method, and property declarations', async () => {
await loadLanguage(SupportedLanguages.CSharp);
const content = readFixture('simple.cs');
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');
});
});
describe('Rust', () => {
it('parses fn, struct, impl, trait, and enum', async () => {
await loadLanguage(SupportedLanguages.Rust);
const content = readFixture('simple.rs');
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.Rust]);
const defs = extractDefinitions(matches);
expect(defs.length).toBeGreaterThan(0);
const defTypes = defs.map(d => d.type);
expect(defTypes).toContain('definition.function');
});
});
describe('PHP', () => {
it('parses class, function, and method declarations', async () => {
await loadLanguage(SupportedLanguages.PHP);
const content = readFixture('simple.php');
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.PHP]);
const defs = extractDefinitions(matches);
expect(defs.length).toBeGreaterThan(0);
const defTypes = defs.map(d => d.type);
expect(defTypes).toContain('definition.class');
});
});
describe('Swift', () => {
it('parses class, struct, protocol, and function if tree-sitter-swift is available', async () => {
try {
await loadLanguage(SupportedLanguages.Swift);
} catch {
// tree-sitter-swift not installed — skip
return;
}
const content = readFixture('simple.swift');
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.Swift]);
const defs = extractDefinitions(matches);
expect(defs.length).toBeGreaterThan(0);
});
it('gracefully handles missing tree-sitter-swift', async () => {
// If Swift is NOT available, loadLanguage should throw
// If it IS available, this test just passes
try {
await loadLanguage(SupportedLanguages.Swift);
} catch (e: any) {
expect(e.message).toContain('Unsupported language');
}
});
});
describe('unhappy path', () => {
it('returns null/undefined for unsupported file extensions', () => {
expect(getLanguageFromFilename('archive.xyz')).toBeNull();
expect(getLanguageFromFilename('data.unknown')).toBeNull();
});
it('handles empty string file path', () => {
expect(getLanguageFromFilename('')).toBeNull();
});
it('returns null/undefined for binary file extensions', () => {
expect(getLanguageFromFilename('program.exe')).toBeNull();
expect(getLanguageFromFilename('library.dll')).toBeNull();
expect(getLanguageFromFilename('object.so')).toBeNull();
});
});
describe('cross-language assertions', () => {
it('all supported languages produce at least one definition from fixtures', async () => {
const langFixtures: [SupportedLanguages, string, string?][] = [
[SupportedLanguages.TypeScript, 'simple.ts'],
[SupportedLanguages.JavaScript, 'simple.js'],
[SupportedLanguages.Python, 'simple.py'],
[SupportedLanguages.Java, 'simple.java'],
[SupportedLanguages.Go, 'simple.go'],
[SupportedLanguages.C, 'simple.c'],
[SupportedLanguages.CPlusPlus, 'simple.cpp'],
[SupportedLanguages.CSharp, 'simple.cs'],
[SupportedLanguages.Rust, 'simple.rs'],
[SupportedLanguages.PHP, 'simple.php'],
];
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;
}
}
});
});
describe('parser edge cases', () => {
it('loadLanguage throws for unsupported language', async () => {
await expect(loadLanguage('brainfuck' as any)).rejects.toThrow(/unsupported language/i);
});
it('parsing empty file content produces empty matches', async () => {
await loadLanguage(SupportedLanguages.TypeScript, 'empty.ts');
const tree = parser.parse('');
expect(tree.rootNode).toBeDefined();
const lang = parser.getLanguage();
const query = new Parser.Query(lang, LANGUAGE_QUERIES[SupportedLanguages.TypeScript]);
const matches = query.matches(tree.rootNode);
expect(matches).toEqual([]);
});
it('parsing malformed code does not crash', async () => {
await loadLanguage(SupportedLanguages.TypeScript, 'malformed.ts');
const tree = parser.parse('function {{{ class >>><< if(( end');
expect(tree.rootNode).toBeDefined();
expect(tree.rootNode.hasError).toBe(true);
});
});
});