aislop 0.1.3 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -260
- package/dist/cli.js +254 -126
- package/dist/{engine-info-Bi8pE12U.js → engine-info-DFze-2GQ.js} +1 -1
- package/dist/index.js +464 -362
- package/dist/{json-66-1kHeg.js → json-Ci_gvHLS.js} +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { n as getEngineLabel, r as APP_VERSION, t as ENGINE_INFO } from "./engine-info-
|
|
1
|
+
import { n as getEngineLabel, r as APP_VERSION, t as ENGINE_INFO } from "./engine-info-DFze-2GQ.js";
|
|
2
2
|
import { n as runSubprocess, t as isToolInstalled } from "./subprocess-99puEEGl.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import pc from "picocolors";
|
|
7
|
-
import {
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { performance } from "node:perf_hooks";
|
|
10
10
|
import os from "node:os";
|
|
@@ -52,8 +52,7 @@ const logger = {
|
|
|
52
52
|
//#region src/output/layout.ts
|
|
53
53
|
const formatElapsed$1 = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
|
|
54
54
|
const printCommandHeader = (commandName) => {
|
|
55
|
-
logger.log(highlighter.bold(`aislop ${commandName}`));
|
|
56
|
-
logger.log(highlighter.dim(`v${APP_VERSION}`));
|
|
55
|
+
logger.log(`${highlighter.bold(`aislop ${commandName.toLowerCase()}`)} ${highlighter.dim(`v${APP_VERSION}`)}`);
|
|
57
56
|
logger.break();
|
|
58
57
|
};
|
|
59
58
|
const formatProjectSummary = (project) => `Project ${highlighter.info(project.projectName)} (${highlighter.info(project.languages.join(", "))})`;
|
|
@@ -529,6 +528,439 @@ const doctorCommand = async (directory) => {
|
|
|
529
528
|
printDoctorConclusion(isAllGood());
|
|
530
529
|
};
|
|
531
530
|
|
|
531
|
+
//#endregion
|
|
532
|
+
//#region src/engines/ai-slop/unused-imports.ts
|
|
533
|
+
const JS_EXTENSIONS$1 = new Set([
|
|
534
|
+
".ts",
|
|
535
|
+
".tsx",
|
|
536
|
+
".js",
|
|
537
|
+
".jsx",
|
|
538
|
+
".mjs",
|
|
539
|
+
".cjs"
|
|
540
|
+
]);
|
|
541
|
+
const PY_EXTENSIONS = new Set([".py"]);
|
|
542
|
+
const REMOVE_MARKER = "\0__AISLOP_REMOVE__";
|
|
543
|
+
const extractJsImportedSymbols = (lines) => {
|
|
544
|
+
const symbols = [];
|
|
545
|
+
const importLines = /* @__PURE__ */ new Set();
|
|
546
|
+
for (let i = 0; i < lines.length; i++) {
|
|
547
|
+
const trimmed = lines[i].trim();
|
|
548
|
+
if (!trimmed.startsWith("import ")) continue;
|
|
549
|
+
importLines.add(i);
|
|
550
|
+
if (/^import\s+["']/.test(trimmed)) continue;
|
|
551
|
+
if (/^import\s+type\s/.test(trimmed)) continue;
|
|
552
|
+
let fullImport = trimmed;
|
|
553
|
+
let endLine = i;
|
|
554
|
+
while (!fullImport.includes("from") && endLine < lines.length - 1) {
|
|
555
|
+
endLine++;
|
|
556
|
+
fullImport += ` ${lines[endLine].trim()}`;
|
|
557
|
+
importLines.add(endLine);
|
|
558
|
+
}
|
|
559
|
+
const namespaceMatch = fullImport.match(/import\s+\*\s+as\s+(\w+)\s+from/);
|
|
560
|
+
if (namespaceMatch) {
|
|
561
|
+
symbols.push({
|
|
562
|
+
name: namespaceMatch[1],
|
|
563
|
+
line: i + 1,
|
|
564
|
+
isDefault: false,
|
|
565
|
+
isNamespace: true
|
|
566
|
+
});
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const defaultMatch = fullImport.match(/import\s+(\w+)\s*(?:,\s*\{[^}]*\})?\s+from/);
|
|
570
|
+
if (defaultMatch && defaultMatch[1] !== "type") symbols.push({
|
|
571
|
+
name: defaultMatch[1],
|
|
572
|
+
line: i + 1,
|
|
573
|
+
isDefault: true,
|
|
574
|
+
isNamespace: false
|
|
575
|
+
});
|
|
576
|
+
const namedMatch = fullImport.match(/\{([^}]+)\}/);
|
|
577
|
+
if (namedMatch) {
|
|
578
|
+
const namedImports = namedMatch[1].split(",");
|
|
579
|
+
for (const ni of namedImports) {
|
|
580
|
+
const parts = ni.trim().split(/\s+as\s+/);
|
|
581
|
+
if (parts.length === 0 || !parts[0]) continue;
|
|
582
|
+
const cleanParts = parts.map((p) => p.trim().replace(/^type\s+/, ""));
|
|
583
|
+
const localName = cleanParts.length > 1 ? cleanParts[1] : cleanParts[0];
|
|
584
|
+
if (localName && /^\w+$/.test(localName)) symbols.push({
|
|
585
|
+
name: localName,
|
|
586
|
+
line: i + 1,
|
|
587
|
+
isDefault: false,
|
|
588
|
+
isNamespace: false
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
symbols,
|
|
595
|
+
importLines
|
|
596
|
+
};
|
|
597
|
+
};
|
|
598
|
+
const extractPyImportedSymbols = (lines) => {
|
|
599
|
+
const symbols = [];
|
|
600
|
+
const importLines = /* @__PURE__ */ new Set();
|
|
601
|
+
for (let i = 0; i < lines.length; i++) {
|
|
602
|
+
const trimmed = lines[i].trim();
|
|
603
|
+
const fromMatch = trimmed.match(/^from\s+[\w.]+\s+import\s+(.+)/);
|
|
604
|
+
if (fromMatch) {
|
|
605
|
+
importLines.add(i);
|
|
606
|
+
const importPart = fromMatch[1].replace(/#.*$/, "").trim();
|
|
607
|
+
if (importPart === "*") continue;
|
|
608
|
+
const cleaned = importPart.replace(/[()]/g, "");
|
|
609
|
+
for (const item of cleaned.split(",")) {
|
|
610
|
+
const parts = item.trim().split(/\s+as\s+/);
|
|
611
|
+
const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim();
|
|
612
|
+
if (localName && /^\w+$/.test(localName)) symbols.push({
|
|
613
|
+
name: localName,
|
|
614
|
+
line: i + 1,
|
|
615
|
+
isDefault: false,
|
|
616
|
+
isNamespace: false
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
const importMatch = trimmed.match(/^import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
|
|
622
|
+
if (importMatch) {
|
|
623
|
+
importLines.add(i);
|
|
624
|
+
const simpleName = (importMatch[2] ?? importMatch[1]).split(".")[0];
|
|
625
|
+
if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
|
|
626
|
+
name: simpleName,
|
|
627
|
+
line: i + 1,
|
|
628
|
+
isDefault: false,
|
|
629
|
+
isNamespace: true
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
symbols,
|
|
635
|
+
importLines
|
|
636
|
+
};
|
|
637
|
+
};
|
|
638
|
+
const isSymbolUsed = (name, content, importLines, lines) => {
|
|
639
|
+
const pattern = new RegExp(`\\b${name}\\b`, "g");
|
|
640
|
+
let match;
|
|
641
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
642
|
+
const lineIndex = content.slice(0, match.index).split("\n").length - 1;
|
|
643
|
+
if (!importLines.has(lineIndex)) return true;
|
|
644
|
+
}
|
|
645
|
+
for (let i = 0; i < lines.length; i++) {
|
|
646
|
+
if (importLines.has(i)) continue;
|
|
647
|
+
if (lines[i].includes(name)) return true;
|
|
648
|
+
}
|
|
649
|
+
return false;
|
|
650
|
+
};
|
|
651
|
+
const analyzeFile = (filePath) => {
|
|
652
|
+
if (isAutoGenerated(filePath)) return null;
|
|
653
|
+
let content;
|
|
654
|
+
try {
|
|
655
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
656
|
+
} catch {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
const ext = path.extname(filePath);
|
|
660
|
+
const lines = content.split("\n");
|
|
661
|
+
let symbols;
|
|
662
|
+
let importLines;
|
|
663
|
+
if (JS_EXTENSIONS$1.has(ext)) {
|
|
664
|
+
const result = extractJsImportedSymbols(lines);
|
|
665
|
+
symbols = result.symbols;
|
|
666
|
+
importLines = result.importLines;
|
|
667
|
+
} else if (PY_EXTENSIONS.has(ext)) {
|
|
668
|
+
const result = extractPyImportedSymbols(lines);
|
|
669
|
+
symbols = result.symbols;
|
|
670
|
+
importLines = result.importLines;
|
|
671
|
+
} else return null;
|
|
672
|
+
return {
|
|
673
|
+
lines,
|
|
674
|
+
symbols,
|
|
675
|
+
importLines,
|
|
676
|
+
ext
|
|
677
|
+
};
|
|
678
|
+
};
|
|
679
|
+
const getUnusedSymbols = (lines, symbols, importLines) => {
|
|
680
|
+
const content = lines.join("\n");
|
|
681
|
+
return symbols.filter((symbol) => !isSymbolUsed(symbol.name, content, importLines, lines));
|
|
682
|
+
};
|
|
683
|
+
const detectUnusedImports = async (context) => {
|
|
684
|
+
const files = getSourceFiles(context);
|
|
685
|
+
const diagnostics = [];
|
|
686
|
+
for (const filePath of files) {
|
|
687
|
+
const analysis = analyzeFile(filePath);
|
|
688
|
+
if (!analysis) continue;
|
|
689
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
690
|
+
const unused = getUnusedSymbols(analysis.lines, analysis.symbols, analysis.importLines);
|
|
691
|
+
for (const symbol of unused) diagnostics.push({
|
|
692
|
+
filePath: relativePath,
|
|
693
|
+
engine: "ai-slop",
|
|
694
|
+
rule: "ai-slop/unused-import",
|
|
695
|
+
severity: "warning",
|
|
696
|
+
message: `Imported symbol '${symbol.name}' is never used`,
|
|
697
|
+
help: "Remove unused imports to keep the code clean",
|
|
698
|
+
line: symbol.line,
|
|
699
|
+
column: 0,
|
|
700
|
+
category: "AI Slop",
|
|
701
|
+
fixable: true
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
return diagnostics;
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
//#endregion
|
|
708
|
+
//#region src/engines/ai-slop/unused-imports-fix.ts
|
|
709
|
+
const fixUnusedImports = async (context) => {
|
|
710
|
+
const files = getSourceFiles(context);
|
|
711
|
+
for (const filePath of files) {
|
|
712
|
+
const analysis = analyzeFile(filePath);
|
|
713
|
+
if (!analysis) continue;
|
|
714
|
+
const unused = getUnusedSymbols(analysis.lines, analysis.symbols, analysis.importLines);
|
|
715
|
+
if (unused.length === 0) continue;
|
|
716
|
+
const unusedNames = new Set(unused.map((u) => u.name));
|
|
717
|
+
const lines = [...analysis.lines];
|
|
718
|
+
const symbolsByLine = /* @__PURE__ */ new Map();
|
|
719
|
+
for (const sym of analysis.symbols) {
|
|
720
|
+
const arr = symbolsByLine.get(sym.line) ?? [];
|
|
721
|
+
arr.push(sym);
|
|
722
|
+
symbolsByLine.set(sym.line, arr);
|
|
723
|
+
}
|
|
724
|
+
const linesToRemove = /* @__PURE__ */ new Set();
|
|
725
|
+
for (const [lineNo, syms] of symbolsByLine) {
|
|
726
|
+
const lineIdx = lineNo - 1;
|
|
727
|
+
const allUnused = syms.every((s) => unusedNames.has(s.name));
|
|
728
|
+
const importSpan = getImportSpan(lineIdx, analysis.importLines);
|
|
729
|
+
if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
|
|
730
|
+
else if (JS_EXTENSIONS$1.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
|
|
731
|
+
else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, syms, unusedNames);
|
|
732
|
+
}
|
|
733
|
+
if (linesToRemove.size === 0 && unused.length === 0) continue;
|
|
734
|
+
const sortedRemove = [...linesToRemove].sort((a, b) => b - a);
|
|
735
|
+
for (const idx of sortedRemove) lines.splice(idx, 1);
|
|
736
|
+
const filtered = lines.filter((l) => l !== REMOVE_MARKER);
|
|
737
|
+
while (filtered.length > 0 && filtered[0].trim() === "") filtered.shift();
|
|
738
|
+
fs.writeFileSync(filePath, filtered.join("\n"));
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
const getImportSpan = (startIdx, importLines) => {
|
|
742
|
+
const span = [startIdx];
|
|
743
|
+
let idx = startIdx + 1;
|
|
744
|
+
while (importLines.has(idx)) {
|
|
745
|
+
span.push(idx);
|
|
746
|
+
idx++;
|
|
747
|
+
}
|
|
748
|
+
return span;
|
|
749
|
+
};
|
|
750
|
+
const rewriteJsImportSpan = (lines, span, syms, unusedNames) => {
|
|
751
|
+
const fullImport = span.map((i) => lines[i]).join("\n");
|
|
752
|
+
const namedMatch = fullImport.match(/\{([^}]+)\}/s);
|
|
753
|
+
if (!namedMatch) return;
|
|
754
|
+
const unusedNamed = syms.filter((s) => !s.isDefault && !s.isNamespace && unusedNames.has(s.name));
|
|
755
|
+
if (unusedNamed.length === 0) return;
|
|
756
|
+
const unusedNamedSet = new Set(unusedNamed.map((s) => s.name));
|
|
757
|
+
const keptSpecifiers = namedMatch[1].split(",").map((s) => s.trim()).filter(Boolean).filter((spec) => {
|
|
758
|
+
const parts = spec.split(/\s+as\s+/);
|
|
759
|
+
const localName = parts.length > 1 ? parts[1].trim().replace(/^type\s+/, "") : parts[0].trim().replace(/^type\s+/, "");
|
|
760
|
+
return !unusedNamedSet.has(localName);
|
|
761
|
+
});
|
|
762
|
+
if (keptSpecifiers.length === 0) {
|
|
763
|
+
if (syms.find((s) => s.isDefault && !unusedNames.has(s.name))) {
|
|
764
|
+
const rewritten = fullImport.replace(/,\s*\{[^}]*\}/s, "").replace(/\{[^}]*\}\s*,?\s*/s, "");
|
|
765
|
+
lines[span[0]] = rewritten.replace(/\n/g, " ").replace(/\s+/g, " ");
|
|
766
|
+
for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
|
|
767
|
+
}
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const fromMatch = fullImport.match(/\}\s*(from\s+.+)$/s);
|
|
771
|
+
const fromClause = fromMatch ? fromMatch[1].trim() : "";
|
|
772
|
+
const importPrefix = fullImport.match(/^(import\s+(?:\w+\s*,\s*)?)/);
|
|
773
|
+
const prefix = importPrefix ? importPrefix[1] : "import ";
|
|
774
|
+
const wasMultiLine = span.length > 1;
|
|
775
|
+
let newImport;
|
|
776
|
+
if (wasMultiLine && keptSpecifiers.length > 2) {
|
|
777
|
+
const indentMatch = lines[span[1]]?.match(/^(\s+)/);
|
|
778
|
+
const indent = indentMatch ? indentMatch[1] : " ";
|
|
779
|
+
newImport = `${prefix}{\n${keptSpecifiers.map((s) => `${indent}${s},`).join("\n")}\n} ${fromClause}`;
|
|
780
|
+
} else newImport = `${prefix}{ ${keptSpecifiers.join(", ")} } ${fromClause}`;
|
|
781
|
+
lines[span[0]] = newImport;
|
|
782
|
+
for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
|
|
783
|
+
};
|
|
784
|
+
const rewritePyImportLine = (lines, lineIdx, syms, unusedNames) => {
|
|
785
|
+
const fromMatch = lines[lineIdx].match(/^(\s*from\s+[\w.]+\s+import\s+)(.+)$/);
|
|
786
|
+
if (!fromMatch) return;
|
|
787
|
+
const prefix = fromMatch[1];
|
|
788
|
+
const importPart = fromMatch[2].replace(/#.*$/, "").trim();
|
|
789
|
+
const hasParen = importPart.startsWith("(");
|
|
790
|
+
const keptSpecifiers = importPart.replace(/[()]/g, "").split(",").map((s) => s.trim()).filter((spec) => {
|
|
791
|
+
const parts = spec.split(/\s+as\s+/);
|
|
792
|
+
const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim();
|
|
793
|
+
return !unusedNames.has(localName);
|
|
794
|
+
});
|
|
795
|
+
if (keptSpecifiers.length === 0) return;
|
|
796
|
+
const joined = keptSpecifiers.join(", ");
|
|
797
|
+
lines[lineIdx] = hasParen ? `${prefix}(${joined})` : `${prefix}${joined}`;
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
//#endregion
|
|
801
|
+
//#region src/engines/code-quality/knip.ts
|
|
802
|
+
const KNIP_MESSAGE_MAP = {
|
|
803
|
+
files: "Unused file",
|
|
804
|
+
dependencies: "Unused dependency",
|
|
805
|
+
devDependencies: "Unused devDependency",
|
|
806
|
+
unlisted: "Unlisted dependency",
|
|
807
|
+
unresolved: "Unresolved import",
|
|
808
|
+
binaries: "Unlisted binary",
|
|
809
|
+
exports: "Unused export",
|
|
810
|
+
types: "Unused type",
|
|
811
|
+
duplicates: "Duplicate export"
|
|
812
|
+
};
|
|
813
|
+
const DEPENDENCY_TYPES = [
|
|
814
|
+
"dependencies",
|
|
815
|
+
"devDependencies",
|
|
816
|
+
"unlisted",
|
|
817
|
+
"unresolved",
|
|
818
|
+
"binaries"
|
|
819
|
+
];
|
|
820
|
+
const isDependencyType = (type) => DEPENDENCY_TYPES.includes(type);
|
|
821
|
+
const getIssueItems = (fileIssue, issueType) => {
|
|
822
|
+
const items = fileIssue[issueType];
|
|
823
|
+
return Array.isArray(items) ? items : [];
|
|
824
|
+
};
|
|
825
|
+
const DEPENDENCY_HELP = {
|
|
826
|
+
dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
827
|
+
devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
828
|
+
unlisted: "This package is imported in code but not declared in package.json. Run `npm install` to add it.",
|
|
829
|
+
unresolved: "This import cannot be resolved. Check for typos or missing packages.",
|
|
830
|
+
binaries: "This binary is used but its package is not in package.json."
|
|
831
|
+
};
|
|
832
|
+
const collectIssues = (fileIssue, issueType, rootDir, knipCwd) => {
|
|
833
|
+
const diagnostics = [];
|
|
834
|
+
const issues = getIssueItems(fileIssue, issueType);
|
|
835
|
+
const category = isDependencyType(issueType) ? "Dependencies" : "Dead Code";
|
|
836
|
+
const severity = issueType === "unlisted" || issueType === "unresolved" ? "error" : "warning";
|
|
837
|
+
const fixable = issueType === "dependencies" || issueType === "devDependencies";
|
|
838
|
+
const help = DEPENDENCY_HELP[issueType] ?? "";
|
|
839
|
+
for (const issue of issues) {
|
|
840
|
+
const symbol = issue.name ?? issue.symbol ?? "unknown";
|
|
841
|
+
const absolutePath = path.resolve(knipCwd, fileIssue.file);
|
|
842
|
+
diagnostics.push({
|
|
843
|
+
filePath: path.relative(rootDir, absolutePath),
|
|
844
|
+
engine: "code-quality",
|
|
845
|
+
rule: `knip/${issueType}`,
|
|
846
|
+
severity,
|
|
847
|
+
message: `${KNIP_MESSAGE_MAP[issueType]}: ${symbol}`,
|
|
848
|
+
help,
|
|
849
|
+
line: issue.line ?? 0,
|
|
850
|
+
column: issue.col ?? 0,
|
|
851
|
+
category,
|
|
852
|
+
fixable
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
return diagnostics;
|
|
856
|
+
};
|
|
857
|
+
const findMonorepoRoot = (directory) => {
|
|
858
|
+
let current = path.dirname(directory);
|
|
859
|
+
while (current !== path.dirname(current)) {
|
|
860
|
+
if (fs.existsSync(path.join(current, "pnpm-workspace.yaml")) || (() => {
|
|
861
|
+
const pkgPath = path.join(current, "package.json");
|
|
862
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
863
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
864
|
+
return Array.isArray(pkg.workspaces) || pkg.workspaces?.packages;
|
|
865
|
+
})()) return current;
|
|
866
|
+
current = path.dirname(current);
|
|
867
|
+
}
|
|
868
|
+
return null;
|
|
869
|
+
};
|
|
870
|
+
const KNIP_RELATIVE_BIN = path.join("node_modules", "knip", "bin", "knip.js");
|
|
871
|
+
const findKnipBin = (rootDirectory, monorepoRoot) => {
|
|
872
|
+
const localPath = path.join(rootDirectory, KNIP_RELATIVE_BIN);
|
|
873
|
+
if (fs.existsSync(localPath)) return {
|
|
874
|
+
binPath: localPath,
|
|
875
|
+
cwd: rootDirectory
|
|
876
|
+
};
|
|
877
|
+
if (monorepoRoot) {
|
|
878
|
+
const monorepoPath = path.join(monorepoRoot, KNIP_RELATIVE_BIN);
|
|
879
|
+
if (fs.existsSync(monorepoPath)) return {
|
|
880
|
+
binPath: monorepoPath,
|
|
881
|
+
cwd: monorepoRoot
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
return null;
|
|
885
|
+
};
|
|
886
|
+
const runKnipDependencyCheck = async (rootDirectory) => {
|
|
887
|
+
return (await runKnip(rootDirectory)).filter((d) => d.rule === "knip/dependencies" || d.rule === "knip/devDependencies");
|
|
888
|
+
};
|
|
889
|
+
const fixUnusedDependencies = async (rootDirectory) => {
|
|
890
|
+
const diagnostics = await runKnipDependencyCheck(rootDirectory);
|
|
891
|
+
if (diagnostics.length === 0) return;
|
|
892
|
+
const pkgPath = path.join(rootDirectory, "package.json");
|
|
893
|
+
if (!fs.existsSync(pkgPath)) return;
|
|
894
|
+
const raw = fs.readFileSync(pkgPath, "utf-8");
|
|
895
|
+
const pkg = JSON.parse(raw);
|
|
896
|
+
const unusedDeps = /* @__PURE__ */ new Set();
|
|
897
|
+
const unusedDevDeps = /* @__PURE__ */ new Set();
|
|
898
|
+
for (const d of diagnostics) {
|
|
899
|
+
const pkgName = d.message.replace(/^Unused (dev)?[Dd]ependency: /, "");
|
|
900
|
+
if (d.rule === "knip/dependencies") unusedDeps.add(pkgName);
|
|
901
|
+
if (d.rule === "knip/devDependencies") unusedDevDeps.add(pkgName);
|
|
902
|
+
}
|
|
903
|
+
let changed = false;
|
|
904
|
+
if (pkg.dependencies) {
|
|
905
|
+
for (const name of unusedDeps) if (name in pkg.dependencies) {
|
|
906
|
+
delete pkg.dependencies[name];
|
|
907
|
+
changed = true;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (pkg.devDependencies) {
|
|
911
|
+
for (const name of unusedDevDeps) if (name in pkg.devDependencies) {
|
|
912
|
+
delete pkg.devDependencies[name];
|
|
913
|
+
changed = true;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (changed) fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, " ")}\n`);
|
|
917
|
+
};
|
|
918
|
+
const runKnip = async (rootDirectory) => {
|
|
919
|
+
const knipRuntime = findKnipBin(rootDirectory, findMonorepoRoot(rootDirectory));
|
|
920
|
+
if (!knipRuntime) return [];
|
|
921
|
+
try {
|
|
922
|
+
const args = [
|
|
923
|
+
knipRuntime.binPath,
|
|
924
|
+
"--no-progress",
|
|
925
|
+
"--reporter",
|
|
926
|
+
"json",
|
|
927
|
+
"--no-exit-code"
|
|
928
|
+
];
|
|
929
|
+
const result = await runSubprocess(process.execPath, args, {
|
|
930
|
+
cwd: knipRuntime.cwd,
|
|
931
|
+
timeout: 2e4,
|
|
932
|
+
env: { FORCE_COLOR: "0" }
|
|
933
|
+
});
|
|
934
|
+
if (!result.stdout) return [];
|
|
935
|
+
const parsed = JSON.parse(result.stdout);
|
|
936
|
+
const diagnostics = [];
|
|
937
|
+
const files = parsed.files ?? [];
|
|
938
|
+
for (const unusedFile of files) diagnostics.push({
|
|
939
|
+
filePath: path.relative(rootDirectory, path.resolve(knipRuntime.cwd, unusedFile)),
|
|
940
|
+
engine: "code-quality",
|
|
941
|
+
rule: "knip/files",
|
|
942
|
+
severity: "warning",
|
|
943
|
+
message: KNIP_MESSAGE_MAP.files,
|
|
944
|
+
help: "This file is not imported by any other file in the project.",
|
|
945
|
+
line: 0,
|
|
946
|
+
column: 0,
|
|
947
|
+
category: "Dead Code",
|
|
948
|
+
fixable: false
|
|
949
|
+
});
|
|
950
|
+
const issues = parsed.issues ?? [];
|
|
951
|
+
const issueTypes = [
|
|
952
|
+
...DEPENDENCY_TYPES,
|
|
953
|
+
"exports",
|
|
954
|
+
"types",
|
|
955
|
+
"duplicates"
|
|
956
|
+
];
|
|
957
|
+
for (const fileIssue of issues) for (const type of issueTypes) diagnostics.push(...collectIssues(fileIssue, type, rootDirectory, knipRuntime.cwd));
|
|
958
|
+
return diagnostics;
|
|
959
|
+
} catch {
|
|
960
|
+
return [];
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
|
|
532
964
|
//#endregion
|
|
533
965
|
//#region src/engines/format/biome.ts
|
|
534
966
|
const esmRequire$1 = createRequire(import.meta.url);
|
|
@@ -627,14 +1059,11 @@ const parseBiomeJsonOutput = (output, rootDir) => {
|
|
|
627
1059
|
const fixBiomeFormat = async (context) => {
|
|
628
1060
|
const targets = getBiomeTargets(context);
|
|
629
1061
|
if (targets.length === 0) return;
|
|
630
|
-
|
|
631
|
-
"
|
|
1062
|
+
await runBiome([
|
|
1063
|
+
"format",
|
|
632
1064
|
"--write",
|
|
633
|
-
"--formatter-enabled=true",
|
|
634
|
-
"--linter-enabled=false",
|
|
635
1065
|
...targets
|
|
636
1066
|
], context.rootDirectory, 6e4);
|
|
637
|
-
if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `Biome exited with code ${result.exitCode}`);
|
|
638
1067
|
};
|
|
639
1068
|
|
|
640
1069
|
//#endregion
|
|
@@ -893,6 +1322,7 @@ const fixOxlint = async (context) => {
|
|
|
893
1322
|
"-c",
|
|
894
1323
|
configPath,
|
|
895
1324
|
"--fix",
|
|
1325
|
+
"--fix-suggestions",
|
|
896
1326
|
"."
|
|
897
1327
|
];
|
|
898
1328
|
const result = await runSubprocess(process.execPath, args, {
|
|
@@ -947,81 +1377,6 @@ const fixRuffLint = async (rootDirectory) => {
|
|
|
947
1377
|
if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `ruff check --fix exited with code ${result.exitCode}`);
|
|
948
1378
|
};
|
|
949
1379
|
|
|
950
|
-
//#endregion
|
|
951
|
-
//#region src/output/pager.ts
|
|
952
|
-
const DEFAULT_COLUMNS = 80;
|
|
953
|
-
const DEFAULT_ROWS = 24;
|
|
954
|
-
const ANSI_PATTERN = new RegExp(String.raw`\u001B\[[0-?]*[ -/]*[@-~]`, "g");
|
|
955
|
-
const stripAnsi = (text) => text.replace(ANSI_PATTERN, "");
|
|
956
|
-
const resolvePagerCommand = () => {
|
|
957
|
-
const pager = process.env.PAGER?.trim();
|
|
958
|
-
if (pager) {
|
|
959
|
-
const [command, ...args] = pager.split(/\s+/);
|
|
960
|
-
if (command) return {
|
|
961
|
-
command,
|
|
962
|
-
args
|
|
963
|
-
};
|
|
964
|
-
}
|
|
965
|
-
return {
|
|
966
|
-
command: "less",
|
|
967
|
-
args: [
|
|
968
|
-
"-R",
|
|
969
|
-
"-F",
|
|
970
|
-
"-X"
|
|
971
|
-
]
|
|
972
|
-
};
|
|
973
|
-
};
|
|
974
|
-
const writeToStdout = (text) => {
|
|
975
|
-
process.stdout.write(text);
|
|
976
|
-
};
|
|
977
|
-
const pipeToPager = async (command, args, text) => new Promise((resolve) => {
|
|
978
|
-
let settled = false;
|
|
979
|
-
const finish = (success) => {
|
|
980
|
-
if (settled) return;
|
|
981
|
-
settled = true;
|
|
982
|
-
resolve(success);
|
|
983
|
-
};
|
|
984
|
-
try {
|
|
985
|
-
const child = spawn(command, args, {
|
|
986
|
-
stdio: [
|
|
987
|
-
"pipe",
|
|
988
|
-
"inherit",
|
|
989
|
-
"inherit"
|
|
990
|
-
],
|
|
991
|
-
windowsHide: true
|
|
992
|
-
});
|
|
993
|
-
child.once("error", () => finish(false));
|
|
994
|
-
child.once("close", (code) => finish(code === 0));
|
|
995
|
-
child.stdin?.on("error", () => void 0);
|
|
996
|
-
child.stdin?.end(text);
|
|
997
|
-
} catch {
|
|
998
|
-
finish(false);
|
|
999
|
-
}
|
|
1000
|
-
});
|
|
1001
|
-
const countRenderedLines = (text, columns = DEFAULT_COLUMNS) => {
|
|
1002
|
-
const width = Math.max(1, columns);
|
|
1003
|
-
return text.split("\n").reduce((count, line) => {
|
|
1004
|
-
const visibleLine = stripAnsi(line).replaceAll(" ", " ");
|
|
1005
|
-
return count + Math.max(1, Math.ceil(visibleLine.length / width));
|
|
1006
|
-
}, 0);
|
|
1007
|
-
};
|
|
1008
|
-
const shouldPageOutput = (text, options = {}) => {
|
|
1009
|
-
if (text.trim().length === 0) return false;
|
|
1010
|
-
const stdinIsTTY = options.stdinIsTTY ?? Boolean(process.stdin.isTTY);
|
|
1011
|
-
const stdoutIsTTY = options.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
|
|
1012
|
-
if (!stdinIsTTY || !stdoutIsTTY) return false;
|
|
1013
|
-
const rows = Math.max(1, options.rows ?? process.stdout.rows ?? DEFAULT_ROWS);
|
|
1014
|
-
return countRenderedLines(text, Math.max(1, options.columns ?? process.stdout.columns ?? DEFAULT_COLUMNS)) > rows - 1;
|
|
1015
|
-
};
|
|
1016
|
-
const printMaybePaged = async (text) => {
|
|
1017
|
-
if (!shouldPageOutput(text)) {
|
|
1018
|
-
writeToStdout(text);
|
|
1019
|
-
return;
|
|
1020
|
-
}
|
|
1021
|
-
const pager = resolvePagerCommand();
|
|
1022
|
-
if (!await pipeToPager(pager.command, pager.args, text)) writeToStdout(text);
|
|
1023
|
-
};
|
|
1024
|
-
|
|
1025
1380
|
//#endregion
|
|
1026
1381
|
//#region src/utils/telemetry.ts
|
|
1027
1382
|
/**
|
|
@@ -1078,9 +1433,13 @@ const getAnonymousId = () => {
|
|
|
1078
1433
|
for (let i = 0; i < raw.length; i++) hash = hash * 33 ^ raw.charCodeAt(i);
|
|
1079
1434
|
return `aislop_${(hash >>> 0).toString(36)}`;
|
|
1080
1435
|
};
|
|
1436
|
+
/** Pending telemetry request — kept alive so Node doesn't exit before it completes. */
|
|
1437
|
+
let pendingRequest = null;
|
|
1081
1438
|
/**
|
|
1082
1439
|
* Fire-and-forget telemetry event to PostHog.
|
|
1083
|
-
* Never throws, never blocks
|
|
1440
|
+
* Never throws, never blocks CLI output.
|
|
1441
|
+
* The request is kept alive via `flushTelemetry()` so Node doesn't
|
|
1442
|
+
* exit before it completes.
|
|
1084
1443
|
*/
|
|
1085
1444
|
const trackEvent = (event) => {
|
|
1086
1445
|
const payload = {
|
|
@@ -1103,12 +1462,12 @@ const trackEvent = (event) => {
|
|
|
1103
1462
|
},
|
|
1104
1463
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1105
1464
|
};
|
|
1106
|
-
fetch(`${POSTHOG_HOST}/capture/`, {
|
|
1465
|
+
pendingRequest = fetch(`${POSTHOG_HOST}/capture/`, {
|
|
1107
1466
|
method: "POST",
|
|
1108
1467
|
headers: { "Content-Type": "application/json" },
|
|
1109
1468
|
body: JSON.stringify(payload),
|
|
1110
1469
|
signal: AbortSignal.timeout(3e3)
|
|
1111
|
-
}).catch(() => {});
|
|
1470
|
+
}).then(() => {}).catch(() => {});
|
|
1112
1471
|
};
|
|
1113
1472
|
|
|
1114
1473
|
//#endregion
|
|
@@ -1165,7 +1524,7 @@ const runFixStep = async (name, detect, applyFix, options) => {
|
|
|
1165
1524
|
}
|
|
1166
1525
|
lines.push(...getFilePreviewLines("Affected", uniqueFiles(before), options.verbose));
|
|
1167
1526
|
if (after.length > 0) lines.push(...getFilePreviewLines("Remaining", uniqueFiles(after), options.verbose));
|
|
1168
|
-
|
|
1527
|
+
process.stdout.write(`${lines.join("\n")}\n\n`);
|
|
1169
1528
|
return result;
|
|
1170
1529
|
};
|
|
1171
1530
|
const createEngineContext = (rootDirectory, projectInfo, config) => ({
|
|
@@ -1208,6 +1567,15 @@ const fixCommand = async (directory, config, options = {
|
|
|
1208
1567
|
printProjectMetadata(projectInfo);
|
|
1209
1568
|
const context = createEngineContext(resolvedDir, projectInfo, config);
|
|
1210
1569
|
const steps = [];
|
|
1570
|
+
if (config.engines["ai-slop"]) steps.push(await runFixStep("Unused imports", () => detectUnusedImports(context), () => fixUnusedImports(context), options));
|
|
1571
|
+
if (config.engines.lint) {
|
|
1572
|
+
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS lint fixes", () => runOxlint(context), () => fixOxlint(context), options));
|
|
1573
|
+
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => fixRuffLint(resolvedDir), options));
|
|
1574
|
+
else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python lint fixes.");
|
|
1575
|
+
}
|
|
1576
|
+
if (config.engines["code-quality"]) {
|
|
1577
|
+
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("Unused dependencies", () => runKnipDependencyCheck(resolvedDir), () => fixUnusedDependencies(resolvedDir), options));
|
|
1578
|
+
}
|
|
1211
1579
|
if (config.engines.format) {
|
|
1212
1580
|
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS formatting", () => runBiomeFormat(context), () => fixBiomeFormat(context), options));
|
|
1213
1581
|
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python formatting", () => runRuffFormat(context), () => fixRuffFormat(resolvedDir), options));
|
|
@@ -1215,11 +1583,6 @@ const fixCommand = async (directory, config, options = {
|
|
|
1215
1583
|
if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) steps.push(await runFixStep("Go formatting", () => runGofmt(context), () => fixGofmt(resolvedDir), options));
|
|
1216
1584
|
else if (projectInfo.languages.includes("go")) logger.warn(" Go detected but gofmt is not installed; skipping Go formatting fixes.");
|
|
1217
1585
|
}
|
|
1218
|
-
if (config.engines.lint) {
|
|
1219
|
-
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS lint fixes", () => runOxlint(context), () => fixOxlint(context), options));
|
|
1220
|
-
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => fixRuffLint(resolvedDir), options));
|
|
1221
|
-
else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python lint fixes.");
|
|
1222
|
-
}
|
|
1223
1586
|
if (steps.length === 0) logger.dim(" No applicable auto-fixers found for this project.");
|
|
1224
1587
|
else {
|
|
1225
1588
|
logger.break();
|
|
@@ -1702,7 +2065,7 @@ const detectTrivialComments = async (context) => {
|
|
|
1702
2065
|
|
|
1703
2066
|
//#endregion
|
|
1704
2067
|
//#region src/engines/ai-slop/dead-patterns.ts
|
|
1705
|
-
const JS_EXTENSIONS
|
|
2068
|
+
const JS_EXTENSIONS = new Set([
|
|
1706
2069
|
".ts",
|
|
1707
2070
|
".tsx",
|
|
1708
2071
|
".js",
|
|
@@ -1714,7 +2077,7 @@ const CONSOLE_LOG_PATTERN = /\bconsole\.(?:log|debug|info|trace|dir|table)\s*\(/
|
|
|
1714
2077
|
const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
|
|
1715
2078
|
const SCRIPT_DIR_PATTERN = /(?:^|\/)(scripts|bin)\//;
|
|
1716
2079
|
const detectConsoleLeftovers = (content, relativePath, ext) => {
|
|
1717
|
-
if (!JS_EXTENSIONS
|
|
2080
|
+
if (!JS_EXTENSIONS.has(ext)) return [];
|
|
1718
2081
|
if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
|
|
1719
2082
|
if (SCRIPT_DIR_PATTERN.test(relativePath)) return [];
|
|
1720
2083
|
const diagnostics = [];
|
|
@@ -1774,7 +2137,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
|
|
|
1774
2137
|
for (let i = 0; i < lines.length; i++) {
|
|
1775
2138
|
const trimmed = lines[i].trim();
|
|
1776
2139
|
const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
|
|
1777
|
-
if (JS_EXTENSIONS
|
|
2140
|
+
if (JS_EXTENSIONS.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !nextLine.startsWith("}") && !nextLine.startsWith("//") && !nextLine.startsWith("/*") && !nextLine.startsWith("case ") && !nextLine.startsWith("default:") && !nextLine.startsWith("if ") && !nextLine.startsWith("if(") && !nextLine.startsWith("else")) diagnostics.push({
|
|
1778
2141
|
filePath: relativePath,
|
|
1779
2142
|
engine: "ai-slop",
|
|
1780
2143
|
rule: "ai-slop/unreachable-code",
|
|
@@ -1798,7 +2161,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
|
|
|
1798
2161
|
category: "AI Slop",
|
|
1799
2162
|
fixable: false
|
|
1800
2163
|
});
|
|
1801
|
-
if (JS_EXTENSIONS
|
|
2164
|
+
if (JS_EXTENSIONS.has(ext) && /(?:function\s+\w+|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ")) diagnostics.push({
|
|
1802
2165
|
filePath: relativePath,
|
|
1803
2166
|
engine: "ai-slop",
|
|
1804
2167
|
rule: "ai-slop/empty-function",
|
|
@@ -1974,166 +2337,6 @@ const detectSwallowedExceptions = async (context) => {
|
|
|
1974
2337
|
return diagnostics;
|
|
1975
2338
|
};
|
|
1976
2339
|
|
|
1977
|
-
//#endregion
|
|
1978
|
-
//#region src/engines/ai-slop/unused-imports.ts
|
|
1979
|
-
const JS_EXTENSIONS = new Set([
|
|
1980
|
-
".ts",
|
|
1981
|
-
".tsx",
|
|
1982
|
-
".js",
|
|
1983
|
-
".jsx",
|
|
1984
|
-
".mjs",
|
|
1985
|
-
".cjs"
|
|
1986
|
-
]);
|
|
1987
|
-
const PY_EXTENSIONS = new Set([".py"]);
|
|
1988
|
-
const extractJsImportedSymbols = (lines) => {
|
|
1989
|
-
const symbols = [];
|
|
1990
|
-
const importLines = /* @__PURE__ */ new Set();
|
|
1991
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1992
|
-
const trimmed = lines[i].trim();
|
|
1993
|
-
if (!trimmed.startsWith("import ")) continue;
|
|
1994
|
-
importLines.add(i);
|
|
1995
|
-
if (/^import\s+["']/.test(trimmed)) continue;
|
|
1996
|
-
if (/^import\s+type\s/.test(trimmed)) continue;
|
|
1997
|
-
let fullImport = trimmed;
|
|
1998
|
-
let endLine = i;
|
|
1999
|
-
while (!fullImport.includes("from") && endLine < lines.length - 1) {
|
|
2000
|
-
endLine++;
|
|
2001
|
-
fullImport += ` ${lines[endLine].trim()}`;
|
|
2002
|
-
importLines.add(endLine);
|
|
2003
|
-
}
|
|
2004
|
-
const namespaceMatch = fullImport.match(/import\s+\*\s+as\s+(\w+)\s+from/);
|
|
2005
|
-
if (namespaceMatch) {
|
|
2006
|
-
symbols.push({
|
|
2007
|
-
name: namespaceMatch[1],
|
|
2008
|
-
line: i + 1,
|
|
2009
|
-
isDefault: false,
|
|
2010
|
-
isNamespace: true
|
|
2011
|
-
});
|
|
2012
|
-
continue;
|
|
2013
|
-
}
|
|
2014
|
-
const defaultMatch = fullImport.match(/import\s+(\w+)\s*(?:,\s*\{[^}]*\})?\s+from/);
|
|
2015
|
-
if (defaultMatch && defaultMatch[1] !== "type") symbols.push({
|
|
2016
|
-
name: defaultMatch[1],
|
|
2017
|
-
line: i + 1,
|
|
2018
|
-
isDefault: true,
|
|
2019
|
-
isNamespace: false
|
|
2020
|
-
});
|
|
2021
|
-
const namedMatch = fullImport.match(/\{([^}]+)\}/);
|
|
2022
|
-
if (namedMatch) {
|
|
2023
|
-
const namedImports = namedMatch[1].split(",");
|
|
2024
|
-
for (const ni of namedImports) {
|
|
2025
|
-
const parts = ni.trim().split(/\s+as\s+/);
|
|
2026
|
-
if (parts.length === 0 || !parts[0]) continue;
|
|
2027
|
-
const cleanParts = parts.map((p) => p.trim().replace(/^type\s+/, ""));
|
|
2028
|
-
const localName = cleanParts.length > 1 ? cleanParts[1] : cleanParts[0];
|
|
2029
|
-
if (localName && /^\w+$/.test(localName)) symbols.push({
|
|
2030
|
-
name: localName,
|
|
2031
|
-
line: i + 1,
|
|
2032
|
-
isDefault: false,
|
|
2033
|
-
isNamespace: false
|
|
2034
|
-
});
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
}
|
|
2038
|
-
return {
|
|
2039
|
-
symbols,
|
|
2040
|
-
importLines
|
|
2041
|
-
};
|
|
2042
|
-
};
|
|
2043
|
-
const extractPyImportedSymbols = (lines) => {
|
|
2044
|
-
const symbols = [];
|
|
2045
|
-
const importLines = /* @__PURE__ */ new Set();
|
|
2046
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2047
|
-
const trimmed = lines[i].trim();
|
|
2048
|
-
const fromMatch = trimmed.match(/^from\s+[\w.]+\s+import\s+(.+)/);
|
|
2049
|
-
if (fromMatch) {
|
|
2050
|
-
importLines.add(i);
|
|
2051
|
-
const importPart = fromMatch[1].replace(/#.*$/, "").trim();
|
|
2052
|
-
if (importPart === "*") continue;
|
|
2053
|
-
const cleaned = importPart.replace(/[()]/g, "");
|
|
2054
|
-
for (const item of cleaned.split(",")) {
|
|
2055
|
-
const parts = item.trim().split(/\s+as\s+/);
|
|
2056
|
-
const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim();
|
|
2057
|
-
if (localName && /^\w+$/.test(localName)) symbols.push({
|
|
2058
|
-
name: localName,
|
|
2059
|
-
line: i + 1,
|
|
2060
|
-
isDefault: false,
|
|
2061
|
-
isNamespace: false
|
|
2062
|
-
});
|
|
2063
|
-
}
|
|
2064
|
-
continue;
|
|
2065
|
-
}
|
|
2066
|
-
const importMatch = trimmed.match(/^import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
|
|
2067
|
-
if (importMatch) {
|
|
2068
|
-
importLines.add(i);
|
|
2069
|
-
const simpleName = (importMatch[2] ?? importMatch[1]).split(".")[0];
|
|
2070
|
-
if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
|
|
2071
|
-
name: simpleName,
|
|
2072
|
-
line: i + 1,
|
|
2073
|
-
isDefault: false,
|
|
2074
|
-
isNamespace: true
|
|
2075
|
-
});
|
|
2076
|
-
}
|
|
2077
|
-
}
|
|
2078
|
-
return {
|
|
2079
|
-
symbols,
|
|
2080
|
-
importLines
|
|
2081
|
-
};
|
|
2082
|
-
};
|
|
2083
|
-
const isSymbolUsed = (name, content, importLines, lines) => {
|
|
2084
|
-
const pattern = new RegExp(`\\b${name}\\b`, "g");
|
|
2085
|
-
let match;
|
|
2086
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
2087
|
-
const lineIndex = content.slice(0, match.index).split("\n").length - 1;
|
|
2088
|
-
if (!importLines.has(lineIndex)) return true;
|
|
2089
|
-
}
|
|
2090
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2091
|
-
if (importLines.has(i)) continue;
|
|
2092
|
-
if (lines[i].includes(name)) return true;
|
|
2093
|
-
}
|
|
2094
|
-
return false;
|
|
2095
|
-
};
|
|
2096
|
-
const detectUnusedImports = async (context) => {
|
|
2097
|
-
const files = getSourceFiles(context);
|
|
2098
|
-
const diagnostics = [];
|
|
2099
|
-
for (const filePath of files) {
|
|
2100
|
-
if (isAutoGenerated(filePath)) continue;
|
|
2101
|
-
let content;
|
|
2102
|
-
try {
|
|
2103
|
-
content = fs.readFileSync(filePath, "utf-8");
|
|
2104
|
-
} catch {
|
|
2105
|
-
continue;
|
|
2106
|
-
}
|
|
2107
|
-
const ext = path.extname(filePath);
|
|
2108
|
-
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2109
|
-
const lines = content.split("\n");
|
|
2110
|
-
let symbols;
|
|
2111
|
-
let importLinesSet;
|
|
2112
|
-
if (JS_EXTENSIONS.has(ext)) {
|
|
2113
|
-
const result = extractJsImportedSymbols(lines);
|
|
2114
|
-
symbols = result.symbols;
|
|
2115
|
-
importLinesSet = result.importLines;
|
|
2116
|
-
} else if (PY_EXTENSIONS.has(ext)) {
|
|
2117
|
-
const result = extractPyImportedSymbols(lines);
|
|
2118
|
-
symbols = result.symbols;
|
|
2119
|
-
importLinesSet = result.importLines;
|
|
2120
|
-
} else continue;
|
|
2121
|
-
for (const symbol of symbols) if (!isSymbolUsed(symbol.name, content, importLinesSet, lines)) diagnostics.push({
|
|
2122
|
-
filePath: relativePath,
|
|
2123
|
-
engine: "ai-slop",
|
|
2124
|
-
rule: "ai-slop/unused-import",
|
|
2125
|
-
severity: "warning",
|
|
2126
|
-
message: `Imported symbol '${symbol.name}' is never used`,
|
|
2127
|
-
help: "Remove unused imports to keep the code clean",
|
|
2128
|
-
line: symbol.line,
|
|
2129
|
-
column: 0,
|
|
2130
|
-
category: "AI Slop",
|
|
2131
|
-
fixable: true
|
|
2132
|
-
});
|
|
2133
|
-
}
|
|
2134
|
-
return diagnostics;
|
|
2135
|
-
};
|
|
2136
|
-
|
|
2137
2340
|
//#endregion
|
|
2138
2341
|
//#region src/engines/ai-slop/index.ts
|
|
2139
2342
|
const aiSlopEngine = {
|
|
@@ -2696,108 +2899,6 @@ const checkDuplication = async (context) => {
|
|
|
2696
2899
|
return diagnostics;
|
|
2697
2900
|
};
|
|
2698
2901
|
|
|
2699
|
-
//#endregion
|
|
2700
|
-
//#region src/engines/code-quality/knip.ts
|
|
2701
|
-
const KNIP_MESSAGE_MAP = {
|
|
2702
|
-
files: "Unused file",
|
|
2703
|
-
exports: "Unused export",
|
|
2704
|
-
types: "Unused type",
|
|
2705
|
-
duplicates: "Duplicate export"
|
|
2706
|
-
};
|
|
2707
|
-
const collectIssues = (fileIssue, issueType, rootDir, knipCwd) => {
|
|
2708
|
-
const diagnostics = [];
|
|
2709
|
-
const issues = issueType === "exports" ? fileIssue.exports ?? [] : issueType === "types" ? fileIssue.types ?? [] : fileIssue.duplicates ?? [];
|
|
2710
|
-
for (const issue of issues) {
|
|
2711
|
-
const symbol = issue.name ?? issue.symbol ?? "unknown";
|
|
2712
|
-
const absolutePath = path.resolve(knipCwd, fileIssue.file);
|
|
2713
|
-
diagnostics.push({
|
|
2714
|
-
filePath: path.relative(rootDir, absolutePath),
|
|
2715
|
-
engine: "code-quality",
|
|
2716
|
-
rule: `knip/${issueType}`,
|
|
2717
|
-
severity: "warning",
|
|
2718
|
-
message: `${KNIP_MESSAGE_MAP[issueType]}: ${symbol}`,
|
|
2719
|
-
help: "",
|
|
2720
|
-
line: issue.line ?? 0,
|
|
2721
|
-
column: issue.col ?? 0,
|
|
2722
|
-
category: "Dead Code",
|
|
2723
|
-
fixable: false
|
|
2724
|
-
});
|
|
2725
|
-
}
|
|
2726
|
-
return diagnostics;
|
|
2727
|
-
};
|
|
2728
|
-
const findMonorepoRoot = (directory) => {
|
|
2729
|
-
let current = path.dirname(directory);
|
|
2730
|
-
while (current !== path.dirname(current)) {
|
|
2731
|
-
if (fs.existsSync(path.join(current, "pnpm-workspace.yaml")) || (() => {
|
|
2732
|
-
const pkgPath = path.join(current, "package.json");
|
|
2733
|
-
if (!fs.existsSync(pkgPath)) return false;
|
|
2734
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
2735
|
-
return Array.isArray(pkg.workspaces) || pkg.workspaces?.packages;
|
|
2736
|
-
})()) return current;
|
|
2737
|
-
current = path.dirname(current);
|
|
2738
|
-
}
|
|
2739
|
-
return null;
|
|
2740
|
-
};
|
|
2741
|
-
const KNIP_RELATIVE_BIN = path.join("node_modules", "knip", "bin", "knip.js");
|
|
2742
|
-
const findKnipBin = (rootDirectory, monorepoRoot) => {
|
|
2743
|
-
const localPath = path.join(rootDirectory, KNIP_RELATIVE_BIN);
|
|
2744
|
-
if (fs.existsSync(localPath)) return {
|
|
2745
|
-
binPath: localPath,
|
|
2746
|
-
cwd: rootDirectory
|
|
2747
|
-
};
|
|
2748
|
-
if (monorepoRoot) {
|
|
2749
|
-
const monorepoPath = path.join(monorepoRoot, KNIP_RELATIVE_BIN);
|
|
2750
|
-
if (fs.existsSync(monorepoPath)) return {
|
|
2751
|
-
binPath: monorepoPath,
|
|
2752
|
-
cwd: monorepoRoot
|
|
2753
|
-
};
|
|
2754
|
-
}
|
|
2755
|
-
return null;
|
|
2756
|
-
};
|
|
2757
|
-
const runKnip = async (rootDirectory) => {
|
|
2758
|
-
const knipRuntime = findKnipBin(rootDirectory, findMonorepoRoot(rootDirectory));
|
|
2759
|
-
if (!knipRuntime) return [];
|
|
2760
|
-
try {
|
|
2761
|
-
const args = [
|
|
2762
|
-
knipRuntime.binPath,
|
|
2763
|
-
"--no-progress",
|
|
2764
|
-
"--reporter",
|
|
2765
|
-
"json",
|
|
2766
|
-
"--no-exit-code"
|
|
2767
|
-
];
|
|
2768
|
-
const result = await runSubprocess(process.execPath, args, {
|
|
2769
|
-
cwd: knipRuntime.cwd,
|
|
2770
|
-
timeout: 2e4,
|
|
2771
|
-
env: { FORCE_COLOR: "0" }
|
|
2772
|
-
});
|
|
2773
|
-
if (!result.stdout) return [];
|
|
2774
|
-
const parsed = JSON.parse(result.stdout);
|
|
2775
|
-
const diagnostics = [];
|
|
2776
|
-
const files = parsed.files ?? [];
|
|
2777
|
-
for (const unusedFile of files) diagnostics.push({
|
|
2778
|
-
filePath: path.relative(rootDirectory, path.resolve(knipRuntime.cwd, unusedFile)),
|
|
2779
|
-
engine: "code-quality",
|
|
2780
|
-
rule: "knip/files",
|
|
2781
|
-
severity: "warning",
|
|
2782
|
-
message: KNIP_MESSAGE_MAP.files,
|
|
2783
|
-
help: "This file is not imported by any other file in the project.",
|
|
2784
|
-
line: 0,
|
|
2785
|
-
column: 0,
|
|
2786
|
-
category: "Dead Code",
|
|
2787
|
-
fixable: false
|
|
2788
|
-
});
|
|
2789
|
-
const issues = parsed.issues ?? [];
|
|
2790
|
-
for (const fileIssue of issues) for (const type of [
|
|
2791
|
-
"exports",
|
|
2792
|
-
"types",
|
|
2793
|
-
"duplicates"
|
|
2794
|
-
]) diagnostics.push(...collectIssues(fileIssue, type, rootDirectory, knipRuntime.cwd));
|
|
2795
|
-
return diagnostics;
|
|
2796
|
-
} catch {
|
|
2797
|
-
return [];
|
|
2798
|
-
}
|
|
2799
|
-
};
|
|
2800
|
-
|
|
2801
2902
|
//#endregion
|
|
2802
2903
|
//#region src/engines/code-quality/index.ts
|
|
2803
2904
|
const codeQualityEngine = {
|
|
@@ -4000,17 +4101,18 @@ const scanCommand = async (directory, config, options) => {
|
|
|
4000
4101
|
});
|
|
4001
4102
|
}
|
|
4002
4103
|
if (options.json) {
|
|
4003
|
-
const { buildJsonOutput } = await import("./json-
|
|
4104
|
+
const { buildJsonOutput } = await import("./json-Ci_gvHLS.js");
|
|
4004
4105
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
4005
4106
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
4006
4107
|
return { exitCode };
|
|
4007
4108
|
}
|
|
4008
|
-
|
|
4109
|
+
const output = [
|
|
4009
4110
|
"",
|
|
4010
4111
|
allDiagnostics.length === 0 ? `${highlighter.success(" ✓ No issues found.")}\n` : renderDiagnostics(allDiagnostics, options.verbose),
|
|
4011
4112
|
renderSummary(allDiagnostics, scoreResult, elapsedMs, projectInfo.sourceFileCount, config.scoring.thresholds),
|
|
4012
4113
|
""
|
|
4013
|
-
].join("\n")
|
|
4114
|
+
].join("\n");
|
|
4115
|
+
process.stdout.write(output);
|
|
4014
4116
|
return { exitCode };
|
|
4015
4117
|
};
|
|
4016
4118
|
|