UNPKG

@invisiblecities/sidequest-cqo

Version:

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

964 lines 40.9 kB
/** * @fileoverview TypeScript Compilation Engine * * Runs the client's TypeScript compiler configuration without imposing opinions. * Reports compilation errors exactly as TypeScript reports them. * Respects the client's tsconfig.json and compiler options. */ import { spawnSync } from "node:child_process"; import path from "node:path"; import * as fs from "node:fs"; import { BaseAuditEngine } from "./base-engine.js"; import { safeJsonParse, TSConfigSchema, } from "../utils/validation-schemas.js"; import { debugLog } from "../utils/debug-logger.js"; import { getPreferencesManager } from "../services/index.js"; /** * Engine for TypeScript compilation validation * * Runs `tsc --noEmit` using the client's tsconfig.json without modification. * Reports TypeScript compiler errors without categorization or opinion. * Optional: Includes pattern-based checks for unknown/any usage. */ export class TypeScriptAuditEngine extends BaseAuditEngine { baseDir; constructor(config) { const defaultConfig = { enabled: true, options: { includeAny: false, // Optional pattern checking strict: false, // For pattern checks only targetPath: "app", checkCompilation: true, // Primary function: run tsc --noEmit enableCustomScripts: true, // Automatically detect and run custom TSC scripts customScriptPreset: "safe", // Use safe preset to avoid overwhelming output }, priority: 1, timeout: 30_000, allowFailure: false, }; const mergedConfig = { ...defaultConfig, ...config }; super("TypeScript Compiler", "typescript", mergedConfig); this.baseDir = process.cwd(); } /** * Analyze TypeScript files for compilation errors and optional pattern violations */ async analyze(targetPath, options = {}) { const violations = []; const checkCompilation = options["checkCompilation"] ?? this.config.options["checkCompilation"] ?? true; const includeAny = options["includeAny"] ?? this.config.options["includeAny"] ?? false; // 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?.customTypeScriptScripts; 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("TypeScriptEngine", "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: Run TypeScript compiler to catch actual compilation errors if (checkCompilation) { const compilationViolations = await Promise.resolve(this.checkTypeScriptCompilation(searchPath)); violations.push(...compilationViolations); } // SECOND: Run custom TypeScript quality scripts if available if (enableCustomScripts) { debugLog("TypeScriptEngine", "Checking for custom TypeScript scripts", { enableCustomScripts, customScriptPreset, baseDir: this.baseDir, }); const customViolations = await this.runCustomTypeScriptScripts(customScriptPreset); debugLog("TypeScriptEngine", "Custom script violations found", { count: customViolations.length, }); violations.push(...customViolations); } // OPTIONAL: Run pattern-based checks for unknown/any usage if (includeAny) { const patternViolations = this.checkPatternViolations(targetPath, options); violations.push(...patternViolations); } return violations; } /** * Run TypeScript compiler to detect compilation errors */ checkTypeScriptCompilation(searchPath) { const violations = []; // Find tsconfig.json const tsConfigPath = this.findTsConfig(searchPath); if (!tsConfigPath) { // If no tsconfig, try to run tsc on the directory directly return this.runTscOnDirectory(searchPath); } // Store client's TypeScript configuration in database for fast access this.cacheTypeScriptConfig(tsConfigPath); try { // Run tsc --noEmit with the found tsconfig (respecting their exact configuration) const result = spawnSync("npx", ["tsc", "--noEmit", "--project", tsConfigPath], { encoding: "utf8", cwd: this.baseDir, signal: this.abortController?.signal, }); if (result.error) { console.warn("[TypeScript Engine] Failed to run tsc:", result.error.message); // Add warning violation instead of silently returning empty violations.push(this.createViolation("typescript-setup", 1, `TypeScript compiler failed: ${result.error.message}`, "setup-issue", "error", "TS-SETUP-001", `Failed to run TypeScript compiler. This may indicate TypeScript is not installed or configured properly. Error: ${result.error.message}`)); return violations; } // Parse TypeScript compiler output if (result.stderr) { const compilationViolations = this.parseTypeScriptErrors(result.stderr, tsConfigPath); violations.push(...compilationViolations); } // Some errors might be in stdout if (result.stdout && result.stdout.includes("error TS")) { const compilationViolations = this.parseTypeScriptErrors(result.stdout, tsConfigPath); violations.push(...compilationViolations); } } catch (error) { console.warn("[TypeScript Engine] TypeScript compilation check failed:", error); // Add warning violation for unexpected errors violations.push(this.createViolation("typescript-setup", 1, `TypeScript compilation check failed: ${error}`, "setup-issue", "error", "TS-SETUP-002", `TypeScript compilation check encountered an unexpected error. This may indicate a configuration issue. Error: ${error}`)); } return violations; } /** * Find tsconfig.json starting from search path and moving up */ findTsConfig(searchPath) { let currentDirectory = searchPath; while (currentDirectory !== path.dirname(currentDirectory)) { const tsConfigPath = path.join(currentDirectory, "tsconfig.json"); if (fs.existsSync(tsConfigPath)) { return tsConfigPath; } currentDirectory = path.dirname(currentDirectory); } // Check project root const rootTsConfig = path.join(this.baseDir, "tsconfig.json"); if (fs.existsSync(rootTsConfig)) { return rootTsConfig; } return undefined; } /** * Run tsc directly on directory when no tsconfig found */ runTscOnDirectory(searchPath) { const violations = []; try { const result = spawnSync("npx", [ "tsc", "--noEmit", "--target", "ES2024", "--module", "ESNext", "--strict", `${searchPath}/**/*.ts`, ], { encoding: "utf8", cwd: this.baseDir, signal: this.abortController?.signal, }); if (result.stderr) { const compilationViolations = this.parseTypeScriptErrors(result.stderr); violations.push(...compilationViolations); } } catch (error) { console.warn("[TypeScript Engine] Direct tsc compilation failed:", error); // Add warning violation for direct compilation failures violations.push(this.createViolation("typescript-setup", 1, `Direct TypeScript compilation failed: ${error}`, "setup-issue", "error", "TS-SETUP-003", `Direct TypeScript compilation failed when no tsconfig.json was found. This may indicate TypeScript is not installed. Error: ${error}`)); } return violations; } /** * Parse TypeScript compiler error output into violations */ parseTypeScriptErrors(errorOutput, _tsConfigPath) { const violations = []; const lines = errorOutput.split("\n"); for (const line of lines) { if (this.abortController?.signal.aborted) { break; } // Match TypeScript error format: file(line,col): error TSxxxx: message const match = line.match(/^(.+?)\((\d+),(\d+)\):\s*(error|warning)\s+(TS\d+):\s*(.+)$/); if (match) { const [, filePath, lineString, , severityString, ruleCode, message] = match; if (!filePath || !lineString || !message) { continue; } const line = Number.parseInt(lineString, 10); // const _column = parseInt(colStr || '0', 10); // Make path relative to base directory const relativePath = path.relative(this.baseDir, filePath); // Use TypeScript's own severity determination const severity = severityString === "error" ? "error" : "warn"; // Get category from database mapping or use default const category = this.getCategoryForRule(ruleCode || "TS0000"); violations.push(this.createViolation(relativePath, line, message.trim(), category, severity, ruleCode || "TS0000", message.trim())); } } return violations; } /** * Cache TypeScript configuration in database for fast access during watch mode */ cacheTypeScriptConfig(tsConfigPath) { try { const configContent = fs.readFileSync(tsConfigPath, "utf8"); // Use Zod validation for secure tsconfig.json parsing const config = safeJsonParse(configContent, TSConfigSchema, "tsconfig.json"); // Use debug logger instead of console.log to avoid EPIPE errors when output is piped if (process.stdout.writable) { console.log("[Security] TypeScript configuration validated successfully"); } // Store in database (pseudo-code - would need actual DB connection) // This ensures watch mode and reports can access client configuration quickly const configSummary = { path: tsConfigPath, strict: config.compilerOptions?.strict ?? false, exactOptionalPropertyTypes: config.compilerOptions?.exactOptionalPropertyTypes ?? false, noUncheckedIndexedAccess: config.compilerOptions?.noUncheckedIndexedAccess ?? false, noImplicitAny: config.compilerOptions?.noImplicitAny ?? false, target: config.compilerOptions?.target ?? "ES5", module: config.compilerOptions?.module ?? "CommonJS", lastScanned: new Date().toISOString(), }; // Log configuration discovery (without imposing opinion) console.log(`[TypeScript Engine] Client configuration loaded: ${tsConfigPath}`); console.log(`[TypeScript Engine] Strict mode: ${configSummary.strict}`); console.log(`[TypeScript Engine] Target: ${configSummary.target}`); // TODO: Store configSummary in database for watch mode access } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.warn("[TypeScript Engine] Could not validate TypeScript config:", errorMessage); console.warn("[TypeScript Engine] Continuing with default configuration..."); } } /** * Get category for rule from database mapping or create new mapping * Uses dynamic database-driven approach instead of hard-coded mappings */ getCategoryForRule(ruleCode) { // TODO: Implement database lookup for rule category mapping // For now, use pattern-based fallback until database service is connected return this.getDefaultCategoryFromPattern(ruleCode); } /** * Pattern-based fallback for rule categorization until database is integrated */ getDefaultCategoryFromPattern(ruleCode) { const numericCode = ruleCode.replace("TS", ""); // Module resolution and import/export issues if ([ "2307", "2305", "2306", "1016", "1259", "1192", "1149", "2451", "2393", "2440", "2300", "1038", "2339", ].includes(numericCode)) { return "module-resolution"; } // Null safety and undefined issues if ([ "2532", "2533", "2531", "18048", "18047", "2454", "2722", "2721", "2345", "2322", "2349", ].includes(numericCode)) { return "null-safety"; } // Type annotation and type issues (most 7xxx codes and common type errors) if (/^7\d{3}$/.test(numericCode) || ["2322", "2304", "2314", "2315", "2344", "2362", "2355", "2741"].includes(numericCode)) { return "type-alias"; } // Unused code issues (6xxx codes and specific unused patterns) if (/^6\d{3}$/.test(numericCode) || ["2695", "2578"].includes(numericCode)) { return "unused-code"; } // Class/inheritance and override issues if ([ "4114", "2515", "2564", "2334", "2335", "2336", "2337", "2510", "2511", "2512", "2513", "2416", "2417", ].includes(numericCode)) { return "inheritance"; } // Index access and element access issues if (["4111", "2339", "2740", "2538", "7053"].includes(numericCode)) { return "index-access"; } // Strict config and exactOptionalPropertyTypes issues if (["2375", "2379", "2412", "2783", "2784"].includes(numericCode)) { return "strict-config"; } // Setup and configuration issues if (ruleCode.startsWith("TS-SETUP-")) { return "setup-issue"; } // Syntax and parsing errors if (/^1\d{3}$/.test(numericCode) || ["1005", "1109", "1161", "1434"].includes(numericCode)) { return "syntax-error"; } // Modernization (decorators, async/await, newer TS features) if ([ "1206", "1207", "1208", "1219", "1308", "1353", "2794", "2705", "2706", ].includes(numericCode)) { return "modernization"; } // Generic/template issues if (["2313", "2314", "2430"].includes(numericCode)) { return "generic-constraint"; } // Syntax/parsing errors (1xxx codes) if (/^1\d{3}$/.test(numericCode)) { return "syntax-error"; } // Everything else is type-alias (type system issues) return "type-alias"; } /** * Run pattern-based checks for unknown/any usage (legacy functionality) */ checkPatternViolations(targetPath, options) { const violations = []; const includeAny = options["includeAny"] || this.config.options["includeAny"]; const strict = options["strict"] || this.config.options["strict"]; const searchPath = path.join(this.baseDir, targetPath); // Get search patterns based on configuration const patterns = this.getSearchPatterns(includeAny); const seen = new Set(); // Run ripgrep for each pattern for (const pattern of patterns) { if (this.abortController?.signal.aborted) { break; } try { const result = spawnSync("rg", [ "--no-heading", "--line-number", "--glob", "*.ts", "--glob", "*.tsx", "-e", pattern, searchPath, ], { encoding: "utf8", signal: this.abortController?.signal, }); if (result.error) { continue; // Skip pattern if ripgrep fails } // Process results result.stdout.split("\n").forEach((line) => { if (line.trim()) { seen.add(line.trim()); } }); } catch { continue; // Skip pattern if ripgrep fails } } // Process found pattern violations for (const entry of seen) { if (this.abortController?.signal.aborted) { break; } const [filePath, lineString, ...rest] = entry.split(":"); const lineNumber = Number.parseInt(lineString || "0", 10); const code = rest.join(":").trim(); // Skip invalid entries if (!filePath || Number.isNaN(lineNumber) || !code) { continue; } // Apply filtering rules if (this.shouldSkipViolation(code, filePath, strict)) { continue; } const { category, severity } = this.categorizePatternViolation(code); const relativePath = path.relative(this.baseDir, filePath); violations.push(this.createViolation(relativePath, lineNumber, code, category, severity, "pattern-check", this.generatePatternViolationMessage(category, code))); } return violations; } /** * Get search patterns for ripgrep based on configuration */ getSearchPatterns(includeAny) { const patterns = [ String.raw `:\s*unknown\b`, String.raw `=\s*unknown\b`, "<unknown>", "as unknown", "Record<string, unknown>", ]; if (includeAny) { patterns.push(String.raw `:\s*any\b`, String.raw `=\s*any\b`, "<any>", "as any"); } return patterns; } /** * Determine if a pattern violation should be skipped based on filtering rules */ shouldSkipViolation(code, filePath, strict) { // Skip if already using BrandedUnknown if (/BrandedUnknown/.test(code)) { return true; } // Skip comments if (/^\s*(\/\/|\*|\/\*)/.test(code)) { return true; } // In non-strict mode, skip legitimate usage patterns if (!strict && this.isLegitimateUsage(code, filePath)) { return true; } return false; } /** * Check if unknown/any usage is legitimate based on established patterns */ isLegitimateUsage(code, filePath) { // TypeScript Declaration Files (.d.ts) if (filePath.endsWith(".d.ts")) { return true; } // Type Guard Functions if (/function\s+\w*(is|validate)\w*\s*\([^)]*value\s*:\s*unknown\s*\)/.test(code)) { return true; } // Error Handling Patterns if (/catch\s*\([^)]*error\s*:\s*unknown\s*\)/.test(code) || /error\s*instanceof\s+Error\s*\?/.test(code)) { return true; } // API Boundary Validation with Zod if (/\.parse\(.*unknown.*\)/.test(code) || /schema\.parse\(/.test(code) || /validateSchema\(/.test(code)) { return true; } return false; } /** * Categorize pattern-based violations (legacy functionality) */ categorizePatternViolation(code) { // Explicit any usage (no-explicit-any equivalent) if (/:\s*any\s*([&),;=\]|}]|$)/.test(code)) { return { category: "no-explicit-any", severity: "error" }; } // Type alias definitions if (/^(export\s+)?type\s+\w+.*=.*unknown/.test(code)) { return { category: "type-alias", severity: "error" }; } // Type annotations in parameters/variables (unknown) if (/:\s*unknown\s*([&),;=\]|}]|$)/.test(code)) { return { category: "annotation", severity: "warn" }; } // Type casting if (/(as\s+(unknown|any)|<(unknown|any)>)/.test(code)) { return { category: "cast", severity: "warn" }; } // Record types if (/Record<.*,\s*(unknown|any)>/.test(code)) { return { category: "record-type", severity: "info" }; } // References to unknown in expressions if (/\bunknown\b/.test(code)) { return { category: "unknown-reference", severity: "info" }; } // Default fallback return { category: "other", severity: "info" }; } /** * Generate human-readable violation messages for pattern violations */ generatePatternViolationMessage(category, _code) { switch (category) { case "no-explicit-any": { return "Explicit any type usage - replace with specific type or branded unknown"; } case "type-alias": { return "Type alias uses unknown/any - consider defining proper interface"; } case "annotation": { return "Parameter/variable uses unknown - consider specific typing or branded unknown"; } case "cast": { return "Type casting to unknown/any - consider type guards or validation"; } case "record-type": { return "Record type uses unknown - consider specific value types or branded unknown"; } case "unknown-reference": { return "Reference to unknown type - verify if proper typing is possible"; } default: { return "Type system violation detected - review for proper typing"; } } } /** * Provide fix suggestions for TypeScript violations */ generateFixSuggestion(category, rule, _code) { // For TypeScript compiler errors, let the compiler message speak for itself if (rule?.startsWith("TS")) { return "Refer to TypeScript error message for specific fix guidance"; } // For pattern violations (legacy functionality) switch (category) { case "type-alias": { return "Define a proper interface based on the actual data structure"; } case "annotation": { return "Add specific type annotation based on expected value type"; } case "cast": { return "Use Zod validation with schema.parse() instead of type casting"; } case "record-type": { return "Replace Record<string, unknown> with specific property types"; } default: { return "Review TypeScript configuration and error message for guidance"; } } } /** * Detect and run custom TypeScript quality scripts */ async runCustomTypeScriptScripts(preset) { const violations = []; try { // Check for package.json const packageJsonPath = path.join(this.baseDir, "package.json"); debugLog("TypeScriptEngine", "Checking for package.json", { packageJsonPath, exists: fs.existsSync(packageJsonPath), }); if (!fs.existsSync(packageJsonPath)) { debugLog("TypeScriptEngine", "No package.json found, skipping custom scripts"); return violations; } // Read and parse package.json const packageJsonContent = fs.readFileSync(packageJsonPath, "utf8"); const packageJson = JSON.parse(packageJsonContent); debugLog("TypeScriptEngine", "package.json parsed", { hasScripts: !!packageJson.scripts, scriptCount: packageJson.scripts ? Object.keys(packageJson.scripts).length : 0, }); if (!packageJson.scripts) { debugLog("TypeScriptEngine", "No scripts section in package.json"); return violations; } // Detect custom TypeScript scripts const customScripts = this.detectCustomTypeScriptScripts(packageJson.scripts); debugLog("TypeScriptEngine", "Custom script detection results", { customScripts, count: customScripts.length, }); if (customScripts.length === 0) { return violations; } // Check for TypeScript quality system directory const hasTypeScriptSystem = this.hasCustomTypeScriptSystem(); debugLog("TypeScriptEngine", "TypeScript system detection", { hasTypeScriptSystem, customScripts: customScripts.length, }); if (!hasTypeScriptSystem) { debugLog("TypeScriptEngine", "No custom TypeScript system detected"); return violations; } // Run the most appropriate custom script const scriptToRun = this.selectBestCustomScript(customScripts, preset); debugLog("TypeScriptEngine", "Selected script to run", { scriptToRun, preset, availableScripts: customScripts, }); if (scriptToRun) { debugLog("TypeScriptEngine", "Executing custom TypeScript script", { scriptName: scriptToRun, }); const customViolations = await this.executeCustomTypeScriptScript(scriptToRun, preset); debugLog("TypeScriptEngine", "Custom script execution complete", { violationsFound: customViolations.length, }); violations.push(...customViolations); } else { debugLog("TypeScriptEngine", "No suitable custom script found to run"); } } catch (error) { console.warn("[TypeScript Engine] Failed to run custom TypeScript scripts:", error); // Don't fail the entire analysis if custom scripts fail } return violations; } /** * Detect custom TypeScript scripts in package.json */ detectCustomTypeScriptScripts(scripts) { const customScripts = []; for (const [scriptName, scriptCommand] of Object.entries(scripts)) { // Look for scripts that start with 'tsc:' or 'type-check:' if ((scriptName.startsWith("tsc:") || scriptName.startsWith("type-check:")) && !scriptName.includes("legacy") && !scriptName.includes("original") && // Check if it's a custom quality script (not just tsc --noEmit) (scriptCommand.includes("scripts/typescript/") || scriptCommand.includes("run-all.js") || scriptCommand.includes("--preset"))) { customScripts.push(scriptName); } } return customScripts; } /** * Check if the project has a custom TypeScript quality system */ hasCustomTypeScriptSystem() { const typeScriptSystemPaths = [ path.join(this.baseDir, "scripts/typescript/config.js"), path.join(this.baseDir, "scripts/typescript/run-all.js"), path.join(this.baseDir, "scripts/typescript"), ]; return typeScriptSystemPaths.some((tsPath) => fs.existsSync(tsPath)); } /** * 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?.customTypeScriptScripts; // 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("TypeScriptEngine", "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: ["tsc:safe", "type-check", "tsc:dev"], strict: ["tsc:strict", "type-check:strict", "tsc:ci"], dev: ["tsc:dev", "tsc:safe", "type-check"], ci: ["tsc:ci", "tsc:strict", "type-check:strict"], }; const fallbackScripts = defaultMappings[preset] || defaultMappings["safe"] || []; for (const fallbackScript of fallbackScripts) { if (customScripts.includes(fallbackScript)) { debugLog("TypeScriptEngine", "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("TypeScriptEngine", "Selected first available script", { selectedScript: firstScript, allScripts: customScripts, }); } return firstScript; } catch (error) { debugLog("TypeScriptEngine", "Error selecting custom script, using fallback", { error: String(error), availableScripts: customScripts, }); // Error fallback - just return first script return customScripts[0] || undefined; } } /** * Execute a custom TypeScript script and parse its output */ // eslint-disable-next-line require-await async executeCustomTypeScriptScript(scriptName, _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?.customTypeScriptScripts; scriptTimeout = customScriptConfig?.scriptTimeout ?? 60_000; } catch (error) { debugLog("TypeScriptEngine", "Using default script timeout", { error: String(error), }); } // Determine the package manager const packageManager = this.detectPackageManager(); const runCommand = packageManager === "yarn" ? "yarn" : `${packageManager} run`; // Execute the custom script const command = runCommand.split(" ")[0]; if (!command) { throw new Error(`Invalid package manager command: ${runCommand}`); } debugLog("TypeScriptEngine", "Executing custom script with config", { scriptName, packageManager, timeout: scriptTimeout, }); const result = spawnSync(command, [...(packageManager === "yarn" ? [] : ["run"]), scriptName], { encoding: "utf8", cwd: this.baseDir, timeout: scriptTimeout, signal: this.abortController?.signal, }); if (result.error) { console.warn(`[TypeScript Engine] Failed to run custom script ${scriptName}:`, result.error.message); return violations; } // Parse the output to extract violations const customViolations = this.parseCustomScriptOutput(result.stdout || "", result.stderr || "", scriptName); violations.push(...customViolations); } catch (error) { console.warn(`[TypeScript Engine] Error executing custom script ${scriptName}:`, error); } return violations; } /** * Parse output from custom TypeScript scripts */ parseCustomScriptOutput(stdout, stderr, scriptName) { const violations = []; const output = stdout + stderr; // Look for common custom script error patterns const errorPatterns = [ // Pattern: filename(line,col): message /^(.+?)\((\d+),(\d+)\):\s*(.+)$/gm, // Pattern: filename:line:col: message /^(.+?):(\d+):(\d+):\s*(.+)$/gm, // Pattern: Error: message in filename:line /Error:\s*(.+?)\s+in\s+(.+?):(\d+)/gm, // Pattern: [rule-name] message (filename:line:col) /\[([^\]]+)]\s*(.+?)\s*\((.+?):(\d+):(\d+)\)/gm, ]; for (const pattern of errorPatterns) { let match; while ((match = pattern.exec(output)) !== null) { const violation = this.createCustomScriptViolation(match, scriptName); if (violation) { violations.push(violation); } } } // If no specific patterns matched, look for summary information if (violations.length === 0) { const summaryMatch = output.match(/(\d+)\s+(error|warning)s?\s+found/i); if (summaryMatch && summaryMatch[1] && summaryMatch[2]) { const count = Number.parseInt(summaryMatch[1], 10); const severity = summaryMatch[2].toLowerCase(); if (count > 0) { violations.push(this.createViolation("custom-script-summary", 1, `Custom TypeScript script '${scriptName}' found ${count} ${severity}s`, "type-quality", severity, `CUSTOM-${scriptName.toUpperCase()}`, `The custom TypeScript quality script '${scriptName}' detected ${count} ${severity}s. Run '${scriptName}' directly for detailed output.`)); } } } return violations; } /** * Create a violation from custom script output */ createCustomScriptViolation(match, scriptName) { try { // Different patterns have different capture groups let filePath; let line; let message; let ruleName; if (match.length === 5 && match[1] && match[2] && match[3] && match[4]) { // Pattern: filename(line,col): message filePath = match[1]; line = Number.parseInt(match[2], 10); message = match[4]; } else if (match.length === 6 && match[1] && match[2] && match[3] && match[4] && match[5]) { // Pattern: [rule-name] message (filename:line:col) ruleName = match[1]; message = match[2]; filePath = match[3]; line = Number.parseInt(match[4], 10); } else { return undefined; } // Determine severity based on script name and message const severity = this.determineCustomScriptSeverity(scriptName, message); // Categorize the violation const category = this.categorizeCustomScriptViolation(scriptName, ruleName || "", message); return this.createViolation(filePath, line, message, category, severity, ruleName ? `CUSTOM-${ruleName.toUpperCase()}` : `CUSTOM-${scriptName.toUpperCase()}`, `Custom TypeScript quality check detected: ${message}`); } catch (error) { console.warn("[TypeScript Engine] Failed to parse custom script violation:", error); return undefined; } } /** * Determine severity for custom script violations */ determineCustomScriptSeverity(scriptName, message) { // High severity scripts/messages if (scriptName.includes("floating-promises") || scriptName.includes("unsafe") || message.toLowerCase().includes("error")) { return "error"; } // Medium severity if (scriptName.includes("explicit-any") || scriptName.includes("return-type") || message.toLowerCase().includes("warning")) { return "warn"; } // Default to info for architectural/quality checks return "info"; } /** * Categorize custom script violations */ categorizeCustomScriptViolation(scriptName, ruleName, message) { // Map common custom script types to categories if (scriptName.includes("floating-promises") || ruleName.includes("floating-promises")) { return "async-issues"; } if (scriptName.includes("explicit-any") || ruleName.includes("explicit-any") || message.includes("any")) { return "no-explicit-any"; } if (scriptName.includes("return-type") || ruleName.includes("return-type")) { return "annotation"; } if (scriptName.includes("domain") || scriptName.includes("layer") || ruleName.includes("domain")) { return "architecture"; } if (scriptName.includes("branded") || ruleName.includes("branded")) { return "type-alias"; } // Default category return "type-quality"; } /** * Detect the package manager being used */ detectPackageManager() { if (fs.existsSync(path.join(this.baseDir, "pnpm-lock.yaml"))) { return "pnpm"; } if (fs.existsSync(path.join(this.baseDir, "yarn.lock"))) { return "yarn"; } if (fs.existsSync(path.join(this.baseDir, "bun.lockb"))) { return "bun"; } return "npm"; } } //# sourceMappingURL=typescript-engine.js.map