UNPKG

@ton-ai-core/vibecode-linter

Version:

Advanced TypeScript linter with Git integration, dependency analysis, and comprehensive error reporting

240 lines 11.5 kB
// CHANGE: Extracted Biome runner from lint.ts // WHY: Biome operations should be in a separate module // QUOTE(ТЗ): "Разбить lint.ts на подфайлы, каждый файл желательно должен быть не больше 300 строчек кода" // REF: REQ-20250210-MODULAR-ARCH // PURITY: SHELL // EFFECT: Effect<BiomeResult[], ExternalToolError | ParseError> // SOURCE: lint.ts lines 1058-1073, 1362-1556 // CHANGE: Use node: protocol for Node.js built-in modules // WHY: Biome lint rule requires explicit node: prefix for clarity // REF: lint/style/useNodejsImportProtocol // SOURCE: https://biomejs.dev/linter/rules/lint/style/useNodejsImportProtocol import { exec } from "node:child_process"; import { promisify } from "node:util"; import { Effect, pipe } from "effect"; import { ExternalToolError } from "../../core/errors.js"; import { extractStdoutFromError } from "../../core/types/index.js"; import { parseBiomeOutput } from "./biome-parser.js"; import { extractStdoutOrThrow } from "./linter-helpers.js"; const execAsync = promisify(exec); /** * Запускает Biome auto-fix на указанном пути. * Выполняет несколько проходов для исправления зависимых ошибок. * * CHANGE: Use Effect.gen for typed error handling with multiple passes * WHY: Replace Promise + try/catch with Effect for provability * QUOTE(ТЗ): "Effect-TS для всех эффектов" * REF: Architecture plan - Effect-based SHELL * * @param targetPath Путь для линтинга * @returns Effect с void или typed error * * @pure false - modifies files via Biome * @effect Effect<void, ExternalToolError> * @invariant targetPath не пустой * @complexity O(1) - runs 3 passes unconditionally */ export function runBiomeFix(targetPath) { return Effect.gen(function* () { const biomeFixCommand = `npx biome check --write "${targetPath}"`; console.log(`🔧 Running Biome auto-fix on: ${targetPath}`); // CHANGE: Log exact Biome CLI command for reproducibility // WHY: Allows manual reruns that mirror automatic auto-fix behavior // QUOTE(USER-LOG-CMDS): "Хочу добавить это в лог ... Что бы если что я мог бы повторить этот результат" // REF: USER-LOG-CMDS // SOURCE: n/a // FORMAT THEOREM: ∀target: autoFix(target) uses same biomeFixCommand(target) in every pass // PURITY: SHELL // INVARIANT: Logged command equals the one executed inside each attempt // COMPLEXITY: O(1) console.log(` ↳ Command: ${biomeFixCommand}`); const maxAttempts = 3; // CHANGE: Use Effect.forEach for sequential execution of fix passes // WHY: Makes iteration explicit with Effect composition // INVARIANT: Executes exactly maxAttempts passes for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { yield* Effect.tryPromise({ try: async () => execAsync(biomeFixCommand), catch: (error) => { // Biome returns non-zero on fixed issues - this is expected const out = extractStdoutFromError(error); if (typeof out === "string") { return undefined; // Success with fixes } console.error(`❌ Biome auto-fix failed:`, error); return new ExternalToolError({ tool: "biome", reason: `Biome auto-fix failed: ${String(error)}`, }); }, }).pipe(Effect.catchAll((err) => { if (err === undefined) { return Effect.succeed(undefined); } return Effect.fail(err); })); } console.log(`✅ Biome auto-fix completed (${maxAttempts} passes)`); }); } /** * Получает диагностику Biome для указанного пути. * * CHANGE: Use Effect.gen for typed error handling with fallback * WHY: Replace Promise + try/catch with Effect for provability * QUOTE(ТЗ): "Effect-TS для всех эффектов" * REF: Architecture plan - Effect-based SHELL * * @param targetPath Путь для линтинга * @returns Effect с массивом результатов или typed error * * @pure false - executes external process * @effect Effect<BiomeResult[], ExternalToolError | ParseError> * @invariant targetPath не пустой * @complexity O(n) where n = number of files (with fallback) */ export function getBiomeDiagnostics(targetPath) { return Effect.gen(function* () { // CHANGE: Use Effect.promise to always get stdout (even on non-zero exit) // WHY: Biome returns non-zero on lint errors but with valid JSON const biomeDiagnosticsCommand = `npx biome check "${targetPath}" --reporter=json`; // CHANGE: Log Biome diagnostics invocation when it actually executes // WHY: Display reproducible command inline instead of at start // QUOTE(USER-LOG-CMDS): "как только их вызывает он бы писал что за команду" // REF: USER-LOG-CMDS // SOURCE: n/a // FORMAT THEOREM: ∀target: logged command equals CLI invoked // PURITY: SHELL // INVARIANT: Message emitted once per diagnostics run // COMPLEXITY: O(1) console.log(`🧪 Running Biome diagnostics on: ${targetPath}`); console.log(` ↳ Command: ${biomeDiagnosticsCommand}`); const stdout = yield* Effect.promise(async () => { try { const result = await execAsync(biomeDiagnosticsCommand); return result.stdout; } catch (error) { // CHANGE: Use extractStdoutOrThrow to remove code duplication // WHY: Identical pattern in eslint.ts (jscpd DUPLICATE #1) // REF: linter-helpers.ts, REQ-LINT-FIX return extractStdoutOrThrow(error); } }).pipe(Effect.catchAll((error) => { console.error("❌ Biome diagnostics failed:", error); return Effect.fail(new ExternalToolError({ tool: "biome", reason: `Biome diagnostics failed: ${String(error)}`, })); })); // CHANGE: Use Effect.sync for parsing // WHY: parseBiomeOutput is synchronous const parsed = yield* Effect.sync(() => parseBiomeOutput(stdout)); // CHANGE: Trigger fallback only when Biome JSON is invalid (parsed=false) // WHY: Avoid spurious per-file scans when Biome simply found no diagnostics // QUOTE(RTM-BIOME-FALLBACK): "Не показывать fallback, если Biome вернул пустой, но валидный отчёт" // REF: RTM-BIOME-FALLBACK const shouldFallback = !parsed.parsed && !targetPath.endsWith(".ts") && !targetPath.endsWith(".tsx"); if (shouldFallback) { console.log("🔄 Biome: Falling back to individual file checking..."); return yield* getBiomeDiagnosticsPerFileEffect(targetPath); } return parsed.diagnostics; }); } /** * Получает диагностику Biome для каждого файла отдельно. * * CHANGE: Use Effect.gen for typed error handling per-file * WHY: Replace Promise + try/catch with Effect for provability * QUOTE(ТЗ): "Effect-TS для всех эффектов" * REF: Architecture plan - Effect-based SHELL * * @param targetPath Путь к директории * @returns Effect с массивом результатов или typed error * * @pure false - executes external processes * @effect Effect<BiomeResult[], ExternalToolError> * @complexity O(n) where n = number of files */ function getBiomeDiagnosticsPerFileEffect(targetPath) { return Effect.gen(function* () { const files = yield* listBiomeTargetFiles(targetPath); return yield* collectPerFileDiagnostics(files); }); } // CHANGE: Extracted file listing effect for Biome per-file fallback // WHY: Keep getBiomeDiagnosticsPerFileEffect under max-lines while isolating IO concerns // QUOTE(LINT): "Function 'getBiomeDiagnosticsPerFileEffect' has too many lines (51). Maximum allowed is 50." // REF: ESLint max-lines-per-function // SOURCE: n/a // FORMAT THEOREM: ∀targetPath: listBiomeTargetFiles(targetPath).length ≤ 20 // PURITY: SHELL // EFFECT: Effect<ReadonlyArray<string>, ExternalToolError> // INVARIANT: Returned list contains only non-empty .ts/.tsx paths // COMPLEXITY: O(n) where n = matched files (bounded by head -20) const listBiomeTargetFiles = (targetPath) => Effect.promise(async () => { try { return await execAsync(`find "${targetPath}" -name "*.ts" -o -name "*.tsx" | head -20`); } catch (error) { console.error("Failed to list files:", error); throw error; } }).pipe(Effect.catchAll((error) => Effect.fail(new ExternalToolError({ tool: "git", reason: `Failed to list TypeScript files: ${String(error)}`, }))), Effect.map((lsOutput) => pipe(lsOutput.stdout.trim().split("\n"), (lines) => lines.filter((f) => f.trim().length > 0)))); // CHANGE: Extracted per-file aggregation logic // WHY: Maintain single responsibility and keep orchestrator slim // QUOTE(LINT): "Function 'getBiomeDiagnosticsPerFileEffect' has too many lines (51). Maximum allowed is 50." // REF: ESLint max-lines-per-function // SOURCE: n/a // FORMAT THEOREM: ∀files: collectPerFileDiagnostics(files)=⋃ runBiomeCheckForFile(file) // PURITY: SHELL // EFFECT: Effect<ReadonlyArray<BiomeResult>, never> // INVARIANT: Results preserve concatenation order of input files // COMPLEXITY: O(n) where n = |files| const collectPerFileDiagnostics = (files) => Effect.gen(function* () { const allResults = []; for (const file of files) { const diagnostics = yield* runBiomeCheckForFile(file); allResults.push(...diagnostics); } return allResults; }); // CHANGE: Isolated single-file Biome execution with parse guard // WHY: Reuse per file and keep control flow explicit // QUOTE(RTM-BIOME-FALLBACK): "Не показывать fallback, если Biome вернул пустой, но валидный отчёт" // REF: RTM-BIOME-FALLBACK // SOURCE: n/a // FORMAT THEOREM: stdout="" → diagnostics=[] // PURITY: SHELL // EFFECT: Effect<ReadonlyArray<BiomeResult>, never> // INVARIANT: parsed=false ⇒ emitted diagnostics=[] // COMPLEXITY: O(1) per file (Biome CLI dominates) const runBiomeCheckForFile = (file) => Effect.promise(async () => { try { const result = await execAsync(`npx biome check "${file}" --reporter=json`); return result.stdout; } catch (error) { const stdout = extractStdoutFromError(error); return stdout ?? ""; } }) .pipe(Effect.catchAll(() => Effect.succeed(""))) .pipe(Effect.flatMap((stdout) => Effect.sync(() => { if (stdout.length === 0) { return []; } const parsed = parseBiomeOutput(stdout); if (!parsed.parsed) { console.warn(`⚠️ Biome JSON parse failed for ${file}; skipping.`); return []; } return parsed.diagnostics; }))); //# sourceMappingURL=biome.js.map