mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
test-phase8-add-field-extraction-unit-tests
This commit is contained in:
parent
6dcb00ae69
commit
a22ac0c5a7
2 changed files with 417 additions and 5 deletions
|
|
@ -58,7 +58,18 @@ export class TypeScriptFieldExtractor extends BaseFieldExtractor {
|
|||
* Extract visibility modifier from a field node
|
||||
*/
|
||||
protected extractVisibility(node: SyntaxNode): string {
|
||||
// Check for modifiers in the field's unnamed children
|
||||
// Check for accessibility_modifier named child (tree-sitter typescript)
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child && child.type === 'accessibility_modifier') {
|
||||
const text = child.text.trim();
|
||||
if (TypeScriptFieldExtractor.VISIBILITY_MODIFIERS.has(text)) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for modifiers in the field's unnamed children (fallback)
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child && !child.isNamed) {
|
||||
|
|
@ -340,10 +351,9 @@ export class TypeScriptFieldExtractor extends BaseFieldExtractor {
|
|||
}
|
||||
|
||||
// Find nested type declarations
|
||||
const nestedDeclarations = node.descendantsOfType([
|
||||
'class_declaration',
|
||||
'interface_declaration',
|
||||
].join(','));
|
||||
const nestedClasses = node.descendantsOfType('class_declaration');
|
||||
const nestedInterfaces = node.descendantsOfType('interface_declaration');
|
||||
const nestedDeclarations = [...nestedClasses, ...nestedInterfaces];
|
||||
|
||||
for (const nested of nestedDeclarations) {
|
||||
// Skip the current node itself
|
||||
|
|
|
|||
402
gitnexus/test/unit/field-extraction.test.ts
Normal file
402
gitnexus/test/unit/field-extraction.test.ts
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { TypeScriptFieldExtractor } from '../../src/core/ingestion/field-extractors/typescript.js';
|
||||
import type { FieldExtractorContext, ExtractedFields } from '../../src/core/ingestion/field-types.js';
|
||||
import type { TypeEnv } from '../../src/core/ingestion/type-env.js';
|
||||
import { createSymbolTable } from '../../src/core/ingestion/symbol-table.js';
|
||||
import Parser from 'tree-sitter';
|
||||
import TypeScript from 'tree-sitter-typescript';
|
||||
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
|
||||
const createMockContext = (): FieldExtractorContext => ({
|
||||
typeEnv: new Map() as TypeEnv,
|
||||
symbolTable: createSymbolTable(),
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue