mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-10 22:43:40 +00:00
* fix(zig): vendor tree-sitter-zig so npm i -g no longer warns on peers Published overrides do not apply to dependents, so the Zig optionalDependency kept warning that tree-sitter@0.21.1 does not satisfy peerOptional ^0.22.1. Load it from vendor/ like Dart/Kotlin/Swift instead. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ci): add zig parse snippet to prebuild validate The six zig prebuild jobs failed at "Validate the .node loads and parses" because snippets[GRAMMAR] was undefined and tree-sitter threw "Input must be a function". Co-authored-by: Cursor <cursoragent@cursor.com> * chore: drop Unreleased changelog note from the Zig vendor PR CHANGELOG.md is owned by the release process, not individual PRs. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(vendor): rebuild native prebuilds (tree-sitter-zig) Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33949409205 * chore(vendor): rebuild native prebuilds (tree-sitter-zig) Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33949616521 * chore(vendor): rebuild native prebuilds (tree-sitter-zig) Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33949829377 * chore(vendor): rebuild native prebuilds (tree-sitter-zig) Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33950025077 * chore(vendor): rebuild native prebuilds (tree-sitter-zig) Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33950220655 * chore(vendor): rebuild native prebuilds (tree-sitter-zig) Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33950400275 * chore(vendor): rebuild native prebuilds (tree-sitter-zig) Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33950607933 * chore(vendor): rebuild native prebuilds (tree-sitter-zig) Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33950882912 * chore(vendor): rebuild native prebuilds (tree-sitter-zig) Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33951170483 * chore(vendor): rebuild native prebuilds (tree-sitter-zig) Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33951386477 * chore(vendor): rebuild native prebuilds (tree-sitter-zig) Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33951624305 * chore(vendor): rebuild native prebuilds (tree-sitter-zig) Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33951813452 * chore(vendor): rebuild native prebuilds (tree-sitter-zig) Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33951998309 * chore(vendor): rebuild native prebuilds (tree-sitter-zig) Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33952225172 * chore(vendor): rebuild native prebuilds (tree-sitter-zig) Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33952524393 * fix(ci): stop native prebuild rebuild loops PR path filters and source checks see the cumulative diff, so generated binaries kept rebuilding the original source change. Skip output-only synchronize events using their exact before/head range, failing closed when Git cannot compare it. Exercise the workflow against real commit histories, including multi-commit source pushes and merge-ref drift. * chore(vendor): rebuild native prebuilds (tree-sitter-zig) Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33953226606 * fix: address Zig PR review feedback (#3180) Check for the vendored package without loading its native binding so a broken installed Zig grammar fails the parsing test instead of skipping. Match the optional child descriptor in the Zig metadata declaration and include Zig in the two optional/vendored grammar comments. Validation: 159 targeted tests, TypeScript, metadata type fixture, and formatting passed. Injected native-load failure now fails instead of skipping; explicit Zig opt-out still skips. * chore(vendor): rebuild native prebuilds (tree-sitter-zig) Built by https://github.com/abhigyanpatwari/GitNexus/actions/runs/33956342308 --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: gitnexus-release-bot[bot] <gitnexus-release-bot[bot]@users.noreply.github.com>
1036 lines
44 KiB
TypeScript
1036 lines
44 KiB
TypeScript
import { describe, it, expect, beforeAll, vi } from 'vitest';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import {
|
|
loadParser,
|
|
loadLanguage,
|
|
isLanguageAvailable,
|
|
isGrammarRuntimeSkipped,
|
|
} from '../../src/core/tree-sitter/parser-loader.js';
|
|
import { SupportedLanguages, getLanguageFromFilename } from 'gitnexus-shared';
|
|
import { getProvider } from '../../src/core/ingestion/languages/index.js';
|
|
import Parser from 'tree-sitter';
|
|
import { vendoredGrammarDir } from '../../src/core/tree-sitter/vendored-grammars.js';
|
|
|
|
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;
|
|
}
|
|
|
|
function extractCapturedCallNames(matches: any[]) {
|
|
const names: string[] = [];
|
|
for (const match of matches) {
|
|
if (!match.captures.some((c: any) => c.name === 'call')) continue;
|
|
const nameCapture = match.captures.find((c: any) => c.name === 'call.name');
|
|
if (nameCapture) names.push(nameCapture.node.text);
|
|
}
|
|
return names;
|
|
}
|
|
|
|
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 provider = getProvider(SupportedLanguages.TypeScript);
|
|
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
|
|
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 provider = getProvider(SupportedLanguages.TypeScript);
|
|
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
|
|
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 provider = getProvider(SupportedLanguages.JavaScript);
|
|
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
|
|
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 provider = getProvider(SupportedLanguages.Python);
|
|
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
|
|
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 provider = getProvider(SupportedLanguages.Java);
|
|
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
|
|
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 provider = getProvider(SupportedLanguages.Go);
|
|
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
expect(defs.length).toBeGreaterThan(0);
|
|
const defTypes = defs.map((d) => d.type);
|
|
expect(defTypes).toContain('definition.function');
|
|
});
|
|
|
|
it('captures every name in multi-name const, var, and field declarations', async () => {
|
|
await loadLanguage(SupportedLanguages.Go);
|
|
const provider = getProvider(SupportedLanguages.Go);
|
|
const code = `
|
|
package main
|
|
const X, Y,Z,A,B = 1, 2,3,4,5
|
|
var a, b int
|
|
var (
|
|
c, d string
|
|
)
|
|
const (
|
|
C, D = 3, 4
|
|
)
|
|
type Point struct { X, y int }
|
|
`;
|
|
const { matches } = parseAndQuery(parser, code, provider.treeSitterQueries);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
const constNames = defs
|
|
.filter((def) => def.type === 'definition.const')
|
|
.map((def) => def.name);
|
|
expect(constNames.sort()).toEqual(['A', 'B', 'C', 'D', 'X', 'Y', 'Z']);
|
|
|
|
const variableNames = defs
|
|
.filter((def) => def.type === 'definition.variable')
|
|
.map((def) => def.name);
|
|
expect(variableNames.sort()).toEqual(['a', 'b', 'c', 'd']);
|
|
|
|
const propertyNames = defs
|
|
.filter((def) => def.type === 'definition.property')
|
|
.map((def) => def.name);
|
|
expect(propertyNames.sort()).toEqual(['X', 'y']);
|
|
});
|
|
});
|
|
|
|
describe('C', () => {
|
|
it('parses function definitions and structs', async () => {
|
|
await loadLanguage(SupportedLanguages.C);
|
|
const content = readFixture('simple.c');
|
|
const provider = getProvider(SupportedLanguages.C);
|
|
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
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 provider = getProvider(SupportedLanguages.C);
|
|
const { matches } = parseAndQuery(parser, code, provider.treeSitterQueries);
|
|
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 provider = getProvider(SupportedLanguages.C);
|
|
const { matches } = parseAndQuery(parser, code, provider.treeSitterQueries);
|
|
const defs = extractDefinitions(matches);
|
|
const names = defs.map((d) => d.name);
|
|
expect(names).toContain('MAX_SIZE');
|
|
expect(names).toContain('uint');
|
|
expect(names).toContain('Point');
|
|
});
|
|
|
|
it('captures C typedef anonymous structs, enums, and enumerators', async () => {
|
|
await loadLanguage(SupportedLanguages.C);
|
|
const code = `
|
|
typedef struct { int x; int y; } Point;
|
|
typedef enum { RED, GREEN, BLUE } Color;
|
|
`;
|
|
const provider = getProvider(SupportedLanguages.C);
|
|
const { matches } = parseAndQuery(parser, code, provider.treeSitterQueries);
|
|
const defs = extractDefinitions(matches);
|
|
const names = defs.map((d) => d.name);
|
|
expect(defs.some((d) => d.type === 'definition.struct' && d.name === 'Point')).toBe(true);
|
|
expect(defs.some((d) => d.type === 'definition.enum' && d.name === 'Color')).toBe(true);
|
|
expect(names).toEqual(expect.arrayContaining(['RED', 'GREEN', 'BLUE']));
|
|
});
|
|
});
|
|
|
|
describe('C++', () => {
|
|
it('parses class, function, and namespace declarations', async () => {
|
|
await loadLanguage(SupportedLanguages.CPlusPlus);
|
|
const content = readFixture('simple.cpp');
|
|
const provider = getProvider(SupportedLanguages.CPlusPlus);
|
|
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
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 provider = getProvider(SupportedLanguages.CPlusPlus);
|
|
const { matches } = parseAndQuery(parser, code, provider.treeSitterQueries);
|
|
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 provider = getProvider(SupportedLanguages.CPlusPlus);
|
|
const { matches } = parseAndQuery(parser, code, provider.treeSitterQueries);
|
|
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 provider = getProvider(SupportedLanguages.CPlusPlus);
|
|
const { matches } = parseAndQuery(parser, code, provider.treeSitterQueries);
|
|
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 provider = getProvider(SupportedLanguages.CPlusPlus);
|
|
const { matches } = parseAndQuery(parser, code, provider.treeSitterQueries);
|
|
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 provider = getProvider(SupportedLanguages.CPlusPlus);
|
|
const { matches } = parseAndQuery(parser, code, provider.treeSitterQueries);
|
|
const defs = extractDefinitions(matches);
|
|
const names = defs.map((d) => d.name);
|
|
expect(names).toContain('utils');
|
|
expect(names).toContain('helper');
|
|
});
|
|
|
|
it('treats CUDA .cu and .cuh files as C++ for definition extraction', async () => {
|
|
expect(getLanguageFromFilename('src/kernels/force.cu')).toBe(SupportedLanguages.CPlusPlus);
|
|
expect(getLanguageFromFilename('src/force/nep.cuh')).toBe(SupportedLanguages.CPlusPlus);
|
|
|
|
await loadLanguage(SupportedLanguages.CPlusPlus, 'src/kernels/force.cu');
|
|
const code = `class Force { public: void apply(); };\nvoid launchKernel() {}`;
|
|
const provider = getProvider(SupportedLanguages.CPlusPlus);
|
|
const { matches } = parseAndQuery(parser, code, provider.treeSitterQueries);
|
|
const defs = extractDefinitions(matches);
|
|
const names = defs.map((d) => d.name);
|
|
|
|
expect(defs.some((d) => d.type === 'definition.class' && d.name === 'Force')).toBe(true);
|
|
expect(names).toContain('launchKernel');
|
|
});
|
|
|
|
it('characterizes CUDA syntax when routed through the C++ parser', async () => {
|
|
await loadLanguage(SupportedLanguages.CPlusPlus, 'src/kernels/force.cu');
|
|
const code = `
|
|
__global__ void axpy(float *x) { x[0] = 1.0f; }
|
|
void host() {
|
|
axpy<<<1, 32>>>(nullptr);
|
|
cudaDeviceSynchronize();
|
|
}
|
|
`;
|
|
const provider = getProvider(SupportedLanguages.CPlusPlus);
|
|
const { tree, matches } = parseAndQuery(parser, code, provider.treeSitterQueries);
|
|
const defs = extractDefinitions(matches);
|
|
const callNames = extractCapturedCallNames(matches);
|
|
const ordinaryCalls = extractCapturedCallNames(
|
|
parseAndQuery(
|
|
parser,
|
|
'void host() { cudaDeviceSynchronize(); }',
|
|
provider.treeSitterQueries,
|
|
).matches,
|
|
);
|
|
|
|
expect(tree.rootNode.hasError).toBe(true);
|
|
expect(defs.some((d) => d.name === 'axpy')).toBe(true);
|
|
expect(ordinaryCalls).toContain('cudaDeviceSynchronize');
|
|
expect(callNames).not.toContain('axpy');
|
|
});
|
|
|
|
it('captures C++ typedef anonymous structs, enums, and enumerators', async () => {
|
|
await loadLanguage(SupportedLanguages.CPlusPlus);
|
|
const code = `
|
|
typedef struct { int x; int y; } Point;
|
|
typedef enum { Red, Green, Blue } Color;
|
|
`;
|
|
const provider = getProvider(SupportedLanguages.CPlusPlus);
|
|
const { matches } = parseAndQuery(parser, code, provider.treeSitterQueries);
|
|
const defs = extractDefinitions(matches);
|
|
const names = defs.map((d) => d.name);
|
|
expect(defs.some((d) => d.type === 'definition.struct' && d.name === 'Point')).toBe(true);
|
|
expect(defs.some((d) => d.type === 'definition.enum' && d.name === 'Color')).toBe(true);
|
|
expect(names).toEqual(expect.arrayContaining(['Red', 'Green', 'Blue']));
|
|
});
|
|
});
|
|
|
|
describe('C#', () => {
|
|
it('parses class, method, and namespace declarations', async () => {
|
|
await loadLanguage(SupportedLanguages.CSharp);
|
|
const content = readFixture('simple.cs');
|
|
const provider = getProvider(SupportedLanguages.CSharp);
|
|
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
|
|
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 provider = getProvider(SupportedLanguages.CSharp);
|
|
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
|
|
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 provider = getProvider(SupportedLanguages.CSharp);
|
|
const { matches } = parseAndQuery(parser, code, provider.treeSitterQueries);
|
|
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 provider = getProvider(SupportedLanguages.CSharp);
|
|
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
|
|
const defs = extractDefinitions(matches);
|
|
const defTypes = defs.map((d) => d.type);
|
|
expect(defTypes).toContain('definition.constructor');
|
|
expect(defTypes).toContain('definition.property');
|
|
});
|
|
});
|
|
|
|
describe('Rust', () => {
|
|
it('parses fn, struct, impl, trait, and enum', async () => {
|
|
await loadLanguage(SupportedLanguages.Rust);
|
|
const content = readFixture('simple.rs');
|
|
const provider = getProvider(SupportedLanguages.Rust);
|
|
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
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 provider = getProvider(SupportedLanguages.Rust);
|
|
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
|
|
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 provider = getProvider(SupportedLanguages.Rust);
|
|
const { matches } = parseAndQuery(parser, code, provider.treeSitterQueries);
|
|
const defs = extractDefinitions(matches);
|
|
const names = defs.map((d) => d.name);
|
|
expect(names).toContain('Vec');
|
|
});
|
|
|
|
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 provider = getProvider(SupportedLanguages.Rust);
|
|
const { matches } = parseAndQuery(parser, code, provider.treeSitterQueries);
|
|
const defs = extractDefinitions(matches);
|
|
const names = defs.map((d) => d.name);
|
|
expect(names).toContain('utils');
|
|
expect(names).toContain('MAX');
|
|
expect(names).toContain('INSTANCE');
|
|
});
|
|
});
|
|
|
|
describe('PHP', () => {
|
|
it('parses class, function, and method declarations', async () => {
|
|
await loadLanguage(SupportedLanguages.PHP);
|
|
const content = readFixture('simple.php');
|
|
const provider = getProvider(SupportedLanguages.PHP);
|
|
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
|
|
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 provider = getProvider(SupportedLanguages.Swift);
|
|
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
expect(defs.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
// Grammar-load failures are expected on the platform-sensitive matrix, so
|
|
// skip rather than hard-fail — same guard the sibling tests apply inline.
|
|
describe.skipIf(!isLanguageAvailable(SupportedLanguages.Swift))(
|
|
'conditional-compilation directives',
|
|
() => {
|
|
const provider = getProvider(SupportedLanguages.Swift);
|
|
const preprocess = (content: string): string =>
|
|
provider.preprocessSource?.(content, 'Fixture.swift') ?? content;
|
|
|
|
beforeAll(async () => {});
|
|
|
|
it('captures a class whose body contains indented conditional directives after preprocessing', () => {
|
|
const content = [
|
|
'class Outer {',
|
|
' enum A { case x }',
|
|
' #if os(iOS)',
|
|
' enum B { case y }',
|
|
' #endif',
|
|
'}',
|
|
].join('\n');
|
|
const { tree, matches } = parseAndQuery(
|
|
parser,
|
|
preprocess(content),
|
|
provider.treeSitterQueries,
|
|
);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
expect(tree.rootNode.hasError).toBe(false);
|
|
expect(defs).toEqual([
|
|
{ type: 'definition.class', name: 'Outer' },
|
|
{ type: 'definition.enum', name: 'A' },
|
|
{ type: 'definition.property', name: 'x' },
|
|
{ type: 'definition.enum', name: 'B' },
|
|
{ type: 'definition.property', name: 'y' },
|
|
]);
|
|
});
|
|
|
|
it('captures a class whose body contains column-zero conditional directives', () => {
|
|
const content = [
|
|
'class Outer {',
|
|
' enum A { case x }',
|
|
'#if os(iOS)',
|
|
' enum B { case y }',
|
|
'#endif',
|
|
'}',
|
|
].join('\n');
|
|
const { tree, matches } = parseAndQuery(
|
|
parser,
|
|
preprocess(content),
|
|
provider.treeSitterQueries,
|
|
);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
expect(tree.rootNode.hasError).toBe(false);
|
|
expect(defs).toEqual([
|
|
{ type: 'definition.class', name: 'Outer' },
|
|
{ type: 'definition.enum', name: 'A' },
|
|
{ type: 'definition.property', name: 'x' },
|
|
{ type: 'definition.enum', name: 'B' },
|
|
{ type: 'definition.property', name: 'y' },
|
|
]);
|
|
});
|
|
|
|
it('leaves top-level conditional directives intact while capturing their declarations', () => {
|
|
const content = [
|
|
'#if os(iOS)',
|
|
'struct PlatformValue {',
|
|
' let value: Int = 1',
|
|
'}',
|
|
'#else',
|
|
'struct PlatformValue {',
|
|
' let value: Int = 2',
|
|
'}',
|
|
'#endif',
|
|
].join('\n');
|
|
const parseContent = preprocess(content);
|
|
const { tree, matches } = parseAndQuery(parser, parseContent, provider.treeSitterQueries);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
expect(tree.rootNode.hasError).toBe(false);
|
|
expect(parseContent).toBe(content);
|
|
expect(defs).toEqual([
|
|
{ type: 'definition.struct', name: 'PlatformValue' },
|
|
{ type: 'definition.property', name: 'value' },
|
|
{ type: 'definition.struct', name: 'PlatformValue' },
|
|
{ type: 'definition.property', name: 'value' },
|
|
]);
|
|
});
|
|
|
|
it('keeps source that comments out a conditional block parseable', () => {
|
|
const content = [
|
|
'class Foo {',
|
|
' /* temporarily disabled:',
|
|
' #if DEBUG',
|
|
' func f() {}',
|
|
' #endif */',
|
|
' func g() {}',
|
|
'}',
|
|
].join('\n');
|
|
const parseContent = preprocess(content);
|
|
const { tree, matches } = parseAndQuery(parser, parseContent, provider.treeSitterQueries);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
// Erasing the comment terminator would swallow `g()` and the rest.
|
|
expect(parseContent).toBe(content);
|
|
expect(tree.rootNode.hasError).toBe(false);
|
|
expect(defs).toEqual([
|
|
{ type: 'definition.class', name: 'Foo' },
|
|
{ type: 'definition.function', name: 'g' },
|
|
]);
|
|
});
|
|
|
|
it('does not re-parent later declarations when branches split a declaration header', () => {
|
|
const content = [
|
|
'class NetworkClient {',
|
|
' #if swift(>=5.5)',
|
|
' func fetch() async {',
|
|
' #else',
|
|
' func fetch() {',
|
|
' #endif',
|
|
' perform()',
|
|
' }',
|
|
'}',
|
|
'struct SessionStore {}',
|
|
'enum Unrelated { case a }',
|
|
].join('\n');
|
|
const parseContent = preprocess(content);
|
|
const { tree } = parseAndQuery(parser, parseContent, provider.treeSitterQueries);
|
|
const topLevelTypes = tree.rootNode.namedChildren.map((child) => child.type);
|
|
|
|
// Blanking both markers would leave `NetworkClient` unterminated and
|
|
// collapse every later top-level declaration into it (5 nodes -> 1).
|
|
// `struct`/`enum` both surface as `class_declaration` in this grammar.
|
|
expect(parseContent).toBe(content);
|
|
expect(topLevelTypes).toEqual([
|
|
'class_declaration',
|
|
'directive',
|
|
'function_declaration',
|
|
'class_declaration',
|
|
'class_declaration',
|
|
]);
|
|
});
|
|
|
|
it('keeps a declaration from every branch once the directives are blanked', () => {
|
|
const content = [
|
|
'class Themed {',
|
|
' #if os(iOS)',
|
|
' func accent(alpha: Int) {}',
|
|
' #else',
|
|
' func accent() {}',
|
|
' #endif',
|
|
'}',
|
|
].join('\n');
|
|
const { tree, matches } = parseAndQuery(
|
|
parser,
|
|
preprocess(content),
|
|
provider.treeSitterQueries,
|
|
);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
// Branch selection is not modelled: mutually exclusive declarations
|
|
// both reach the graph. Pinned so changing it shows up as a diff.
|
|
expect(tree.rootNode.hasError).toBe(false);
|
|
expect(defs).toEqual([
|
|
{ type: 'definition.class', name: 'Themed' },
|
|
{ type: 'definition.function', name: 'accent' },
|
|
{ type: 'definition.function', name: 'accent' },
|
|
]);
|
|
});
|
|
},
|
|
);
|
|
|
|
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('Zig', () => {
|
|
// Gate on whether the optional PACKAGE is installed, not on the loader's
|
|
// `isLanguageAvailable` (which is false for absent AND for
|
|
// installed-but-broken bindings — the loader swallows every load error
|
|
// for optional grammars). With the package present, a load failure (ABI
|
|
// mismatch, bad export) must fail this test, not silently skip it. A
|
|
// deliberate `GITNEXUS_SKIP_OPTIONAL_GRAMMARS` opt-out in the environment
|
|
// is the one non-failure reason an installed grammar reports unavailable.
|
|
const zigPackageInstalled =
|
|
!isGrammarRuntimeSkipped(SupportedLanguages.Zig) &&
|
|
fs.existsSync(path.join(vendoredGrammarDir('tree-sitter-zig'), 'package.json'));
|
|
it.skipIf(!zigPackageInstalled)('parses functions, structs, enums, and imports', async () => {
|
|
expect(isLanguageAvailable(SupportedLanguages.Zig)).toBe(true);
|
|
await loadLanguage(SupportedLanguages.Zig);
|
|
|
|
const content = readFixture('simple.zig');
|
|
const provider = getProvider(SupportedLanguages.Zig);
|
|
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
const defTypes = defs.map((d) => d.type);
|
|
expect(defTypes).toContain('definition.function');
|
|
expect(defTypes).toContain('definition.struct');
|
|
expect(defTypes).toContain('definition.enum');
|
|
|
|
// `opaque {}` is a Struct-labelled container; a named `test "…"` block
|
|
// is a Function whose @name is the string node WITH quotes (so it never
|
|
// collides with a same-named fn); an anonymous `test {}` is not a def.
|
|
const named = defs.map((d) => `${d.type}:${d.name}`);
|
|
expect(named).toContain('definition.struct:Handle');
|
|
expect(named).toContain('definition.function:"add works"');
|
|
expect(named.filter((n) => n.startsWith('definition.function:')).length).toBe(
|
|
['add', 'private_helper', 'init', 'distance', 'main', 'c_add', 'close', '"add works"']
|
|
.length,
|
|
);
|
|
|
|
// `const std = @import("std");` must yield an import.source capture —
|
|
// without this assertion a query change that drops Zig import matching
|
|
// would pass this test unchanged.
|
|
const imports: string[] = [];
|
|
for (const match of matches) {
|
|
for (const capture of match.captures) {
|
|
if (capture.name === 'import.source') imports.push(capture.node.text);
|
|
}
|
|
}
|
|
expect(imports).toEqual(['"std"']);
|
|
});
|
|
|
|
it('reports Zig unavailable and throws "Unsupported language" when the grammar is absent', async () => {
|
|
// Exercise the loader's real absent-binding branch (the `source.load()`
|
|
// catch in `loadGrammar`), not the `GITNEXUS_SKIP_OPTIONAL_GRAMMARS`
|
|
// opt-out, which is a separate code path with its own test
|
|
// (parser-loader-skip-optional.test.ts). Zig loads through
|
|
// `requireVendoredGrammar`, so stand in for that helper and report the
|
|
// grammar missing. A fresh module copy is needed because the loader
|
|
// memoizes load results.
|
|
//
|
|
// The fresh copy also re-reads `GITNEXUS_SKIP_OPTIONAL_GRAMMARS` (parsed
|
|
// lazily once per module). Under `=zig` / `=all` — a supported way to
|
|
// run — the loader would take the opt-out branch and the absent-binding
|
|
// path below would never run, so the variable is cleared for this test
|
|
// and restored afterwards. Clearing (not skipping) keeps the branch
|
|
// exercised in every environment.
|
|
const SKIP_ENV = 'GITNEXUS_SKIP_OPTIONAL_GRAMMARS';
|
|
const savedSkip = process.env[SKIP_ENV];
|
|
delete process.env[SKIP_ENV];
|
|
vi.doMock('../../src/core/tree-sitter/vendored-grammars.js', async (importOriginal) => {
|
|
const actual =
|
|
await importOriginal<typeof import('../../src/core/tree-sitter/vendored-grammars.js')>();
|
|
return {
|
|
...actual,
|
|
requireVendoredGrammar: (packageName: string) => {
|
|
if (packageName === 'tree-sitter-zig') {
|
|
const err = new Error(`Cannot find module '${packageName}'`) as NodeJS.ErrnoException;
|
|
err.code = 'MODULE_NOT_FOUND';
|
|
throw err;
|
|
}
|
|
return actual.requireVendoredGrammar(packageName);
|
|
},
|
|
};
|
|
});
|
|
vi.resetModules();
|
|
try {
|
|
const fresh = await import('../../src/core/tree-sitter/parser-loader.js');
|
|
// Not the opt-out path: the variable was cleared above.
|
|
expect(fresh.isGrammarRuntimeSkipped(SupportedLanguages.Zig)).toBe(false);
|
|
expect(fresh.isLanguageAvailable(SupportedLanguages.Zig)).toBe(false);
|
|
await expect(fresh.loadLanguage(SupportedLanguages.Zig)).rejects.toThrow(
|
|
/Unsupported language/,
|
|
);
|
|
// Optional-grammar failure is non-fatal: the other grammars still load.
|
|
expect(fresh.isLanguageAvailable(SupportedLanguages.TypeScript)).toBe(true);
|
|
} finally {
|
|
if (savedSkip === undefined) delete process.env[SKIP_ENV];
|
|
else process.env[SKIP_ENV] = savedSkip;
|
|
vi.doUnmock('../../src/core/tree-sitter/vendored-grammars.js');
|
|
vi.resetModules();
|
|
}
|
|
});
|
|
});
|
|
|
|
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('Dart', () => {
|
|
const dartQueries = () => getProvider(SupportedLanguages.Dart).treeSitterQueries;
|
|
|
|
function loadDartOrSkip() {
|
|
return loadLanguage(SupportedLanguages.Dart).catch(() => null);
|
|
}
|
|
|
|
// ── Definition extraction ──────────────────────────────────────────
|
|
|
|
it('parses classes, functions, mixins, enums, and type aliases', async () => {
|
|
if (!(await loadDartOrSkip())) return;
|
|
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
|
|
const defs = extractDefinitions(matches);
|
|
const defTypes = defs.map((d) => d.type);
|
|
const defNames = defs.map((d) => d.name);
|
|
|
|
expect(defTypes).toContain('definition.class');
|
|
expect(defTypes).toContain('definition.function');
|
|
expect(defTypes).toContain('definition.method');
|
|
expect(defTypes).toContain('definition.enum');
|
|
expect(defTypes).toContain('definition.trait'); // mixin
|
|
expect(defTypes).toContain('definition.type'); // typedef
|
|
expect(defTypes).toContain('definition.constructor');
|
|
expect(defTypes).toContain('definition.property'); // getter/setter
|
|
|
|
expect(defNames).toContain('Animal');
|
|
expect(defNames).toContain('Dog');
|
|
expect(defNames).toContain('greet');
|
|
expect(defNames).toContain('Swimming');
|
|
expect(defNames).toContain('Status');
|
|
expect(defNames).toContain('main');
|
|
expect(defNames).toContain('StringExtension');
|
|
});
|
|
|
|
it('captures factory constructor variant name (not class name)', async () => {
|
|
if (!(await loadDartOrSkip())) return;
|
|
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
|
|
const defs = extractDefinitions(matches);
|
|
const constructors = defs.filter((d) => d.type === 'definition.constructor');
|
|
expect(constructors.length).toBeGreaterThan(0);
|
|
// factory Dog.unknown() should capture 'unknown', not 'Dog'
|
|
const constructorNames = constructors.map((c) => c.name);
|
|
expect(constructorNames).toContain('unknown');
|
|
expect(constructorNames).not.toContain('Dog');
|
|
});
|
|
|
|
it('captures getter and setter as definition.property', async () => {
|
|
if (!(await loadDartOrSkip())) return;
|
|
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
|
|
const defs = extractDefinitions(matches);
|
|
const props = defs.filter((d) => d.type === 'definition.property');
|
|
expect(props.map((p) => p.name)).toContain('info'); // getter
|
|
expect(props.map((p) => p.name)).toContain('nickname'); // setter
|
|
});
|
|
|
|
it('captures typedef names without duplicates', async () => {
|
|
if (!(await loadDartOrSkip())) return;
|
|
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
|
|
const defs = extractDefinitions(matches);
|
|
const typedefs = defs.filter((d) => d.type === 'definition.type');
|
|
const typedefNames = typedefs.map((d) => d.name);
|
|
expect(typedefNames).toContain('JsonMap');
|
|
expect(typedefNames).toContain('Callback');
|
|
// Should not capture RHS types as names
|
|
expect(typedefNames).not.toContain('Map');
|
|
expect(typedefNames).not.toContain('Function');
|
|
});
|
|
|
|
// ── Export detection (private underscore convention) ────────────────
|
|
|
|
it('filters private symbols via underscore convention', async () => {
|
|
if (!(await loadDartOrSkip())) return;
|
|
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
|
|
const defs = extractDefinitions(matches);
|
|
const { dartExportChecker } = await import('../../src/core/ingestion/export-detection.js');
|
|
const publicNames = defs
|
|
.filter((d) => dartExportChecker(null as any, d.name))
|
|
.map((d) => d.name);
|
|
const privateNames = defs
|
|
.filter((d) => !dartExportChecker(null as any, d.name))
|
|
.map((d) => d.name);
|
|
|
|
expect(publicNames).toContain('Animal');
|
|
expect(publicNames).toContain('greet');
|
|
expect(privateNames).toContain('_privateHelper');
|
|
});
|
|
|
|
// ── Import extraction ──────────────────────────────────────────────
|
|
|
|
it('extracts imports', async () => {
|
|
if (!(await loadDartOrSkip())) return;
|
|
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
|
|
|
|
const imports: string[] = [];
|
|
for (const match of matches) {
|
|
for (const capture of match.captures) {
|
|
if (capture.name === 'import.source') imports.push(capture.node.text);
|
|
}
|
|
}
|
|
expect(imports.length).toBe(3);
|
|
});
|
|
|
|
it('extracts re-exports', async () => {
|
|
if (!(await loadDartOrSkip())) return;
|
|
const { matches } = parseAndQuery(parser, readFixture('dart-advanced.dart'), dartQueries());
|
|
|
|
const imports: string[] = [];
|
|
for (const match of matches) {
|
|
for (const capture of match.captures) {
|
|
if (capture.name === 'import.source') imports.push(capture.node.text);
|
|
}
|
|
}
|
|
// 2 imports + 1 re-export = 3 import.source captures
|
|
expect(imports.length).toBe(3);
|
|
});
|
|
|
|
// ── Call extraction ────────────────────────────────────────────────
|
|
|
|
it('extracts calls in expression statements', async () => {
|
|
if (!(await loadDartOrSkip())) return;
|
|
const { matches } = parseAndQuery(parser, readFixture('dart-advanced.dart'), dartQueries());
|
|
|
|
const callNames: string[] = [];
|
|
for (const match of matches) {
|
|
for (const capture of match.captures) {
|
|
if (capture.name === 'call.name') callNames.push(capture.node.text);
|
|
}
|
|
}
|
|
expect(callNames).toContain('fetchUsers');
|
|
expect(callNames).toContain('processData');
|
|
});
|
|
|
|
it('extracts calls in return statements', async () => {
|
|
if (!(await loadDartOrSkip())) return;
|
|
const { matches } = parseAndQuery(parser, readFixture('dart-advanced.dart'), dartQueries());
|
|
|
|
const callNames: string[] = [];
|
|
for (const match of matches) {
|
|
for (const capture of match.captures) {
|
|
if (capture.name === 'call.name') callNames.push(capture.node.text);
|
|
}
|
|
}
|
|
expect(callNames).toContain('formatOutput');
|
|
});
|
|
|
|
it('extracts calls in variable assignments', async () => {
|
|
if (!(await loadDartOrSkip())) return;
|
|
const { matches } = parseAndQuery(parser, readFixture('dart-advanced.dart'), dartQueries());
|
|
|
|
const callNames: string[] = [];
|
|
for (const match of matches) {
|
|
for (const capture of match.captures) {
|
|
if (capture.name === 'call.name') callNames.push(capture.node.text);
|
|
}
|
|
}
|
|
expect(callNames).toContain('computeScore');
|
|
expect(callNames).toContain('loadUser');
|
|
});
|
|
|
|
// ── Framework detection (path-based) ───────────────────────────────
|
|
|
|
it('detects Flutter framework from file paths', async () => {
|
|
const { detectFrameworkFromPath } =
|
|
await import('../../src/core/ingestion/framework-detection.js');
|
|
|
|
expect(detectFrameworkFromPath('lib/main.dart')?.framework).toBe('flutter');
|
|
expect(detectFrameworkFromPath('lib/app.dart')?.framework).toBe('flutter');
|
|
expect(detectFrameworkFromPath('lib/screens/home.dart')?.framework).toBe('flutter');
|
|
expect(detectFrameworkFromPath('lib/pages/login.dart')?.framework).toBe('flutter');
|
|
expect(detectFrameworkFromPath('lib/widgets/button.dart')?.framework).toBe('flutter');
|
|
expect(detectFrameworkFromPath('lib/bloc/user_bloc.dart')?.framework).toBe('flutter');
|
|
expect(detectFrameworkFromPath('lib/services/api.dart')?.framework).toBe('flutter');
|
|
expect(detectFrameworkFromPath('lib/routes/app_router.dart')?.framework).toBe('flutter');
|
|
|
|
// main.dart gets highest boost
|
|
expect(detectFrameworkFromPath('lib/main.dart')?.entryPointMultiplier).toBe(3.0);
|
|
// widgets get lowest
|
|
expect(detectFrameworkFromPath('lib/widgets/button.dart')?.entryPointMultiplier).toBe(1.5);
|
|
});
|
|
});
|
|
|
|
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'],
|
|
// Dart, Swift, and Zig (vendored optional grammars) are excluded —
|
|
// none of the three is guaranteed to be installed on every platform.
|
|
];
|
|
|
|
for (const [lang, fixture, filePath] of langFixtures) {
|
|
await loadLanguage(lang, filePath || fixture);
|
|
const content = readFixture(fixture);
|
|
const provider = getProvider(lang);
|
|
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
|
|
const defs = extractDefinitions(matches);
|
|
expect(defs.length, `${lang} (${fixture}) should have definitions`).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
});
|
|
|
|
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 tsProvider = getProvider(SupportedLanguages.TypeScript);
|
|
const query = new Parser.Query(lang, tsProvider.treeSitterQueries);
|
|
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);
|
|
});
|
|
});
|
|
});
|