GitNexus/gitnexus/test/unit/field-extraction.test.ts
Gergő Magyar 999cc9f913
fix(deps): upgrade native tree-sitter to 0.25.1 (#3531)
* fix(deps): upgrade native tree-sitter to 0.25.1

* fix(tree-sitter): preserve sync loading and update verified capture baselines

Route C# scope queries through the synchronous native grammar loader so the compiled analyze module remains require-compatible on Node 22.18. Refresh only the audited empty-Python-module and renamed-PHP-fixture captures, and pin the runtime assertion to 0.25.1.

Validated 479 targeted tests, all 16 scope-capture benchmark cases, Python capture/import guards, compiled C# worker parsing, build, typecheck and formatting. Performance budgets are unchanged.

* fix(tree-sitter): refresh upstream vendors and preserve parser semantics

* fix(ci): refresh prebuilds and support Windows ARM

Refresh macOS, Linux ARM, and Windows x64 grammar binaries from the successful native CI builds. Pin node-gyp 12.4.0 so the Windows ARM jobs recognize Visual Studio 2026.

Use local Git filtering for E2E to avoid PR-files API timeouts on generated parser diffs.

Validation: 143 focused tests, Actionlint, and all 16 scope-capture benchmark checks passed. Full npm test run started; native database suites still running.

* fix(ci): refresh Windows ARM grammar prebuilds

Import all seven Windows ARM grammar artifacts from successful native build run 37965761760. Artifact filenames and SHA-256 hashes verified; all 42 native build jobs passed.

Validation: 31 prebuild coverage tests passed.

* fix(bench): stabilize scope capture timing guard

Warm each input size seven times and use 21 median samples while retaining isolated timing blocks. Yield between languages so native finalizers release allocations outside the measured sections. Keep all fixture sizes, fingerprints, scaling formulas, and budgets unchanged.

Validation: all 16 language guards passed with a 2 GB heap limit (1.38 deep-chain ratio; 1518 MB peak RSS). Linear and quadratic controls passed their expected guard outcomes. Formatting and graph change analysis passed. Full npm test is running without failures so far.

* fix(parser): reduce TypeScript capture traversal cost

* fix(test): await parser worker readiness before teardown

* fix(parser): load C# synchronously in workers

* fix(grpc): resolve native proto detections against their own file

With the native proto grammar active, sibling .proto files that declare
the same short service name under different packages tied in the
directory-based resolveProtoConflict heuristic and both providers were
dropped. A proto-origin detection already knows its defining file, so
use that file's entry before falling back to the heuristic.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci(packaging): assert native grammars load from the installed tarball

The packaged-install smoke only ran `gitnexus --version`, so a grammar
that failed to load from the published package (bundled runtime, patched
peer manifests, vendored prebuilds, C# sync loader, Protobuf wrapper)
would ship silently; the dev-tree ABI smoke also accepts vendored
grammars that report unavailable. Parse a sample for every registered
grammar plus the native Protobuf plugin from the installed package, on
the main thread and in a worker thread.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci(tree-sitter): track compatibility workarounds instead of 0.25 readiness

The daily readiness report still targeted tree-sitter@0.25.0, so after the
upgrade it would keep reporting "Blocked" and never close its tracking issue.
Repurpose it to track what we carry for the bundled runtime: derive the target
from the package.json pin, list each pack-time peer patch from
prepare-tree-sitter-bundle.cjs with its drop condition (bump, wait for an
upstream release, transitive, or intentionally pinned), and reword the
buckets. The workflow adopts the issue opened under the old title.

Also update the grammar-update-monitor header and auto-PR text, which still
described the 0.21.1 runtime and a tree-sitter-c hold.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(dart): assert both nested labels target the loop; fix Zig call-shape doc

The nested-label CFG test only jumped to `outer` and checked reachability,
which still passes when a jump is retargeted to the inner loop (its exit
reaches the outer header). Jump to both labels and assert the exact
`continue` edges; dropping the inner label now fails the test.

Update the Zig callable-flow comment: call_expression now wraps actuals in a
required `arguments:` field.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:07:59 +03:00

1503 lines
53 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import { TypeScriptFieldExtractor } from '../../src/core/ingestion/field-extractors/typescript.js';
import { createFieldExtractor } from '../../src/core/ingestion/field-extractors/generic.js';
import { typescriptConfig } from '../../src/core/ingestion/field-extractors/configs/typescript-javascript.js';
import { pythonConfig } from '../../src/core/ingestion/field-extractors/configs/python.js';
import { goConfig } from '../../src/core/ingestion/field-extractors/configs/go.js';
import { cppConfig } from '../../src/core/ingestion/field-extractors/configs/c-cpp.js';
import { rubyConfig } from '../../src/core/ingestion/field-extractors/configs/ruby.js';
import { dartConfig } from '../../src/core/ingestion/field-extractors/configs/dart.js';
import { javaConfig, kotlinConfig } from '../../src/core/ingestion/field-extractors/configs/jvm.js';
import { swiftConfig } from '../../src/core/ingestion/field-extractors/configs/swift.js';
import type { FieldExtractorContext } from '../../src/core/ingestion/field-types.js';
import type { TypeEnvironment } from '../../src/core/ingestion/type-env.js';
import { createSemanticModel } from '../../src/core/ingestion/model/semantic-model.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 Cpp from 'tree-sitter-cpp';
import Ruby from 'tree-sitter-ruby';
import Java from 'tree-sitter-java';
import CSharp from 'tree-sitter-c-sharp/bindings/node/index.js';
import { requireVendoredGrammar } from '../../src/core/tree-sitter/vendored-grammars.js';
// Vendored grammars — loaded from vendor/ by absolute path, never node_modules (#2111).
const Dart = requireVendoredGrammar('tree-sitter-dart');
let Kotlin: Parser.Language | null = null;
try {
Kotlin = requireVendoredGrammar('tree-sitter-kotlin');
} catch {
// Kotlin grammar may not have a prebuild for this platform
}
let Swift: Parser.Language | null = null;
try {
Swift = requireVendoredGrammar('tree-sitter-swift');
} catch {
// Swift grammar may not have a prebuild for this platform
}
import { csharpConfig as csharpFieldConfig } from '../../src/core/ingestion/field-extractors/configs/csharp.js';
import { SupportedLanguages } from '../../src/config/supported-languages.js';
const parser = new Parser();
const parse = (code: string) => {
parser.setLanguage(TypeScript.typescript);
return parser.parse(code);
};
// Mock context for tests. symbolTable comes from createSemanticModel().symbols
// (the facade) rather than createSymbolTable() (the raw leaf) — this mirrors
// production, where FieldExtractorContext always receives the SemanticModel-
// wrapped facade so any .add() write dispatches through the owner-scoped
// registries. No current field extractor calls symbolTable.add(), but
// matching the production shape prevents silent drift if a future extractor
// starts registering dynamically-discovered properties.
const createMockContext = (): FieldExtractorContext => ({
typeEnv: {
lookup: () => undefined,
constructorBindings: [],
fileScope: () => new Map(),
allScopes: () => new Map(),
constructorTypeMap: new Map(),
flush: () => {},
} satisfies TypeEnvironment,
symbolTable: createSemanticModel().symbols,
filePath: 'test.ts',
language: SupportedLanguages.TypeScript,
});
describe('TypeScriptFieldExtractor', () => {
let extractor: TypeScriptFieldExtractor;
let mockContext: FieldExtractorContext;
beforeEach(() => {
extractor = new TypeScriptFieldExtractor();
mockContext = createMockContext();
});
describe('isTypeDeclaration', () => {
it('recognizes class_declaration', () => {
const tree = parse('class User {}');
const classNode = tree.rootNode.child(0);
expect(classNode).toBeDefined();
expect(extractor.isTypeDeclaration(classNode!)).toBe(true);
});
it('recognizes interface_declaration', () => {
const tree = parse('interface IUser {}');
const interfaceNode = tree.rootNode.child(0);
expect(interfaceNode).toBeDefined();
expect(extractor.isTypeDeclaration(interfaceNode!)).toBe(true);
});
it('recognizes abstract_class_declaration', () => {
const tree = parse('abstract class BaseService {}');
const abstractNode = tree.rootNode.child(0);
expect(abstractNode).toBeDefined();
expect(extractor.isTypeDeclaration(abstractNode!)).toBe(true);
});
it('rejects function_declaration', () => {
const tree = parse('function getUser() {}');
const functionNode = tree.rootNode.child(0);
expect(functionNode).toBeDefined();
expect(extractor.isTypeDeclaration(functionNode!)).toBe(false);
});
it('rejects variable declaration', () => {
const tree = parse('const user = {};');
const variableNode = tree.rootNode.child(0);
expect(variableNode).toBeDefined();
expect(extractor.isTypeDeclaration(variableNode!)).toBe(false);
});
});
describe('extract', () => {
it('extracts single field with type', () => {
const tree = parse(`
class User {
name: string;
}
`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.ownerFqn).toBe('User');
expect(result!.fields).toHaveLength(1);
expect(result!.fields[0].name).toBe('name');
expect(result!.fields[0].type).toBe('string');
expect(result!.fields[0].visibility).toBe('public');
});
it('extracts private field', () => {
const tree = parse(`
class User {
private password: string;
}
`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(1);
expect(result!.fields[0].name).toBe('password');
expect(result!.fields[0].type).toBe('string');
expect(result!.fields[0].visibility).toBe('private');
});
it('extracts static readonly field', () => {
const tree = parse(`
class Config {
static readonly VERSION: string = '1.0';
}
`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(1);
expect(result!.fields[0].name).toBe('VERSION');
expect(result!.fields[0].type).toBe('string');
expect(result!.fields[0].isStatic).toBe(true);
expect(result!.fields[0].isReadonly).toBe(true);
expect(result!.fields[0].visibility).toBe('public');
});
it('extracts optional field (?:)', () => {
const tree = parse(`
interface User {
email?: string;
}
`);
const interfaceNode = tree.rootNode.child(0);
const result = extractor.extract(interfaceNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(1);
expect(result!.fields[0].name).toBe('email');
// Note: optional fields may have type modified to include undefined
expect(result!.fields[0].type).toContain('string');
});
it('extracts multiple fields with different visibilities', () => {
const tree = parse(`
class User {
public id: number;
private secretKey: string;
protected createdAt: Date;
name: string;
}
`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(4);
const fields = result!.fields;
const idField = fields.find((f) => f.name === 'id');
expect(idField).toBeDefined();
expect(idField!.visibility).toBe('public');
expect(idField!.type).toBe('number');
const secretKeyField = fields.find((f) => f.name === 'secretKey');
expect(secretKeyField).toBeDefined();
expect(secretKeyField!.visibility).toBe('private');
const createdAtField = fields.find((f) => f.name === 'createdAt');
expect(createdAtField).toBeDefined();
expect(createdAtField!.visibility).toBe('protected');
const nameField = fields.find((f) => f.name === 'name');
expect(nameField).toBeDefined();
expect(nameField!.visibility).toBe('public'); // default
});
it('handles field without type annotation', () => {
const tree = parse(`
class User {
name;
age;
}
`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(2);
const nameField = result!.fields.find((f) => f.name === 'name');
expect(nameField).toBeDefined();
expect(nameField!.type).toBeNull();
const ageField = result!.fields.find((f) => f.name === 'age');
expect(ageField).toBeDefined();
expect(ageField!.type).toBeNull();
});
it('extracts complex generic types (Map<string, User>, Array<number>)', () => {
const tree = parse(`
class Repository {
users: Map<string, User>;
ids: Array<number>;
}
`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(2);
const usersField = result!.fields.find((f) => f.name === 'users');
expect(usersField).toBeDefined();
expect(usersField!.type).toBe('Map<string, User>');
const idsField = result!.fields.find((f) => f.name === 'ids');
expect(idsField).toBeDefined();
expect(idsField!.type).toBe('Array<number>');
});
it('extracts nested types', () => {
const tree = parse(`
class Container {
data: OuterType<InnerType<string>>;
}
`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(1);
expect(result!.fields[0].name).toBe('data');
expect(result!.fields[0].type).toBe('OuterType<InnerType<string>>');
});
it('extracts fields from interface', () => {
const tree = parse(`
interface UserDTO {
id: number;
name: string;
email?: string;
}
`);
const interfaceNode = tree.rootNode.child(0);
const result = extractor.extract(interfaceNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.ownerFqn).toBe('UserDTO');
expect(result!.fields).toHaveLength(3);
const idField = result!.fields.find((f) => f.name === 'id');
expect(idField).toBeDefined();
expect(idField!.type).toBe('number');
const nameField = result!.fields.find((f) => f.name === 'name');
expect(nameField).toBeDefined();
expect(nameField!.type).toBe('string');
const emailField = result!.fields.find((f) => f.name === 'email');
expect(emailField).toBeDefined();
});
it('extracts fields from abstract class', () => {
const tree = parse(`
abstract class BaseEntity {
protected id: number;
createdAt: Date;
}
`);
const abstractNode = tree.rootNode.child(0);
const result = extractor.extract(abstractNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.ownerFqn).toBe('BaseEntity');
expect(result!.fields).toHaveLength(2);
const idField = result!.fields.find((f) => f.name === 'id');
expect(idField).toBeDefined();
expect(idField!.visibility).toBe('protected');
const createdAtField = result!.fields.find((f) => f.name === 'createdAt');
expect(createdAtField).toBeDefined();
expect(createdAtField!.visibility).toBe('public');
});
it('extracts array types', () => {
const tree = parse(`
class UserService {
users: User[];
ids: number[];
}
`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(2);
const usersField = result!.fields.find((f) => f.name === 'users');
expect(usersField).toBeDefined();
expect(usersField!.type).toBe('User[]');
const idsField = result!.fields.find((f) => f.name === 'ids');
expect(idsField).toBeDefined();
expect(idsField!.type).toBe('number[]');
});
it('extracts union types', () => {
const tree = parse(`
class Field {
value: string | number | null;
}
`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(1);
expect(result!.fields[0].name).toBe('value');
expect(result!.fields[0].type).toBe('string | number | null');
});
it('returns null for non-type declaration nodes', () => {
const tree = parse('function getUser() {}');
const functionNode = tree.rootNode.child(0);
const result = extractor.extract(functionNode!, mockContext);
expect(result).toBeNull();
});
it('extracts fields from type alias with object type', () => {
const tree = parse(`
type UserDTO = {
id: number;
name: string;
}
`);
const typeAliasNode = tree.rootNode.child(0);
const result = extractor.extract(typeAliasNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.ownerFqn).toBe('UserDTO');
expect(result!.fields).toHaveLength(2);
const idField = result!.fields.find((f) => f.name === 'id');
expect(idField).toBeDefined();
expect(idField!.type).toBe('number');
const nameField = result!.fields.find((f) => f.name === 'name');
expect(nameField).toBeDefined();
expect(nameField!.type).toBe('string');
});
it('includes source file path in field info', () => {
const tree = parse(`
class User {
name: string;
}
`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(1);
expect(result!.fields[0].sourceFile).toBe('test.ts');
});
it('includes line number in field info', () => {
const tree = parse(`
class User {
name: string;
}
`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(1);
expect(result!.fields[0].line).toBeGreaterThan(0);
});
it('detects nested interface declarations in methods', () => {
const tree = parse(`
class Container {
data: string;
process() {
interface LocalInterface {
value: number;
}
}
}
`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.ownerFqn).toBe('Container');
// Note: Nested types within method bodies are detected
expect(result!.nestedTypes).toContain('LocalInterface');
// Should only extract fields from the outer class
expect(result!.fields).toHaveLength(1);
expect(result!.fields[0].name).toBe('data');
});
});
});
// ---------------------------------------------------------------------------
// Generic factory tests — TypeScript config
// ---------------------------------------------------------------------------
describe('GenericFieldExtractor — TypeScript config', () => {
const parser = new Parser();
const extractor = createFieldExtractor(typescriptConfig);
const mockContext = createMockContext();
mockContext.language = SupportedLanguages.TypeScript;
it('extracts public and private fields from a class', () => {
parser.setLanguage(TypeScript.typescript);
const tree = parser.parse(`
class User {
public name: string;
private age: number;
}
`);
const classNode = tree.rootNode.child(0);
expect(classNode).toBeDefined();
expect(extractor.isTypeDeclaration(classNode!)).toBe(true);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.ownerFqn).toBe('User');
expect(result!.fields).toHaveLength(2);
const nameField = result!.fields.find((f) => f.name === 'name');
expect(nameField).toBeDefined();
expect(nameField!.visibility).toBe('public');
expect(nameField!.type).toBe('string');
const ageField = result!.fields.find((f) => f.name === 'age');
expect(ageField).toBeDefined();
expect(ageField!.visibility).toBe('private');
expect(ageField!.type).toBe('number');
});
it('uses body-discovery fallback when body type does not match config', () => {
parser.setLanguage(TypeScript.typescript);
// interface_declaration has an interface_body — which IS in the config bodyNodeTypes
// but type_alias_declaration has an object_type body which is also in bodyNodeTypes
const tree = parser.parse(`
interface Settings {
theme: string;
debug: boolean;
}
`);
const ifaceNode = tree.rootNode.child(0);
expect(ifaceNode).toBeDefined();
expect(extractor.isTypeDeclaration(ifaceNode!)).toBe(true);
const result = extractor.extract(ifaceNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.ownerFqn).toBe('Settings');
expect(result!.fields).toHaveLength(2);
expect(result!.fields.map((f) => f.name)).toContain('theme');
expect(result!.fields.map((f) => f.name)).toContain('debug');
});
it('returns null for non-type-declaration nodes', () => {
parser.setLanguage(TypeScript.typescript);
const tree = parser.parse('function hello() {}');
const fnNode = tree.rootNode.child(0);
expect(extractor.isTypeDeclaration(fnNode!)).toBe(false);
expect(extractor.extract(fnNode!, mockContext)).toBeNull();
});
it('extracts static and readonly modifiers', () => {
parser.setLanguage(TypeScript.typescript);
const tree = parser.parse(`
class Config {
static readonly MAX: number;
private count: number;
}
`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(2);
const maxField = result!.fields.find((f) => f.name === 'MAX');
expect(maxField).toBeDefined();
expect(maxField!.isStatic).toBe(true);
expect(maxField!.isReadonly).toBe(true);
const countField = result!.fields.find((f) => f.name === 'count');
expect(countField).toBeDefined();
expect(countField!.isStatic).toBe(false);
expect(countField!.isReadonly).toBe(false);
});
it('does not false-positive visibility on fields named after visibility keywords', () => {
parser.setLanguage(TypeScript.typescript);
// A class field literally named 'private' with no accessibility modifier —
// findVisibility must not confuse the name with a keyword
const tree = parser.parse(`
class Flags {
private: boolean;
}
`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(1);
expect(result!.fields[0].name).toBe('private');
// Default visibility is public — the field NAME 'private' must not be treated as a keyword
expect(result!.fields[0].visibility).toBe('public');
});
});
// ---------------------------------------------------------------------------
// Python config
// ---------------------------------------------------------------------------
describe('GenericFieldExtractor — Python', () => {
const parser = new Parser();
const extractor = createFieldExtractor(pythonConfig);
const mockContext = createMockContext();
mockContext.language = SupportedLanguages.Python;
mockContext.filePath = 'test.py';
it('extracts annotated class fields', () => {
parser.setLanguage(Python);
const tree = parser.parse(`class User:
name: str
email: str
`);
const classNode = tree.rootNode.child(0);
expect(classNode).toBeDefined();
expect(extractor.isTypeDeclaration(classNode!)).toBe(true);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.ownerFqn).toBe('User');
expect(result!.fields).toHaveLength(2);
const nameField = result!.fields.find((f) => f.name === 'name');
expect(nameField).toBeDefined();
expect(nameField!.type).toBe('str');
expect(nameField!.visibility).toBe('public');
const emailField = result!.fields.find((f) => f.name === 'email');
expect(emailField).toBeDefined();
expect(emailField!.type).toBe('str');
});
it('detects underscore-based visibility: _protected and __private', () => {
parser.setLanguage(Python);
const tree = parser.parse(`class Settings:
name: str
_internal: int
__secret: str
`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(3);
const nameField = result!.fields.find((f) => f.name === 'name');
expect(nameField).toBeDefined();
expect(nameField!.visibility).toBe('public');
const internalField = result!.fields.find((f) => f.name === '_internal');
expect(internalField).toBeDefined();
expect(internalField!.visibility).toBe('protected');
const secretField = result!.fields.find((f) => f.name === '__secret');
expect(secretField).toBeDefined();
expect(secretField!.visibility).toBe('private');
});
it('does not mark dunder attributes as private', () => {
parser.setLanguage(Python);
// __slots__ starts with __ but also ends with __ so the private check
// (startsWith('__') && !endsWith('__')) is skipped.
// However it still starts with _ so the config classifies it as protected.
const tree = parser.parse(`class Meta:
__slots__: list
`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(1);
// __slots__ ends with __ → not private, but still starts with _ → protected
expect(result!.fields[0].visibility).toBe('protected');
});
it('reports isStatic and isReadonly as false', () => {
parser.setLanguage(Python);
const tree = parser.parse(`class A:
x: int
`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields[0].isStatic).toBe(false);
expect(result!.fields[0].isReadonly).toBe(false);
});
});
// ---------------------------------------------------------------------------
// Go config
// ---------------------------------------------------------------------------
describe('GenericFieldExtractor — Go', () => {
const parser = new Parser();
const extractor = createFieldExtractor(goConfig);
const mockContext = createMockContext();
mockContext.language = SupportedLanguages.Go;
mockContext.filePath = 'test.go';
// Helper: Go AST nests name under type_spec, not type_declaration.
// The generic factory's extract() requires a node with a 'name' field,
// so we walk to type_spec which holds the name.
function findTypeSpec(src: string) {
parser.setLanguage(Go);
const tree = parser.parse(src);
const typeDecl = tree.rootNode.child(0)!;
return { typeDecl, typeSpec: typeDecl.namedChild(0)! };
}
it('recognizes type_declaration via isTypeDeclaration', () => {
const { typeDecl } = findTypeSpec(`type User struct {\n\tName string\n}`);
expect(typeDecl.type).toBe('type_declaration');
expect(extractor.isTypeDeclaration(typeDecl)).toBe(true);
});
it('rejects non-type-declaration nodes', () => {
parser.setLanguage(Go);
const tree = parser.parse(`func main() {}`);
const fnNode = tree.rootNode.child(0);
expect(extractor.isTypeDeclaration(fnNode!)).toBe(false);
expect(extractor.extract(fnNode!, mockContext)).toBeNull();
});
it('detects uppercase-based visibility via extractVisibility on field nodes', () => {
const { typeDecl } = findTypeSpec(
`type Config struct {\n\tHost string\n\tport int\n\tTimeout int\n}`,
);
// Navigate to field_declaration nodes inside the struct
// type_declaration > type_spec > struct_type > field_declaration_list > field_declaration
const typeSpec = typeDecl.namedChild(0)!;
const structType = typeSpec.namedChild(1)!;
const fieldList = structType.namedChild(0)!;
expect(fieldList.type).toBe('field_declaration_list');
const hostNode = fieldList.namedChild(0)!;
const portNode = fieldList.namedChild(1)!;
const timeoutNode = fieldList.namedChild(2)!;
expect(goConfig.extractName(hostNode)).toBe('Host');
expect(goConfig.extractVisibility(hostNode)).toBe('public');
expect(goConfig.extractName(portNode)).toBe('port');
expect(goConfig.extractVisibility(portNode)).toBe('package');
expect(goConfig.extractName(timeoutNode)).toBe('Timeout');
expect(goConfig.extractVisibility(timeoutNode)).toBe('public');
});
it('extracts field types', () => {
const { typeDecl } = findTypeSpec(`type Point struct {\n\tX float64\n\tY float64\n}`);
const typeSpec = typeDecl.namedChild(0)!;
const structType = typeSpec.namedChild(1)!;
const fieldList = structType.namedChild(0)!;
const xNode = fieldList.namedChild(0)!;
expect(goConfig.extractName(xNode)).toBe('X');
expect(goConfig.extractType(xNode)).toBe('float64');
});
it('extracts every name from Go multi-name struct fields', () => {
const { typeDecl } = findTypeSpec(`type Point struct {\n\tX, y int\n}`);
const result = extractor.extract(typeDecl, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(2);
const xField = result!.fields.find((field) => field.name === 'X');
expect(xField).toBeDefined();
expect(xField!.type).toBe('int');
expect(xField!.visibility).toBe('public');
const yField = result!.fields.find((field) => field.name === 'y');
expect(yField).toBeDefined();
expect(yField!.type).toBe('int');
expect(yField!.visibility).toBe('package');
});
it('extracts Go multi-name struct fields when called with the runtime struct_type owner', () => {
const { typeDecl } = findTypeSpec(`type Point struct {\n\tX, y int\n}`);
const typeSpec = typeDecl.namedChild(0)!;
const structType = typeSpec.namedChild(1)!;
expect(structType.type).toBe('struct_type');
expect(extractor.isTypeDeclaration(structType)).toBe(true);
const result = extractor.extract(structType, mockContext);
expect(result).not.toBeNull();
expect(result!.ownerFqn).toBe('Point');
expect(result!.fields).toHaveLength(2);
const xField = result!.fields.find((field) => field.name === 'X');
expect(xField).toBeDefined();
expect(xField!.type).toBe('int');
expect(xField!.visibility).toBe('public');
const yField = result!.fields.find((field) => field.name === 'y');
expect(yField).toBeDefined();
expect(yField!.type).toBe('int');
expect(yField!.visibility).toBe('package');
});
it('reports isStatic and isReadonly as false for all fields', () => {
const { typeDecl } = findTypeSpec(`type S struct {\n\tX int\n}`);
const typeSpec = typeDecl.namedChild(0)!;
const structType = typeSpec.namedChild(1)!;
const fieldList = structType.namedChild(0)!;
const fieldNode = fieldList.namedChild(0)!;
expect(goConfig.isStatic(fieldNode)).toBe(false);
expect(goConfig.isReadonly(fieldNode)).toBe(false);
});
});
// ---------------------------------------------------------------------------
// C++ config
// ---------------------------------------------------------------------------
describe('GenericFieldExtractor — C++', () => {
const parser = new Parser();
const extractor = createFieldExtractor(cppConfig);
const mockContext = createMockContext();
mockContext.language = SupportedLanguages.CPlusPlus;
mockContext.filePath = 'test.cpp';
it('extracts fields from a class with access specifiers', () => {
parser.setLanguage(Cpp);
const tree = parser.parse(`class User {
public:
int id;
std::string name;
private:
std::string password;
};`);
const classNode = tree.rootNode.child(0);
expect(classNode).toBeDefined();
expect(extractor.isTypeDeclaration(classNode!)).toBe(true);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.ownerFqn).toBe('User');
expect(result!.fields).toHaveLength(3);
const idField = result!.fields.find((f) => f.name === 'id');
expect(idField).toBeDefined();
expect(idField!.visibility).toBe('public');
expect(idField!.type).toBe('int');
const nameField = result!.fields.find((f) => f.name === 'name');
expect(nameField).toBeDefined();
expect(nameField!.visibility).toBe('public');
const pwField = result!.fields.find((f) => f.name === 'password');
expect(pwField).toBeDefined();
expect(pwField!.visibility).toBe('private');
});
it('uses backward-sibling walk to find access specifier', () => {
parser.setLanguage(Cpp);
// protected section followed by more fields
const tree = parser.parse(`class Base {
protected:
int x;
int y;
public:
int z;
};`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(3);
const xField = result!.fields.find((f) => f.name === 'x');
expect(xField).toBeDefined();
expect(xField!.visibility).toBe('protected');
const yField = result!.fields.find((f) => f.name === 'y');
expect(yField).toBeDefined();
expect(yField!.visibility).toBe('protected');
const zField = result!.fields.find((f) => f.name === 'z');
expect(zField).toBeDefined();
expect(zField!.visibility).toBe('public');
});
it('defaults to private for class without access specifiers', () => {
parser.setLanguage(Cpp);
const tree = parser.parse(`class Secret {
int value;
};`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(1);
expect(result!.fields[0].name).toBe('value');
expect(result!.fields[0].visibility).toBe('private');
});
it('defaults to public for struct without access specifiers', () => {
parser.setLanguage(Cpp);
const tree = parser.parse(`struct Point {
double x;
double y;
};`);
const structNode = tree.rootNode.child(0);
expect(extractor.isTypeDeclaration(structNode!)).toBe(true);
const result = extractor.extract(structNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(2);
expect(result!.fields[0].visibility).toBe('public');
expect(result!.fields[1].visibility).toBe('public');
});
it('detects static and const fields', () => {
parser.setLanguage(Cpp);
const tree = parser.parse(`class Config {
public:
static int count;
const int MAX_SIZE;
};`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(2);
const countField = result!.fields.find((f) => f.name === 'count');
expect(countField).toBeDefined();
expect(countField!.isStatic).toBe(true);
const maxField = result!.fields.find((f) => f.name === 'MAX_SIZE');
expect(maxField).toBeDefined();
expect(maxField!.isReadonly).toBe(true);
});
});
// ---------------------------------------------------------------------------
// Ruby config
// ---------------------------------------------------------------------------
describe('GenericFieldExtractor — Ruby', () => {
const parser = new Parser();
const extractor = createFieldExtractor(rubyConfig);
const mockContext = createMockContext();
mockContext.language = SupportedLanguages.Ruby;
mockContext.filePath = 'test.rb';
it('extracts multiple fields from attr_accessor', () => {
parser.setLanguage(Ruby);
const tree = parser.parse(`class User
attr_accessor :name, :email, :age
end`);
const classNode = tree.rootNode.child(0);
expect(classNode).toBeDefined();
expect(extractor.isTypeDeclaration(classNode!)).toBe(true);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.ownerFqn).toBe('User');
expect(result!.fields).toHaveLength(3);
const names = result!.fields.map((f) => f.name);
expect(names).toContain('name');
expect(names).toContain('email');
expect(names).toContain('age');
// All attr_accessor fields are public
for (const field of result!.fields) {
expect(field.visibility).toBe('public');
expect(field.type).toBeNull();
}
});
it('extracts fields from attr_reader as readonly', () => {
parser.setLanguage(Ruby);
const tree = parser.parse(`class Config
attr_reader :host, :port
end`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(2);
expect(result!.fields[0].isReadonly).toBe(true);
expect(result!.fields[1].isReadonly).toBe(true);
});
it('extracts fields from attr_writer as non-readonly', () => {
parser.setLanguage(Ruby);
const tree = parser.parse(`class Settings
attr_writer :theme
end`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(1);
expect(result!.fields[0].name).toBe('theme');
expect(result!.fields[0].isReadonly).toBe(false);
});
it('handles multiple attr_* calls in the same class', () => {
parser.setLanguage(Ruby);
const tree = parser.parse(`class Person
attr_accessor :name
attr_reader :id
attr_writer :password
end`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(3);
const nameField = result!.fields.find((f) => f.name === 'name');
expect(nameField).toBeDefined();
expect(nameField!.isReadonly).toBe(false);
const idField = result!.fields.find((f) => f.name === 'id');
expect(idField).toBeDefined();
expect(idField!.isReadonly).toBe(true);
const pwField = result!.fields.find((f) => f.name === 'password');
expect(pwField).toBeDefined();
expect(pwField!.isReadonly).toBe(false);
});
it('reports type as null (Ruby is dynamically typed)', () => {
parser.setLanguage(Ruby);
const tree = parser.parse(`class Item
attr_accessor :value
end`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(1);
expect(result!.fields[0].type).toBeNull();
});
it('reports isStatic as false', () => {
parser.setLanguage(Ruby);
const tree = parser.parse(`class Demo
attr_accessor :data
end`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.fields[0].isStatic).toBe(false);
});
});
// ---------------------------------------------------------------------------
// C# config — primary constructor fields
// ---------------------------------------------------------------------------
describe('GenericFieldExtractor — C# primary constructor fields', () => {
const parser = new Parser();
const extractor = createFieldExtractor(csharpFieldConfig);
const mockContext = createMockContext();
mockContext.language = SupportedLanguages.CSharp;
mockContext.filePath = 'test.cs';
it('extracts record positional parameters as public readonly fields', () => {
parser.setLanguage(CSharp);
const tree = parser.parse('public record Person(string Name, int Age);');
const classNode = tree.rootNode.child(0);
expect(classNode).toBeDefined();
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.ownerFqn).toBe('Person');
expect(result!.fields).toHaveLength(2);
const nameField = result!.fields.find((f) => f.name === 'Name');
expect(nameField).toBeDefined();
expect(nameField!.type).toBe('string');
expect(nameField!.visibility).toBe('public');
expect(nameField!.isReadonly).toBe(true);
expect(nameField!.isStatic).toBe(false);
const ageField = result!.fields.find((f) => f.name === 'Age');
expect(ageField).toBeDefined();
expect(ageField!.type).toBe('int');
expect(ageField!.visibility).toBe('public');
expect(ageField!.isReadonly).toBe(true);
});
it('extracts class primary constructor parameters as private fields', () => {
parser.setLanguage(CSharp);
const tree = parser.parse(`public class Point(int x, int y) {
public int X => x;
}`);
const classNode = tree.rootNode.child(0);
expect(classNode).toBeDefined();
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
const xField = result!.fields.find((f) => f.name === 'x');
expect(xField).toBeDefined();
expect(xField!.type).toBe('int');
expect(xField!.visibility).toBe('private');
expect(xField!.isReadonly).toBe(false);
});
it('combines body fields with primary constructor fields', () => {
parser.setLanguage(CSharp);
const tree = parser.parse(`public class Service(string name) {
public int Count;
}`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
// Body field + primary constructor field
expect(result!.fields.length).toBeGreaterThanOrEqual(2);
const nameField = result!.fields.find((f) => f.name === 'name');
expect(nameField).toBeDefined();
expect(nameField!.visibility).toBe('private');
const countField = result!.fields.find((f) => f.name === 'Count');
expect(countField).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Dart config — F26: static const / static final class fields
// ---------------------------------------------------------------------------
describe('GenericFieldExtractor — Dart', () => {
const parser = new Parser();
const extractor = createFieldExtractor(dartConfig);
const mockContext = createMockContext();
mockContext.language = SupportedLanguages.Dart;
mockContext.filePath = 'test.dart';
it('extracts a static const field as static + readonly (F26)', () => {
parser.setLanguage(Dart);
const tree = parser.parse(`class C {
static const a = 1;
}`);
const classNode = tree.rootNode.child(0);
expect(classNode).toBeDefined();
expect(extractor.isTypeDeclaration(classNode!)).toBe(true);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
expect(result!.ownerFqn).toBe('C');
const a = result!.fields.find((f) => f.name === 'a');
expect(a).toBeDefined();
expect(a!.isStatic).toBe(true);
expect(a!.isReadonly).toBe(true);
expect(a!.visibility).toBe('public');
});
it('extracts multi-name static final fields, all static + readonly (F26)', () => {
parser.setLanguage(Dart);
const tree = parser.parse(`class C {
static final String b = 'x', c = 'y';
}`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
// Exact-count guard (#1919 review CF4): `find()` below passes even on a
// double-emit, so assert b/c surface exactly twice total — one field each,
// no duplicate from the static_final_declaration_list multi-name path.
expect(result!.fields.filter((f) => f.name === 'b' || f.name === 'c')).toHaveLength(2);
const b = result!.fields.find((f) => f.name === 'b');
const c = result!.fields.find((f) => f.name === 'c');
expect(b).toBeDefined();
expect(c).toBeDefined();
for (const f of [b!, c!]) {
expect(f.isStatic).toBe(true);
expect(f.isReadonly).toBe(true);
expect(f.type).toBe('String');
}
});
it('still extracts instance fields (regression)', () => {
parser.setLanguage(Dart);
const tree = parser.parse(`class C {
int z = 0;
}`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
const z = result!.fields.find((f) => f.name === 'z');
expect(z).toBeDefined();
expect(z!.isStatic).toBe(false);
expect(z!.isReadonly).toBe(false);
expect(z!.type).toBe('int');
});
it('marks an underscore-prefixed static const as private (F26)', () => {
parser.setLanguage(Dart);
const tree = parser.parse(`class C {
static const _p = 1;
}`);
const classNode = tree.rootNode.child(0);
const result = extractor.extract(classNode!, mockContext);
expect(result).not.toBeNull();
const p = result!.fields.find((f) => f.name === '_p');
expect(p).toBeDefined();
expect(p!.visibility).toBe('private');
expect(p!.isStatic).toBe(true);
expect(p!.isReadonly).toBe(true);
});
});
// ---------------------------------------------------------------------------
// Java config — rawDeclaredType: verbatim generic type text (PR #2200 U1)
// and annotations: '@Name' strings from the modifiers child (PR #2200 U2)
// ---------------------------------------------------------------------------
describe('GenericFieldExtractor — Java (rawDeclaredType + annotations)', () => {
const parser = new Parser();
const extractor = createFieldExtractor(javaConfig);
const mockContext = createMockContext();
mockContext.language = SupportedLanguages.Java;
mockContext.filePath = 'Test.java';
/** Parse `src` and return the first class_declaration node. */
function classNode(src: string) {
parser.setLanguage(Java);
const tree = parser.parse(src);
const node = tree.rootNode.child(0);
if (!node) throw new Error('no class node');
return node;
}
it.each([
{
field: 'private List<Shape> shapes;',
name: 'shapes',
type: 'List',
rawDeclaredType: 'List<Shape>',
},
{
field: 'private Set<T> items;',
name: 'items',
type: 'Set',
rawDeclaredType: 'Set<T>',
},
{
field: 'private Map<String, Foo> byName;',
name: 'byName',
type: 'Map',
rawDeclaredType: 'Map<String, Foo>',
},
{
// Non-generic field: rawDeclaredType is PRESENT and equals the type text.
field: 'private String name;',
name: 'name',
type: 'String',
rawDeclaredType: 'String',
},
{
// Qualified generic: raw text preserved verbatim; simple name still last segment.
field: 'private java.util.List<Shape> shapes;',
name: 'shapes',
type: 'List',
rawDeclaredType: 'java.util.List<Shape>',
},
])(
'extracts type "$type" and rawDeclaredType "$rawDeclaredType" from `$field`',
({ field, name, type, rawDeclaredType }) => {
const result = extractor.extract(classNode(`class C { ${field} }`), mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(1);
expect(result!.fields[0]).toMatchObject({ name, type, rawDeclaredType });
},
);
it.each([
{
// marker_annotation node type (no arguments).
field: '@Autowired private List<Shape> shapes;',
annotations: ['@Autowired'],
},
{
// `annotation` node type (with arguments), not `marker_annotation` —
// the name comes from the annotation's `name` field.
field: '@Autowired(required=false) private List<Shape> shapes;',
annotations: ['@Autowired'],
},
{
// Multiple annotations on one field — all are collected, in order.
field: '@Nullable @Autowired @Qualifier("shapeBeans") private List<Shape> shapes;',
annotations: ['@Nullable', '@Autowired', '@Qualifier'],
},
])('extracts annotations $annotations from `$field`', ({ field, annotations }) => {
const result = extractor.extract(classNode(`class C { ${field} }`), mockContext);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(1);
expect(result!.fields[0]).toMatchObject({ name: 'shapes', annotations });
});
it('omits annotations entirely for a non-annotated field', () => {
const result = extractor.extract(
classNode('class C { private List<Shape> shapes; }'),
mockContext,
);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(1);
expect(result!.fields[0]).not.toHaveProperty('annotations');
});
it('still extracts the field when extractRawType/extractAnnotations throw (per-hook isolation)', () => {
// A throwing hook must degrade to a field WITHOUT raw/annotations — never
// escape buildField: an escaped throw reaches the language-group catch
// upstream (processFileGroup) and silently drops every remaining file in
// the group (#2286-review guard pattern).
const throwingExtractor = createFieldExtractor({
...javaConfig,
extractRawType: () => {
throw new Error('unexpected node shape');
},
extractAnnotations: () => {
throw new Error('unexpected node shape');
},
});
const result = throwingExtractor.extract(
classNode('class C { @Autowired private List<Shape> shapes; }'),
mockContext,
);
expect(result).not.toBeNull();
expect(result!.fields).toHaveLength(1);
expect(result!.fields[0]).toMatchObject({ name: 'shapes', type: 'List' });
expect(result!.fields[0]).not.toHaveProperty('rawDeclaredType');
expect(result!.fields[0]).not.toHaveProperty('annotations');
});
});
// ---------------------------------------------------------------------------
// Kotlin config — F52: companion-object properties indexed as fields
// ---------------------------------------------------------------------------
const describeKotlin = Kotlin ? describe : describe.skip;
describeKotlin('GenericFieldExtractor — Kotlin (F52 companion)', () => {
const parser = new Parser();
const extractor = createFieldExtractor(kotlinConfig);
const mockContext = createMockContext();
mockContext.language = SupportedLanguages.Kotlin;
mockContext.filePath = 'test.kt';
/** The first companion_object node in `src`. */
function companion(src: string): Parser.SyntaxNode {
parser.setLanguage(Kotlin);
const tree = parser.parse(src);
let found: Parser.SyntaxNode | undefined;
const walk = (n: Parser.SyntaxNode) => {
if (n.type === 'companion_object') 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 companion_object found');
return found;
}
it('extracts a `const val` companion property as a static, readonly field', () => {
const node = companion(`class C {
companion object {
const val TAG = "c"
}
}`);
expect(extractor.isTypeDeclaration(node)).toBe(true);
const result = extractor.extract(node, mockContext);
expect(result).not.toBeNull();
const tag = result!.fields.find((f) => f.name === 'TAG');
expect(tag).toBeDefined();
expect(tag!.isStatic).toBe(true);
expect(tag!.isReadonly).toBe(true);
});
it('extracts a property from a NAMED companion object', () => {
const node = companion(`class C {
companion object Factory {
val x = 1
}
}`);
const result = extractor.extract(node, mockContext);
const x = result!.fields.find((f) => f.name === 'x');
expect(x).toBeDefined();
expect(x!.isStatic).toBe(true);
expect(x!.isReadonly).toBe(true);
});
it('indexes only the property, not the function, and emits it exactly once', () => {
const node = companion(`class C {
companion object {
val onlyField = 1
fun create() {}
}
}`);
const result = extractor.extract(node, mockContext);
const fieldNames = result!.fields.map((f) => f.name);
expect(fieldNames).toEqual(['onlyField']); // function excluded, no duplication
});
// CF4 (#1919 review): guard the new `isInsideKotlinCompanion` walk against
// false-positives — a plain (non-companion) class property must be isStatic=false.
it('reports a plain non-companion class property as isStatic=false (CF4)', () => {
const classNode = firstNodeOfType(
`class C {
val x: Int = 1
}`,
'class_declaration',
);
expect(extractor.isTypeDeclaration(classNode)).toBe(true);
const result = extractor.extract(classNode, mockContext);
expect(result).not.toBeNull();
const x = result!.fields.find((f) => f.name === 'x');
expect(x).toBeDefined();
expect(x!.isStatic).toBe(false);
});
/** Parse `src` and return the first node of the given type (depth-first). */
function firstNodeOfType(src: string, type: string): Parser.SyntaxNode {
parser.setLanguage(Kotlin);
const tree = parser.parse(src);
let found: Parser.SyntaxNode | undefined;
const walk = (n: Parser.SyntaxNode) => {
if (found) return;
if (n.type === type) {
found = n;
return;
}
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 ${type} found`);
return found;
}
});
// ---------------------------------------------------------------------------
// Swift config — F75: protocol property requirements extracted as fields
// ---------------------------------------------------------------------------
const describeSwift = Swift ? describe : describe.skip;
describeSwift('GenericFieldExtractor — Swift (F75 protocol property requirements)', () => {
const parser = new Parser();
const extractor = createFieldExtractor(swiftConfig);
const mockContext = createMockContext();
mockContext.language = SupportedLanguages.Swift;
mockContext.filePath = 'test.swift';
/** Parse `src` and return the first class/protocol declaration node. */
function declNode(src: string): Parser.SyntaxNode {
parser.setLanguage(Swift);
const tree = parser.parse(src);
const node = tree.rootNode.child(0);
if (!node) throw new Error('no declaration node');
return node;
}
it('extracts a `{ get }` protocol property requirement as a field (F75)', () => {
const node = declNode(`protocol P {
var title: String { get }
}`);
expect(extractor.isTypeDeclaration(node)).toBe(true);
const result = extractor.extract(node, mockContext);
expect(result).not.toBeNull();
expect(result!.ownerFqn).toBe('P');
const title = result!.fields.find((f) => f.name === 'title');
expect(title).toBeDefined();
expect(title!.type).toBe('String');
expect(title!.isStatic).toBe(false);
});
it('extracts a `{ get set }` protocol property requirement (F75)', () => {
const node = declNode(`protocol P {
var count: Int { get set }
}`);
const result = extractor.extract(node, mockContext);
const count = result!.fields.find((f) => f.name === 'count');
expect(count).toBeDefined();
expect(count!.type).toBe('Int');
});
it('extracts a static protocol property requirement as static (F75)', () => {
const node = declNode(`protocol P {
static var shared: P { get }
}`);
const result = extractor.extract(node, mockContext);
const shared = result!.fields.find((f) => f.name === 'shared');
expect(shared).toBeDefined();
expect(shared!.type).toBe('P');
expect(shared!.isStatic).toBe(true);
});
it('extracts all requirements from a multi-property protocol (F75)', () => {
const node = declNode(`protocol P {
var title: String { get }
var count: Int { get set }
static var shared: P { get }
}`);
const result = extractor.extract(node, mockContext);
const names = result!.fields.map((f) => f.name).sort();
expect(names).toEqual(['count', 'shared', 'title']);
});
it('still extracts a class stored property exactly once (regression)', () => {
const node = declNode(`class C {
var name: String = ""
}`);
expect(extractor.isTypeDeclaration(node)).toBe(true);
const result = extractor.extract(node, mockContext);
const matches = result!.fields.filter((f) => f.name === 'name');
expect(matches).toHaveLength(1);
expect(matches[0].type).toBe('String');
});
});