mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-13 03:58:33 +00:00
* fix(deps): upgrade native tree-sitter to 0.25.1 * fix(tree-sitter): preserve sync loading and update verified capture baselines Route C# scope queries through the synchronous native grammar loader so the compiled analyze module remains require-compatible on Node 22.18. Refresh only the audited empty-Python-module and renamed-PHP-fixture captures, and pin the runtime assertion to 0.25.1. Validated 479 targeted tests, all 16 scope-capture benchmark cases, Python capture/import guards, compiled C# worker parsing, build, typecheck and formatting. Performance budgets are unchanged. * fix(tree-sitter): refresh upstream vendors and preserve parser semantics * fix(ci): refresh prebuilds and support Windows ARM Refresh macOS, Linux ARM, and Windows x64 grammar binaries from the successful native CI builds. Pin node-gyp 12.4.0 so the Windows ARM jobs recognize Visual Studio 2026. Use local Git filtering for E2E to avoid PR-files API timeouts on generated parser diffs. Validation: 143 focused tests, Actionlint, and all 16 scope-capture benchmark checks passed. Full npm test run started; native database suites still running. * fix(ci): refresh Windows ARM grammar prebuilds Import all seven Windows ARM grammar artifacts from successful native build run 37965761760. Artifact filenames and SHA-256 hashes verified; all 42 native build jobs passed. Validation: 31 prebuild coverage tests passed. * fix(bench): stabilize scope capture timing guard Warm each input size seven times and use 21 median samples while retaining isolated timing blocks. Yield between languages so native finalizers release allocations outside the measured sections. Keep all fixture sizes, fingerprints, scaling formulas, and budgets unchanged. Validation: all 16 language guards passed with a 2 GB heap limit (1.38 deep-chain ratio; 1518 MB peak RSS). Linear and quadratic controls passed their expected guard outcomes. Formatting and graph change analysis passed. Full npm test is running without failures so far. * fix(parser): reduce TypeScript capture traversal cost * fix(test): await parser worker readiness before teardown * fix(parser): load C# synchronously in workers * fix(grpc): resolve native proto detections against their own file With the native proto grammar active, sibling .proto files that declare the same short service name under different packages tied in the directory-based resolveProtoConflict heuristic and both providers were dropped. A proto-origin detection already knows its defining file, so use that file's entry before falling back to the heuristic. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ci(packaging): assert native grammars load from the installed tarball The packaged-install smoke only ran `gitnexus --version`, so a grammar that failed to load from the published package (bundled runtime, patched peer manifests, vendored prebuilds, C# sync loader, Protobuf wrapper) would ship silently; the dev-tree ABI smoke also accepts vendored grammars that report unavailable. Parse a sample for every registered grammar plus the native Protobuf plugin from the installed package, on the main thread and in a worker thread. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ci(tree-sitter): track compatibility workarounds instead of 0.25 readiness The daily readiness report still targeted tree-sitter@0.25.0, so after the upgrade it would keep reporting "Blocked" and never close its tracking issue. Repurpose it to track what we carry for the bundled runtime: derive the target from the package.json pin, list each pack-time peer patch from prepare-tree-sitter-bundle.cjs with its drop condition (bump, wait for an upstream release, transitive, or intentionally pinned), and reword the buckets. The workflow adopts the issue opened under the old title. Also update the grammar-update-monitor header and auto-PR text, which still described the 0.21.1 runtime and a tree-sitter-c hold. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(dart): assert both nested labels target the loop; fix Zig call-shape doc The nested-label CFG test only jumped to `outer` and checked reachability, which still passes when a jump is retargeted to the inner loop (its exit reaches the outer header). Jump to both labels and assert the exact `continue` edges; dropping the inner label now fails the test. Update the Zig callable-flow comment: call_expression now wraps actuals in a required `arguments:` field. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Codex <codex@openai.com> Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
931 lines
35 KiB
TypeScript
931 lines
35 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
||
import { createVariableExtractor } from '../../src/core/ingestion/variable-extractors/generic.js';
|
||
import {
|
||
typescriptVariableConfig,
|
||
javascriptVariableConfig,
|
||
} from '../../src/core/ingestion/variable-extractors/configs/typescript-javascript.js';
|
||
import { pythonVariableConfig } from '../../src/core/ingestion/variable-extractors/configs/python.js';
|
||
import { goVariableConfig } from '../../src/core/ingestion/variable-extractors/configs/go.js';
|
||
import { rustVariableConfig } from '../../src/core/ingestion/variable-extractors/configs/rust.js';
|
||
import {
|
||
cVariableConfig,
|
||
cppVariableConfig,
|
||
} from '../../src/core/ingestion/variable-extractors/configs/c-cpp.js';
|
||
import { rubyVariableConfig } from '../../src/core/ingestion/variable-extractors/configs/ruby.js';
|
||
import { dartVariableConfig } from '../../src/core/ingestion/variable-extractors/configs/dart.js';
|
||
import { kotlinVariableConfig } from '../../src/core/ingestion/variable-extractors/configs/jvm.js';
|
||
import type { SyntaxNode } from '../../src/core/ingestion/utils/ast-helpers.js';
|
||
import type { VariableExtractorContext } from '../../src/core/ingestion/variable-types.js';
|
||
import { SupportedLanguages } from '../../src/config/supported-languages.js';
|
||
import Parser from 'tree-sitter';
|
||
import TypeScript from 'tree-sitter-typescript';
|
||
import Python from 'tree-sitter-python';
|
||
import Go from 'tree-sitter-go';
|
||
import Rust from 'tree-sitter-rust';
|
||
import Cpp from 'tree-sitter-cpp';
|
||
import Ruby from 'tree-sitter-ruby';
|
||
import { requireVendoredGrammar } from '../../src/core/tree-sitter/vendored-grammars.js';
|
||
|
||
// Vendored grammars — loaded from vendor/ by absolute path, never node_modules (#2111).
|
||
const C = requireVendoredGrammar('tree-sitter-c');
|
||
const Dart = requireVendoredGrammar('tree-sitter-dart');
|
||
|
||
let Kotlin: Parser.Language | null = null;
|
||
try {
|
||
Kotlin = requireVendoredGrammar('tree-sitter-kotlin');
|
||
} catch {
|
||
// Kotlin grammar may not have a prebuild for this platform
|
||
}
|
||
|
||
const parser = new Parser();
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// TypeScript config
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('VariableExtractor — TypeScript', () => {
|
||
const extractor = createVariableExtractor(typescriptVariableConfig);
|
||
const ctx: VariableExtractorContext = {
|
||
filePath: 'test.ts',
|
||
language: SupportedLanguages.TypeScript,
|
||
};
|
||
|
||
it('extracts const declaration', () => {
|
||
parser.setLanguage(TypeScript.typescript);
|
||
const tree = parser.parse('const MAX_SIZE = 100;');
|
||
const node = tree.rootNode.child(0)!;
|
||
expect(extractor.isVariableDeclaration(node)).toBe(true);
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('MAX_SIZE');
|
||
expect(info!.isConst).toBe(true);
|
||
expect(info!.isMutable).toBe(false);
|
||
expect(info!.visibility).toBe('private');
|
||
});
|
||
|
||
it('extracts let declaration as mutable', () => {
|
||
parser.setLanguage(TypeScript.typescript);
|
||
const tree = parser.parse('let counter = 0;');
|
||
const node = tree.rootNode.child(0)!;
|
||
expect(extractor.isVariableDeclaration(node)).toBe(true);
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('counter');
|
||
expect(info!.isConst).toBe(false);
|
||
expect(info!.isMutable).toBe(true);
|
||
});
|
||
|
||
it('extracts typed const declaration', () => {
|
||
parser.setLanguage(TypeScript.typescript);
|
||
const tree = parser.parse('const name: string = "hello";');
|
||
const node = tree.rootNode.child(0)!;
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('name');
|
||
expect(info!.type).toBe('string');
|
||
expect(info!.isConst).toBe(true);
|
||
});
|
||
|
||
it('detects export as public visibility', () => {
|
||
parser.setLanguage(TypeScript.typescript);
|
||
const tree = parser.parse('export const API_KEY = "abc";');
|
||
// export_statement wraps lexical_declaration
|
||
const exportStatement = tree.rootNode.child(0)!;
|
||
// The lexical_declaration is the child of export_statement
|
||
const declNode = exportStatement.namedChildren.find((c) => c.type === 'lexical_declaration');
|
||
expect(declNode).toBeDefined();
|
||
const info = extractor.extract(declNode!, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('API_KEY');
|
||
expect(info!.visibility).toBe('public');
|
||
});
|
||
|
||
it('rejects non-variable nodes', () => {
|
||
parser.setLanguage(TypeScript.typescript);
|
||
const tree = parser.parse('function foo() {}');
|
||
const node = tree.rootNode.child(0)!;
|
||
expect(extractor.isVariableDeclaration(node)).toBe(false);
|
||
expect(extractor.extract(node, ctx)).toBeNull();
|
||
});
|
||
|
||
it('extracts var declaration as mutable variable', () => {
|
||
parser.setLanguage(TypeScript.typescript);
|
||
const tree = parser.parse('var x = 5;');
|
||
const node = tree.rootNode.child(0)!;
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('x');
|
||
expect(info!.isMutable).toBe(true);
|
||
expect(info!.isConst).toBe(false);
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// JavaScript config
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('VariableExtractor — JavaScript', () => {
|
||
const extractor = createVariableExtractor(javascriptVariableConfig);
|
||
const ctx: VariableExtractorContext = {
|
||
filePath: 'test.js',
|
||
language: SupportedLanguages.JavaScript,
|
||
};
|
||
|
||
it('extracts const declaration', () => {
|
||
parser.setLanguage(TypeScript.typescript); // JS subset of TS grammar
|
||
const tree = parser.parse('const PORT = 3000;');
|
||
const node = tree.rootNode.child(0)!;
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('PORT');
|
||
expect(info!.isConst).toBe(true);
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Python config
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('VariableExtractor — Python', () => {
|
||
const extractor = createVariableExtractor(pythonVariableConfig);
|
||
const ctx: VariableExtractorContext = {
|
||
filePath: 'test.py',
|
||
language: SupportedLanguages.Python,
|
||
};
|
||
|
||
it('extracts UPPER_CASE constant', () => {
|
||
parser.setLanguage(Python);
|
||
const tree = parser.parse('MAX_SIZE = 100');
|
||
const node = tree.rootNode.child(0)!;
|
||
expect(extractor.isVariableDeclaration(node)).toBe(true);
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('MAX_SIZE');
|
||
expect(info!.isConst).toBe(true);
|
||
expect(info!.isMutable).toBe(false);
|
||
expect(info!.visibility).toBe('public');
|
||
});
|
||
|
||
it('extracts regular assignment as mutable', () => {
|
||
parser.setLanguage(Python);
|
||
const tree = parser.parse('counter = 0');
|
||
const node = tree.rootNode.child(0)!;
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('counter');
|
||
expect(info!.isConst).toBe(false);
|
||
expect(info!.isMutable).toBe(true);
|
||
});
|
||
|
||
it('extracts annotated assignment with type', () => {
|
||
parser.setLanguage(Python);
|
||
const tree = parser.parse('name: str = "hello"');
|
||
const node = tree.rootNode.child(0)!;
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('name');
|
||
expect(info!.type).toBe('str');
|
||
});
|
||
|
||
it('detects underscore prefix as protected', () => {
|
||
parser.setLanguage(Python);
|
||
const tree = parser.parse('_internal = 42');
|
||
const node = tree.rootNode.child(0)!;
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.visibility).toBe('protected');
|
||
});
|
||
|
||
it('detects double underscore prefix as private', () => {
|
||
parser.setLanguage(Python);
|
||
const tree = parser.parse('__secret = 42');
|
||
const node = tree.rootNode.child(0)!;
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.visibility).toBe('private');
|
||
});
|
||
|
||
it('does not treat dunder names as private', () => {
|
||
parser.setLanguage(Python);
|
||
const tree = parser.parse('__name__ = "main"');
|
||
const node = tree.rootNode.child(0)!;
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.visibility).toBe('public');
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Go config
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('VariableExtractor — Go', () => {
|
||
const extractor = createVariableExtractor(goVariableConfig);
|
||
const ctx: VariableExtractorContext = {
|
||
filePath: 'test.go',
|
||
language: SupportedLanguages.Go,
|
||
};
|
||
|
||
it('extracts const declaration', () => {
|
||
parser.setLanguage(Go);
|
||
const tree = parser.parse('package main\nconst MaxSize = 100');
|
||
// Find const_declaration
|
||
let constNode = null;
|
||
for (let i = 0; i < tree.rootNode.namedChildCount; i++) {
|
||
const child = tree.rootNode.namedChild(i);
|
||
if (child?.type === 'const_declaration') {
|
||
constNode = child;
|
||
break;
|
||
}
|
||
}
|
||
expect(constNode).not.toBeNull();
|
||
expect(extractor.isVariableDeclaration(constNode!)).toBe(true);
|
||
|
||
const info = extractor.extract(constNode!, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('MaxSize');
|
||
expect(info!.isConst).toBe(true);
|
||
expect(info!.isMutable).toBe(false);
|
||
expect(info!.visibility).toBe('public'); // uppercase = exported
|
||
});
|
||
|
||
it('extracts var declaration', () => {
|
||
parser.setLanguage(Go);
|
||
const tree = parser.parse('package main\nvar counter int = 0');
|
||
let varNode = null;
|
||
for (let i = 0; i < tree.rootNode.namedChildCount; i++) {
|
||
const child = tree.rootNode.namedChild(i);
|
||
if (child?.type === 'var_declaration') {
|
||
varNode = child;
|
||
break;
|
||
}
|
||
}
|
||
expect(varNode).not.toBeNull();
|
||
|
||
const info = extractor.extract(varNode!, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('counter');
|
||
expect(info!.isConst).toBe(false);
|
||
expect(info!.isMutable).toBe(true);
|
||
expect(info!.type).toBe('int');
|
||
});
|
||
|
||
it('extracts every name from multi-name const declarations', () => {
|
||
parser.setLanguage(Go);
|
||
const tree = parser.parse('package main\nconst X, Y,Z,A,B = 1, 2,3,4,5');
|
||
const constNode = tree.rootNode.namedChildren.find(
|
||
(child) => child.type === 'const_declaration',
|
||
);
|
||
expect(constNode).toBeDefined();
|
||
|
||
const infos = extractor.extractAll(constNode!, ctx);
|
||
expect(infos.map((info) => info.name)).toEqual(['X', 'Y', 'Z', 'A', 'B']);
|
||
for (const info of infos) {
|
||
expect(info.isConst).toBe(true);
|
||
expect(info.isMutable).toBe(false);
|
||
expect(info.visibility).toBe('public');
|
||
expect(info.type).toBeNull();
|
||
}
|
||
});
|
||
|
||
it('extracts every name from multi-name var declarations with a shared type', () => {
|
||
parser.setLanguage(Go);
|
||
const tree = parser.parse('package main\nvar a, b int');
|
||
const varNode = tree.rootNode.namedChildren.find((child) => child.type === 'var_declaration');
|
||
expect(varNode).toBeDefined();
|
||
|
||
const infos = extractor.extractAll(varNode!, ctx);
|
||
expect(infos.map((info) => info.name)).toEqual(['a', 'b']);
|
||
for (const info of infos) {
|
||
expect(info.isConst).toBe(false);
|
||
expect(info.isMutable).toBe(true);
|
||
expect(info.visibility).toBe('package');
|
||
expect(info.type).toBe('int');
|
||
}
|
||
});
|
||
|
||
it('preserves per-spec types in grouped multi-name var declarations', () => {
|
||
parser.setLanguage(Go);
|
||
const tree = parser.parse('package main\nvar (\n a, b int\n c string\n)');
|
||
const varNode = tree.rootNode.namedChildren.find((child) => child.type === 'var_declaration');
|
||
expect(varNode).toBeDefined();
|
||
|
||
const infos = extractor.extractAll(varNode!, ctx);
|
||
expect(infos.map((info) => [info.name, info.type])).toEqual([
|
||
['a', 'int'],
|
||
['b', 'int'],
|
||
['c', 'string'],
|
||
]);
|
||
|
||
const cInfo = infos.find((info) => info.name === 'c');
|
||
expect(cInfo).toBeDefined();
|
||
expect(cInfo!.isConst).toBe(false);
|
||
expect(cInfo!.isMutable).toBe(true);
|
||
expect(cInfo!.visibility).toBe('package');
|
||
});
|
||
|
||
it('matches parse-worker nodeName selection for grouped multi-name vars', () => {
|
||
parser.setLanguage(Go);
|
||
const tree = parser.parse('package main\nvar (\n a, b int\n c string\n)');
|
||
const varNode = tree.rootNode.namedChildren.find((child) => child.type === 'var_declaration');
|
||
expect(varNode).toBeDefined();
|
||
|
||
const selected = extractor.extractAll(varNode!, ctx).find((info) => info.name === 'c');
|
||
expect(selected).toMatchObject({
|
||
name: 'c',
|
||
type: 'string',
|
||
visibility: 'package',
|
||
isConst: false,
|
||
isMutable: true,
|
||
});
|
||
});
|
||
|
||
it('detects lowercase as package-private', () => {
|
||
parser.setLanguage(Go);
|
||
const tree = parser.parse('package main\nconst maxSize = 100');
|
||
let constNode = null;
|
||
for (let i = 0; i < tree.rootNode.namedChildCount; i++) {
|
||
const child = tree.rootNode.namedChild(i);
|
||
if (child?.type === 'const_declaration') {
|
||
constNode = child;
|
||
break;
|
||
}
|
||
}
|
||
expect(constNode).not.toBeNull();
|
||
|
||
const info = extractor.extract(constNode!, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.visibility).toBe('package');
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Rust config
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('VariableExtractor — Rust', () => {
|
||
const extractor = createVariableExtractor(rustVariableConfig);
|
||
const ctx: VariableExtractorContext = {
|
||
filePath: 'test.rs',
|
||
language: SupportedLanguages.Rust,
|
||
};
|
||
|
||
it('extracts const_item', () => {
|
||
parser.setLanguage(Rust);
|
||
const tree = parser.parse('const MAX_SIZE: usize = 100;');
|
||
const node = tree.rootNode.child(0)!;
|
||
expect(extractor.isVariableDeclaration(node)).toBe(true);
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('MAX_SIZE');
|
||
expect(info!.isConst).toBe(true);
|
||
expect(info!.isStatic).toBe(false);
|
||
expect(info!.isMutable).toBe(false);
|
||
expect(info!.type).toBe('usize');
|
||
expect(info!.visibility).toBe('private');
|
||
});
|
||
|
||
it('extracts static_item', () => {
|
||
parser.setLanguage(Rust);
|
||
const tree = parser.parse('static COUNTER: i32 = 0;');
|
||
const node = tree.rootNode.child(0)!;
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('COUNTER');
|
||
expect(info!.isStatic).toBe(true);
|
||
expect(info!.isConst).toBe(false);
|
||
expect(info!.isMutable).toBe(false);
|
||
});
|
||
|
||
it('extracts pub const as public', () => {
|
||
parser.setLanguage(Rust);
|
||
const tree = parser.parse('pub const API_VERSION: &str = "v1";');
|
||
const node = tree.rootNode.child(0)!;
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.visibility).toBe('public');
|
||
expect(info!.isConst).toBe(true);
|
||
});
|
||
|
||
it('extracts static mut as mutable', () => {
|
||
parser.setLanguage(Rust);
|
||
const tree = parser.parse('static mut BUFFER: Vec<u8> = Vec::new();');
|
||
const node = tree.rootNode.child(0)!;
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.isStatic).toBe(true);
|
||
expect(info!.isMutable).toBe(true);
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// C/C++ config
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('VariableExtractor — C', () => {
|
||
const extractor = createVariableExtractor(cVariableConfig);
|
||
const ctx: VariableExtractorContext = {
|
||
filePath: 'test.c',
|
||
language: SupportedLanguages.C,
|
||
};
|
||
|
||
it('extracts const declaration', () => {
|
||
parser.setLanguage(C);
|
||
const tree = parser.parse('const int MAX_SIZE = 100;');
|
||
const node = tree.rootNode.child(0)!;
|
||
expect(extractor.isVariableDeclaration(node)).toBe(true);
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('MAX_SIZE');
|
||
expect(info!.isConst).toBe(true);
|
||
expect(info!.isMutable).toBe(false);
|
||
});
|
||
|
||
it('extracts static variable', () => {
|
||
parser.setLanguage(C);
|
||
const tree = parser.parse('static int counter = 0;');
|
||
const node = tree.rootNode.child(0)!;
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('counter');
|
||
expect(info!.isStatic).toBe(true);
|
||
expect(info!.visibility).toBe('private'); // static = file-private
|
||
});
|
||
});
|
||
|
||
describe('VariableExtractor — C++', () => {
|
||
const extractor = createVariableExtractor(cppVariableConfig);
|
||
const ctx: VariableExtractorContext = {
|
||
filePath: 'test.cpp',
|
||
language: SupportedLanguages.CPlusPlus,
|
||
};
|
||
|
||
it('extracts constexpr declaration', () => {
|
||
parser.setLanguage(Cpp);
|
||
const tree = parser.parse('constexpr int SIZE = 10;');
|
||
const node = tree.rootNode.child(0)!;
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('SIZE');
|
||
expect(info!.isConst).toBe(true);
|
||
});
|
||
|
||
// F9 — structured binding declarations emit one Variable per bound name.
|
||
it('emits a Variable per name for `auto [a, b] = make_pair();`', () => {
|
||
parser.setLanguage(Cpp);
|
||
const tree = parser.parse('auto [a, b] = make_pair();');
|
||
const node = tree.rootNode.child(0)!;
|
||
expect(extractor.isVariableDeclaration(node)).toBe(true);
|
||
|
||
const infos = extractor.extractAll(node, ctx);
|
||
expect(infos.map((i) => i.name)).toEqual(['a', 'b']);
|
||
// Top-level declaration → module scope (C++ is in the safe set re: the
|
||
// determineScope class-body block hazard).
|
||
for (const info of infos) expect(info.scope).toBe('module');
|
||
});
|
||
|
||
it('emits a Variable per name for the reference form `auto& [x, y, z] = tup;`', () => {
|
||
parser.setLanguage(Cpp);
|
||
const tree = parser.parse('auto& [x, y, z] = tup;');
|
||
const node = tree.rootNode.child(0)!;
|
||
|
||
const infos = extractor.extractAll(node, ctx);
|
||
expect(infos.map((i) => i.name)).toEqual(['x', 'y', 'z']);
|
||
for (const info of infos) expect(info.scope).toBe('module');
|
||
});
|
||
|
||
it('does not double-emit for an ordinary single-name declaration `int n = 0;`', () => {
|
||
parser.setLanguage(Cpp);
|
||
const tree = parser.parse('int n = 0;');
|
||
const node = tree.rootNode.child(0)!;
|
||
|
||
const infos = extractor.extractAll(node, ctx);
|
||
expect(infos).toHaveLength(1);
|
||
expect(infos[0]!.name).toBe('n');
|
||
expect(infos[0]!.scope).toBe('module');
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Ruby config
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('VariableExtractor — Ruby', () => {
|
||
const extractor = createVariableExtractor(rubyVariableConfig);
|
||
const ctx: VariableExtractorContext = {
|
||
filePath: 'test.rb',
|
||
language: SupportedLanguages.Ruby,
|
||
};
|
||
|
||
it('extracts Ruby constant assignment', () => {
|
||
parser.setLanguage(Ruby);
|
||
const tree = parser.parse('MAX_SIZE = 100');
|
||
const node = tree.rootNode.child(0)!;
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('MAX_SIZE');
|
||
expect(info!.isConst).toBe(true);
|
||
expect(info!.isMutable).toBe(false);
|
||
expect(info!.visibility).toBe('public');
|
||
});
|
||
|
||
it('extracts regular variable assignment', () => {
|
||
parser.setLanguage(Ruby);
|
||
const tree = parser.parse('counter = 0');
|
||
const node = tree.rootNode.child(0)!;
|
||
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('counter');
|
||
expect(info!.isConst).toBe(false);
|
||
expect(info!.isMutable).toBe(true);
|
||
expect(info!.visibility).toBe('private');
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Factory generic tests
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('createVariableExtractor — factory', () => {
|
||
const factoryCtx: VariableExtractorContext = {
|
||
filePath: 'test.ts',
|
||
language: SupportedLanguages.TypeScript,
|
||
};
|
||
|
||
it('creates extractor with correct language', () => {
|
||
const extractor = createVariableExtractor(typescriptVariableConfig);
|
||
expect(extractor.language).toBe(SupportedLanguages.TypeScript);
|
||
});
|
||
|
||
it('returns null for non-variable nodes', () => {
|
||
const extractor = createVariableExtractor(typescriptVariableConfig);
|
||
parser.setLanguage(TypeScript.typescript);
|
||
const tree = parser.parse('class Foo {}');
|
||
const node = tree.rootNode.child(0)!;
|
||
expect(extractor.extract(node, factoryCtx)).toBeNull();
|
||
});
|
||
|
||
it('line number is 1-based', () => {
|
||
const extractor = createVariableExtractor(typescriptVariableConfig);
|
||
parser.setLanguage(TypeScript.typescript);
|
||
const tree = parser.parse('const x = 1;');
|
||
const node = tree.rootNode.child(0)!;
|
||
const info = extractor.extract(node, factoryCtx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.line).toBe(1);
|
||
expect(info!.sourceFile).toBe('test.ts');
|
||
});
|
||
|
||
it('sets scope to module for top-level declarations', () => {
|
||
const extractor = createVariableExtractor(typescriptVariableConfig);
|
||
parser.setLanguage(TypeScript.typescript);
|
||
const tree = parser.parse('const x = 1;');
|
||
const node = tree.rootNode.child(0)!;
|
||
const info = extractor.extract(node, factoryCtx);
|
||
expect(info).not.toBeNull();
|
||
// rootNode is 'program' → module scope
|
||
expect(info!.scope).toBe('module');
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Block-scoped variable extraction tests
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('VariableExtractor — block-scoped declarations', () => {
|
||
it('TypeScript: detects block scope for const inside function', () => {
|
||
const extractor = createVariableExtractor(typescriptVariableConfig);
|
||
const ctx: VariableExtractorContext = {
|
||
filePath: 'test.ts',
|
||
language: SupportedLanguages.TypeScript,
|
||
};
|
||
parser.setLanguage(TypeScript.typescript);
|
||
const tree = parser.parse('function foo() { const x = 5; }');
|
||
// program > function_declaration > statement_block > lexical_declaration
|
||
const fnBody = tree.rootNode.child(0)!.childForFieldName('body')!;
|
||
const constDecl = fnBody.namedChildren.find((c) => c.type === 'lexical_declaration');
|
||
expect(constDecl).toBeDefined();
|
||
const info = extractor.extract(constDecl!, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('x');
|
||
expect(info!.scope).toBe('block');
|
||
expect(info!.isConst).toBe(true);
|
||
expect(info!.isMutable).toBe(false);
|
||
});
|
||
|
||
it('TypeScript: detects block scope for let inside arrow function', () => {
|
||
const extractor = createVariableExtractor(typescriptVariableConfig);
|
||
const ctx: VariableExtractorContext = {
|
||
filePath: 'test.ts',
|
||
language: SupportedLanguages.TypeScript,
|
||
};
|
||
parser.setLanguage(TypeScript.typescript);
|
||
const tree = parser.parse('const fn = () => { let y = 10; };');
|
||
// program > lexical_declaration > variable_declarator > arrow_function > statement_block
|
||
const lexDecl = tree.rootNode.child(0)!;
|
||
const varDeclarator = lexDecl.namedChildren.find((c) => c.type === 'variable_declarator')!;
|
||
const arrowFn = varDeclarator.childForFieldName('value')!;
|
||
const body = arrowFn.childForFieldName('body')!;
|
||
const letDecl = body.namedChildren.find((c) => c.type === 'lexical_declaration');
|
||
expect(letDecl).toBeDefined();
|
||
const info = extractor.extract(letDecl!, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('y');
|
||
expect(info!.scope).toBe('block');
|
||
expect(info!.isMutable).toBe(true);
|
||
expect(info!.isConst).toBe(false);
|
||
});
|
||
|
||
it('Go: detects block scope for short var declaration inside function', () => {
|
||
const extractor = createVariableExtractor(goVariableConfig);
|
||
const ctx: VariableExtractorContext = {
|
||
filePath: 'test.go',
|
||
language: SupportedLanguages.Go,
|
||
};
|
||
parser.setLanguage(Go);
|
||
const tree = parser.parse('package main\nfunc foo() { x := 5 }');
|
||
// Go 0.25 inserts statement_list between the block and its statements.
|
||
const funcDecl = tree.rootNode.namedChildren.find((c) => c.type === 'function_declaration')!;
|
||
const body = funcDecl.childForFieldName('body')!;
|
||
const shortVarDecl = body.descendantsOfType('short_var_declaration')[0];
|
||
expect(shortVarDecl).toBeDefined();
|
||
const info = extractor.extract(shortVarDecl!, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('x');
|
||
expect(info!.scope).toBe('block');
|
||
expect(info!.isMutable).toBe(true);
|
||
});
|
||
|
||
it('Rust: detects block scope for let inside function', () => {
|
||
const extractor = createVariableExtractor(rustVariableConfig);
|
||
const ctx: VariableExtractorContext = {
|
||
filePath: 'test.rs',
|
||
language: SupportedLanguages.Rust,
|
||
};
|
||
parser.setLanguage(Rust);
|
||
const tree = parser.parse('fn foo() { let mut x = 5; }');
|
||
// source_file > function_item > block > let_declaration
|
||
const funcItem = tree.rootNode.namedChildren.find((c) => c.type === 'function_item')!;
|
||
const body = funcItem.childForFieldName('body')!;
|
||
const letDecl = body.namedChildren.find((c) => c.type === 'let_declaration');
|
||
expect(letDecl).toBeDefined();
|
||
const info = extractor.extract(letDecl!, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('x');
|
||
expect(info!.scope).toBe('block');
|
||
expect(info!.isMutable).toBe(true);
|
||
});
|
||
|
||
it('C: detects block scope for declaration inside function', () => {
|
||
const extractor = createVariableExtractor(cVariableConfig);
|
||
const ctx: VariableExtractorContext = {
|
||
filePath: 'test.c',
|
||
language: SupportedLanguages.C,
|
||
};
|
||
parser.setLanguage(C);
|
||
const tree = parser.parse('void foo() { int x = 5; }');
|
||
// translation_unit > function_definition > compound_statement > declaration
|
||
const funcDef = tree.rootNode.namedChildren.find((c) => c.type === 'function_definition')!;
|
||
const body = funcDef.childForFieldName('body')!;
|
||
const decl = body.namedChildren.find((c) => c.type === 'declaration');
|
||
expect(decl).toBeDefined();
|
||
const info = extractor.extract(decl!, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('x');
|
||
expect(info!.scope).toBe('block');
|
||
expect(info!.isMutable).toBe(true);
|
||
});
|
||
|
||
it('Python: detects block scope for assignment inside function', () => {
|
||
const extractor = createVariableExtractor(pythonVariableConfig);
|
||
const ctx: VariableExtractorContext = {
|
||
filePath: 'test.py',
|
||
language: SupportedLanguages.Python,
|
||
};
|
||
parser.setLanguage(Python);
|
||
const tree = parser.parse('def foo():\n x = 5');
|
||
// module > function_definition > block > expression_statement > assignment
|
||
const funcDef = tree.rootNode.namedChildren.find((c) => c.type === 'function_definition')!;
|
||
const body = funcDef.childForFieldName('body')!;
|
||
const exprStmt = body.namedChildren.find((c) => c.type === 'expression_statement');
|
||
expect(exprStmt).toBeDefined();
|
||
const info = extractor.extract(exprStmt!, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('x');
|
||
expect(info!.scope).toBe('block');
|
||
});
|
||
|
||
it('Python: class-body attribute is not block-scoped (class body is a `block` node)', () => {
|
||
// Regression: tree-sitter-python models the class body as a `block` node, the
|
||
// same node type as a function body. A class attribute must NOT be classified
|
||
// as a function-local block, or the pruner would silently drop it.
|
||
const extractor = createVariableExtractor(pythonVariableConfig);
|
||
const ctx: VariableExtractorContext = {
|
||
filePath: 'test.py',
|
||
language: SupportedLanguages.Python,
|
||
};
|
||
parser.setLanguage(Python);
|
||
const tree = parser.parse('class Settings:\n MAX_SIZE = 100');
|
||
// module > class_definition > block > expression_statement > assignment
|
||
const classDef = tree.rootNode.namedChildren.find((c) => c.type === 'class_definition')!;
|
||
const body = classDef.childForFieldName('body')!;
|
||
const exprStmt = body.namedChildren.find((c) => c.type === 'expression_statement');
|
||
expect(exprStmt).toBeDefined();
|
||
const info = extractor.extract(exprStmt!, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('MAX_SIZE');
|
||
expect(info!.scope).not.toBe('block');
|
||
expect(info!.scope).toBe('module');
|
||
});
|
||
|
||
it('Python: rejects non-assignment expression statements (e.g. function calls)', () => {
|
||
const extractor = createVariableExtractor(pythonVariableConfig);
|
||
const ctx: VariableExtractorContext = {
|
||
filePath: 'test.py',
|
||
language: SupportedLanguages.Python,
|
||
};
|
||
parser.setLanguage(Python);
|
||
const tree = parser.parse('print("hello")');
|
||
const exprStmt = tree.rootNode.child(0)!;
|
||
expect(exprStmt.type).toBe('expression_statement');
|
||
// extract() should return null because this is a call, not an assignment
|
||
const info = extractor.extract(exprStmt, ctx);
|
||
expect(info).toBeNull();
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Dart config — F29: top-level variable declarations
|
||
//
|
||
// Top-level Dart vars are loose siblings under `program` (no `declaration`
|
||
// wrapper). The structure query captures the container node
|
||
// (initialized_identifier_list / static_final_declaration_list) as
|
||
// @definition.variable, so the extractor is fed that container. The previous
|
||
// config read a `type_identifier` that does not exist as a direct child of the
|
||
// captured node — a dead read. These tests feed the real captured container.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('VariableExtractor — Dart (F29 top-level)', () => {
|
||
const extractor = createVariableExtractor(dartVariableConfig);
|
||
const ctx: VariableExtractorContext = {
|
||
filePath: 'test.dart',
|
||
language: SupportedLanguages.Dart,
|
||
};
|
||
|
||
/** The top-level variable container the structure query captures. */
|
||
function captureContainer(src: string): SyntaxNode {
|
||
parser.setLanguage(Dart);
|
||
const tree = parser.parse(src);
|
||
let found: SyntaxNode | undefined;
|
||
const walk = (n: SyntaxNode) => {
|
||
if (
|
||
(n.type === 'initialized_identifier_list' || n.type === 'static_final_declaration_list') &&
|
||
n.parent?.type === 'program'
|
||
) {
|
||
found ??= n;
|
||
}
|
||
for (let i = 0; i < n.namedChildCount; i++) {
|
||
const c = n.namedChild(i);
|
||
if (c) walk(c);
|
||
}
|
||
};
|
||
walk(tree.rootNode);
|
||
if (!found) throw new Error('no top-level variable container found');
|
||
return found;
|
||
}
|
||
|
||
it('extracts a typed final top-level variable with the real type (F29)', () => {
|
||
const node = captureContainer('final int count = 3;');
|
||
expect(extractor.isVariableDeclaration(node)).toBe(true);
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('count');
|
||
expect(info!.type).toBe('int');
|
||
expect(info!.isConst).toBe(true);
|
||
expect(info!.isMutable).toBe(false);
|
||
expect(info!.scope).toBe('module');
|
||
});
|
||
|
||
it('extracts an inferred var as untyped (not from a phantom field) (F29)', () => {
|
||
const node = captureContainer("var name = 'x';");
|
||
const info = extractor.extract(node, ctx);
|
||
expect(info).not.toBeNull();
|
||
expect(info!.name).toBe('name');
|
||
// `var` is inferred → no type annotation, so type is null (not a phantom).
|
||
expect(info!.type).toBeNull();
|
||
expect(info!.isMutable).toBe(true);
|
||
expect(info!.isConst).toBe(false);
|
||
});
|
||
|
||
it('extracts both names from a multi-name top-level const (F29)', () => {
|
||
const node = captureContainer('const a = 1, b = 2;');
|
||
const infos = extractor.extractAll(node, ctx);
|
||
expect(infos.map((i) => i.name).sort()).toEqual(['a', 'b']);
|
||
for (const i of infos) {
|
||
expect(i.isConst).toBe(true);
|
||
expect(i.isMutable).toBe(false);
|
||
}
|
||
});
|
||
|
||
it('does not let a neighbouring declaration’s type/modifier bleed in', () => {
|
||
// Three declarations under one program. The `var name` container sits after
|
||
// `final int count` — its type/modifier must NOT leak onto `name`.
|
||
parser.setLanguage(Dart);
|
||
const tree = parser.parse("final int count = 3;\nvar name = 'x';\nconst a = 1, b = 2;");
|
||
const containers: SyntaxNode[] = [];
|
||
const walk = (n: SyntaxNode) => {
|
||
if (
|
||
(n.type === 'initialized_identifier_list' || n.type === 'static_final_declaration_list') &&
|
||
n.parent?.type === 'program'
|
||
) {
|
||
containers.push(n);
|
||
}
|
||
for (let i = 0; i < n.namedChildCount; i++) {
|
||
const c = n.namedChild(i);
|
||
if (c) walk(c);
|
||
}
|
||
};
|
||
walk(tree.rootNode);
|
||
|
||
const byName = new Map<string, ReturnType<typeof extractor.extractAll>[number]>();
|
||
for (const c of containers)
|
||
for (const info of extractor.extractAll(c, ctx)) byName.set(info.name, info);
|
||
|
||
expect(byName.get('name')!.type).toBeNull(); // not 'int' from count
|
||
expect(byName.get('name')!.isConst).toBe(false); // not final from count
|
||
expect(byName.get('count')!.type).toBe('int');
|
||
expect(byName.get('a')!.isConst).toBe(true);
|
||
expect(byName.get('b')!.isConst).toBe(true);
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Kotlin destructuring declarations (F51, issue #1919)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const describeKotlin = Kotlin ? describe : describe.skip;
|
||
|
||
describeKotlin('VariableExtractor — Kotlin (F51 destructuring)', () => {
|
||
const extractor = createVariableExtractor(kotlinVariableConfig);
|
||
const ctx: VariableExtractorContext = {
|
||
filePath: 'test.kt',
|
||
language: SupportedLanguages.Kotlin,
|
||
};
|
||
|
||
/** The first property_declaration whose text starts with `prefix`. */
|
||
function propertyDecl(src: string, prefix: string): SyntaxNode {
|
||
parser.setLanguage(Kotlin);
|
||
const tree = parser.parse(src);
|
||
let found: SyntaxNode | undefined;
|
||
const walk = (n: SyntaxNode) => {
|
||
if (n.type === 'property_declaration' && n.text.trimStart().startsWith(prefix)) {
|
||
found ??= n;
|
||
}
|
||
for (let i = 0; i < n.namedChildCount; i++) {
|
||
const c = n.namedChild(i);
|
||
if (c) walk(c);
|
||
}
|
||
};
|
||
walk(tree.rootNode);
|
||
if (!found) throw new Error(`no property_declaration starting with ${prefix}`);
|
||
return found;
|
||
}
|
||
|
||
it('emits one Variable per destructured name (`val (a, b) = pair`)', () => {
|
||
const node = propertyDecl('fun f() { val (a, b) = pair }', 'val (a');
|
||
const infos = extractor.extractAll(node, ctx);
|
||
expect(infos.map((i) => i.name)).toEqual(['a', 'b']);
|
||
});
|
||
|
||
it('skips the `_` discard placeholder (`val (_, second) = pair`)', () => {
|
||
const node = propertyDecl('fun f() { val (_, second) = pair }', 'val (_');
|
||
const infos = extractor.extractAll(node, ctx);
|
||
expect(infos.map((i) => i.name)).toEqual(['second']);
|
||
});
|
||
|
||
it('still emits exactly one name for a plain `val x = 1` (no double-count)', () => {
|
||
const node = propertyDecl('fun f() { val x = 1 }', 'val x');
|
||
const infos = extractor.extractAll(node, ctx);
|
||
expect(infos.map((i) => i.name)).toEqual(['x']);
|
||
});
|
||
});
|