mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-20 00:11:37 +00:00
* fix(c): skip computed #include MACRO instead of emitting a garbage import source (F5) * fix(cpp): emit a Variable per name for structured-binding declarations (F9) * fix(dart): extract static const/final class fields (F26) * fix(dart): capture old-style function typedefs (F28) * fix(dart): read real top-level variable shape instead of a dead type field (F29) * fix(kotlin): capture callable references (F47) * fix(kotlin): anchor infix-call capture to the operator only (F49) * fix(kotlin): extract secondary constructors as members (F48) * fix(kotlin): capture destructuring declarations (F51) * fix(kotlin): index companion-object properties as fields (F52) * test(kotlin): assert callable-reference coverage runs on the worker path (F47) * fix(swift): extract protocol property requirements (F75) * fix(swift): recognize enum_class_body as a method body node (F79) * test(ingestion): rebaseline swift captures-golden + scope-capture fingerprints (#1919) * fix(kotlin): attribute secondary-constructor body calls to the Constructor node (#1919 review CF1) A Kotlin secondary constructor's body executes statements like a method body, but the registry-primary scope-resolution path had no Function scope or Constructor def for it. A call inside the body resolved its caller anchor up to the enclosing Class scope, mis-attributing the CALLS edge to the class rather than the Constructor. Add `(secondary_constructor) @scope.function` to the Kotlin scope query so the body becomes its own scope, and synthesize a `@declaration.constructor` (named `constructor`, qualified `<Class>.constructor`, with parameter metadata) so the scope owns a Constructor def that bridges to the structure-phase Constructor node. Also add an arity-disambiguating lookup key for overloadable callables: two same-name secondary constructors of different arity (e.g. a zero-arg vs a 2-arg) share the qualified key whose first-write-wins assignment is source-order- dependent — so a zero-arg overload could resolve to a sibling. The structure node id encodes `#<arity>`; mirror that in the bridge keyspace and match by the def's parameterCount. Same-arity overloads collapse onto one arity key exactly as before, so no regression there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(kotlin): do not own function-local property bindings under the enclosing class (#1919 review CF3) Kotlin emits destructuring / loop bindings (`val (a,b) = pair`, `for ((k,v) in m)`) as `@definition.property` to dodge the block-scope local-symbol pruner. When such a binding sits inside a method body of a class, the structure-phase owner walk found the enclosing class and emitted a spurious HAS_PROPERTY edge (e.g. `C -> k`), treating a function-local as a class member. Guard the Property owner resolution: if a function-like ancestor is reached before any class container, the property is function-local and gets no owner edge (it falls back to a File DEFINES edge). Language-agnostic — genuine class fields sit directly in the class body with no intervening function, so they keep their HAS_PROPERTY owner edge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(kotlin): guard non-companion property isStatic=false (#1919 review CF4) Add a field-extraction case for a plain non-companion class `class C { val x: Int = 1 }` asserting the property `x` has isStatic=false, guarding the `isInsideKotlinCompanion` walk against false-positives. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(kotlin): dedup type_identifier lookup in extractOwnerName (#1919 review CF5) The `node.namedChildren.find(c => c.type === 'type_identifier')?.text` lookup was duplicated across the companion and non-companion branches of the Kotlin field-extractor's extractOwnerName. Hoist it into a single local, preserving the existing behavior (anonymous companion falls back to "Companion"; other nodes prefer the `name` field, else the type_identifier text, else undefined). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dart): capture generic old-style function typedefs (#1919 review CF2) * test(dart): guard multi-name field count and top-level-var labels (#1919 review CF4) * docs(swift): correct isStatic comment re multi-modifier hasKeyword (#1919 review CF5) * test(ingestion): rebaseline dart+kotlin scope-capture fingerprints after review remediation (#1919) * fix(ingestion): correct CF3 owner-strip boundary set for accessor/init bodies and Dart signatures (#1919 review) The CF3 property-ownership guard used FUNCTION_NODE_TYPES, which (a) includes Dart bare signatures (function_signature/method_signature) — over-stripping every Dart class getter/setter's HAS_PROPERTY owner — and (b) omits Kotlin anonymous_initializer/getter/setter and Swift computed accessors — under- stripping destructuring/locals inside init{} and accessor bodies, emitting spurious Class->local HAS_PROPERTY edges. Introduces a guard-specific LOCAL_SCOPE_BODY_NODE_TYPES set (signatures excluded, accessor/init bodies included). Adds Dart accessor-ownership + Kotlin init/accessor destructuring regression fixtures. Both confirmed on the worker pipeline; no cross-language regression (1597 cross-language tests green). --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
928 lines
35 KiB
TypeScript
928 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 C from 'tree-sitter-c';
|
||
import Ruby from 'tree-sitter-ruby';
|
||
import Dart from 'tree-sitter-dart';
|
||
|
||
let Kotlin: unknown;
|
||
try {
|
||
Kotlin = require('tree-sitter-kotlin');
|
||
} catch {
|
||
// Kotlin grammar may not be installed
|
||
}
|
||
|
||
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 }');
|
||
// 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: 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 as Parser.Language);
|
||
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']);
|
||
});
|
||
});
|