diff --git a/src/utils/astUtils.ts b/src/utils/astUtils.ts new file mode 100644 index 0000000..5ed6036 --- /dev/null +++ b/src/utils/astUtils.ts @@ -0,0 +1,167 @@ +import * as TSESTree from '@typescript-eslint/typescript-estree'; + +export interface ASTUtilsOptions { + includeComments?: boolean; + preserveWhitespace?: boolean; +} + +export class ASTUtils { + private options: ASTUtilsOptions; + + constructor(options: ASTUtilsOptions = {}) { + this.options = { + includeComments: options.includeComments ?? false, + preserveWhitespace: options.preserveWhitespace ?? false + }; + } + + parse(source: string, _filePath?: string): TSESTree.Program { + return TSESTree.parse(source, { + loc: true, + range: true, + comment: this.options.includeComments, + tokens: true, + useJSXTextNode: true, + ecmaVersion: 'latest', + sourceType: 'module' + }); + } + + getNodeByRange(node: TSESTree.Node, start: number, end: number): TSESTree.Node | null { + if (node.range[0] <= start && node.range[1] >= end) { + if (node.range[0] === start && node.range[1] === end) { + return node; + } + for (const child of this.getChildren(node)) { + const found = this.getNodeByRange(child, start, end); + if (found) { + return found; + } + } + } + return null; + } + + getChildren(node: TSESTree.Node): TSESTree.Node[] { + const children: TSESTree.Node[] = []; + for (const key of Object.keys(node)) { + if (key === 'loc' || key === 'range' || key === 'parent') { + continue; + } + const value = (node as Record)[key]; + if (value && typeof value === 'object') { + if (Array.isArray(value)) { + children.push(...value.filter((v): v is TSESTree.Node => this.isNode(v))); + } else if (this.isNode(value)) { + children.push(value); + } + } + } + return children; + } + + private isNode(value: unknown): value is TSESTree.Node { + return ( + typeof value === 'object' && + value !== null && + 'type' in value && + 'range' in value && + Array.isArray((value as { range: unknown }).range) + ); + } + + findNodes( + node: TSESTree.Node, + predicate: (n: TSESTree.Node) => boolean + ): T[] { + const results: T[] = []; + const traverse = (n: TSESTree.Node): void => { + if (predicate(n)) { + results.push(n as T); + } + for (const child of this.getChildren(n)) { + traverse(child); + } + }; + traverse(node); + return results; + } + + findFirstNode( + node: TSESTree.Node, + predicate: (n: TSESTree.Node) => boolean + ): T | null { + if (predicate(node)) { + return node as T; + } + for (const child of this.getChildren(node)) { + const found = this.findFirstNode(child, predicate); + if (found) { + return found; + } + } + return null; + } + + getSourceCode(node: TSESTree.Node, source: string): string { + return source.substring(node.range[0], node.range[1]); + } + + getLineNumber(node: TSESTree.Node): number { + return node.loc?.start?.line ?? 0; + } + + getEndLineNumber(node: TSESTree.Node): number { + return node.loc?.end?.line ?? 0; + } + + isExported(node: TSESTree.Node): boolean { + if (node.parent) { + const parent = node.parent as TSESTree.Node; + if (parent.type === 'ExportNamedDeclaration' || parent.type === 'ExportDefaultDeclaration') { + return true; + } + } + return false; + } + + getImports(node: TSESTree.Program): TSESTree.ImportDeclaration[] { + return this.findNodes( + node, + (n): n is TSESTree.ImportDeclaration => n.type === 'ImportDeclaration' + ); + } + + getExports(node: TSESTree.Program): TSESTree.ExportDeclaration[] { + return this.findNodes( + node, + (n): n is TSESTree.ExportDeclaration => + n.type === 'ExportNamedDeclaration' || n.type === 'ExportDefaultDeclaration' + ); + } + + getInterfaces(node: TSESTree.Program): TSESTree.TSInterfaceDeclaration[] { + return this.findNodes( + node, + (n): n is TSESTree.TSInterfaceDeclaration => n.type === 'TSInterfaceDeclaration' + ); + } + + getTypeAliases(node: TSESTree.Program): TSESTree.TSTypeAliasDeclaration[] { + return this.findNodes( + node, + (n): n is TSESTree.TSTypeAliasDeclaration => n.type === 'TSTypeAliasDeclaration' + ); + } + + getEnums(node: TSESTree.Program): TSESTree.TSEnumDeclaration[] { + return this.findNodes( + node, + (n): n is TSESTree.TSEnumDeclaration => n.type === 'TSEnumDeclaration' + ); + } +} + +export function createASTUtils(options?: ASTUtilsOptions): ASTUtils { + return new ASTUtils(options); +}