/**
* Agentic QE v3 - TypeScript Parser Unit Tests
*/
import { describe, it, expect, beforeEach } from 'vitest';
import {
TypeScriptParser,
typescriptParser,
FunctionInfo,
ClassInfo,
ImportInfo,
ExportInfo,
InterfaceInfo,
} from '../../../../src/shared/parsers/typescript-parser';
describe('TypeScriptParser', () => {
let parser: TypeScriptParser;
beforeEach(() => {
parser = new TypeScriptParser();
});
describe('parseFile', () => {
it('should parse TypeScript source code into AST', () => {
const code = `const x: number = 1;`;
const ast = parser.parseFile('test.ts', code);
expect(ast).toBeDefined();
expect(ast.fileName).toBe('test.ts');
expect(ast.statements.length).toBeGreaterThan(0);
});
it('should handle TSX files', () => {
const code = `const Component = () =>
Hello
;`;
const ast = parser.parseFile('test.tsx', code);
expect(ast).toBeDefined();
expect(ast.fileName).toBe('test.tsx');
});
it('should handle JavaScript files', () => {
const code = `const x = 1;`;
const ast = parser.parseFile('test.js', code);
expect(ast).toBeDefined();
expect(ast.fileName).toBe('test.js');
});
});
describe('extractFunctions', () => {
it('should extract function declarations', () => {
const code = `
function greet(name: string): string {
return \`Hello, \${name}!\`;
}
`;
const ast = parser.parseFile('test.ts', code);
const functions = parser.extractFunctions(ast);
expect(functions).toHaveLength(1);
expect(functions[0].name).toBe('greet');
expect(functions[0].parameters).toHaveLength(1);
expect(functions[0].parameters[0].name).toBe('name');
expect(functions[0].parameters[0].type).toBe('string');
expect(functions[0].returnType).toBe('string');
expect(functions[0].isAsync).toBe(false);
expect(functions[0].isExported).toBe(false);
});
it('should extract arrow functions', () => {
const code = `
const add = (a: number, b: number): number => a + b;
`;
const ast = parser.parseFile('test.ts', code);
const functions = parser.extractFunctions(ast);
expect(functions).toHaveLength(1);
expect(functions[0].name).toBe('add');
expect(functions[0].parameters).toHaveLength(2);
expect(functions[0].returnType).toBe('number');
});
it('should extract async functions', () => {
const code = `
async function fetchData(url: string): Promise {
return fetch(url);
}
`;
const ast = parser.parseFile('test.ts', code);
const functions = parser.extractFunctions(ast);
expect(functions).toHaveLength(1);
expect(functions[0].name).toBe('fetchData');
expect(functions[0].isAsync).toBe(true);
expect(functions[0].returnType).toBe('Promise');
});
it('should extract exported functions', () => {
const code = `
export function publicFunc(): void {}
function privateFunc(): void {}
`;
const ast = parser.parseFile('test.ts', code);
const functions = parser.extractFunctions(ast);
expect(functions).toHaveLength(2);
const publicFn = functions.find((f) => f.name === 'publicFunc');
const privateFn = functions.find((f) => f.name === 'privateFunc');
expect(publicFn?.isExported).toBe(true);
expect(privateFn?.isExported).toBe(false);
});
it('should extract generator functions', () => {
const code = `
function* generateNumbers(): Generator {
yield 1;
yield 2;
}
`;
const ast = parser.parseFile('test.ts', code);
const functions = parser.extractFunctions(ast);
expect(functions).toHaveLength(1);
expect(functions[0].isGenerator).toBe(true);
});
it('should extract optional and rest parameters', () => {
const code = `
function greet(name: string, greeting?: string, ...extras: string[]): void {}
`;
const ast = parser.parseFile('test.ts', code);
const functions = parser.extractFunctions(ast);
expect(functions[0].parameters).toHaveLength(3);
expect(functions[0].parameters[0].isOptional).toBe(false);
expect(functions[0].parameters[0].isRest).toBe(false);
expect(functions[0].parameters[1].isOptional).toBe(true);
expect(functions[0].parameters[2].isRest).toBe(true);
});
it('should extract default parameter values', () => {
const code = `
function greet(name: string = 'World'): string {
return \`Hello, \${name}!\`;
}
`;
const ast = parser.parseFile('test.ts', code);
const functions = parser.extractFunctions(ast);
expect(functions[0].parameters[0].defaultValue).toBe("'World'");
expect(functions[0].parameters[0].isOptional).toBe(true);
});
it('should extract type parameters', () => {
const code = `
function identity(value: T): T {
return value;
}
`;
const ast = parser.parseFile('test.ts', code);
const functions = parser.extractFunctions(ast);
expect(functions[0].typeParameters).toEqual(['T']);
});
it('should include line numbers', () => {
const code = `function test(): void {
// body
}`;
const ast = parser.parseFile('test.ts', code);
const functions = parser.extractFunctions(ast);
expect(functions[0].startLine).toBe(1);
expect(functions[0].endLine).toBe(3);
});
});
describe('extractClasses', () => {
it('should extract class declarations', () => {
const code = `
class User {
name: string;
constructor(name: string) {
this.name = name;
}
greet(): string {
return \`Hello, \${this.name}!\`;
}
}
`;
const ast = parser.parseFile('test.ts', code);
const classes = parser.extractClasses(ast);
expect(classes).toHaveLength(1);
expect(classes[0].name).toBe('User');
expect(classes[0].properties.length).toBeGreaterThanOrEqual(1);
expect(classes[0].methods.length).toBeGreaterThanOrEqual(1);
});
it('should extract class inheritance', () => {
const code = `
class Animal {}
class Dog extends Animal implements Runnable, Walkable {}
`;
const ast = parser.parseFile('test.ts', code);
const classes = parser.extractClasses(ast);
const dog = classes.find((c) => c.name === 'Dog');
expect(dog?.extends).toBe('Animal');
expect(dog?.implements).toEqual(['Runnable', 'Walkable']);
});
it('should extract abstract classes', () => {
const code = `
abstract class Shape {
abstract getArea(): number;
}
`;
const ast = parser.parseFile('test.ts', code);
const classes = parser.extractClasses(ast);
expect(classes[0].isAbstract).toBe(true);
expect(classes[0].methods[0].isAbstract).toBe(true);
});
it('should extract method visibility', () => {
const code = `
class Service {
public publicMethod(): void {}
private privateMethod(): void {}
protected protectedMethod(): void {}
}
`;
const ast = parser.parseFile('test.ts', code);
const classes = parser.extractClasses(ast);
const methods = classes[0].methods;
expect(methods.find((m) => m.name === 'publicMethod')?.visibility).toBe('public');
expect(methods.find((m) => m.name === 'privateMethod')?.visibility).toBe('private');
expect(methods.find((m) => m.name === 'protectedMethod')?.visibility).toBe('protected');
});
it('should extract static members', () => {
const code = `
class Config {
static instance: Config;
static getInstance(): Config {
return this.instance;
}
}
`;
const ast = parser.parseFile('test.ts', code);
const classes = parser.extractClasses(ast);
expect(classes[0].properties[0].isStatic).toBe(true);
expect(classes[0].methods[0].isStatic).toBe(true);
});
it('should extract readonly properties', () => {
const code = `
class Point {
readonly x: number;
readonly y: number;
}
`;
const ast = parser.parseFile('test.ts', code);
const classes = parser.extractClasses(ast);
expect(classes[0].properties[0].isReadonly).toBe(true);
expect(classes[0].properties[1].isReadonly).toBe(true);
});
it('should extract parameter properties from constructor', () => {
const code = `
class Service {
constructor(
private readonly logger: Logger,
public name: string
) {}
}
`;
const ast = parser.parseFile('test.ts', code);
const classes = parser.extractClasses(ast);
const properties = classes[0].properties;
expect(properties.length).toBeGreaterThanOrEqual(2);
const logger = properties.find((p) => p.name === 'logger');
const name = properties.find((p) => p.name === 'name');
expect(logger?.visibility).toBe('private');
expect(logger?.isReadonly).toBe(true);
expect(name?.visibility).toBe('public');
});
it('should extract async methods', () => {
const code = `
class ApiClient {
async fetchData(): Promise {}
}
`;
const ast = parser.parseFile('test.ts', code);
const classes = parser.extractClasses(ast);
expect(classes[0].methods[0].isAsync).toBe(true);
});
it('should extract type parameters', () => {
const code = `
class Container {
value: T;
}
`;
const ast = parser.parseFile('test.ts', code);
const classes = parser.extractClasses(ast);
expect(classes[0].typeParameters).toEqual(['T']);
});
});
describe('extractImports', () => {
it('should extract named imports', () => {
const code = `
import { Component, useState } from 'react';
`;
const ast = parser.parseFile('test.ts', code);
const imports = parser.extractImports(ast);
expect(imports).toHaveLength(1);
expect(imports[0].module).toBe('react');
expect(imports[0].namedImports).toHaveLength(2);
expect(imports[0].namedImports[0].name).toBe('Component');
expect(imports[0].namedImports[1].name).toBe('useState');
});
it('should extract default imports', () => {
const code = `
import React from 'react';
`;
const ast = parser.parseFile('test.ts', code);
const imports = parser.extractImports(ast);
expect(imports[0].defaultImport).toBe('React');
expect(imports[0].namedImports).toHaveLength(0);
});
it('should extract namespace imports', () => {
const code = `
import * as fs from 'fs';
`;
const ast = parser.parseFile('test.ts', code);
const imports = parser.extractImports(ast);
expect(imports[0].namespaceImport).toBe('fs');
});
it('should extract mixed imports', () => {
const code = `
import React, { useState, useEffect } from 'react';
`;
const ast = parser.parseFile('test.ts', code);
const imports = parser.extractImports(ast);
expect(imports[0].defaultImport).toBe('React');
expect(imports[0].namedImports).toHaveLength(2);
});
it('should extract aliased imports', () => {
const code = `
import { Component as Comp } from 'react';
`;
const ast = parser.parseFile('test.ts', code);
const imports = parser.extractImports(ast);
expect(imports[0].namedImports[0].name).toBe('Component');
expect(imports[0].namedImports[0].alias).toBe('Comp');
});
it('should extract type-only imports', () => {
const code = `
import type { User } from './types';
`;
const ast = parser.parseFile('test.ts', code);
const imports = parser.extractImports(ast);
expect(imports[0].isTypeOnly).toBe(true);
});
});
describe('extractExports', () => {
it('should extract exported functions', () => {
const code = `
export function greet(): void {}
`;
const ast = parser.parseFile('test.ts', code);
const exports = parser.extractExports(ast);
expect(exports).toHaveLength(1);
expect(exports[0].name).toBe('greet');
expect(exports[0].type).toBe('function');
expect(exports[0].isDefault).toBe(false);
});
it('should extract exported classes', () => {
const code = `
export class User {}
`;
const ast = parser.parseFile('test.ts', code);
const exports = parser.extractExports(ast);
expect(exports[0].name).toBe('User');
expect(exports[0].type).toBe('class');
});
it('should extract exported variables', () => {
const code = `
export const config = {};
export let counter = 0;
`;
const ast = parser.parseFile('test.ts', code);
const exports = parser.extractExports(ast);
expect(exports).toHaveLength(2);
expect(exports[0].type).toBe('variable');
expect(exports[1].type).toBe('variable');
});
it('should extract default exports', () => {
const code = `
export default function main() {}
`;
const ast = parser.parseFile('test.ts', code);
const exports = parser.extractExports(ast);
expect(exports[0].isDefault).toBe(true);
});
it('should extract named export declarations', () => {
const code = `
const a = 1;
const b = 2;
export { a, b };
`;
const ast = parser.parseFile('test.ts', code);
const exports = parser.extractExports(ast);
expect(exports).toHaveLength(2);
});
it('should extract re-exports', () => {
const code = `
export { User } from './user';
export * from './utils';
`;
const ast = parser.parseFile('test.ts', code);
const exports = parser.extractExports(ast);
expect(exports.length).toBeGreaterThanOrEqual(2);
const userExport = exports.find((e) => e.name === 'User');
const starExport = exports.find((e) => e.name === '*');
expect(userExport?.isReexport).toBe(true);
expect(userExport?.sourceModule).toBe('./user');
expect(starExport?.isReexport).toBe(true);
expect(starExport?.sourceModule).toBe('./utils');
});
it('should extract exported interfaces', () => {
const code = `
export interface User {
name: string;
}
`;
const ast = parser.parseFile('test.ts', code);
const exports = parser.extractExports(ast);
expect(exports[0].name).toBe('User');
expect(exports[0].type).toBe('interface');
});
it('should extract exported type aliases', () => {
const code = `
export type ID = string | number;
`;
const ast = parser.parseFile('test.ts', code);
const exports = parser.extractExports(ast);
expect(exports[0].name).toBe('ID');
expect(exports[0].type).toBe('type');
});
it('should extract exported enums', () => {
const code = `
export enum Status {
Active,
Inactive
}
`;
const ast = parser.parseFile('test.ts', code);
const exports = parser.extractExports(ast);
expect(exports[0].name).toBe('Status');
expect(exports[0].type).toBe('enum');
});
});
describe('extractInterfaces', () => {
it('should extract interface declarations', () => {
const code = `
interface User {
name: string;
age: number;
}
`;
const ast = parser.parseFile('test.ts', code);
const interfaces = parser.extractInterfaces(ast);
expect(interfaces).toHaveLength(1);
expect(interfaces[0].name).toBe('User');
expect(interfaces[0].properties).toHaveLength(2);
});
it('should extract optional properties', () => {
const code = `
interface Config {
required: string;
optional?: string;
}
`;
const ast = parser.parseFile('test.ts', code);
const interfaces = parser.extractInterfaces(ast);
const required = interfaces[0].properties.find((p) => p.name === 'required');
const optional = interfaces[0].properties.find((p) => p.name === 'optional');
expect(required?.isOptional).toBe(false);
expect(optional?.isOptional).toBe(true);
});
it('should extract readonly properties', () => {
const code = `
interface Point {
readonly x: number;
readonly y: number;
}
`;
const ast = parser.parseFile('test.ts', code);
const interfaces = parser.extractInterfaces(ast);
expect(interfaces[0].properties[0].isReadonly).toBe(true);
expect(interfaces[0].properties[1].isReadonly).toBe(true);
});
it('should extract interface methods', () => {
const code = `
interface Repository {
find(id: string): Entity;
save(entity: Entity): void;
}
`;
const ast = parser.parseFile('test.ts', code);
const interfaces = parser.extractInterfaces(ast);
expect(interfaces[0].methods).toHaveLength(2);
expect(interfaces[0].methods[0].name).toBe('find');
expect(interfaces[0].methods[0].parameters).toHaveLength(1);
expect(interfaces[0].methods[0].returnType).toBe('Entity');
});
it('should extract interface extension', () => {
const code = `
interface Entity {
id: string;
}
interface User extends Entity, Auditable {
name: string;
}
`;
const ast = parser.parseFile('test.ts', code);
const interfaces = parser.extractInterfaces(ast);
const user = interfaces.find((i) => i.name === 'User');
expect(user?.extends).toEqual(['Entity', 'Auditable']);
});
it('should extract type parameters', () => {
const code = `
interface Repository {
find(id: ID): T;
}
`;
const ast = parser.parseFile('test.ts', code);
const interfaces = parser.extractInterfaces(ast);
expect(interfaces[0].typeParameters).toEqual(['T', 'ID']);
});
it('should extract exported interfaces', () => {
const code = `
export interface PublicApi {}
interface InternalApi {}
`;
const ast = parser.parseFile('test.ts', code);
const interfaces = parser.extractInterfaces(ast);
const publicApi = interfaces.find((i) => i.name === 'PublicApi');
const internalApi = interfaces.find((i) => i.name === 'InternalApi');
expect(publicApi?.isExported).toBe(true);
expect(internalApi?.isExported).toBe(false);
});
});
describe('default instance', () => {
it('should export a default parser instance', () => {
expect(typescriptParser).toBeInstanceOf(TypeScriptParser);
});
it('should be usable for parsing', () => {
const code = `const x = 1;`;
const ast = typescriptParser.parseFile('test.ts', code);
expect(ast).toBeDefined();
});
});
describe('complex scenarios', () => {
it('should handle a full module with mixed exports', () => {
const code = `
import { Logger } from './logger';
import type { Config } from './config';
export interface Service {
start(): void;
stop(): void;
}
export abstract class BaseService implements Service {
protected logger: Logger;
constructor(protected config: Config) {}
abstract start(): void;
abstract stop(): void;
}
export class MyService extends BaseService {
async start(): Promise {
this.logger.info('Starting...');
}
stop(): void {
this.logger.info('Stopping...');
}
}
export const createService = (config: Config): MyService => {
return new MyService(config);
};
export default MyService;
`;
const ast = parser.parseFile('service.ts', code);
const imports = parser.extractImports(ast);
expect(imports).toHaveLength(2);
const interfaces = parser.extractInterfaces(ast);
expect(interfaces).toHaveLength(1);
expect(interfaces[0].name).toBe('Service');
const classes = parser.extractClasses(ast);
expect(classes).toHaveLength(2);
const baseService = classes.find((c) => c.name === 'BaseService');
expect(baseService?.isAbstract).toBe(true);
expect(baseService?.implements).toContain('Service');
const myService = classes.find((c) => c.name === 'MyService');
expect(myService?.extends).toBe('BaseService');
const functions = parser.extractFunctions(ast);
expect(functions).toHaveLength(1);
expect(functions[0].name).toBe('createService');
const exports = parser.extractExports(ast);
expect(exports.length).toBeGreaterThan(0);
});
});
});