UNPKG

@invisiblecities/sidequest-cqo

Version:

Configuration-agnostic TypeScript and ESLint orchestrator with real-time watch mode, SQLite persistence, and intelligent terminal detection

1,035 lines 43.4 kB
/** * @fileoverview ESLint audit engine with robust error handling * * Provides ESLint integration that can handle syntax errors gracefully, * continuing to analyze valid files while reporting parsing failures separately. */ import { spawnSync } from "node:child_process"; import path from "node:path"; import { BaseAuditEngine } from "./base-engine.js"; import { safeJsonParse, ESLintOutputSchema, } from "../utils/validation-schemas.js"; import { debugLog } from "../utils/debug-logger.js"; import { getPreferencesManager } from "../services/index.js"; /** * Engine for ESLint-based code quality analysis * * Key features: * - Graceful handling of syntax errors * - Round-robin rule checking for performance * - Buffer overflow protection * - Configurable rule sets */ export class ESLintAuditEngine extends BaseAuditEngine { baseDir; currentRuleIndex = 0; eslintRules; violationCache = new Map(); ruleZeroCount = new Map(); ruleLastCheck = new Map(); checksCount = 0; // Adaptive polling constants ZERO_THRESHOLD = 5; REDUCED_INTERVAL = 5; constructor(config) { const defaultConfig = { enabled: true, options: { rules: [ "@typescript-eslint/explicit-function-return-type", "@typescript-eslint/no-unused-vars", "@typescript-eslint/no-explicit-any", "@typescript-eslint/explicit-module-boundary-types", "@typescript-eslint/no-deprecated", "@typescript-eslint/no-non-null-assertion", "@typescript-eslint/ban-ts-comment", ], maxWarnings: 500, timeout: 30_000, roundRobin: false, // Use comprehensive analysis by default enableCustomScripts: true, // Automatically detect and run custom ESLint scripts customScriptPreset: "safe", // Use safe preset to avoid overwhelming output }, priority: 2, timeout: 35_000, allowFailure: true, // ESLint failures shouldn't break the whole analysis }; const mergedConfig = { ...defaultConfig, ...config }; super("ESLint Audit", "eslint", mergedConfig); this.baseDir = process.cwd(); // Extract rules with proper fallback // Following separation of concerns: ESLint for code quality, tsc for types const rulesFromConfig = mergedConfig.options?.rules; this.eslintRules = Array.isArray(rulesFromConfig) ? rulesFromConfig : [ // Code Quality & Style (non-type-aware) "no-console", "no-debugger", "prefer-const", "no-var", "no-unused-vars", // Performance & Architecture "no-floating-promises", "no-restricted-imports", // TypeScript-specific but performance-focused "@typescript-eslint/no-unused-vars", "@typescript-eslint/prefer-nullish-coalescing", "@typescript-eslint/prefer-optional-chain", // Comprehensive Unicorn rules (matching actual ESLint config) "unicorn/prefer-node-protocol", "unicorn/prefer-module", "unicorn/prefer-array-flat-map", "unicorn/prefer-string-starts-ends-with", "unicorn/prefer-number-properties", "unicorn/no-array-instanceof", "unicorn/prefer-spread", "unicorn/explicit-length-check", "unicorn/no-useless-undefined", ]; } /** * Analyze files with ESLint */ async analyze(targetPath, options = {}) { const violations = []; // Get custom script configuration from user preferences let enableCustomScripts = true; let customScriptPreset = "safe"; try { const preferencesManager = getPreferencesManager(); const preferences = preferencesManager.getAllPreferences(); const customScriptConfig = preferences.preferences?.customESLintScripts; enableCustomScripts = customScriptConfig?.enabled ?? true; customScriptPreset = customScriptConfig?.defaultPreset ?? "safe"; // Allow override from options enableCustomScripts = (options["enableCustomScripts"] ?? this.config.options?.["enableCustomScripts"] ?? enableCustomScripts); customScriptPreset = (options["customScriptPreset"] ?? this.config.options?.["customScriptPreset"] ?? customScriptPreset); } catch (error) { // Fallback to defaults if config is unavailable debugLog("ESLintEngine", "Using fallback custom script config", { error: String(error), }); enableCustomScripts = (options["enableCustomScripts"] ?? this.config.options?.["enableCustomScripts"] ?? true); customScriptPreset = (options["customScriptPreset"] ?? this.config.options?.["customScriptPreset"] ?? "safe"); } const searchPath = path.join(this.baseDir, targetPath); // FIRST: Try running custom ESLint scripts if enabled and detected if (enableCustomScripts && this.hasCustomESLintSystem()) { debugLog("ESLintEngine", "Custom ESLint system detected, attempting to run custom scripts"); const customViolations = await this.runCustomESLintScripts(searchPath, customScriptPreset); violations.push(...customViolations); } // SECOND: Run standard ESLint analysis const roundRobin = options["roundRobin"] ?? this.config.options["roundRobin"]; const standardViolations = roundRobin ? await this.analyzeWithRoundRobin(targetPath, options) : await this.analyzeAllRules(targetPath, options); violations.push(...standardViolations); return violations; } /** * Round-robin analysis for better performance and error isolation */ async analyzeWithRoundRobin(targetPath, _options) { this.checksCount++; // Determine which rule to check this cycle const ruleToCheck = this.selectNextRule(); if (!ruleToCheck) { // Return cached violations if no rule selected return this.getAllCachedViolations(); } // Run ESLint for the selected rule const ruleViolations = await this.runESLintForRules([ruleToCheck], targetPath); // Update cache this.violationCache.set(ruleToCheck, ruleViolations); this.ruleLastCheck.set(ruleToCheck, this.checksCount); // Update zero count tracking if (ruleViolations.length === 0) { this.ruleZeroCount.set(ruleToCheck, (this.ruleZeroCount.get(ruleToCheck) || 0) + 1); } else { this.ruleZeroCount.set(ruleToCheck, 0); } // Return all cached violations return this.getAllCachedViolations(); } /** * Analyze with all rules at once (traditional approach) */ analyzeAllRules(targetPath, _options) { // Pass empty array to disable rule filtering and get ALL violations return this.runESLintForRules([], targetPath); } /** * Select the next rule for round-robin checking */ selectNextRule() { if (this.eslintRules.length === 0) { return undefined; } let ruleToCheck = undefined; let attempts = 0; // Find the next rule to check (respecting adaptive intervals) while (attempts < this.eslintRules.length && !ruleToCheck) { const candidateRule = this.eslintRules[this.currentRuleIndex]; if (!candidateRule) { this.currentRuleIndex = (this.currentRuleIndex + 1) % this.eslintRules.length; attempts++; continue; } const zeroCount = this.ruleZeroCount.get(candidateRule) || 0; const lastCheck = this.ruleLastCheck.get(candidateRule) || 0; // First run or normal frequency checking if (this.checksCount <= 1 || zeroCount < this.ZERO_THRESHOLD) { ruleToCheck = candidateRule; } else { // If rule has been zero for >= ZERO_THRESHOLD times, check less frequently const cyclesSinceLastCheck = this.checksCount - lastCheck; if (cyclesSinceLastCheck >= this.REDUCED_INTERVAL) { ruleToCheck = candidateRule; } } // Move to next rule regardless this.currentRuleIndex = (this.currentRuleIndex + 1) % this.eslintRules.length; attempts++; } // Fallback: if no rule was selected, force check the first rule if (!ruleToCheck) { ruleToCheck = this.eslintRules[0] || undefined; this.currentRuleIndex = 1 % this.eslintRules.length; } return ruleToCheck; } /** * Get all violations from the cache */ getAllCachedViolations() { const allViolations = []; for (const violations of this.violationCache.values()) { allViolations.push(...violations); } return allViolations; } /** * Run ESLint for specific rules with robust handling */ runESLintForRules(rules, targetPath) { // For comprehensive analysis (empty rules array), use robust approach if (rules.length === 0) { return this.runESLintRobustly(targetPath); } // For round-robin with specific rules, use the original approach return Promise.resolve(this.runESLintWithBuffer(rules, targetPath)); } /** * Robust ESLint execution with temp file + sequential fallback */ async runESLintRobustly(targetPath) { try { // Try temp file approach first (fastest for complete analysis) console.log("[ESLint Engine] Running comprehensive analysis with temp file..."); return await this.runESLintWithTempFile(targetPath); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes("ENOBUFS") || errorMessage.includes("buffer") || errorMessage.includes("too large")) { console.warn("[ESLint Engine] Large output detected, falling back to sequential rule processing..."); return this.runESLintSequentially(targetPath); } else { console.warn("[ESLint Engine] Temp file approach failed, falling back to sequential:", errorMessage); return this.runESLintSequentially(targetPath); } } } /** * Run ESLint with temp file output (fastest for large results) */ async runESLintWithTempFile(targetPath) { const { mkdtemp, readFile, unlink } = await import("node:fs/promises"); const { tmpdir } = await import("node:os"); const temporaryDirectory = await mkdtemp(path.join(tmpdir(), "sidequest-eslint-")); const temporaryFile = path.join(temporaryDirectory, "results.json"); const maxWarnings = this.config.options["maxWarnings"] || 500; const timeout = this.config.options["timeout"] || 30_000; try { const eslintArguments = [ "--format", "json", "--output-file", temporaryFile, "--max-warnings", maxWarnings.toString(), "--ext", ".ts", targetPath, ]; console.log("[ESLint Engine] Running with temp file:", eslintArguments.join(" ")); const result = spawnSync("npx", ["eslint", ...eslintArguments], { encoding: "utf8", cwd: this.baseDir, timeout, signal: this.abortController?.signal, }); console.log("[ESLint Engine] Temp file command exit status:", result.status); if (result.stderr) { console.log("[ESLint Engine] Temp file stderr:", result.stderr.slice(0, 200)); } // Handle ESLint exit codes if (result.status === 2) { // Return warning violation instead of throwing error return [ this.createViolation("eslint-setup", 1, `ESLint configuration error: ${result.stderr}`, "setup-issue", "error", "ESLINT-SETUP-001", `ESLint configuration error detected. Check your .eslintrc file and ensure ESLint is properly configured. Error: ${result.stderr}`), ]; } // Read results from temp file const output = await readFile(temporaryFile, "utf8"); console.log("[ESLint Engine] Temp file output length:", output.length); if (output.length > 100) { console.log("[ESLint Engine] First 200 chars:", output.slice(0, 200)); } if (!output.trim()) { console.warn("[ESLint Engine] Temp file is empty"); return []; } return this.parseESLintOutput(output, []); } finally { // Cleanup temp file try { await unlink(temporaryFile); } catch { // Ignore cleanup errors } } } /** * Run ESLint sequentially by rule groups (reliable fallback) */ runESLintSequentially(targetPath) { const allViolations = []; // Get all rules from project's ESLint config const projectRules = this.getProjectESLintRules(); // Group rules to avoid too many individual calls const ruleGroups = this.chunkRules(projectRules, 10); // Process 10 rules at a time console.log(`[ESLint Engine] Processing ${ruleGroups.length} rule groups sequentially...`); for (let index = 0; index < ruleGroups.length; index++) { const ruleGroup = ruleGroups[index]; if (!ruleGroup) { continue; // Skip undefined groups } console.log(`[ESLint Engine] Processing group ${index + 1}/${ruleGroups.length}: ${ruleGroup.slice(0, 3).join(", ")}${ruleGroup.length > 3 ? "..." : ""}`); try { const groupViolations = this.runESLintWithSpecificRules(ruleGroup, targetPath); allViolations.push(...groupViolations); } catch (error) { console.warn(`[ESLint Engine] Failed to process rule group ${index + 1}:`, error); // Continue with other groups } } console.log(`[ESLint Engine] Sequential processing complete: ${allViolations.length} violations found`); return allViolations; } /** * Get rules from project's ESLint config */ getProjectESLintRules() { try { // Try to get rules from the actual ESLint config using a test file const result = spawnSync("npx", ["eslint", "--print-config", "cli.ts"], { encoding: "utf8", cwd: this.baseDir, timeout: 10_000, }); if (result.status === 0 && result.stdout) { const config = JSON.parse(result.stdout); const rules = Object.keys(config.rules || {}); console.log(`[ESLint Engine] Found ${rules.length} rules from project config`); if (rules.length > 0) { return rules; } } else { console.warn("[ESLint Engine] Failed to get project config:", result.stderr); } } catch (error) { console.warn("[ESLint Engine] Could not parse project rules:", error); } // Fallback to rules explicitly defined in .eslintrc.cjs console.log("[ESLint Engine] Using fallback rules from .eslintrc.cjs"); return [ // Core ESLint rules from .eslintrc.cjs "no-debugger", "no-alert", "no-eval", "no-implied-eval", "no-new-func", "no-script-url", "no-self-compare", "no-sequences", "no-throw-literal", "no-unmodified-loop-condition", "no-unused-expressions", "no-useless-call", "no-useless-concat", "no-useless-return", "no-void", "prefer-promise-reject-errors", "require-await", "indent", "quotes", "semi", "comma-dangle", "object-curly-spacing", "array-bracket-spacing", "space-before-function-paren", "keyword-spacing", "space-infix-ops", "eol-last", "no-trailing-spaces", "no-multiple-empty-lines", "curly", "eqeqeq", "no-var", "prefer-const", "prefer-arrow-callback", "arrow-spacing", "no-duplicate-imports", "object-shorthand", "prefer-template", // Unicorn rules that are enabled in .eslintrc.cjs "unicorn/prefer-string-slice", "unicorn/prefer-array-some", "unicorn/prefer-includes", "unicorn/prefer-object-from-entries", "unicorn/no-useless-undefined", "unicorn/prefer-ternary", // Additional unicorn rules from plugin:unicorn/recommended "unicorn/prevent-abbreviations", "unicorn/no-null", "unicorn/no-array-reduce", "unicorn/prefer-node-protocol", "unicorn/prefer-array-flat-map", "unicorn/prefer-string-starts-ends-with", "unicorn/prefer-number-properties", "unicorn/no-array-instanceof", "unicorn/prefer-spread", "unicorn/explicit-length-check", ]; } /** * Chunk rules into groups for sequential processing */ chunkRules(rules, chunkSize) { const chunks = []; for (let index = 0; index < rules.length; index += chunkSize) { chunks.push(rules.slice(index, index + chunkSize)); } return chunks; } /** * Run ESLint with specific rules enabled using project config */ runESLintWithSpecificRules(rules, targetPath) { const maxWarnings = this.config.options["maxWarnings"] || 500; const timeout = this.config.options["timeout"] || 30_000; // Use project's ESLint config but filter results to specific rules const eslintArguments = [ "--format", "json", "--max-warnings", maxWarnings.toString(), "--ext", ".ts", targetPath, ]; const result = spawnSync("npx", ["eslint", ...eslintArguments], { encoding: "utf8", cwd: this.baseDir, maxBuffer: 1024 * 1024 * 2, // Smaller buffer for rule groups timeout, signal: this.abortController?.signal, }); if (result.error) { throw new Error(`ESLint execution failed for rules ${rules.join(", ")}: ${result.error.message}`); } if (result.status === 2) { throw new Error(`ESLint configuration error for rules ${rules.join(", ")}: ${result.stderr}`); } if (!result.stdout) { return []; } // Parse and filter to only the specified rules return this.parseESLintOutput(result.stdout, rules); } /** * Original buffer-based approach for round-robin mode */ runESLintWithBuffer(rules, targetPath) { const maxWarnings = this.config.options["maxWarnings"] || 500; const timeout = this.config.options["timeout"] || 30_000; const eslintArguments = [ "--format", "json", "--max-warnings", maxWarnings.toString(), "--ext", ".ts", targetPath, ]; try { const result = spawnSync("npx", ["eslint", ...eslintArguments], { encoding: "utf8", cwd: this.baseDir, maxBuffer: 1024 * 1024 * 10, // 10MB buffer timeout, signal: this.abortController?.signal, }); if (result.error) { const errorCode = result.error.code; if (errorCode === "ENOBUFS") { throw new Error("Buffer overflow - switching to robust mode"); } else if (errorCode === "ETIMEDOUT") { console.warn("[ESLint Engine] Timeout - skipping ESLint results for this check"); return []; } else { console.warn("[ESLint Engine] Execution error:", result.error.message); return [ this.createViolation("eslint-setup", 1, `ESLint execution failed: ${result.error.message}`, "setup-issue", "error", "ESLINT-SETUP-002", `ESLint execution failed. This may indicate ESLint is not installed or there's a configuration issue. Error: ${result.error.message}`), ]; } } if (result.status === 2) { console.warn("[ESLint Engine] ESLint configuration error:", result.stderr?.slice(0, 500)); return [ this.createViolation("eslint-setup", 1, `ESLint configuration error: ${result.stderr}`, "setup-issue", "error", "ESLINT-SETUP-003", `ESLint configuration error in buffer mode. Check your .eslintrc file and ensure ESLint is properly configured. Error: ${result.stderr}`), ]; } if (result.stderr) { console.warn("[ESLint Engine] stderr:", result.stderr.slice(0, 200)); } if (!result.stdout) { console.warn("[ESLint Engine] No output received"); return []; } return this.parseESLintOutput(result.stdout, rules); } catch (error) { console.warn("[ESLint Engine] Buffer-based execution failed:", error); return [ this.createViolation("eslint-setup", 1, `ESLint buffer execution failed: ${error}`, "setup-issue", "error", "ESLINT-SETUP-004", `ESLint buffer-based execution failed unexpectedly. This may indicate a system or configuration issue. Error: ${error}`), ]; } } /** * Parse ESLint JSON output into violations */ parseESLintOutput(output, filterRules) { let eslintResults; try { // Use Zod validation for secure JSON parsing eslintResults = safeJsonParse(output, ESLintOutputSchema, "ESLint output"); console.log("[Security] ESLint output validated successfully"); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.warn("[ESLint Engine] Failed to parse and validate JSON output:", errorMessage); // Try to extract partial JSON if output was truncated const lastBracket = output.lastIndexOf("]"); if (lastBracket > 0) { try { const partialOutput = output.slice(0, Math.max(0, lastBracket + 1)); eslintResults = safeJsonParse(partialOutput, ESLintOutputSchema, "partial ESLint output"); console.warn("[ESLint Engine] Recovered partial ESLint results with validation"); } catch { console.warn("[ESLint Engine] Could not recover and validate partial JSON"); return []; } } else { return []; } } const violations = []; for (const fileResult of eslintResults) { const relativePath = path.relative(this.baseDir, fileResult.filePath); for (const message of fileResult.messages) { // Filter by specific rules if provided (for round-robin mode) // If filterRules is empty, include ALL violations (comprehensive mode) if (filterRules && filterRules.length > 0 && (!message.ruleId || !filterRules.includes(message.ruleId))) { continue; } const { category, severity } = this.categorizeESLintRule(message.ruleId || "unknown"); violations.push(this.createViolation(relativePath, message.line || 1, message.message || "ESLint violation", category, severity, message.ruleId || undefined, message.message, message.column)); } } return violations; } /** * Dynamically categorize ESLint violations based on rule patterns * Uses pattern matching instead of hard-coded lists for maintainability */ categorizeESLintRule(rule) { // Pattern-based categorization for unused variables if (rule.includes("unused-vars") || rule.includes("no-unused")) { return { category: "unused-vars", severity: "warn" }; } // Pattern-based categorization for modernization (prefer-* and no-legacy patterns) if (rule.startsWith("unicorn/prefer-") || rule.startsWith("unicorn/no-") || rule.includes("prefer-") || rule === "no-var") { return { category: "modernization", severity: "info" }; } // Pattern-based categorization for style/formatting if (rule.includes("consistent") || rule.includes("abbreviations") || rule.includes("destructuring") || rule.includes("spacing") || rule.includes("indent") || rule.includes("quote") || rule.includes("semi") || rule.includes("comma") || rule.includes("import") || rule.includes("duplicate")) { return { category: "style", severity: "info" }; } // Pattern-based categorization for code quality if (rule.includes("undef") || rule.includes("console") || rule.includes("debugger") || rule.includes("await") || rule.includes("async") || rule.includes("quality")) { return { category: "code-quality", severity: "warn" }; } // Pattern-based categorization for TypeScript best practices if (rule.startsWith("@typescript-eslint/") && (rule.includes("explicit") || rule.includes("any") || rule.includes("boundary"))) { return { category: "best-practices", severity: "warn" }; } // Pattern-based categorization for syntax/parsing errors if (rule.includes("parse") || rule.includes("syntax") || rule.includes("error")) { return { category: "syntax-error", severity: "error" }; } // Setup and configuration issues if (rule.startsWith("ESLINT-SETUP-")) { return { category: "setup-issue", severity: "error" }; } // Default fallback - let the session discovery handle unknown rules return { category: "other-eslint", severity: "info" }; } /** * Get round-robin status for reporting */ getRoundRobinStatus() { const lastCheckedRule = [...this.ruleLastCheck.entries()].sort(([, a], [, b]) => b - a)[0]?.[0] || "none"; const progress = `${this.currentRuleIndex}/${this.eslintRules.length}`; const adaptiveRules = this.eslintRules.filter((rule) => (this.ruleZeroCount.get(rule) || 0) >= this.ZERO_THRESHOLD).length; return { lastCheckedRule, progress, adaptiveRules, totalChecks: this.checksCount, }; } /** * Reset round-robin state (useful for testing or cache clearing) */ resetRoundRobinState() { this.currentRuleIndex = 0; this.violationCache.clear(); this.ruleZeroCount.clear(); this.ruleLastCheck.clear(); this.checksCount = 0; } /** * Update ESLint rules */ updateRules(newRules) { this.eslintRules = newRules; this.resetRoundRobinState(); // Reset state when rules change } /** * Get current ESLint rules */ getRules() { return [...this.eslintRules]; } /** * Check if the project has a custom ESLint system */ hasCustomESLintSystem() { const eslintSystemPaths = [ path.join(this.baseDir, "scripts/eslint/config.js"), path.join(this.baseDir, "scripts/eslint/run-all.js"), path.join(this.baseDir, "scripts/eslint"), ]; return eslintSystemPaths.some((eslintPath) => { try { const fs = require("node:fs"); return fs.existsSync(eslintPath); } catch { return false; } }); } /** * Run custom ESLint scripts if detected */ async runCustomESLintScripts(searchPath, preset) { const violations = []; try { // Get configured timeout let scriptTimeout = 60_000; // Default 60 seconds try { const preferencesManager = getPreferencesManager(); const preferences = preferencesManager.getAllPreferences(); const customScriptConfig = preferences.preferences?.customESLintScripts; scriptTimeout = customScriptConfig?.scriptTimeout ?? 60_000; } catch (error) { debugLog("ESLintEngine", "Using default script timeout", { error: String(error), }); } // Detect available custom ESLint scripts const customScripts = this.detectCustomESLintScripts(); if (customScripts.length === 0) { debugLog("ESLintEngine", "No custom ESLint scripts found in package.json"); return violations; } debugLog("ESLintEngine", "Detected custom ESLint scripts", { scripts: customScripts, preset, searchPath, }); // Select best script based on preset const selectedScript = this.selectBestCustomScript(customScripts, preset); if (!selectedScript) { debugLog("ESLintEngine", "No suitable custom script found for preset", { preset, availableScripts: customScripts, }); return violations; } // Execute the selected custom script const customViolations = await this.executeCustomESLintScript(selectedScript, searchPath, scriptTimeout); violations.push(...customViolations); } catch (error) { debugLog("ESLintEngine", "Error running custom ESLint scripts", { error: String(error), }); // Don't throw - gracefully degrade to standard ESLint } return violations; } /** * Detect custom ESLint scripts in package.json */ detectCustomESLintScripts() { const customScripts = []; try { const fs = require("node:fs"); const packageJsonPath = path.join(this.baseDir, "package.json"); if (!fs.existsSync(packageJsonPath)) { return customScripts; } const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); const scripts = packageJson.scripts || {}; // Look for ESLint-related scripts const eslintScriptPatterns = [ /^lint$/, /^eslint$/, /^lint:/, /^eslint:/, /lint$/, /eslint$/, ]; for (const [scriptName, scriptCommand] of Object.entries(scripts)) { if (typeof scriptCommand === "string" && eslintScriptPatterns.some((pattern) => pattern.test(scriptName)) && // Verify it actually runs ESLint (scriptCommand.includes("eslint") || scriptCommand.includes("lint"))) { customScripts.push(scriptName); } } debugLog("ESLintEngine", "Custom ESLint script detection completed", { totalScripts: Object.keys(scripts).length, eslintScripts: customScripts.length, foundScripts: customScripts, }); } catch (error) { debugLog("ESLintEngine", "Error detecting custom ESLint scripts", { error: String(error), }); } return customScripts; } /** * Select the best custom script to run based on preset and user configuration */ selectBestCustomScript(customScripts, preset) { try { // Get user preferences for custom script mappings const preferencesManager = getPreferencesManager(); const preferences = preferencesManager.getAllPreferences(); const customScriptConfig = preferences.preferences?.customESLintScripts; // Use configured preset mappings if available const presetMappings = customScriptConfig?.presetMappings || {}; const preferredScripts = presetMappings[preset]; if (preferredScripts && Array.isArray(preferredScripts)) { // Find the first preferred script that exists for (const preferredScript of preferredScripts) { if (customScripts.includes(preferredScript)) { debugLog("ESLintEngine", "Selected script from user config", { preset, selectedScript: preferredScript, configuredPreferences: preferredScripts, }); return preferredScript; } } } // Fallback to default mappings if no user config or no matches const defaultMappings = { safe: ["lint:check", "eslint", "lint"], fix: ["lint:fix", "eslint:fix"], strict: ["lint:strict", "eslint:strict"], ci: ["lint:ci", "eslint:ci"], }; const fallbackScripts = defaultMappings[preset] || defaultMappings["safe"] || []; for (const fallbackScript of fallbackScripts) { if (customScripts.includes(fallbackScript)) { debugLog("ESLintEngine", "Selected script from fallback defaults", { preset, selectedScript: fallbackScript, fallbackPreferences: fallbackScripts, }); return fallbackScript; } } // Final fallback to the first available custom script const firstScript = customScripts[0] || undefined; if (firstScript) { debugLog("ESLintEngine", "Selected first available script", { selectedScript: firstScript, allScripts: customScripts, }); } return firstScript; } catch (error) { debugLog("ESLintEngine", "Error selecting custom script, using first available", { error: String(error), fallbackScript: customScripts[0] || undefined, }); return customScripts[0] || undefined; } } /** * Execute a custom ESLint script and parse its output */ // eslint-disable-next-line require-await async executeCustomESLintScript(scriptName, searchPath, timeout) { const violations = []; try { debugLog("ESLintEngine", "Executing custom ESLint script", { scriptName, searchPath, timeout, }); // Detect package manager const fs = require("node:fs"); const packageManager = fs.existsSync(path.join(this.baseDir, "pnpm-lock.yaml")) ? "pnpm" : fs.existsSync(path.join(this.baseDir, "yarn.lock")) ? "yarn" : "npm"; // Build command based on package manager const runCmd = packageManager === "yarn" ? "yarn" : `${packageManager} run`; const fullCommand = `${runCmd} ${scriptName}`; debugLog("ESLintEngine", "Running custom ESLint command", { command: fullCommand, cwd: this.baseDir, packageManager, }); // Execute the script const cmdParts = runCmd.split(" "); const command = cmdParts[0]; const arguments_ = [...cmdParts.slice(1), scriptName]; if (!command) { throw new Error(`Invalid command: ${runCmd}`); } const result = spawnSync(command, arguments_, { encoding: "utf8", cwd: this.baseDir, timeout, stdio: ["ignore", "pipe", "pipe"], }); debugLog("ESLintEngine", "Custom ESLint script completed", { scriptName, exitCode: result.status, signal: result.signal, hasStdout: !!result.stdout, hasStderr: !!result.stderr, stdoutLength: result.stdout?.length || 0, stderrLength: result.stderr?.length || 0, }); // Parse output as ESLint JSON if available if (result.stdout) { try { const output = JSON.parse(result.stdout); if (Array.isArray(output)) { // Process ESLint JSON output const customViolations = this.parseCustomESLintOutput(output, scriptName); violations.push(...customViolations); debugLog("ESLintEngine", "Successfully parsed custom ESLint output", { scriptName, violationsFound: customViolations.length, }); } } catch (parseError) { debugLog("ESLintEngine", "Could not parse custom ESLint output as JSON", { scriptName, parseError: String(parseError), outputPreview: result.stdout.slice(0, 200), }); } } // Add a summary violation for the custom script execution violations.push({ file: "custom-script-execution", line: 1, column: 1, code: `Custom ESLint script "${scriptName}" executed successfully`, category: "custom-script-summary", severity: "info", source: "custom", rule: "custom-script-execution", message: `Executed custom ESLint script: ${scriptName} (exit code: ${result.status})`, }); } catch (error) { debugLog("ESLintEngine", "Error executing custom ESLint script", { scriptName, error: String(error), }); // Add error violation violations.push({ file: "custom-script-execution", line: 1, column: 1, code: `Failed to execute custom ESLint script "${scriptName}": ${String(error)}`, category: "setup-issue", severity: "warn", source: "custom", rule: "custom-script-execution-error", message: `Custom ESLint script execution failed: ${String(error)}`, }); } return violations; } /** * Parse custom ESLint output into violation format */ parseCustomESLintOutput(eslintOutput, scriptName) { const violations = []; try { for (const fileResult of eslintOutput) { if (!fileResult.filePath || !Array.isArray(fileResult.messages)) { continue; } for (const message of fileResult.messages) { const violation = { file: path.relative(this.baseDir, fileResult.filePath), line: message.line || 1, column: message.column || 1, code: message.message || "ESLint violation", category: this.mapESLintSeverityToCategory(message.severity), severity: this.mapESLintSeverityToSeverity(message.severity), source: "custom", rule: message.ruleId || "unknown", message: `[${scriptName}] ${message.message || "ESLint violation"}`, }; violations.push(violation); } } debugLog("ESLintEngine", "Parsed custom ESLint violations", { scriptName, filesProcessed: eslintOutput.length, violationsExtracted: violations.length, }); } catch (error) { debugLog("ESLintEngine", "Error parsing custom ESLint output", { scriptName, error: String(error), }); } return violations; } /** * Map ESLint severity to our category system */ mapESLintSeverityToCategory(severity) { switch (severity) { case 2: { return "syntax-error"; } case 1: { return "best-practices"; } default: { return "style"; } } } /** * Map ESLint severity to our severity system */ mapESLintSeverityToSeverity(severity) { switch (severity) { case 2: { return "error"; } case 1: { return "warn"; } default: { return "info"; } } } } //# sourceMappingURL=eslint-engine.js.map