Ao analisar grandes bases de código, carregar todos os arquivos na memória de uma vez leva à falha. Ao coletar primeiro apenas caminhos e stats, completar a análise de estrutura e então dividir por orçamento de bytes para ler/parsear/liberar apenas arquivos necessários sequencialmente, você pode manter o uso de memória previsível.
Código
import { promises as fs } from 'node:fs'import { join, extname } from 'node:path'
interface FileEntry { path: string size: number}
interface ParsedFile { path: string content: string ast?: unknown // AST ou resultado de análise}
interface WalkOptions { /** Extensões para parsear (ex: ['.ts', '.js']) */ parseableExtensions: string[] /** Padrões para ignorar (ex: node_modules, .git) */ ignorePatterns: RegExp[] /** Bytes máximos por chunk */ chunkBudget: number /** Tamanho máximo de arquivo único (pula arquivos que excedem) */ maxFileSize: number}
/** * Fase 1: Escanear apenas caminhos e tamanhos */async function scanPaths( rootPath: string, options: WalkOptions): Promise<FileEntry[]> { const entries: FileEntry[] = []
async function walk(dir: string): Promise<void> { const items = await fs.readdir(dir, { withFileTypes: true })
for (const item of items) { const fullPath = join(dir, item.name)
// Verificar padrões de ignorar (julgamento precoce para eficiência de memória) if (options.ignorePatterns.some((pattern) => pattern.test(fullPath))) { continue }
if (item.isDirectory()) { await walk(fullPath) } else if (item.isFile()) { const stat = await fs.stat(fullPath)
// Verificação de tamanho (excluir arquivos enormes cedo) if (stat.size > options.maxFileSize) { continue }
entries.push({ path: fullPath, size: stat.size }) } } }
await walk(rootPath) return entries}
/** * Fase 2: Análise de estrutura e filtragem */function analyzeStructure( entries: FileEntry[], options: WalkOptions): FileEntry[] { // Filtrar por extensão const parseableFiles = entries.filter((entry) => { const ext = extname(entry.path) return options.parseableExtensions.includes(ext) })
// Ordenar por tamanho (processar arquivos menores primeiro gera resultados precoces) return parseableFiles.toSorted((a, b) => a.size - b.size)}
/** * Fase 3: Dividir por orçamento de bytes e ler/parsear sequencialmente */async function parseWithBudget( entries: FileEntry[], options: WalkOptions, parseFile: (content: string, path: string) => unknown): Promise<ParsedFile[]> { const results: ParsedFile[] = [] let currentBudget = 0 let chunk: FileEntry[] = []
for (const entry of entries) { // Processar chunk quando orçamento excedido if (currentBudget + entry.size > options.chunkBudget) { // Processar chunk atual const parsed = await processChunk(chunk, parseFile) results.push(...parsed)
// Reiniciar (permite ao GC recuperar memória) chunk = [] currentBudget = 0 }
chunk.push(entry) currentBudget += entry.size }
// Processar último chunk if (chunk.length > 0) { const parsed = await processChunk(chunk, parseFile) results.push(...parsed) }
return results}
/** * Leitura e parsing em lote de arquivos no chunk */async function processChunk( chunk: FileEntry[], parseFile: (content: string, path: string) => unknown): Promise<ParsedFile[]> { const results: ParsedFile[] = []
// Leitura paralela (execução concorrente no chunk para eficiência de I/O) const readPromises = chunk.map(async (entry) => { try { const content = await fs.readFile(entry.path, 'utf-8') const ast = parseFile(content, entry.path) return { path: entry.path, content, ast } } catch (error) { console.warn(`Failed to parse ${entry.path}:`, error) return null } })
const parsed = await Promise.all(readPromises)
for (const result of parsed) { if (result) { results.push(result) } }
return results}
/** * Interface unificada */export async function walkRepository( rootPath: string, options: WalkOptions, parseFile: (content: string, path: string) => unknown): Promise<ParsedFile[]> { // Fase 1: Escanear caminhos e tamanhos const entries = await scanPaths(rootPath, options)
// Fase 2: Análise de estrutura const parseableFiles = analyzeStructure(entries, options)
// Fase 3: Parsear sequencialmente por orçamento de bytes const results = await parseWithBudget(parseableFiles, options, parseFile)
return results}Uso
import { parse } from '@typescript-eslint/typescript-estree'
// Analisar repositório TypeScriptconst results = await walkRepository( './my-project', { parseableExtensions: ['.ts', '.tsx', '.js', '.jsx'], ignorePatterns: [ /node_modules/, /\.git/, /dist/, /build/ ], chunkBudget: 50 * 1024 * 1024, // 50MB maxFileSize: 10 * 1024 * 1024 // 10MB }, (content, path) => { // Aplicar parser AST return parse(content, { filePath: path, jsx: true }) })
console.log(`Parsed ${results.length} files`)Como Funciona
- Fase 1 (Scan): Percorrer recursivamente diretórios com
fs.readdirefs.stat, coletando apenas path e size. Conteúdos de arquivos não são lidos. - Fase 2 (Análise de Estrutura): Extrair arquivos parseáveis dos metadados coletados e otimizar ordem de processamento (por tamanho, prioridade, etc.).
- Fase 3 (Chunking + Parsing): Dividir arquivos em chunks não excedendo orçamento de bytes, então read → parse → free cada chunk.
- Leitura Paralela: Arquivos dentro de um chunk são lidos simultaneamente com
Promise.allpara reduzir tempo de espera de I/O. - Liberação de Memória: Reiniciar variáveis após processamento de chunk para que o GC possa recuperar memória.
Benefícios
- Controle de Memória: Evitar ler todos os arquivos de uma vez; definir limite superior com orçamento de bytes
- Filtragem Precoce: Excluir arquivos desnecessários usando apenas stats, reduzindo I/O
- Rastreamento de Progresso: Relatar progresso por chunk, facilitando atualização de UI ou manipulação de cancelamento
- Eficiência de I/O: Otimizar tempo de espera de disco com leitura paralela em chunks
Precauções
Para determinar “quais arquivos priorizar” na análise de estrutura da Fase 2 requer algum conhecimento de domínio. Por exemplo, ler package.json ou arquivos de configuração primeiro pode informar a estratégia de processamento para outros arquivos. Além disso, chunkBudget e maxFileSize precisam de ajuste de acordo com os limites de memória do ambiente.
Aplicações
- Indexador de código: Extrair e indexar definições de função/tipo no repositório
- Analisador estático: Aplicar regras de lint em massa a grande base de código
- Ferramenta de migração baseada em AST: Aplicar refatoração automática a toda base de código
- Linter em massa: Verificar dezenas de milhares de arquivos sem falha de memória
- Ingestão de grande repositório: Analisar mudanças em grandes repositórios em CI/CD
hsb.horse