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 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 CSharp from 'tree-sitter-c-sharp'; 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(), } as 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, Array)', () => { const tree = parse(` class Repository { users: Map; ids: Array; } `); 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'); const idsField = result!.fields.find((f) => f.name === 'ids'); expect(idsField).toBeDefined(); expect(idsField!.type).toBe('Array'); }); it('extracts nested types', () => { const tree = parse(` class Container { data: OuterType>; } `); 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>'); }); 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('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(); }); });