@invisiblecities/sidequest-cqo
Version:
Configuration-agnostic TypeScript and ESLint orchestrator with real-time watch mode, SQLite persistence, and intelligent terminal detection
194 lines • 7.92 kB
JavaScript
/**
* Unused Exports Engine for Code Quality Orchestrator
* Detects unused TypeScript exports using ts-unused-exports
*/
import { execPromise } from "../utils/exec-utils.js";
import { BaseAuditEngine } from "./base-engine.js";
/**
* Engine that analyzes TypeScript exports to find unused code
*/
export class UnusedExportsEngine extends BaseAuditEngine {
constructor() {
super("Unused Exports Analysis", "unused-exports", {
enabled: true,
options: {},
priority: 3,
timeout: 30_000,
allowFailure: true,
});
}
/**
* Analyze TypeScript project for unused exports
*/
async analyze(_targetPath, _options) {
try {
// Run ts-unused-exports command with smart filtering to reduce false positives
const command = [
"npx ts-unused-exports tsconfig.json",
"--showLineNumber",
"--allowUnusedTypes", // Allow unused type/interface exports (often part of public API)
"--excludeDeclarationFiles", // Skip .d.ts files
"--ignoreTestFiles", // Focus on production code
"--ignoreLocallyUsed", // Don't report exports used in same file
'--excludePathsFromReport="shared/types;shared/constants;utils/types"', // Exclude likely public API files
].join(" ");
const { stdout, stderr } = await execPromise(command, {
cwd: process.cwd(),
timeout: this.config.timeout || 30_000,
});
if (stderr && !stdout) {
throw new Error(`ts-unused-exports failed: ${stderr}`);
}
return this.parseUnusedExportsOutput(stdout);
}
catch (error) {
// ts-unused-exports exits with code 1 when it finds unused exports, this is expected
if (error &&
typeof error === "object" &&
"code" in error &&
error.code === 1 &&
"stdout" in error &&
error.stdout) {
console.log("[UnusedExportsEngine] Found unused exports, processing results...");
return this.parseUnusedExportsOutput(error.stdout);
}
console.error("[UnusedExportsEngine] Unexpected error:", error);
if (this.config.allowFailure) {
console.warn(`[UnusedExportsEngine] Analysis failed: ${error}`);
return [];
}
throw error;
}
}
/**
* Parse ts-unused-exports output into violations
*/
parseUnusedExportsOutput(output) {
const violations = [];
const lines = output.split("\n").filter((line) => line.trim());
// let moduleCount = 0;
// let currentFile = '';
for (const line of lines) {
// Skip the summary line
if (line.includes("modules with unused exports")) {
const match = line.match(/(\d+) modules with unused exports/);
if (match) {
// moduleCount = parseInt(match[1], 10);
}
continue;
}
// Parse file paths with line numbers: /path/file.ts[line,column]: exportName
const match = line.match(/^(.+?)\[(\d+),(\d+)]:\s*(.+)$/);
if (match) {
const [, filePath, lineString, columnString, exportName] = match;
if (!filePath || !lineString || !columnString || !exportName) {
continue;
}
const lineNumber = Number.parseInt(lineString, 10);
const columnNumber = Number.parseInt(columnString, 10);
// Convert absolute path to relative
const relativePath = filePath.replace(`${process.cwd()}/`, "");
violations.push({
file: relativePath,
line: lineNumber,
column: columnNumber,
message: `Unused export '${exportName}' - consider removing if not needed`,
code: exportName,
category: this.categorizeUnusedExport(relativePath, exportName),
severity: this.getSeverityForUnusedExport(relativePath, exportName),
source: "unused-exports",
rule: "unused-export",
});
}
}
return violations;
}
/**
* Categorize unused exports based on file path and export name
*/
categorizeUnusedExport(filePath, exportName) {
// Public API indicators - likely intentional exports
if (this.isLikelyPublicAPI(filePath, exportName)) {
return "best-practices"; // Lower severity for potential public API
}
// Type definitions are usually less critical
if (filePath.includes("/types.ts") || filePath.endsWith("types.ts")) {
return "unused-code";
}
// Configuration exports might be intentional for future use
if (filePath.includes("config") || exportName.includes("Config")) {
return "unused-code";
}
// Test files and utilities
if (filePath.includes("test") || filePath.includes("spec")) {
return "unused-code";
}
// Default category for unused exports
return "unused-code";
}
/**
* Detect patterns that suggest this export is part of a public API
*/
isLikelyPublicAPI(filePath, exportName) {
// Service classes that likely have factory functions
if (exportName.endsWith("Service") ||
exportName.endsWith("Engine") ||
exportName.endsWith("Manager")) {
return true;
}
// Class constructors that are likely used indirectly
if (exportName.endsWith("Display") ||
exportName.endsWith("Tracker") ||
exportName.endsWith("Detector")) {
return true;
}
// Index files are usually public API entry points
if (filePath.endsWith("/index.ts") || filePath.endsWith("index.ts")) {
return true;
}
// Shared modules are often public API
if (filePath.includes("/shared/") || filePath.includes("shared/")) {
return true;
}
// Configuration and constants often exported for external use
if (exportName.includes("DEFAULT_") ||
exportName.includes("VALIDATION_") ||
exportName.includes("CONFIG")) {
return true;
}
return false;
}
/**
* Determine severity based on context
*/
getSeverityForUnusedExport(filePath, exportName) {
// Likely public API exports should be informational only
if (this.isLikelyPublicAPI(filePath, exportName)) {
return "info"; // Low severity - might be intentional public API
}
// Core API exports should be warnings (might be used externally)
if (filePath.includes("index.ts") || exportName === "default") {
return "warn";
}
// Test files are lower priority
if (filePath.includes("test") || filePath.includes("spec")) {
return "info";
}
// Type definitions are informational
if (filePath.includes("/types.ts") || filePath.endsWith("types.ts")) {
return "info";
}
// Default to info level for unused exports
return "info";
}
/**
* Generate fix suggestion for unused exports
*/
generateFixSuggestion(_category, _rule, code) {
if (!code) {
return undefined;
}
return `Consider removing unused export '${code}' if it's not needed, or keep it if it's part of the public API`;
}
}
//# sourceMappingURL=unused-exports-engine.js.map