aislop 0.2.0 → 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 +86 -38
- package/dist/cli.js +148 -115
- package/dist/{engine-info-DpU0WTTj.js → engine-info-DFze-2GQ.js} +1 -1
- package/dist/index.js +459 -426
- package/dist/{json-UG8l_sLC.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(", "))})`;
|
|
@@ -530,193 +529,272 @@ const doctorCommand = async (directory) => {
|
|
|
530
529
|
};
|
|
531
530
|
|
|
532
531
|
//#endregion
|
|
533
|
-
//#region src/engines/
|
|
534
|
-
const
|
|
535
|
-
const resolveLocalBiomeScript = () => {
|
|
536
|
-
try {
|
|
537
|
-
const packageJsonPath = esmRequire$1.resolve("@biomejs/biome/package.json");
|
|
538
|
-
return path.join(path.dirname(packageJsonPath), "bin", "biome");
|
|
539
|
-
} catch {
|
|
540
|
-
return null;
|
|
541
|
-
}
|
|
542
|
-
};
|
|
543
|
-
const runBiome = async (args, rootDirectory, timeout) => {
|
|
544
|
-
const localScript = resolveLocalBiomeScript();
|
|
545
|
-
if (localScript) return runSubprocess(process.execPath, [localScript, ...args], {
|
|
546
|
-
cwd: rootDirectory,
|
|
547
|
-
timeout
|
|
548
|
-
});
|
|
549
|
-
return runSubprocess("biome", args, {
|
|
550
|
-
cwd: rootDirectory,
|
|
551
|
-
timeout
|
|
552
|
-
});
|
|
553
|
-
};
|
|
554
|
-
const BIOME_EXTENSIONS = new Set([
|
|
555
|
-
".js",
|
|
556
|
-
".jsx",
|
|
532
|
+
//#region src/engines/ai-slop/unused-imports.ts
|
|
533
|
+
const JS_EXTENSIONS$1 = new Set([
|
|
557
534
|
".ts",
|
|
558
535
|
".tsx",
|
|
536
|
+
".js",
|
|
537
|
+
".jsx",
|
|
559
538
|
".mjs",
|
|
560
539
|
".cjs"
|
|
561
540
|
]);
|
|
562
|
-
const
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
|
589
575
|
});
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
+
}
|
|
593
592
|
}
|
|
593
|
+
return {
|
|
594
|
+
symbols,
|
|
595
|
+
importLines
|
|
596
|
+
};
|
|
594
597
|
};
|
|
595
|
-
const
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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;
|
|
605
620
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
severity,
|
|
616
|
-
message: entry.message ?? "File is not formatted correctly",
|
|
617
|
-
help: "Run `aislop fix` to auto-format",
|
|
618
|
-
line: entry.location?.start?.line ?? 0,
|
|
619
|
-
column: entry.location?.start?.column ?? 0,
|
|
620
|
-
category: "Format",
|
|
621
|
-
fixable: true
|
|
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
|
|
622
630
|
});
|
|
623
631
|
}
|
|
624
632
|
}
|
|
625
|
-
return
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
if (targets.length === 0) return;
|
|
630
|
-
const result = await runBiome([
|
|
631
|
-
"check",
|
|
632
|
-
"--write",
|
|
633
|
-
"--formatter-enabled=true",
|
|
634
|
-
"--linter-enabled=false",
|
|
635
|
-
...targets
|
|
636
|
-
], context.rootDirectory, 6e4);
|
|
637
|
-
if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `Biome exited with code ${result.exitCode}`);
|
|
633
|
+
return {
|
|
634
|
+
symbols,
|
|
635
|
+
importLines
|
|
636
|
+
};
|
|
638
637
|
};
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
cwd: context.rootDirectory,
|
|
646
|
-
timeout: 6e4
|
|
647
|
-
});
|
|
648
|
-
if (!result.stdout) return [];
|
|
649
|
-
return result.stdout.split("\n").filter((f) => f.length > 0).map((file) => ({
|
|
650
|
-
filePath: path.relative(context.rootDirectory, file),
|
|
651
|
-
engine: "format",
|
|
652
|
-
rule: "go-formatting",
|
|
653
|
-
severity: "warning",
|
|
654
|
-
message: "Go file is not formatted correctly",
|
|
655
|
-
help: "Run `aislop fix` to auto-format with gofmt",
|
|
656
|
-
line: 0,
|
|
657
|
-
column: 0,
|
|
658
|
-
category: "Format",
|
|
659
|
-
fixable: true
|
|
660
|
-
}));
|
|
661
|
-
} catch {
|
|
662
|
-
return [];
|
|
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;
|
|
663
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;
|
|
664
650
|
};
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
timeout: 6e4
|
|
669
|
-
});
|
|
670
|
-
if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `gofmt exited with code ${result.exitCode}`);
|
|
671
|
-
};
|
|
672
|
-
|
|
673
|
-
//#endregion
|
|
674
|
-
//#region src/engines/format/ruff-format.ts
|
|
675
|
-
const runRuffFormat = async (context) => {
|
|
676
|
-
const ruffBinary = resolveToolBinary("ruff");
|
|
651
|
+
const analyzeFile = (filePath) => {
|
|
652
|
+
if (isAutoGenerated(filePath)) return null;
|
|
653
|
+
let content;
|
|
677
654
|
try {
|
|
678
|
-
|
|
679
|
-
"format",
|
|
680
|
-
"--check",
|
|
681
|
-
"--diff",
|
|
682
|
-
context.rootDirectory
|
|
683
|
-
], {
|
|
684
|
-
cwd: context.rootDirectory,
|
|
685
|
-
timeout: 6e4
|
|
686
|
-
});
|
|
687
|
-
if (result.exitCode === 0) return [];
|
|
688
|
-
return parseRuffFormatOutput(result.stdout || result.stderr, context.rootDirectory);
|
|
655
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
689
656
|
} catch {
|
|
690
|
-
return
|
|
657
|
+
return null;
|
|
691
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
|
+
};
|
|
692
678
|
};
|
|
693
|
-
const
|
|
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);
|
|
694
685
|
const diagnostics = [];
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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",
|
|
703
695
|
severity: "warning",
|
|
704
|
-
message:
|
|
705
|
-
help: "
|
|
706
|
-
line:
|
|
696
|
+
message: `Imported symbol '${symbol.name}' is never used`,
|
|
697
|
+
help: "Remove unused imports to keep the code clean",
|
|
698
|
+
line: symbol.line,
|
|
707
699
|
column: 0,
|
|
708
|
-
category: "
|
|
700
|
+
category: "AI Slop",
|
|
709
701
|
fixable: true
|
|
710
702
|
});
|
|
711
703
|
}
|
|
712
704
|
return diagnostics;
|
|
713
705
|
};
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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);
|
|
718
761
|
});
|
|
719
|
-
if (
|
|
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}`;
|
|
720
798
|
};
|
|
721
799
|
|
|
722
800
|
//#endregion
|
|
@@ -862,26 +940,213 @@ const runKnip = async (rootDirectory) => {
|
|
|
862
940
|
engine: "code-quality",
|
|
863
941
|
rule: "knip/files",
|
|
864
942
|
severity: "warning",
|
|
865
|
-
message: KNIP_MESSAGE_MAP.files,
|
|
866
|
-
help: "This file is not imported by any other file in the project.",
|
|
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
|
+
|
|
964
|
+
//#endregion
|
|
965
|
+
//#region src/engines/format/biome.ts
|
|
966
|
+
const esmRequire$1 = createRequire(import.meta.url);
|
|
967
|
+
const resolveLocalBiomeScript = () => {
|
|
968
|
+
try {
|
|
969
|
+
const packageJsonPath = esmRequire$1.resolve("@biomejs/biome/package.json");
|
|
970
|
+
return path.join(path.dirname(packageJsonPath), "bin", "biome");
|
|
971
|
+
} catch {
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
const runBiome = async (args, rootDirectory, timeout) => {
|
|
976
|
+
const localScript = resolveLocalBiomeScript();
|
|
977
|
+
if (localScript) return runSubprocess(process.execPath, [localScript, ...args], {
|
|
978
|
+
cwd: rootDirectory,
|
|
979
|
+
timeout
|
|
980
|
+
});
|
|
981
|
+
return runSubprocess("biome", args, {
|
|
982
|
+
cwd: rootDirectory,
|
|
983
|
+
timeout
|
|
984
|
+
});
|
|
985
|
+
};
|
|
986
|
+
const BIOME_EXTENSIONS = new Set([
|
|
987
|
+
".js",
|
|
988
|
+
".jsx",
|
|
989
|
+
".ts",
|
|
990
|
+
".tsx",
|
|
991
|
+
".mjs",
|
|
992
|
+
".cjs"
|
|
993
|
+
]);
|
|
994
|
+
const getBiomeTargets = (context) => getSourceFiles(context).filter((filePath) => BIOME_EXTENSIONS.has(path.extname(filePath))).map((filePath) => path.relative(context.rootDirectory, filePath));
|
|
995
|
+
const projectUsesDecorators = (rootDir) => {
|
|
996
|
+
try {
|
|
997
|
+
const tsconfigPath = path.join(rootDir, "tsconfig.json");
|
|
998
|
+
if (!fs.existsSync(tsconfigPath)) return false;
|
|
999
|
+
const content = fs.readFileSync(tsconfigPath, "utf-8");
|
|
1000
|
+
return /experimentalDecorators.*true/i.test(content);
|
|
1001
|
+
} catch {
|
|
1002
|
+
return false;
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
const runBiomeFormat = async (context) => {
|
|
1006
|
+
const targets = getBiomeTargets(context);
|
|
1007
|
+
if (targets.length === 0) return [];
|
|
1008
|
+
const args = [
|
|
1009
|
+
"format",
|
|
1010
|
+
"--reporter=json",
|
|
1011
|
+
...targets
|
|
1012
|
+
];
|
|
1013
|
+
try {
|
|
1014
|
+
const result = await runBiome(args, context.rootDirectory, 6e4);
|
|
1015
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
1016
|
+
if (!output) return [];
|
|
1017
|
+
let diagnostics = parseBiomeJsonOutput(output, context.rootDirectory);
|
|
1018
|
+
if (projectUsesDecorators(context.rootDirectory)) diagnostics = diagnostics.filter((d) => {
|
|
1019
|
+
const msg = d.message.toLowerCase();
|
|
1020
|
+
return !msg.includes("decorator") && !msg.includes("parsing error");
|
|
1021
|
+
});
|
|
1022
|
+
return diagnostics;
|
|
1023
|
+
} catch {
|
|
1024
|
+
return [];
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
const parseBiomeJsonOutput = (output, rootDir) => {
|
|
1028
|
+
const diagnostics = [];
|
|
1029
|
+
for (const line of output.split("\n")) {
|
|
1030
|
+
const trimmed = line.trim();
|
|
1031
|
+
if (!trimmed.startsWith("{")) continue;
|
|
1032
|
+
let parsed = null;
|
|
1033
|
+
try {
|
|
1034
|
+
parsed = JSON.parse(trimmed);
|
|
1035
|
+
} catch {
|
|
1036
|
+
parsed = null;
|
|
1037
|
+
}
|
|
1038
|
+
if (!parsed || !Array.isArray(parsed.diagnostics)) continue;
|
|
1039
|
+
for (const entry of parsed.diagnostics) {
|
|
1040
|
+
const rawPath = entry.location?.path;
|
|
1041
|
+
if (!rawPath) continue;
|
|
1042
|
+
const severity = entry.severity === "error" ? "error" : "warning";
|
|
1043
|
+
diagnostics.push({
|
|
1044
|
+
filePath: path.isAbsolute(rawPath) ? path.relative(rootDir, rawPath) : rawPath,
|
|
1045
|
+
engine: "format",
|
|
1046
|
+
rule: "formatting",
|
|
1047
|
+
severity,
|
|
1048
|
+
message: entry.message ?? "File is not formatted correctly",
|
|
1049
|
+
help: "Run `aislop fix` to auto-format",
|
|
1050
|
+
line: entry.location?.start?.line ?? 0,
|
|
1051
|
+
column: entry.location?.start?.column ?? 0,
|
|
1052
|
+
category: "Format",
|
|
1053
|
+
fixable: true
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return diagnostics;
|
|
1058
|
+
};
|
|
1059
|
+
const fixBiomeFormat = async (context) => {
|
|
1060
|
+
const targets = getBiomeTargets(context);
|
|
1061
|
+
if (targets.length === 0) return;
|
|
1062
|
+
await runBiome([
|
|
1063
|
+
"format",
|
|
1064
|
+
"--write",
|
|
1065
|
+
...targets
|
|
1066
|
+
], context.rootDirectory, 6e4);
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
//#endregion
|
|
1070
|
+
//#region src/engines/format/gofmt.ts
|
|
1071
|
+
const runGofmt = async (context) => {
|
|
1072
|
+
try {
|
|
1073
|
+
const result = await runSubprocess("gofmt", ["-l", context.rootDirectory], {
|
|
1074
|
+
cwd: context.rootDirectory,
|
|
1075
|
+
timeout: 6e4
|
|
1076
|
+
});
|
|
1077
|
+
if (!result.stdout) return [];
|
|
1078
|
+
return result.stdout.split("\n").filter((f) => f.length > 0).map((file) => ({
|
|
1079
|
+
filePath: path.relative(context.rootDirectory, file),
|
|
1080
|
+
engine: "format",
|
|
1081
|
+
rule: "go-formatting",
|
|
1082
|
+
severity: "warning",
|
|
1083
|
+
message: "Go file is not formatted correctly",
|
|
1084
|
+
help: "Run `aislop fix` to auto-format with gofmt",
|
|
867
1085
|
line: 0,
|
|
868
1086
|
column: 0,
|
|
869
|
-
category: "
|
|
870
|
-
fixable:
|
|
1087
|
+
category: "Format",
|
|
1088
|
+
fixable: true
|
|
1089
|
+
}));
|
|
1090
|
+
} catch {
|
|
1091
|
+
return [];
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
const fixGofmt = async (rootDirectory) => {
|
|
1095
|
+
const result = await runSubprocess("gofmt", ["-w", rootDirectory], {
|
|
1096
|
+
cwd: rootDirectory,
|
|
1097
|
+
timeout: 6e4
|
|
1098
|
+
});
|
|
1099
|
+
if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `gofmt exited with code ${result.exitCode}`);
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
//#endregion
|
|
1103
|
+
//#region src/engines/format/ruff-format.ts
|
|
1104
|
+
const runRuffFormat = async (context) => {
|
|
1105
|
+
const ruffBinary = resolveToolBinary("ruff");
|
|
1106
|
+
try {
|
|
1107
|
+
const result = await runSubprocess(ruffBinary, [
|
|
1108
|
+
"format",
|
|
1109
|
+
"--check",
|
|
1110
|
+
"--diff",
|
|
1111
|
+
context.rootDirectory
|
|
1112
|
+
], {
|
|
1113
|
+
cwd: context.rootDirectory,
|
|
1114
|
+
timeout: 6e4
|
|
871
1115
|
});
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
...DEPENDENCY_TYPES,
|
|
875
|
-
"exports",
|
|
876
|
-
"types",
|
|
877
|
-
"duplicates"
|
|
878
|
-
];
|
|
879
|
-
for (const fileIssue of issues) for (const type of issueTypes) diagnostics.push(...collectIssues(fileIssue, type, rootDirectory, knipRuntime.cwd));
|
|
880
|
-
return diagnostics;
|
|
1116
|
+
if (result.exitCode === 0) return [];
|
|
1117
|
+
return parseRuffFormatOutput(result.stdout || result.stderr, context.rootDirectory);
|
|
881
1118
|
} catch {
|
|
882
1119
|
return [];
|
|
883
1120
|
}
|
|
884
1121
|
};
|
|
1122
|
+
const parseRuffFormatOutput = (output, rootDir) => {
|
|
1123
|
+
const diagnostics = [];
|
|
1124
|
+
const filePattern = /^--- (.+)$/gm;
|
|
1125
|
+
let match;
|
|
1126
|
+
while ((match = filePattern.exec(output)) !== null) {
|
|
1127
|
+
const filePath = match[1].replace(/^a\//, "");
|
|
1128
|
+
diagnostics.push({
|
|
1129
|
+
filePath: path.relative(rootDir, filePath),
|
|
1130
|
+
engine: "format",
|
|
1131
|
+
rule: "python-formatting",
|
|
1132
|
+
severity: "warning",
|
|
1133
|
+
message: "Python file is not formatted correctly",
|
|
1134
|
+
help: "Run `aislop fix` to auto-format with ruff",
|
|
1135
|
+
line: 0,
|
|
1136
|
+
column: 0,
|
|
1137
|
+
category: "Format",
|
|
1138
|
+
fixable: true
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
return diagnostics;
|
|
1142
|
+
};
|
|
1143
|
+
const fixRuffFormat = async (rootDirectory) => {
|
|
1144
|
+
const result = await runSubprocess(resolveToolBinary("ruff"), ["format", rootDirectory], {
|
|
1145
|
+
cwd: rootDirectory,
|
|
1146
|
+
timeout: 6e4
|
|
1147
|
+
});
|
|
1148
|
+
if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `ruff format exited with code ${result.exitCode}`);
|
|
1149
|
+
};
|
|
885
1150
|
|
|
886
1151
|
//#endregion
|
|
887
1152
|
//#region src/engines/lint/oxlint-config.ts
|
|
@@ -1057,6 +1322,7 @@ const fixOxlint = async (context) => {
|
|
|
1057
1322
|
"-c",
|
|
1058
1323
|
configPath,
|
|
1059
1324
|
"--fix",
|
|
1325
|
+
"--fix-suggestions",
|
|
1060
1326
|
"."
|
|
1061
1327
|
];
|
|
1062
1328
|
const result = await runSubprocess(process.execPath, args, {
|
|
@@ -1111,81 +1377,6 @@ const fixRuffLint = async (rootDirectory) => {
|
|
|
1111
1377
|
if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `ruff check --fix exited with code ${result.exitCode}`);
|
|
1112
1378
|
};
|
|
1113
1379
|
|
|
1114
|
-
//#endregion
|
|
1115
|
-
//#region src/output/pager.ts
|
|
1116
|
-
const DEFAULT_COLUMNS = 80;
|
|
1117
|
-
const DEFAULT_ROWS = 24;
|
|
1118
|
-
const ANSI_PATTERN = new RegExp(String.raw`\u001B\[[0-?]*[ -/]*[@-~]`, "g");
|
|
1119
|
-
const stripAnsi = (text) => text.replace(ANSI_PATTERN, "");
|
|
1120
|
-
const resolvePagerCommand = () => {
|
|
1121
|
-
const pager = process.env.PAGER?.trim();
|
|
1122
|
-
if (pager) {
|
|
1123
|
-
const [command, ...args] = pager.split(/\s+/);
|
|
1124
|
-
if (command) return {
|
|
1125
|
-
command,
|
|
1126
|
-
args
|
|
1127
|
-
};
|
|
1128
|
-
}
|
|
1129
|
-
return {
|
|
1130
|
-
command: "less",
|
|
1131
|
-
args: [
|
|
1132
|
-
"-R",
|
|
1133
|
-
"-F",
|
|
1134
|
-
"-X"
|
|
1135
|
-
]
|
|
1136
|
-
};
|
|
1137
|
-
};
|
|
1138
|
-
const writeToStdout = (text) => {
|
|
1139
|
-
process.stdout.write(text);
|
|
1140
|
-
};
|
|
1141
|
-
const pipeToPager = async (command, args, text) => new Promise((resolve) => {
|
|
1142
|
-
let settled = false;
|
|
1143
|
-
const finish = (success) => {
|
|
1144
|
-
if (settled) return;
|
|
1145
|
-
settled = true;
|
|
1146
|
-
resolve(success);
|
|
1147
|
-
};
|
|
1148
|
-
try {
|
|
1149
|
-
const child = spawn(command, args, {
|
|
1150
|
-
stdio: [
|
|
1151
|
-
"pipe",
|
|
1152
|
-
"inherit",
|
|
1153
|
-
"inherit"
|
|
1154
|
-
],
|
|
1155
|
-
windowsHide: true
|
|
1156
|
-
});
|
|
1157
|
-
child.once("error", () => finish(false));
|
|
1158
|
-
child.once("close", (code) => finish(code === 0));
|
|
1159
|
-
child.stdin?.on("error", () => void 0);
|
|
1160
|
-
child.stdin?.end(text);
|
|
1161
|
-
} catch {
|
|
1162
|
-
finish(false);
|
|
1163
|
-
}
|
|
1164
|
-
});
|
|
1165
|
-
const countRenderedLines = (text, columns = DEFAULT_COLUMNS) => {
|
|
1166
|
-
const width = Math.max(1, columns);
|
|
1167
|
-
return text.split("\n").reduce((count, line) => {
|
|
1168
|
-
const visibleLine = stripAnsi(line).replaceAll(" ", " ");
|
|
1169
|
-
return count + Math.max(1, Math.ceil(visibleLine.length / width));
|
|
1170
|
-
}, 0);
|
|
1171
|
-
};
|
|
1172
|
-
const shouldPageOutput = (text, options = {}) => {
|
|
1173
|
-
if (text.trim().length === 0) return false;
|
|
1174
|
-
const stdinIsTTY = options.stdinIsTTY ?? Boolean(process.stdin.isTTY);
|
|
1175
|
-
const stdoutIsTTY = options.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
|
|
1176
|
-
if (!stdinIsTTY || !stdoutIsTTY) return false;
|
|
1177
|
-
const rows = Math.max(1, options.rows ?? process.stdout.rows ?? DEFAULT_ROWS);
|
|
1178
|
-
return countRenderedLines(text, Math.max(1, options.columns ?? process.stdout.columns ?? DEFAULT_COLUMNS)) > rows - 1;
|
|
1179
|
-
};
|
|
1180
|
-
const printMaybePaged = async (text) => {
|
|
1181
|
-
if (!shouldPageOutput(text)) {
|
|
1182
|
-
writeToStdout(text);
|
|
1183
|
-
return;
|
|
1184
|
-
}
|
|
1185
|
-
const pager = resolvePagerCommand();
|
|
1186
|
-
if (!await pipeToPager(pager.command, pager.args, text)) writeToStdout(text);
|
|
1187
|
-
};
|
|
1188
|
-
|
|
1189
1380
|
//#endregion
|
|
1190
1381
|
//#region src/utils/telemetry.ts
|
|
1191
1382
|
/**
|
|
@@ -1333,7 +1524,7 @@ const runFixStep = async (name, detect, applyFix, options) => {
|
|
|
1333
1524
|
}
|
|
1334
1525
|
lines.push(...getFilePreviewLines("Affected", uniqueFiles(before), options.verbose));
|
|
1335
1526
|
if (after.length > 0) lines.push(...getFilePreviewLines("Remaining", uniqueFiles(after), options.verbose));
|
|
1336
|
-
|
|
1527
|
+
process.stdout.write(`${lines.join("\n")}\n\n`);
|
|
1337
1528
|
return result;
|
|
1338
1529
|
};
|
|
1339
1530
|
const createEngineContext = (rootDirectory, projectInfo, config) => ({
|
|
@@ -1376,13 +1567,7 @@ const fixCommand = async (directory, config, options = {
|
|
|
1376
1567
|
printProjectMetadata(projectInfo);
|
|
1377
1568
|
const context = createEngineContext(resolvedDir, projectInfo, config);
|
|
1378
1569
|
const steps = [];
|
|
1379
|
-
if (config.engines.
|
|
1380
|
-
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS formatting", () => runBiomeFormat(context), () => fixBiomeFormat(context), options));
|
|
1381
|
-
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python formatting", () => runRuffFormat(context), () => fixRuffFormat(resolvedDir), options));
|
|
1382
|
-
else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python formatting fixes.");
|
|
1383
|
-
if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) steps.push(await runFixStep("Go formatting", () => runGofmt(context), () => fixGofmt(resolvedDir), options));
|
|
1384
|
-
else if (projectInfo.languages.includes("go")) logger.warn(" Go detected but gofmt is not installed; skipping Go formatting fixes.");
|
|
1385
|
-
}
|
|
1570
|
+
if (config.engines["ai-slop"]) steps.push(await runFixStep("Unused imports", () => detectUnusedImports(context), () => fixUnusedImports(context), options));
|
|
1386
1571
|
if (config.engines.lint) {
|
|
1387
1572
|
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS lint fixes", () => runOxlint(context), () => fixOxlint(context), options));
|
|
1388
1573
|
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => fixRuffLint(resolvedDir), options));
|
|
@@ -1391,6 +1576,13 @@ const fixCommand = async (directory, config, options = {
|
|
|
1391
1576
|
if (config.engines["code-quality"]) {
|
|
1392
1577
|
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("Unused dependencies", () => runKnipDependencyCheck(resolvedDir), () => fixUnusedDependencies(resolvedDir), options));
|
|
1393
1578
|
}
|
|
1579
|
+
if (config.engines.format) {
|
|
1580
|
+
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS formatting", () => runBiomeFormat(context), () => fixBiomeFormat(context), options));
|
|
1581
|
+
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python formatting", () => runRuffFormat(context), () => fixRuffFormat(resolvedDir), options));
|
|
1582
|
+
else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python formatting fixes.");
|
|
1583
|
+
if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) steps.push(await runFixStep("Go formatting", () => runGofmt(context), () => fixGofmt(resolvedDir), options));
|
|
1584
|
+
else if (projectInfo.languages.includes("go")) logger.warn(" Go detected but gofmt is not installed; skipping Go formatting fixes.");
|
|
1585
|
+
}
|
|
1394
1586
|
if (steps.length === 0) logger.dim(" No applicable auto-fixers found for this project.");
|
|
1395
1587
|
else {
|
|
1396
1588
|
logger.break();
|
|
@@ -1873,7 +2065,7 @@ const detectTrivialComments = async (context) => {
|
|
|
1873
2065
|
|
|
1874
2066
|
//#endregion
|
|
1875
2067
|
//#region src/engines/ai-slop/dead-patterns.ts
|
|
1876
|
-
const JS_EXTENSIONS
|
|
2068
|
+
const JS_EXTENSIONS = new Set([
|
|
1877
2069
|
".ts",
|
|
1878
2070
|
".tsx",
|
|
1879
2071
|
".js",
|
|
@@ -1885,7 +2077,7 @@ const CONSOLE_LOG_PATTERN = /\bconsole\.(?:log|debug|info|trace|dir|table)\s*\(/
|
|
|
1885
2077
|
const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
|
|
1886
2078
|
const SCRIPT_DIR_PATTERN = /(?:^|\/)(scripts|bin)\//;
|
|
1887
2079
|
const detectConsoleLeftovers = (content, relativePath, ext) => {
|
|
1888
|
-
if (!JS_EXTENSIONS
|
|
2080
|
+
if (!JS_EXTENSIONS.has(ext)) return [];
|
|
1889
2081
|
if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
|
|
1890
2082
|
if (SCRIPT_DIR_PATTERN.test(relativePath)) return [];
|
|
1891
2083
|
const diagnostics = [];
|
|
@@ -1945,7 +2137,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
|
|
|
1945
2137
|
for (let i = 0; i < lines.length; i++) {
|
|
1946
2138
|
const trimmed = lines[i].trim();
|
|
1947
2139
|
const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
|
|
1948
|
-
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({
|
|
1949
2141
|
filePath: relativePath,
|
|
1950
2142
|
engine: "ai-slop",
|
|
1951
2143
|
rule: "ai-slop/unreachable-code",
|
|
@@ -1969,7 +2161,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
|
|
|
1969
2161
|
category: "AI Slop",
|
|
1970
2162
|
fixable: false
|
|
1971
2163
|
});
|
|
1972
|
-
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({
|
|
1973
2165
|
filePath: relativePath,
|
|
1974
2166
|
engine: "ai-slop",
|
|
1975
2167
|
rule: "ai-slop/empty-function",
|
|
@@ -2145,166 +2337,6 @@ const detectSwallowedExceptions = async (context) => {
|
|
|
2145
2337
|
return diagnostics;
|
|
2146
2338
|
};
|
|
2147
2339
|
|
|
2148
|
-
//#endregion
|
|
2149
|
-
//#region src/engines/ai-slop/unused-imports.ts
|
|
2150
|
-
const JS_EXTENSIONS = new Set([
|
|
2151
|
-
".ts",
|
|
2152
|
-
".tsx",
|
|
2153
|
-
".js",
|
|
2154
|
-
".jsx",
|
|
2155
|
-
".mjs",
|
|
2156
|
-
".cjs"
|
|
2157
|
-
]);
|
|
2158
|
-
const PY_EXTENSIONS = new Set([".py"]);
|
|
2159
|
-
const extractJsImportedSymbols = (lines) => {
|
|
2160
|
-
const symbols = [];
|
|
2161
|
-
const importLines = /* @__PURE__ */ new Set();
|
|
2162
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2163
|
-
const trimmed = lines[i].trim();
|
|
2164
|
-
if (!trimmed.startsWith("import ")) continue;
|
|
2165
|
-
importLines.add(i);
|
|
2166
|
-
if (/^import\s+["']/.test(trimmed)) continue;
|
|
2167
|
-
if (/^import\s+type\s/.test(trimmed)) continue;
|
|
2168
|
-
let fullImport = trimmed;
|
|
2169
|
-
let endLine = i;
|
|
2170
|
-
while (!fullImport.includes("from") && endLine < lines.length - 1) {
|
|
2171
|
-
endLine++;
|
|
2172
|
-
fullImport += ` ${lines[endLine].trim()}`;
|
|
2173
|
-
importLines.add(endLine);
|
|
2174
|
-
}
|
|
2175
|
-
const namespaceMatch = fullImport.match(/import\s+\*\s+as\s+(\w+)\s+from/);
|
|
2176
|
-
if (namespaceMatch) {
|
|
2177
|
-
symbols.push({
|
|
2178
|
-
name: namespaceMatch[1],
|
|
2179
|
-
line: i + 1,
|
|
2180
|
-
isDefault: false,
|
|
2181
|
-
isNamespace: true
|
|
2182
|
-
});
|
|
2183
|
-
continue;
|
|
2184
|
-
}
|
|
2185
|
-
const defaultMatch = fullImport.match(/import\s+(\w+)\s*(?:,\s*\{[^}]*\})?\s+from/);
|
|
2186
|
-
if (defaultMatch && defaultMatch[1] !== "type") symbols.push({
|
|
2187
|
-
name: defaultMatch[1],
|
|
2188
|
-
line: i + 1,
|
|
2189
|
-
isDefault: true,
|
|
2190
|
-
isNamespace: false
|
|
2191
|
-
});
|
|
2192
|
-
const namedMatch = fullImport.match(/\{([^}]+)\}/);
|
|
2193
|
-
if (namedMatch) {
|
|
2194
|
-
const namedImports = namedMatch[1].split(",");
|
|
2195
|
-
for (const ni of namedImports) {
|
|
2196
|
-
const parts = ni.trim().split(/\s+as\s+/);
|
|
2197
|
-
if (parts.length === 0 || !parts[0]) continue;
|
|
2198
|
-
const cleanParts = parts.map((p) => p.trim().replace(/^type\s+/, ""));
|
|
2199
|
-
const localName = cleanParts.length > 1 ? cleanParts[1] : cleanParts[0];
|
|
2200
|
-
if (localName && /^\w+$/.test(localName)) symbols.push({
|
|
2201
|
-
name: localName,
|
|
2202
|
-
line: i + 1,
|
|
2203
|
-
isDefault: false,
|
|
2204
|
-
isNamespace: false
|
|
2205
|
-
});
|
|
2206
|
-
}
|
|
2207
|
-
}
|
|
2208
|
-
}
|
|
2209
|
-
return {
|
|
2210
|
-
symbols,
|
|
2211
|
-
importLines
|
|
2212
|
-
};
|
|
2213
|
-
};
|
|
2214
|
-
const extractPyImportedSymbols = (lines) => {
|
|
2215
|
-
const symbols = [];
|
|
2216
|
-
const importLines = /* @__PURE__ */ new Set();
|
|
2217
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2218
|
-
const trimmed = lines[i].trim();
|
|
2219
|
-
const fromMatch = trimmed.match(/^from\s+[\w.]+\s+import\s+(.+)/);
|
|
2220
|
-
if (fromMatch) {
|
|
2221
|
-
importLines.add(i);
|
|
2222
|
-
const importPart = fromMatch[1].replace(/#.*$/, "").trim();
|
|
2223
|
-
if (importPart === "*") continue;
|
|
2224
|
-
const cleaned = importPart.replace(/[()]/g, "");
|
|
2225
|
-
for (const item of cleaned.split(",")) {
|
|
2226
|
-
const parts = item.trim().split(/\s+as\s+/);
|
|
2227
|
-
const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim();
|
|
2228
|
-
if (localName && /^\w+$/.test(localName)) symbols.push({
|
|
2229
|
-
name: localName,
|
|
2230
|
-
line: i + 1,
|
|
2231
|
-
isDefault: false,
|
|
2232
|
-
isNamespace: false
|
|
2233
|
-
});
|
|
2234
|
-
}
|
|
2235
|
-
continue;
|
|
2236
|
-
}
|
|
2237
|
-
const importMatch = trimmed.match(/^import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
|
|
2238
|
-
if (importMatch) {
|
|
2239
|
-
importLines.add(i);
|
|
2240
|
-
const simpleName = (importMatch[2] ?? importMatch[1]).split(".")[0];
|
|
2241
|
-
if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
|
|
2242
|
-
name: simpleName,
|
|
2243
|
-
line: i + 1,
|
|
2244
|
-
isDefault: false,
|
|
2245
|
-
isNamespace: true
|
|
2246
|
-
});
|
|
2247
|
-
}
|
|
2248
|
-
}
|
|
2249
|
-
return {
|
|
2250
|
-
symbols,
|
|
2251
|
-
importLines
|
|
2252
|
-
};
|
|
2253
|
-
};
|
|
2254
|
-
const isSymbolUsed = (name, content, importLines, lines) => {
|
|
2255
|
-
const pattern = new RegExp(`\\b${name}\\b`, "g");
|
|
2256
|
-
let match;
|
|
2257
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
2258
|
-
const lineIndex = content.slice(0, match.index).split("\n").length - 1;
|
|
2259
|
-
if (!importLines.has(lineIndex)) return true;
|
|
2260
|
-
}
|
|
2261
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2262
|
-
if (importLines.has(i)) continue;
|
|
2263
|
-
if (lines[i].includes(name)) return true;
|
|
2264
|
-
}
|
|
2265
|
-
return false;
|
|
2266
|
-
};
|
|
2267
|
-
const detectUnusedImports = async (context) => {
|
|
2268
|
-
const files = getSourceFiles(context);
|
|
2269
|
-
const diagnostics = [];
|
|
2270
|
-
for (const filePath of files) {
|
|
2271
|
-
if (isAutoGenerated(filePath)) continue;
|
|
2272
|
-
let content;
|
|
2273
|
-
try {
|
|
2274
|
-
content = fs.readFileSync(filePath, "utf-8");
|
|
2275
|
-
} catch {
|
|
2276
|
-
continue;
|
|
2277
|
-
}
|
|
2278
|
-
const ext = path.extname(filePath);
|
|
2279
|
-
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2280
|
-
const lines = content.split("\n");
|
|
2281
|
-
let symbols;
|
|
2282
|
-
let importLinesSet;
|
|
2283
|
-
if (JS_EXTENSIONS.has(ext)) {
|
|
2284
|
-
const result = extractJsImportedSymbols(lines);
|
|
2285
|
-
symbols = result.symbols;
|
|
2286
|
-
importLinesSet = result.importLines;
|
|
2287
|
-
} else if (PY_EXTENSIONS.has(ext)) {
|
|
2288
|
-
const result = extractPyImportedSymbols(lines);
|
|
2289
|
-
symbols = result.symbols;
|
|
2290
|
-
importLinesSet = result.importLines;
|
|
2291
|
-
} else continue;
|
|
2292
|
-
for (const symbol of symbols) if (!isSymbolUsed(symbol.name, content, importLinesSet, lines)) diagnostics.push({
|
|
2293
|
-
filePath: relativePath,
|
|
2294
|
-
engine: "ai-slop",
|
|
2295
|
-
rule: "ai-slop/unused-import",
|
|
2296
|
-
severity: "warning",
|
|
2297
|
-
message: `Imported symbol '${symbol.name}' is never used`,
|
|
2298
|
-
help: "Remove unused imports to keep the code clean",
|
|
2299
|
-
line: symbol.line,
|
|
2300
|
-
column: 0,
|
|
2301
|
-
category: "AI Slop",
|
|
2302
|
-
fixable: true
|
|
2303
|
-
});
|
|
2304
|
-
}
|
|
2305
|
-
return diagnostics;
|
|
2306
|
-
};
|
|
2307
|
-
|
|
2308
2340
|
//#endregion
|
|
2309
2341
|
//#region src/engines/ai-slop/index.ts
|
|
2310
2342
|
const aiSlopEngine = {
|
|
@@ -4069,17 +4101,18 @@ const scanCommand = async (directory, config, options) => {
|
|
|
4069
4101
|
});
|
|
4070
4102
|
}
|
|
4071
4103
|
if (options.json) {
|
|
4072
|
-
const { buildJsonOutput } = await import("./json-
|
|
4104
|
+
const { buildJsonOutput } = await import("./json-Ci_gvHLS.js");
|
|
4073
4105
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
4074
4106
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
4075
4107
|
return { exitCode };
|
|
4076
4108
|
}
|
|
4077
|
-
|
|
4109
|
+
const output = [
|
|
4078
4110
|
"",
|
|
4079
4111
|
allDiagnostics.length === 0 ? `${highlighter.success(" ✓ No issues found.")}\n` : renderDiagnostics(allDiagnostics, options.verbose),
|
|
4080
4112
|
renderSummary(allDiagnostics, scoreResult, elapsedMs, projectInfo.sourceFileCount, config.scoring.thresholds),
|
|
4081
4113
|
""
|
|
4082
|
-
].join("\n")
|
|
4114
|
+
].join("\n");
|
|
4115
|
+
process.stdout.write(output);
|
|
4083
4116
|
return { exitCode };
|
|
4084
4117
|
};
|
|
4085
4118
|
|