GitNexus/gitnexus/test/unit/variable-extraction.test.ts
Copilot ed5a4220dd
feat(ingestion): language-agnostic variable extractor with config+factory pattern (#878)
* 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>
2026-04-16 13:57:25 +01:00

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();
});
});