Bei der Analyse großer Codebasen führt das gleichzeitige Laden aller Dateien in den Speicher zum Scheitern. Durch das vorherige Sammeln nur von Pfaden und Stats, Abschließen der Strukturanalyse und anschließendes Chunking nach Byte-Budget zum sequentiellen Lesen/Parsen/Freigeben nur benötigter Dateien kann die Speichernutzung vorhersagbar gehalten werden.
Code
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 oder Analyseergebnis}
interface WalkOptions { /** Zu parsende Erweiterungen (z.B. ['.ts', '.js']) */ parseableExtensions: string[] /** Zu ignorierende Muster (z.B. node_modules, .git) */ ignorePatterns: RegExp[] /** Maximale Bytes pro Chunk */ chunkBudget: number /** Maximale Einzeldateigröße (Dateien darüber werden übersprungen) */ maxFileSize: number}
/** * Phase 1: Nur Pfade und Größen scannen */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)
// Ignorier-Muster prüfen (frühe Beurteilung für Speichereffizienz) 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)
// Größenprüfung (riesige Dateien früh ausschließen) if (stat.size > options.maxFileSize) { continue }
entries.push({ path: fullPath, size: stat.size }) } } }
await walk(rootPath) return entries}
/** * Phase 2: Strukturanalyse und Filterung */function analyzeStructure( entries: FileEntry[], options: WalkOptions): FileEntry[] { // Nach Erweiterung filtern const parseableFiles = entries.filter((entry) => { const ext = extname(entry.path) return options.parseableExtensions.includes(ext) })
// Nach Größe sortieren (kleinere Dateien zuerst zu verarbeiten liefert frühe Ergebnisse) return parseableFiles.toSorted((a, b) => a.size - b.size)}
/** * Phase 3: Nach Byte-Budget chunken und sequentiell lesen/parsen */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) { // Chunk verarbeiten wenn Budget überschritten if (currentBudget + entry.size > options.chunkBudget) { // Aktuellen Chunk verarbeiten const parsed = await processChunk(chunk, parseFile) results.push(...parsed)
// Zurücksetzen (ermöglicht GC Speicher freizugeben) chunk = [] currentBudget = 0 }
chunk.push(entry) currentBudget += entry.size }
// Letzten Chunk verarbeiten if (chunk.length > 0) { const parsed = await processChunk(chunk, parseFile) results.push(...parsed) }
return results}
/** * Batch-Lesen und Parsen von Dateien im Chunk */async function processChunk( chunk: FileEntry[], parseFile: (content: string, path: string) => unknown): Promise<ParsedFile[]> { const results: ParsedFile[] = []
// Paralleles Lesen (gleichzeitige Ausführung im Chunk für I/O-Effizienz) 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}
/** * Einheitliche Schnittstelle */export async function walkRepository( rootPath: string, options: WalkOptions, parseFile: (content: string, path: string) => unknown): Promise<ParsedFile[]> { // Phase 1: Pfade und Größen scannen const entries = await scanPaths(rootPath, options)
// Phase 2: Strukturanalyse const parseableFiles = analyzeStructure(entries, options)
// Phase 3: Sequentiell nach Byte-Budget parsen const results = await parseWithBudget(parseableFiles, options, parseFile)
return results}Verwendung
import { parse } from '@typescript-eslint/typescript-estree'
// TypeScript-Repository analysierenconst 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) => { // AST-Parser anwenden return parse(content, { filePath: path, jsx: true }) })
console.log(`Parsed ${results.length} files`)Funktionsweise
- Phase 1 (Scan): Verzeichnisse rekursiv mit
fs.readdirundfs.statdurchlaufen, dabei nur path und size sammeln. Dateiinhalte werden nicht gelesen. - Phase 2 (Strukturanalyse): Parsbare Dateien aus gesammelten Metadaten extrahieren und Verarbeitungsreihenfolge optimieren (nach Größe, Priorität, etc.).
- Phase 3 (Chunking + Parsen): Dateien in Chunks aufteilen, die Byte-Budget nicht überschreiten, dann jeden Chunk read → parse → free.
- Paralleles Lesen: Dateien innerhalb eines Chunks werden gleichzeitig mit
Promise.allgelesen, um I/O-Wartezeit zu reduzieren. - Speicherfreigabe: Variablen nach Chunk-Verarbeitung zurücksetzen, damit GC Speicher freigeben kann.
Vorteile
- Speicherkontrolle: Nicht alle Dateien auf einmal lesen; Obergrenze mit Byte-Budget festlegen
- Frühe Filterung: Unnötige Dateien nur mit Stats ausschließen, I/O reduzieren
- Fortschrittsverfolgung: Fortschritt pro Chunk melden, erleichtert UI-Aktualisierung oder Abbruchbehandlung
- I/O-Effizienz: Festplatten-Wartezeit mit parallelem Lesen in Chunks optimieren
Vorsichtsmaßnahmen
Um in Phase 2 Strukturanalyse zu bestimmen “welche Dateien zu priorisieren” erfordert etwas Domänenwissen. Zum Beispiel kann das zuerst Lesen von package.json oder Konfigurationsdateien die Verarbeitungsstrategie für andere Dateien informieren. Außerdem benötigen chunkBudget und maxFileSize Anpassung je nach Umgebungs-Speicherlimits.
Anwendungen
- Code-Indexer: Funktions-/Typdefinitionen im Repository extrahieren und indizieren
- Statischer Analyzer: Lint-Regeln massenhaft auf große Codebasis anwenden
- AST-basiertes Migrations-Tool: Automatisches Refactoring auf gesamte Codebasis anwenden
- Massen-Linter: Zehntausende Dateien prüfen ohne Speicherausfall
- Große-Repo-Aufnahme: Änderungen in großen Repositories in CI/CD analysieren
hsb.horse