mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* Initial plan * feat(ingestion): add variable extraction types, factory, configs, and wire into language providers - Create variable-types.ts with VariableInfo, VariableExtractionConfig, VariableExtractor interfaces - Create variable-extractors/generic.ts with createVariableExtractor() factory - Add variableExtractor field to LanguageProvider interface - Create per-language variable extraction configs for all 16 languages - Wire variableExtractor into all language providers - Add variable metadata enrichment to parse-worker for Const/Static/Variable labels Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/3cb85c68-1792-473e-9a46-ea2588da0e5e Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * feat(ingestion): add variable extraction tests and fix Python/TS config issues - Create test/unit/variable-extraction.test.ts with 29 tests covering TypeScript, JavaScript, Python, Go, Rust, C, C++, Ruby, and factory behavior - Fix isConst in generic factory to use config.isConst over node-type membership (TS let/const both use lexical_declaration) - Fix Python type extraction for annotated assignments at module scope - Fix Python dunder name visibility (e.g., __name__ is public, not protected) Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/3cb85c68-1792-473e-9a46-ea2588da0e5e Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * fix: address code review feedback — move imports, clarify scope comment, use shared test context Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/3cb85c68-1792-473e-9a46-ea2588da0e5e Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * fix: address review comments, fix prettier formatting and lint errors - Fix prettier formatting in 5 files (c-cpp, jvm, swift configs, test file) - Remove unused SyntaxNode imports in php.ts and ruby.ts (lint errors) - Remove unused constNodeSet/variableNodeSet variables in generic.ts (warnings) - Remove semantically wrong `methodProps.isReadonly = varInfo.isConst` (review) - Remove dead `nodeLabel === 'Variable'` guard in parse-worker (review) - Fix test guard: replace `if (declNode)` with `expect(declNode).toBeDefined()` (review) - Add comment about Python expression_statement broadness (review) Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/040edbbf-65b5-40e1-80c8-e98f7c4bb54a * feat(ingestion): add block-scoped variable extraction via tree-sitter queries Add @definition.const and @definition.variable tree-sitter query patterns for TypeScript, JavaScript, Python, Go, Java, C, C++, C#, PHP, Ruby, and Dart. Add parse-worker dedup logic to avoid duplicate nodes when variable captures overlap with existing function/property captures. Add 'Variable' label support in getLabelFromCaptures and DEFINITION_CAPTURE_KEYS. Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/9fa828c1-87b7-4482-8f26-d2079fb4c58a Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * test: add block-scoped variable extraction tests and query capture tests Add 6 tests for block-scoped variable extraction (TypeScript, Go, Rust, C, Python). Add 14 tests verifying @definition.const/@definition.variable query patterns exist in all language query strings. Import RUBY_QUERIES in test file. Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/9fa828c1-87b7-4482-8f26-d2079fb4c58a Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * test: add Python non-assignment expression statement rejection test Addresses code review feedback: verify that the Python variable extractor returns null for expression_statement nodes that contain function calls rather than assignments (e.g. `print("hello")`). Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/9fa828c1-87b7-4482-8f26-d2079fb4c58a Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * fix: Dart query node type, add Variable schema, update schema counts - Change `top_level_variable_declaration` → `declaration` in DART_QUERIES (the former doesn't exist in tree-sitter-dart grammar, causing all Dart integration tests to fail with TSQueryErrorNodeType) - Add VARIABLE_SCHEMA to schema.ts and register in initLbug() so that Variable-labeled nodes are persisted to LadybugDB (not silently dropped) - Add 'Variable' to MULTI_LANG_TYPES in csv-generator.ts - Update Dart variable config to remove invalid node type - Update schema test counts (30→31 node schemas, 32→33 total) Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/f79931d1-207f-4fbb-91da-259d44f7fd88 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * fix: address code review comment improvements - Clarify processedDefinitionNodes tracks start indices, not nodes - Improve Python variableNodeTypes comment wording Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/f79931d1-207f-4fbb-91da-259d44f7fd88 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * fix: add Variable to NODE_TABLES, RELATION_SCHEMA, update golden snapshot - Add 'Variable' to NODE_TABLES in gitnexus-shared so validTables.has('Variable') returns true and Variable graph edges are not silently dropped - Add FROM File TO Variable, FROM Variable TO Community, FROM Variable TO Process to RELATION_SCHEMA so KuzuDB can represent edges connecting Variable nodes - Update schema.test.ts: add Variable to multiLang list, fix count 30→31 - Regenerate pipeline-graph-golden snapshot for mini-repo fixture Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e3aad558-e7bb-40d1-b53f-0a2c0132ca96 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * fix: isolate golden test from cli-e2e fixture pollution The pipeline-graph-golden test was non-deterministic because cli-e2e.test.ts creates AGENTS.md, CLAUDE.md, .claude/skills/, and .gitignore in the shared mini-repo fixture during analyze. These leftover files caused the golden test to find 9 files instead of 7 when tests ran in parallel. Fixes: - Golden test now copies the fixture to a temp dir before running, making it immune to concurrent test pollution - cli-e2e afterAll cleanup now removes ALL generated files (AGENTS.md, CLAUDE.md, .claude/, .gitignore) not just .git/ and .gitnexus/ - Golden snapshot regenerated from clean 7-file fixture Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/bd378e73-6f37-49c6-aed6-7fabf4dc6183 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
633 lines
23 KiB
TypeScript
633 lines
23 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 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 C from 'tree-sitter-c';
|
|
import Ruby from 'tree-sitter-ruby';
|
|
|
|
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('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);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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 }');
|
|
// source_file > function_declaration > block > short_var_declaration
|
|
const funcDecl = tree.rootNode.namedChildren.find((c) => c.type === 'function_declaration')!;
|
|
const body = funcDecl.childForFieldName('body')!;
|
|
const shortVarDecl = body.namedChildren.find((c) => c.type === 'short_var_declaration');
|
|
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: 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();
|
|
});
|
|
});
|