This commit is contained in:
260
src/parsers/typeParser.ts
Normal file
260
src/parsers/typeParser.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
import { TSInterfaceDeclaration, TSInterfaceHeritage, TSTypeParameterDeclaration, TSTypeParameter, TSTypeReference, TSPropertySignature, TSMethodSignature, TSTypeAliasDeclaration, TSEnumDeclaration } from '../types/typescript-estree';
|
||||
import { createASTUtils } from '../utils/astUtils';
|
||||
import { typeToString } from '../utils/typeUtils';
|
||||
import { TypeDeclaration, ParsedFile, TypeReference, ImportDeclaration as CustomImportDeclaration } from '../types';
|
||||
import type * as TSESTree from '@typescript-eslint/typescript-estree';
|
||||
|
||||
export interface TypeParserOptions {
|
||||
includePrivate?: boolean;
|
||||
includeInternal?: boolean;
|
||||
skipErrors?: boolean;
|
||||
}
|
||||
|
||||
export class TypeParser {
|
||||
private astUtils: ReturnType<typeof createASTUtils>;
|
||||
private options: TypeParserOptions;
|
||||
|
||||
constructor(options: TypeParserOptions = {}) {
|
||||
this.astUtils = createASTUtils();
|
||||
this.options = {
|
||||
includePrivate: options.includePrivate ?? false,
|
||||
includeInternal: options.includeInternal ?? false,
|
||||
skipErrors: options.skipErrors ?? false
|
||||
};
|
||||
}
|
||||
|
||||
parse(source: string, filePath: string): ParsedFile {
|
||||
const result: ParsedFile = {
|
||||
filePath,
|
||||
types: [],
|
||||
imports: [],
|
||||
errors: []
|
||||
};
|
||||
|
||||
try {
|
||||
const ast = this.astUtils.parse(source, filePath);
|
||||
|
||||
const interfaces = this.astUtils.getInterfaces(ast);
|
||||
const typeAliases = this.astUtils.getTypeAliases(ast);
|
||||
const enums = this.astUtils.getEnums(ast);
|
||||
const imports = this.astUtils.getImports(ast);
|
||||
|
||||
for (const iface of interfaces) {
|
||||
try {
|
||||
const typeDecl = this.parseInterface(iface, filePath);
|
||||
if (typeDecl) {
|
||||
result.types.push(typeDecl);
|
||||
}
|
||||
} catch (error) {
|
||||
this.addError(result, `Failed to parse interface: ${(error as Error).message}`, iface.loc?.start?.line ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
for (const typeAlias of typeAliases) {
|
||||
try {
|
||||
const typeDecl = this.parseTypeAlias(typeAlias, filePath);
|
||||
if (typeDecl) {
|
||||
result.types.push(typeDecl);
|
||||
}
|
||||
} catch (error) {
|
||||
this.addError(result, `Failed to parse type alias: ${(error as Error).message}`, typeAlias.loc?.start?.line ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
for (const enumDecl of enums) {
|
||||
try {
|
||||
const typeDecl = this.parseEnum(enumDecl, filePath);
|
||||
if (typeDecl) {
|
||||
result.types.push(typeDecl);
|
||||
}
|
||||
} catch (error) {
|
||||
this.addError(result, `Failed to parse enum: ${(error as Error).message}`, enumDecl.loc?.start?.line ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
for (const importDecl of imports) {
|
||||
try {
|
||||
const importType = this.parseImport(importDecl, filePath);
|
||||
if (importType) {
|
||||
result.imports.push(importType);
|
||||
}
|
||||
} catch (error) {
|
||||
this.addError(result, `Failed to parse import: ${(error as Error).message}`, importDecl.loc?.start?.line ?? 0);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.addError(result, `Failed to parse file: ${(error as Error).message}`, 1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private parseInterface(node: TSInterfaceDeclaration, filePath: string): TypeDeclaration | null {
|
||||
const members = this.parseInterfaceMembers(node.body.body);
|
||||
const extendsTypes = this.parseExtendsClause(node.extends ?? null);
|
||||
const generics = this.parseGenericParameters(node.typeParameters ?? null);
|
||||
|
||||
return {
|
||||
name: node.id.name,
|
||||
filePath,
|
||||
startLine: node.loc?.start?.line ?? 0,
|
||||
endLine: node.loc?.end?.line ?? 0,
|
||||
kind: 'interface',
|
||||
dependencies: this.extractDependencies(node),
|
||||
rawNode: node
|
||||
};
|
||||
}
|
||||
|
||||
private parseInterfaceMembers(members: any[]): import('../types').InterfaceMember[] {
|
||||
return members
|
||||
.filter((m): m is TSPropertySignature | TSMethodSignature => {
|
||||
return m.type === 'TSPropertySignature' || m.type === 'TSMethodSignature';
|
||||
})
|
||||
.map((member) => {
|
||||
const isProperty = member.type === 'TSPropertySignature';
|
||||
const propertyMember = member as TSPropertySignature;
|
||||
return {
|
||||
name: member.key.type === 'Identifier' ? member.key.name : typeToString(member.key),
|
||||
type: isProperty && propertyMember.typeAnnotation ? typeToString(propertyMember.typeAnnotation.typeAnnotation) : 'unknown',
|
||||
isOptional: isProperty ? (propertyMember.optional ?? false) : false,
|
||||
isReadonly: isProperty ? (propertyMember.readonly ?? false) : false,
|
||||
modifiers: []
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private parseExtendsClause(extendsClause: TSInterfaceHeritage[] | null | undefined): string[] {
|
||||
if (!extendsClause) {
|
||||
return [];
|
||||
}
|
||||
return extendsClause.map((ext) => {
|
||||
if (ext.expression.type === 'Identifier') {
|
||||
return ext.expression.name;
|
||||
}
|
||||
return typeToString(ext.expression);
|
||||
});
|
||||
}
|
||||
|
||||
private parseGenericParameters(params: TSTypeParameterDeclaration | null | undefined): import('../types').GenericParameter[] {
|
||||
if (!params || !params.params) {
|
||||
return [];
|
||||
}
|
||||
return params.params.map((param: TSTypeParameter) => ({
|
||||
name: param.name.name,
|
||||
constraint: param.constraint ? typeToString(param.constraint) : undefined,
|
||||
default: param.default ? typeToString(param.default) : undefined
|
||||
}));
|
||||
}
|
||||
|
||||
private parseTypeAlias(node: TSTypeAliasDeclaration, filePath: string): TypeDeclaration | null {
|
||||
const generics = this.parseGenericParameters(node.typeParameters ?? null);
|
||||
|
||||
return {
|
||||
name: node.id.name,
|
||||
filePath,
|
||||
startLine: node.loc?.start?.line ?? 0,
|
||||
endLine: node.loc?.end?.line ?? 0,
|
||||
kind: 'type_alias',
|
||||
dependencies: this.extractDependencies(node),
|
||||
rawNode: node
|
||||
};
|
||||
}
|
||||
|
||||
private parseEnum(node: TSEnumDeclaration, filePath: string): TypeDeclaration | null {
|
||||
return {
|
||||
name: node.id.name,
|
||||
filePath,
|
||||
startLine: node.loc?.start?.line ?? 0,
|
||||
endLine: node.loc?.end?.line ?? 0,
|
||||
kind: 'enum',
|
||||
dependencies: [],
|
||||
rawNode: node
|
||||
};
|
||||
}
|
||||
|
||||
private parseImport(node: TSESTree.ImportDeclaration, filePath: string): CustomImportDeclaration | null {
|
||||
const namedImports: string[] = [];
|
||||
let defaultImport: string | undefined;
|
||||
let namespaceImport: string | undefined;
|
||||
|
||||
if (node.specifiers) {
|
||||
for (const specifier of node.specifiers) {
|
||||
if (specifier.type === 'ImportSpecifier') {
|
||||
namedImports.push(specifier.local.name);
|
||||
} else if (specifier.type === 'ImportDefaultSpecifier') {
|
||||
defaultImport = specifier.local.name;
|
||||
} else if (specifier.type === 'ImportNamespaceSpecifier') {
|
||||
namespaceImport = specifier.local.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: node.source.value,
|
||||
filePath,
|
||||
startLine: node.loc?.start?.line ?? 0,
|
||||
endLine: node.loc?.end?.line ?? 0,
|
||||
kind: 'import',
|
||||
dependencies: [],
|
||||
rawNode: node,
|
||||
moduleName: node.source.value,
|
||||
namedImports,
|
||||
defaultImport,
|
||||
namespaceImport
|
||||
};
|
||||
}
|
||||
|
||||
private extractDependencies(node: any): string[] {
|
||||
const dependencies: string[] = [];
|
||||
const typeRefs = this.astUtils.findNodes<TSTypeReference>(
|
||||
node,
|
||||
(n): n is TSTypeReference => n.type === 'TSTypeReference'
|
||||
);
|
||||
|
||||
for (const ref of typeRefs) {
|
||||
if (ref.typeName.type === 'Identifier') {
|
||||
const typeName = (ref.typeName as { type: 'Identifier'; name: string }).name;
|
||||
if (!dependencies.includes(typeName)) {
|
||||
dependencies.push(typeName);
|
||||
}
|
||||
} else if (ref.typeName.type === 'TSQualifiedName') {
|
||||
const qualifiedName = ref.typeName as { left: { name: string }; right: { name: string } };
|
||||
const typeName = `${qualifiedName.left.name}.${qualifiedName.right.name}`;
|
||||
if (!dependencies.includes(typeName)) {
|
||||
dependencies.push(typeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const heritage = this.astUtils.findNodes<TSInterfaceHeritage>(
|
||||
node,
|
||||
(n): n is TSInterfaceHeritage => n.type === 'TSInterfaceHeritage'
|
||||
);
|
||||
|
||||
for (const h of heritage) {
|
||||
if (h.expression.type === 'Identifier') {
|
||||
const typeName = h.expression.name;
|
||||
if (!dependencies.includes(typeName)) {
|
||||
dependencies.push(typeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
private addError(result: ParsedFile, message: string, line: number): void {
|
||||
if (!this.options.skipErrors) {
|
||||
result.errors.push({
|
||||
message,
|
||||
line,
|
||||
column: 0,
|
||||
severity: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createTypeParser(options?: TypeParserOptions): TypeParser {
|
||||
return new TypeParser(options);
|
||||
}
|
||||
Reference in New Issue
Block a user