261 lines
8.5 KiB
TypeScript
261 lines
8.5 KiB
TypeScript
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);
|
|
}
|