aislop 0.2.0 → 0.3.0
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 +88 -38
- package/dist/cli.js +797 -187
- package/dist/{engine-info-DpU0WTTj.js → engine-info-CXT2Q_Jy.js} +1 -1
- package/dist/{expo-doctor-CGXGLgMJ.js → expo-doctor-Cm5892Y8.js} +76 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2921 -2453
- package/dist/{json-UG8l_sLC.js → json-C52xnH_4.js} +1 -1
- package/package.json +1 -1
- package/dist/expo-doctor-vDz4kh9-.js +0 -127
- package/dist/subprocess-99puEEGl.js +0 -59
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,23 @@ import pc from "picocolors";
|
|
|
13
13
|
import ora from "ora";
|
|
14
14
|
import { emitKeypressEvents } from "node:readline";
|
|
15
15
|
|
|
16
|
+
//#region \0rolldown/runtime.js
|
|
17
|
+
var __defProp = Object.defineProperty;
|
|
18
|
+
var __exportAll = (all, no_symbols) => {
|
|
19
|
+
let target = {};
|
|
20
|
+
for (var name in all) {
|
|
21
|
+
__defProp(target, name, {
|
|
22
|
+
get: all[name],
|
|
23
|
+
enumerable: true
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (!no_symbols) {
|
|
27
|
+
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
28
|
+
}
|
|
29
|
+
return target;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
16
33
|
//#region src/config/defaults.ts
|
|
17
34
|
const DEFAULT_CONFIG = {
|
|
18
35
|
version: 1,
|
|
@@ -36,18 +53,18 @@ const DEFAULT_CONFIG = {
|
|
|
36
53
|
},
|
|
37
54
|
scoring: {
|
|
38
55
|
weights: {
|
|
39
|
-
format: .
|
|
40
|
-
lint:
|
|
41
|
-
"code-quality":
|
|
42
|
-
"ai-slop":
|
|
56
|
+
format: .3,
|
|
57
|
+
lint: .6,
|
|
58
|
+
"code-quality": .8,
|
|
59
|
+
"ai-slop": 2.5,
|
|
43
60
|
architecture: 1,
|
|
44
|
-
security:
|
|
61
|
+
security: 1.5
|
|
45
62
|
},
|
|
46
63
|
thresholds: {
|
|
47
64
|
good: 75,
|
|
48
65
|
ok: 50
|
|
49
66
|
},
|
|
50
|
-
smoothing:
|
|
67
|
+
smoothing: 20
|
|
51
68
|
},
|
|
52
69
|
ci: {
|
|
53
70
|
failBelow: 0,
|
|
@@ -77,15 +94,16 @@ security:
|
|
|
77
94
|
|
|
78
95
|
scoring:
|
|
79
96
|
weights:
|
|
80
|
-
format: 0.
|
|
81
|
-
lint:
|
|
82
|
-
code-quality:
|
|
83
|
-
ai-slop:
|
|
97
|
+
format: 0.3
|
|
98
|
+
lint: 0.6
|
|
99
|
+
code-quality: 0.8
|
|
100
|
+
ai-slop: 2.5
|
|
84
101
|
architecture: 1.0
|
|
85
|
-
security:
|
|
102
|
+
security: 1.5
|
|
86
103
|
thresholds:
|
|
87
104
|
good: 75
|
|
88
105
|
ok: 50
|
|
106
|
+
smoothing: 20
|
|
89
107
|
|
|
90
108
|
ci:
|
|
91
109
|
failBelow: 0
|
|
@@ -113,12 +131,12 @@ const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
|
|
|
113
131
|
//#endregion
|
|
114
132
|
//#region src/config/schema.ts
|
|
115
133
|
const DEFAULT_WEIGHTS = {
|
|
116
|
-
format: .
|
|
117
|
-
lint:
|
|
118
|
-
"code-quality":
|
|
119
|
-
"ai-slop":
|
|
134
|
+
format: .3,
|
|
135
|
+
lint: .6,
|
|
136
|
+
"code-quality": .8,
|
|
137
|
+
"ai-slop": 2.5,
|
|
120
138
|
architecture: 1,
|
|
121
|
-
security:
|
|
139
|
+
security: 1.5
|
|
122
140
|
};
|
|
123
141
|
const EnginesSchema = z.object({
|
|
124
142
|
format: z.boolean().default(true),
|
|
@@ -148,7 +166,7 @@ const ScoringSchema = z.object({
|
|
|
148
166
|
good: 75,
|
|
149
167
|
ok: 50
|
|
150
168
|
})),
|
|
151
|
-
smoothing: z.number().nonnegative().default(
|
|
169
|
+
smoothing: z.number().nonnegative().default(20)
|
|
152
170
|
});
|
|
153
171
|
const CiSchema = z.object({
|
|
154
172
|
failBelow: z.number().default(0),
|
|
@@ -181,7 +199,7 @@ const AislopConfigSchema = z.object({
|
|
|
181
199
|
good: 75,
|
|
182
200
|
ok: 50
|
|
183
201
|
},
|
|
184
|
-
smoothing:
|
|
202
|
+
smoothing: 20
|
|
185
203
|
})),
|
|
186
204
|
ci: CiSchema.default(() => ({
|
|
187
205
|
failBelow: 0,
|
|
@@ -872,6 +890,7 @@ const JS_EXTENSIONS = new Set([
|
|
|
872
890
|
".cjs"
|
|
873
891
|
]);
|
|
874
892
|
const PY_EXTENSIONS = new Set([".py"]);
|
|
893
|
+
const REMOVE_MARKER = "\0__AISLOP_REMOVE__";
|
|
875
894
|
const extractJsImportedSymbols = (lines) => {
|
|
876
895
|
const symbols = [];
|
|
877
896
|
const importLines = /* @__PURE__ */ new Set();
|
|
@@ -980,32 +999,47 @@ const isSymbolUsed = (name, content, importLines, lines) => {
|
|
|
980
999
|
}
|
|
981
1000
|
return false;
|
|
982
1001
|
};
|
|
1002
|
+
const analyzeFile = (filePath) => {
|
|
1003
|
+
if (isAutoGenerated(filePath)) return null;
|
|
1004
|
+
let content;
|
|
1005
|
+
try {
|
|
1006
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
1007
|
+
} catch {
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
const ext = path.extname(filePath);
|
|
1011
|
+
const lines = content.split("\n");
|
|
1012
|
+
let symbols;
|
|
1013
|
+
let importLines;
|
|
1014
|
+
if (JS_EXTENSIONS.has(ext)) {
|
|
1015
|
+
const result = extractJsImportedSymbols(lines);
|
|
1016
|
+
symbols = result.symbols;
|
|
1017
|
+
importLines = result.importLines;
|
|
1018
|
+
} else if (PY_EXTENSIONS.has(ext)) {
|
|
1019
|
+
const result = extractPyImportedSymbols(lines);
|
|
1020
|
+
symbols = result.symbols;
|
|
1021
|
+
importLines = result.importLines;
|
|
1022
|
+
} else return null;
|
|
1023
|
+
return {
|
|
1024
|
+
lines,
|
|
1025
|
+
symbols,
|
|
1026
|
+
importLines,
|
|
1027
|
+
ext
|
|
1028
|
+
};
|
|
1029
|
+
};
|
|
1030
|
+
const getUnusedSymbols = (lines, symbols, importLines) => {
|
|
1031
|
+
const content = lines.join("\n");
|
|
1032
|
+
return symbols.filter((symbol) => !isSymbolUsed(symbol.name, content, importLines, lines));
|
|
1033
|
+
};
|
|
983
1034
|
const detectUnusedImports = async (context) => {
|
|
984
1035
|
const files = getSourceFiles(context);
|
|
985
1036
|
const diagnostics = [];
|
|
986
1037
|
for (const filePath of files) {
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
try {
|
|
990
|
-
content = fs.readFileSync(filePath, "utf-8");
|
|
991
|
-
} catch {
|
|
992
|
-
continue;
|
|
993
|
-
}
|
|
994
|
-
const ext = path.extname(filePath);
|
|
1038
|
+
const analysis = analyzeFile(filePath);
|
|
1039
|
+
if (!analysis) continue;
|
|
995
1040
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
996
|
-
const
|
|
997
|
-
|
|
998
|
-
let importLinesSet;
|
|
999
|
-
if (JS_EXTENSIONS.has(ext)) {
|
|
1000
|
-
const result = extractJsImportedSymbols(lines);
|
|
1001
|
-
symbols = result.symbols;
|
|
1002
|
-
importLinesSet = result.importLines;
|
|
1003
|
-
} else if (PY_EXTENSIONS.has(ext)) {
|
|
1004
|
-
const result = extractPyImportedSymbols(lines);
|
|
1005
|
-
symbols = result.symbols;
|
|
1006
|
-
importLinesSet = result.importLines;
|
|
1007
|
-
} else continue;
|
|
1008
|
-
for (const symbol of symbols) if (!isSymbolUsed(symbol.name, content, importLinesSet, lines)) diagnostics.push({
|
|
1041
|
+
const unused = getUnusedSymbols(analysis.lines, analysis.symbols, analysis.importLines);
|
|
1042
|
+
for (const symbol of unused) diagnostics.push({
|
|
1009
1043
|
filePath: relativePath,
|
|
1010
1044
|
engine: "ai-slop",
|
|
1011
1045
|
rule: "ai-slop/unused-import",
|
|
@@ -1826,10 +1860,10 @@ const codeQualityEngine = {
|
|
|
1826
1860
|
|
|
1827
1861
|
//#endregion
|
|
1828
1862
|
//#region src/engines/format/biome.ts
|
|
1829
|
-
const esmRequire$
|
|
1863
|
+
const esmRequire$3 = createRequire(import.meta.url);
|
|
1830
1864
|
const resolveLocalBiomeScript = () => {
|
|
1831
1865
|
try {
|
|
1832
|
-
const packageJsonPath = esmRequire$
|
|
1866
|
+
const packageJsonPath = esmRequire$3.resolve("@biomejs/biome/package.json");
|
|
1833
1867
|
return path.join(path.dirname(packageJsonPath), "bin", "biome");
|
|
1834
1868
|
} catch {
|
|
1835
1869
|
return null;
|
|
@@ -1922,14 +1956,11 @@ const parseBiomeJsonOutput = (output, rootDir) => {
|
|
|
1922
1956
|
const fixBiomeFormat = async (context) => {
|
|
1923
1957
|
const targets = getBiomeTargets(context);
|
|
1924
1958
|
if (targets.length === 0) return;
|
|
1925
|
-
|
|
1926
|
-
"
|
|
1959
|
+
await runBiome([
|
|
1960
|
+
"format",
|
|
1927
1961
|
"--write",
|
|
1928
|
-
"--formatter-enabled=true",
|
|
1929
|
-
"--linter-enabled=false",
|
|
1930
1962
|
...targets
|
|
1931
1963
|
], context.rootDirectory, 6e4);
|
|
1932
|
-
if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `Biome exited with code ${result.exitCode}`);
|
|
1933
1964
|
};
|
|
1934
1965
|
|
|
1935
1966
|
//#endregion
|
|
@@ -2079,7 +2110,7 @@ const fixGofmt = async (rootDirectory) => {
|
|
|
2079
2110
|
//#endregion
|
|
2080
2111
|
//#region src/utils/tooling.ts
|
|
2081
2112
|
const THIS_FILE = fileURLToPath(import.meta.url);
|
|
2082
|
-
const esmRequire$
|
|
2113
|
+
const esmRequire$2 = createRequire(import.meta.url);
|
|
2083
2114
|
const resolvePackageRoot = (startFile) => {
|
|
2084
2115
|
let current = path.dirname(startFile);
|
|
2085
2116
|
while (true) {
|
|
@@ -2110,7 +2141,7 @@ const isToolAvailable = async (toolName) => {
|
|
|
2110
2141
|
};
|
|
2111
2142
|
const isNodePackageAvailable = (packageName) => {
|
|
2112
2143
|
try {
|
|
2113
|
-
esmRequire$
|
|
2144
|
+
esmRequire$2.resolve(`${packageName}/package.json`);
|
|
2114
2145
|
return true;
|
|
2115
2146
|
} catch {
|
|
2116
2147
|
return false;
|
|
@@ -2393,10 +2424,10 @@ const createOxlintConfig = (options) => {
|
|
|
2393
2424
|
|
|
2394
2425
|
//#endregion
|
|
2395
2426
|
//#region src/engines/lint/oxlint.ts
|
|
2396
|
-
const esmRequire = createRequire(import.meta.url);
|
|
2427
|
+
const esmRequire$1 = createRequire(import.meta.url);
|
|
2397
2428
|
const resolveOxlintBinary = () => {
|
|
2398
2429
|
try {
|
|
2399
|
-
const oxlintMainPath = esmRequire.resolve("oxlint");
|
|
2430
|
+
const oxlintMainPath = esmRequire$1.resolve("oxlint");
|
|
2400
2431
|
const oxlintDir = path.resolve(path.dirname(oxlintMainPath), "..");
|
|
2401
2432
|
return path.join(oxlintDir, "bin", "oxlint");
|
|
2402
2433
|
} catch {
|
|
@@ -2431,6 +2462,144 @@ const detectTestFramework = (rootDir) => {
|
|
|
2431
2462
|
} catch {}
|
|
2432
2463
|
return null;
|
|
2433
2464
|
};
|
|
2465
|
+
const extractUnusedVarName = (message) => {
|
|
2466
|
+
const variableMatch = message.match(/Variable '([^']+)' is declared but never used/);
|
|
2467
|
+
if (variableMatch?.[1]) return {
|
|
2468
|
+
name: variableMatch[1],
|
|
2469
|
+
type: "variable"
|
|
2470
|
+
};
|
|
2471
|
+
const paramMatch = message.match(/Parameter '([^']+)' is declared but never used/);
|
|
2472
|
+
if (paramMatch?.[1]) return {
|
|
2473
|
+
name: paramMatch[1],
|
|
2474
|
+
type: "parameter"
|
|
2475
|
+
};
|
|
2476
|
+
const catchMatch = message.match(/Catch parameter '([^']+)' is caught but never used/);
|
|
2477
|
+
if (catchMatch?.[1]) return {
|
|
2478
|
+
name: catchMatch[1],
|
|
2479
|
+
type: "parameter"
|
|
2480
|
+
};
|
|
2481
|
+
return null;
|
|
2482
|
+
};
|
|
2483
|
+
const collectUnusedVarCandidates = (diagnostics) => diagnostics.filter((d) => d.rule === "eslint/no-unused-vars").map((d) => {
|
|
2484
|
+
const extracted = extractUnusedVarName(d.message);
|
|
2485
|
+
if (!extracted || extracted.name.startsWith("_")) return null;
|
|
2486
|
+
return {
|
|
2487
|
+
filePath: d.filePath,
|
|
2488
|
+
line: d.line,
|
|
2489
|
+
column: d.column,
|
|
2490
|
+
name: extracted.name,
|
|
2491
|
+
type: extracted.type
|
|
2492
|
+
};
|
|
2493
|
+
}).filter((candidate) => candidate !== null);
|
|
2494
|
+
const prefixIdentifierOnLine = (line, name, column, type) => {
|
|
2495
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2496
|
+
if (type === "parameter") {
|
|
2497
|
+
const paramPattern = new RegExp(`\\b${escaped}\\b`);
|
|
2498
|
+
if (paramPattern.test(line)) return line.replace(paramPattern, `_${name}`);
|
|
2499
|
+
return line;
|
|
2500
|
+
}
|
|
2501
|
+
const assignPattern = new RegExp(`(\\s*)(const|let|var)\\s+${escaped}\\s*=\\s*(.+)$`);
|
|
2502
|
+
const assignMatch = line.match(assignPattern);
|
|
2503
|
+
if (assignMatch) {
|
|
2504
|
+
const indent = assignMatch[1];
|
|
2505
|
+
const expression = assignMatch[3];
|
|
2506
|
+
if (/await\s/.test(expression) || /\w+\s*\(/.test(expression)) return `${indent}${expression}`;
|
|
2507
|
+
return "";
|
|
2508
|
+
}
|
|
2509
|
+
const destructureMatch = line.match(/\{[^}]*\}/);
|
|
2510
|
+
if (destructureMatch) {
|
|
2511
|
+
const destructureContent = destructureMatch[0];
|
|
2512
|
+
const destructureStart = destructureMatch.index;
|
|
2513
|
+
if (new RegExp(`\\b${escaped}\\b`).test(destructureContent)) {
|
|
2514
|
+
let updated = destructureContent;
|
|
2515
|
+
const propPattern = new RegExp(`\\b${escaped}\\b\\s*,?`);
|
|
2516
|
+
updated = updated.replace(propPattern, (match) => {
|
|
2517
|
+
if (match.endsWith(",")) return "";
|
|
2518
|
+
return "";
|
|
2519
|
+
});
|
|
2520
|
+
updated = updated.replace(/,\s*\},/, "}");
|
|
2521
|
+
updated = updated.replace(/\{,\s*/, "{");
|
|
2522
|
+
updated = updated.replace(/\s*,\s*\}/, "}");
|
|
2523
|
+
if (updated !== destructureContent) return line.slice(0, destructureStart) + updated + line.slice(destructureStart + destructureContent.length);
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
let bestStart = -1;
|
|
2527
|
+
let bestEnd = -1;
|
|
2528
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
2529
|
+
const target = Math.max(0, column - 1);
|
|
2530
|
+
for (const match of line.matchAll(new RegExp(`\\b${escaped}\\b`, "g"))) {
|
|
2531
|
+
if (match.index === void 0) continue;
|
|
2532
|
+
const start = match.index;
|
|
2533
|
+
const end = start + name.length;
|
|
2534
|
+
if (start > 0 && line[start - 1] === "_") continue;
|
|
2535
|
+
const distance = target >= start && target <= end ? 0 : Math.abs(start - target);
|
|
2536
|
+
if (distance < bestDistance) {
|
|
2537
|
+
bestDistance = distance;
|
|
2538
|
+
bestStart = start;
|
|
2539
|
+
bestEnd = end;
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
if (bestStart < 0 || bestEnd < 0) return line;
|
|
2543
|
+
return `${line.slice(0, bestStart)}_${name}${line.slice(bestEnd)}`;
|
|
2544
|
+
};
|
|
2545
|
+
const applyUnusedVarPrefixFixes = (rootDirectory, candidates) => {
|
|
2546
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
2547
|
+
for (const candidate of candidates) {
|
|
2548
|
+
const absolute = path.isAbsolute(candidate.filePath) ? candidate.filePath : path.join(rootDirectory, candidate.filePath);
|
|
2549
|
+
const entries = byFile.get(absolute) ?? [];
|
|
2550
|
+
entries.push(candidate);
|
|
2551
|
+
byFile.set(absolute, entries);
|
|
2552
|
+
}
|
|
2553
|
+
for (const [filePath, fileCandidates] of byFile.entries()) {
|
|
2554
|
+
if (!fs.existsSync(filePath)) continue;
|
|
2555
|
+
const lines = fs.readFileSync(filePath, "utf-8").split("\n");
|
|
2556
|
+
const ordered = [...fileCandidates].sort((a, b) => {
|
|
2557
|
+
if (a.line !== b.line) return a.line - b.line;
|
|
2558
|
+
return a.column - b.column;
|
|
2559
|
+
});
|
|
2560
|
+
let changed = false;
|
|
2561
|
+
for (const candidate of ordered) {
|
|
2562
|
+
const lineIndex = candidate.line - 1;
|
|
2563
|
+
if (lineIndex < 0 || lineIndex >= lines.length) continue;
|
|
2564
|
+
const current = lines[lineIndex];
|
|
2565
|
+
const updated = prefixIdentifierOnLine(current, candidate.name, candidate.column, candidate.type);
|
|
2566
|
+
if (updated !== current) {
|
|
2567
|
+
lines[lineIndex] = updated;
|
|
2568
|
+
changed = true;
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
if (changed) fs.writeFileSync(filePath, lines.join("\n"));
|
|
2572
|
+
}
|
|
2573
|
+
};
|
|
2574
|
+
const removeDuplicateKeyLines = (rootDirectory, diagnostics) => {
|
|
2575
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
2576
|
+
for (const d of diagnostics) {
|
|
2577
|
+
const keyMatch = d.message.match(/Duplicate key '([^']+)'/);
|
|
2578
|
+
if (!keyMatch) continue;
|
|
2579
|
+
const absolute = path.isAbsolute(d.filePath) ? d.filePath : path.join(rootDirectory, d.filePath);
|
|
2580
|
+
const entries = byFile.get(absolute) ?? [];
|
|
2581
|
+
entries.push({
|
|
2582
|
+
key: keyMatch[1],
|
|
2583
|
+
line: d.line
|
|
2584
|
+
});
|
|
2585
|
+
byFile.set(absolute, entries);
|
|
2586
|
+
}
|
|
2587
|
+
for (const [filePath, dupes] of byFile) {
|
|
2588
|
+
if (!fs.existsSync(filePath)) continue;
|
|
2589
|
+
const lines = fs.readFileSync(filePath, "utf-8").split("\n");
|
|
2590
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
2591
|
+
for (const { key } of dupes) {
|
|
2592
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2593
|
+
const keyPattern = new RegExp(`^\\s*['"]?${escaped}['"]?\\s*:|^\\s*${escaped}\\s*:`);
|
|
2594
|
+
const matches = [];
|
|
2595
|
+
for (let i = 0; i < lines.length; i++) if (keyPattern.test(lines[i])) matches.push(i);
|
|
2596
|
+
for (let j = 1; j < matches.length; j++) toRemove.add(matches[j]);
|
|
2597
|
+
}
|
|
2598
|
+
if (toRemove.size === 0) continue;
|
|
2599
|
+
const filtered = lines.filter((_, i) => !toRemove.has(i));
|
|
2600
|
+
fs.writeFileSync(filePath, filtered.join("\n"));
|
|
2601
|
+
}
|
|
2602
|
+
};
|
|
2434
2603
|
const runOxlint = async (context) => {
|
|
2435
2604
|
const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-${process.pid}.json`);
|
|
2436
2605
|
const config = createOxlintConfig({
|
|
@@ -2492,6 +2661,8 @@ const fixOxlint = async (context) => {
|
|
|
2492
2661
|
"-c",
|
|
2493
2662
|
configPath,
|
|
2494
2663
|
"--fix",
|
|
2664
|
+
"--fix-suggestions",
|
|
2665
|
+
"--fix-dangerously",
|
|
2495
2666
|
"."
|
|
2496
2667
|
];
|
|
2497
2668
|
const result = await runSubprocess(process.execPath, args, {
|
|
@@ -2499,10 +2670,18 @@ const fixOxlint = async (context) => {
|
|
|
2499
2670
|
timeout: 12e4
|
|
2500
2671
|
});
|
|
2501
2672
|
if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `Oxlint exited with code ${result.exitCode}`);
|
|
2673
|
+
const remaining = await runOxlint(context);
|
|
2674
|
+
const candidates = collectUnusedVarCandidates(remaining);
|
|
2675
|
+
if (candidates.length > 0) applyUnusedVarPrefixFixes(context.rootDirectory, candidates);
|
|
2676
|
+
const duplicateKeys = remaining.filter((d) => d.message.startsWith("Duplicate key"));
|
|
2677
|
+
if (duplicateKeys.length > 0) removeDuplicateKeyLines(context.rootDirectory, duplicateKeys);
|
|
2502
2678
|
} finally {
|
|
2503
2679
|
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
|
|
2504
2680
|
}
|
|
2505
2681
|
};
|
|
2682
|
+
const fixOxlintForce = async (context) => {
|
|
2683
|
+
return fixOxlint(context);
|
|
2684
|
+
};
|
|
2506
2685
|
|
|
2507
2686
|
//#endregion
|
|
2508
2687
|
//#region src/engines/lint/ruff.ts
|
|
@@ -2545,6 +2724,18 @@ const fixRuffLint = async (rootDirectory) => {
|
|
|
2545
2724
|
});
|
|
2546
2725
|
if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `ruff check --fix exited with code ${result.exitCode}`);
|
|
2547
2726
|
};
|
|
2727
|
+
const fixRuffLintForce = async (rootDirectory) => {
|
|
2728
|
+
const result = await runSubprocess(resolveToolBinary("ruff"), [
|
|
2729
|
+
"check",
|
|
2730
|
+
"--fix",
|
|
2731
|
+
"--unsafe-fixes",
|
|
2732
|
+
rootDirectory
|
|
2733
|
+
], {
|
|
2734
|
+
cwd: rootDirectory,
|
|
2735
|
+
timeout: 6e4
|
|
2736
|
+
});
|
|
2737
|
+
if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `ruff check --fix exited with code ${result.exitCode}`);
|
|
2738
|
+
};
|
|
2548
2739
|
|
|
2549
2740
|
//#endregion
|
|
2550
2741
|
//#region src/engines/lint/index.ts
|
|
@@ -2555,7 +2746,7 @@ const lintEngine = {
|
|
|
2555
2746
|
const { languages, installedTools } = context;
|
|
2556
2747
|
const promises = [];
|
|
2557
2748
|
if (languages.includes("typescript") || languages.includes("javascript")) promises.push(runOxlint(context));
|
|
2558
|
-
if (context.frameworks.includes("expo")) promises.push(
|
|
2749
|
+
if (context.frameworks.includes("expo")) promises.push(Promise.resolve().then(() => expo_doctor_exports).then((mod) => mod.runExpoDoctor(context)));
|
|
2559
2750
|
if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
|
|
2560
2751
|
if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
|
|
2561
2752
|
if (languages.includes("rust") && installedTools["cargo"]) promises.push(runGenericLinter(context, "rust"));
|
|
@@ -3180,14 +3371,13 @@ const logger = {
|
|
|
3180
3371
|
* Application version — injected at build time by tsdown from package.json.
|
|
3181
3372
|
* The fallback should always match the "version" field in package.json.
|
|
3182
3373
|
*/
|
|
3183
|
-
const APP_VERSION = "0.
|
|
3374
|
+
const APP_VERSION = "0.3.0";
|
|
3184
3375
|
|
|
3185
3376
|
//#endregion
|
|
3186
3377
|
//#region src/output/layout.ts
|
|
3187
|
-
const formatElapsed$
|
|
3378
|
+
const formatElapsed$2 = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
|
|
3188
3379
|
const printCommandHeader = (commandName) => {
|
|
3189
|
-
logger.log(highlighter.bold(`aislop ${commandName}`));
|
|
3190
|
-
logger.log(highlighter.dim(`v${APP_VERSION}`));
|
|
3380
|
+
logger.log(`${highlighter.bold(`aislop ${commandName.toLowerCase()}`)} ${highlighter.dim(`v${APP_VERSION}`)}`);
|
|
3191
3381
|
logger.break();
|
|
3192
3382
|
};
|
|
3193
3383
|
const formatProjectSummary = (project) => `Project ${highlighter.info(project.projectName)} (${highlighter.info(project.languages.join(", "))})`;
|
|
@@ -3198,84 +3388,9 @@ const printProjectMetadata = (project) => {
|
|
|
3198
3388
|
logger.break();
|
|
3199
3389
|
};
|
|
3200
3390
|
|
|
3201
|
-
//#endregion
|
|
3202
|
-
//#region src/output/pager.ts
|
|
3203
|
-
const DEFAULT_COLUMNS = 80;
|
|
3204
|
-
const DEFAULT_ROWS = 24;
|
|
3205
|
-
const ANSI_PATTERN = new RegExp(String.raw`\u001B\[[0-?]*[ -/]*[@-~]`, "g");
|
|
3206
|
-
const stripAnsi = (text) => text.replace(ANSI_PATTERN, "");
|
|
3207
|
-
const resolvePagerCommand = () => {
|
|
3208
|
-
const pager = process.env.PAGER?.trim();
|
|
3209
|
-
if (pager) {
|
|
3210
|
-
const [command, ...args] = pager.split(/\s+/);
|
|
3211
|
-
if (command) return {
|
|
3212
|
-
command,
|
|
3213
|
-
args
|
|
3214
|
-
};
|
|
3215
|
-
}
|
|
3216
|
-
return {
|
|
3217
|
-
command: "less",
|
|
3218
|
-
args: [
|
|
3219
|
-
"-R",
|
|
3220
|
-
"-F",
|
|
3221
|
-
"-X"
|
|
3222
|
-
]
|
|
3223
|
-
};
|
|
3224
|
-
};
|
|
3225
|
-
const writeToStdout = (text) => {
|
|
3226
|
-
process.stdout.write(text);
|
|
3227
|
-
};
|
|
3228
|
-
const pipeToPager = async (command, args, text) => new Promise((resolve) => {
|
|
3229
|
-
let settled = false;
|
|
3230
|
-
const finish = (success) => {
|
|
3231
|
-
if (settled) return;
|
|
3232
|
-
settled = true;
|
|
3233
|
-
resolve(success);
|
|
3234
|
-
};
|
|
3235
|
-
try {
|
|
3236
|
-
const child = spawn(command, args, {
|
|
3237
|
-
stdio: [
|
|
3238
|
-
"pipe",
|
|
3239
|
-
"inherit",
|
|
3240
|
-
"inherit"
|
|
3241
|
-
],
|
|
3242
|
-
windowsHide: true
|
|
3243
|
-
});
|
|
3244
|
-
child.once("error", () => finish(false));
|
|
3245
|
-
child.once("close", (code) => finish(code === 0));
|
|
3246
|
-
child.stdin?.on("error", () => void 0);
|
|
3247
|
-
child.stdin?.end(text);
|
|
3248
|
-
} catch {
|
|
3249
|
-
finish(false);
|
|
3250
|
-
}
|
|
3251
|
-
});
|
|
3252
|
-
const countRenderedLines = (text, columns = DEFAULT_COLUMNS) => {
|
|
3253
|
-
const width = Math.max(1, columns);
|
|
3254
|
-
return text.split("\n").reduce((count, line) => {
|
|
3255
|
-
const visibleLine = stripAnsi(line).replaceAll(" ", " ");
|
|
3256
|
-
return count + Math.max(1, Math.ceil(visibleLine.length / width));
|
|
3257
|
-
}, 0);
|
|
3258
|
-
};
|
|
3259
|
-
const shouldPageOutput = (text, options = {}) => {
|
|
3260
|
-
if (text.trim().length === 0) return false;
|
|
3261
|
-
const stdinIsTTY = options.stdinIsTTY ?? Boolean(process.stdin.isTTY);
|
|
3262
|
-
const stdoutIsTTY = options.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
|
|
3263
|
-
if (!stdinIsTTY || !stdoutIsTTY) return false;
|
|
3264
|
-
const rows = Math.max(1, options.rows ?? process.stdout.rows ?? DEFAULT_ROWS);
|
|
3265
|
-
return countRenderedLines(text, Math.max(1, options.columns ?? process.stdout.columns ?? DEFAULT_COLUMNS)) > rows - 1;
|
|
3266
|
-
};
|
|
3267
|
-
const printMaybePaged = async (text) => {
|
|
3268
|
-
if (!shouldPageOutput(text)) {
|
|
3269
|
-
writeToStdout(text);
|
|
3270
|
-
return;
|
|
3271
|
-
}
|
|
3272
|
-
const pager = resolvePagerCommand();
|
|
3273
|
-
if (!await pipeToPager(pager.command, pager.args, text)) writeToStdout(text);
|
|
3274
|
-
};
|
|
3275
|
-
|
|
3276
3391
|
//#endregion
|
|
3277
3392
|
//#region src/output/scan-progress.ts
|
|
3278
|
-
const SPINNER_FRAMES = [
|
|
3393
|
+
const SPINNER_FRAMES$1 = [
|
|
3279
3394
|
"⠋",
|
|
3280
3395
|
"⠙",
|
|
3281
3396
|
"⠹",
|
|
@@ -3288,20 +3403,20 @@ const SPINNER_FRAMES = [
|
|
|
3288
3403
|
"⠏"
|
|
3289
3404
|
];
|
|
3290
3405
|
const shouldRenderLiveScanProgress = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
|
|
3291
|
-
const formatElapsed = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
|
|
3406
|
+
const formatElapsed$1 = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
|
|
3292
3407
|
const truncateText = (text, maxLength = 52) => text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}…`;
|
|
3293
3408
|
const getIssueSummary = (result) => {
|
|
3294
3409
|
const errors = result.diagnostics.filter((d) => d.severity === "error").length;
|
|
3295
3410
|
const warnings = result.diagnostics.filter((d) => d.severity === "warning").length;
|
|
3296
|
-
if (errors === 0 && warnings === 0) return `Done (0 issues, ${formatElapsed(result.elapsed)})`;
|
|
3411
|
+
if (errors === 0 && warnings === 0) return `Done (0 issues, ${formatElapsed$1(result.elapsed)})`;
|
|
3297
3412
|
const parts = [];
|
|
3298
3413
|
if (errors > 0) parts.push(`${errors} error${errors === 1 ? "" : "s"}`);
|
|
3299
3414
|
if (warnings > 0) parts.push(`${warnings} warning${warnings === 1 ? "" : "s"}`);
|
|
3300
|
-
return `Done (${parts.join(", ")}, ${formatElapsed(result.elapsed)})`;
|
|
3415
|
+
return `Done (${parts.join(", ")}, ${formatElapsed$1(result.elapsed)})`;
|
|
3301
3416
|
};
|
|
3302
|
-
const getStatusParts = (state, frameIndex) => {
|
|
3417
|
+
const getStatusParts$1 = (state, frameIndex) => {
|
|
3303
3418
|
if (state.status === "running") return {
|
|
3304
|
-
icon: highlighter.info(SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length]),
|
|
3419
|
+
icon: highlighter.info(SPINNER_FRAMES$1[frameIndex % SPINNER_FRAMES$1.length]),
|
|
3305
3420
|
detail: highlighter.info("Running")
|
|
3306
3421
|
};
|
|
3307
3422
|
if (state.status === "done") {
|
|
@@ -3340,11 +3455,11 @@ const renderScanProgressBlock = (states, frameIndex) => {
|
|
|
3340
3455
|
const headingStatus = completedCount === states.length ? highlighter.dim("complete") : runningCount > 0 ? highlighter.dim(`${runningCount} running`) : highlighter.dim("starting");
|
|
3341
3456
|
return `${[` ${highlighter.bold(`Engines ${completedCount}/${states.length}`)} ${headingStatus}`, ...states.map((state) => {
|
|
3342
3457
|
const label = getEngineLabel(state.engine).padEnd(labelWidth, " ");
|
|
3343
|
-
const { icon, detail } = getStatusParts(state, frameIndex);
|
|
3458
|
+
const { icon, detail } = getStatusParts$1(state, frameIndex);
|
|
3344
3459
|
return ` ${icon} ${label} ${detail}`;
|
|
3345
3460
|
})].join("\n")}\n`;
|
|
3346
3461
|
};
|
|
3347
|
-
const clearRenderedLines$
|
|
3462
|
+
const clearRenderedLines$2 = (lineCount) => {
|
|
3348
3463
|
if (lineCount === 0) return;
|
|
3349
3464
|
process.stderr.write(`\u001B[${lineCount}F`);
|
|
3350
3465
|
for (let index = 0; index < lineCount; index += 1) {
|
|
@@ -3396,7 +3511,7 @@ var ScanProgressRenderer = class {
|
|
|
3396
3511
|
}
|
|
3397
3512
|
render() {
|
|
3398
3513
|
if (!shouldRenderLiveScanProgress()) return;
|
|
3399
|
-
if (this.previousLineCount > 0) clearRenderedLines$
|
|
3514
|
+
if (this.previousLineCount > 0) clearRenderedLines$2(this.previousLineCount);
|
|
3400
3515
|
const output = renderScanProgressBlock(this.states, this.frameIndex);
|
|
3401
3516
|
process.stderr.write(output);
|
|
3402
3517
|
this.previousLineCount = output.split("\n").length - 1;
|
|
@@ -3882,12 +3997,13 @@ const scanCommand = async (directory, config, options) => {
|
|
|
3882
3997
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
3883
3998
|
return { exitCode };
|
|
3884
3999
|
}
|
|
3885
|
-
|
|
4000
|
+
const output = [
|
|
3886
4001
|
"",
|
|
3887
4002
|
allDiagnostics.length === 0 ? `${highlighter.success(" ✓ No issues found.")}\n` : renderDiagnostics(allDiagnostics, options.verbose),
|
|
3888
4003
|
renderSummary(allDiagnostics, scoreResult, elapsedMs, projectInfo.sourceFileCount, config.scoring.thresholds),
|
|
3889
4004
|
""
|
|
3890
|
-
].join("\n")
|
|
4005
|
+
].join("\n");
|
|
4006
|
+
process.stdout.write(output);
|
|
3891
4007
|
return { exitCode };
|
|
3892
4008
|
};
|
|
3893
4009
|
|
|
@@ -4038,7 +4154,429 @@ const doctorCommand = async (directory) => {
|
|
|
4038
4154
|
};
|
|
4039
4155
|
|
|
4040
4156
|
//#endregion
|
|
4041
|
-
//#region src/
|
|
4157
|
+
//#region src/engines/ai-slop/dead-patterns-fix.ts
|
|
4158
|
+
/**
|
|
4159
|
+
* Removes lines flagged as fixable by the trivial-comment and dead-pattern detectors.
|
|
4160
|
+
* Specifically handles:
|
|
4161
|
+
* - ai-slop/trivial-comment (trivial comments that restate the code)
|
|
4162
|
+
* - ai-slop/console-leftover (console.log/debug/info left in production)
|
|
4163
|
+
*/
|
|
4164
|
+
const fixDeadPatterns = async (context) => {
|
|
4165
|
+
const fixable = [...await detectTrivialComments(context), ...await detectDeadPatterns(context)].filter((d) => d.fixable);
|
|
4166
|
+
if (fixable.length === 0) return;
|
|
4167
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
4168
|
+
for (const d of fixable) {
|
|
4169
|
+
const absolute = path.isAbsolute(d.filePath) ? d.filePath : path.join(context.rootDirectory, d.filePath);
|
|
4170
|
+
const lines = byFile.get(absolute) ?? /* @__PURE__ */ new Set();
|
|
4171
|
+
lines.add(d.line);
|
|
4172
|
+
byFile.set(absolute, lines);
|
|
4173
|
+
}
|
|
4174
|
+
for (const [filePath, lineNumbers] of byFile) {
|
|
4175
|
+
if (!fs.existsSync(filePath)) continue;
|
|
4176
|
+
const lines = fs.readFileSync(filePath, "utf-8").split("\n");
|
|
4177
|
+
const filtered = [];
|
|
4178
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4179
|
+
const lineNo = i + 1;
|
|
4180
|
+
if (lineNumbers.has(lineNo)) continue;
|
|
4181
|
+
filtered.push(lines[i]);
|
|
4182
|
+
}
|
|
4183
|
+
const collapsed = [];
|
|
4184
|
+
for (const line of filtered) {
|
|
4185
|
+
if (line.trim() === "" && collapsed.length > 0 && collapsed[collapsed.length - 1].trim() === "") continue;
|
|
4186
|
+
collapsed.push(line);
|
|
4187
|
+
}
|
|
4188
|
+
fs.writeFileSync(filePath, collapsed.join("\n"));
|
|
4189
|
+
}
|
|
4190
|
+
};
|
|
4191
|
+
|
|
4192
|
+
//#endregion
|
|
4193
|
+
//#region src/engines/ai-slop/unused-imports-fix.ts
|
|
4194
|
+
const fixUnusedImports = async (context) => {
|
|
4195
|
+
const files = getSourceFiles(context);
|
|
4196
|
+
for (const filePath of files) {
|
|
4197
|
+
const analysis = analyzeFile(filePath);
|
|
4198
|
+
if (!analysis) continue;
|
|
4199
|
+
const unused = getUnusedSymbols(analysis.lines, analysis.symbols, analysis.importLines);
|
|
4200
|
+
if (unused.length === 0) continue;
|
|
4201
|
+
const unusedNames = new Set(unused.map((u) => u.name));
|
|
4202
|
+
const lines = [...analysis.lines];
|
|
4203
|
+
const symbolsByLine = /* @__PURE__ */ new Map();
|
|
4204
|
+
for (const sym of analysis.symbols) {
|
|
4205
|
+
const arr = symbolsByLine.get(sym.line) ?? [];
|
|
4206
|
+
arr.push(sym);
|
|
4207
|
+
symbolsByLine.set(sym.line, arr);
|
|
4208
|
+
}
|
|
4209
|
+
const linesToRemove = /* @__PURE__ */ new Set();
|
|
4210
|
+
for (const [lineNo, syms] of symbolsByLine) {
|
|
4211
|
+
const lineIdx = lineNo - 1;
|
|
4212
|
+
const allUnused = syms.every((s) => unusedNames.has(s.name));
|
|
4213
|
+
const importSpan = JS_EXTENSIONS.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
|
|
4214
|
+
if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
|
|
4215
|
+
else if (JS_EXTENSIONS.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
|
|
4216
|
+
else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, syms, unusedNames);
|
|
4217
|
+
}
|
|
4218
|
+
if (linesToRemove.size === 0 && unused.length === 0) continue;
|
|
4219
|
+
const sortedRemove = [...linesToRemove].sort((a, b) => b - a);
|
|
4220
|
+
for (const idx of sortedRemove) lines.splice(idx, 1);
|
|
4221
|
+
const filtered = lines.filter((l) => l !== REMOVE_MARKER);
|
|
4222
|
+
while (filtered.length > 0 && filtered[0].trim() === "") filtered.shift();
|
|
4223
|
+
fs.writeFileSync(filePath, filtered.join("\n"));
|
|
4224
|
+
}
|
|
4225
|
+
};
|
|
4226
|
+
const getJsImportSpan = (lines, startIdx) => {
|
|
4227
|
+
const span = [startIdx];
|
|
4228
|
+
let fullImport = lines[startIdx]?.trim() ?? "";
|
|
4229
|
+
if (!fullImport.startsWith("import ")) return span;
|
|
4230
|
+
let idx = startIdx + 1;
|
|
4231
|
+
while (!fullImport.includes("from") && idx < lines.length) {
|
|
4232
|
+
span.push(idx);
|
|
4233
|
+
fullImport += ` ${lines[idx].trim()}`;
|
|
4234
|
+
idx++;
|
|
4235
|
+
}
|
|
4236
|
+
return span;
|
|
4237
|
+
};
|
|
4238
|
+
const rewriteJsImportSpan = (lines, span, syms, unusedNames) => {
|
|
4239
|
+
const fullImport = span.map((i) => lines[i]).join("\n");
|
|
4240
|
+
const namedMatch = fullImport.match(/\{([^}]+)\}/s);
|
|
4241
|
+
if (!namedMatch) return;
|
|
4242
|
+
const unusedNamed = syms.filter((s) => !s.isDefault && !s.isNamespace && unusedNames.has(s.name));
|
|
4243
|
+
const defaultUnused = syms.some((s) => s.isDefault && unusedNames.has(s.name));
|
|
4244
|
+
if (unusedNamed.length === 0 && !defaultUnused) return;
|
|
4245
|
+
const unusedNamedSet = new Set(unusedNamed.map((s) => s.name));
|
|
4246
|
+
const keptSpecifiers = namedMatch[1].split(",").map((s) => s.trim()).filter(Boolean).filter((spec) => {
|
|
4247
|
+
const parts = spec.split(/\s+as\s+/);
|
|
4248
|
+
const localName = parts.length > 1 ? parts[1].trim().replace(/^type\s+/, "") : parts[0].trim().replace(/^type\s+/, "");
|
|
4249
|
+
return !unusedNamedSet.has(localName);
|
|
4250
|
+
});
|
|
4251
|
+
const fromMatch = fullImport.match(/from\s+["']([^"']+)["'];?/);
|
|
4252
|
+
const fromClause = fromMatch ? `from "${fromMatch[1]}"` : "";
|
|
4253
|
+
if (keptSpecifiers.length === 0) {
|
|
4254
|
+
const usedDefault = syms.find((s) => s.isDefault && !unusedNames.has(s.name));
|
|
4255
|
+
if (usedDefault) {
|
|
4256
|
+
const defaultMatch = fullImport.match(/^import\s+(\w+)/);
|
|
4257
|
+
const defaultName = defaultMatch ? defaultMatch[1] : usedDefault.name;
|
|
4258
|
+
lines[span[0]] = `import ${defaultName} ${fromClause};`;
|
|
4259
|
+
for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
|
|
4260
|
+
} else for (const idx of span) lines[idx] = REMOVE_MARKER;
|
|
4261
|
+
return;
|
|
4262
|
+
}
|
|
4263
|
+
if (defaultUnused) {
|
|
4264
|
+
lines[span[0]] = `import { ${keptSpecifiers.join(", ")} } ${fromClause};`;
|
|
4265
|
+
for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
|
|
4266
|
+
return;
|
|
4267
|
+
}
|
|
4268
|
+
const importPrefix = fullImport.match(/^(import\s+(?:\w+\s*,\s*)?)/);
|
|
4269
|
+
const prefix = importPrefix ? importPrefix[1] : "import ";
|
|
4270
|
+
const wasMultiLine = span.length > 1;
|
|
4271
|
+
let newImport;
|
|
4272
|
+
if (wasMultiLine && keptSpecifiers.length > 2) {
|
|
4273
|
+
const indentMatch = lines[span[1]]?.match(/^(\s+)/);
|
|
4274
|
+
const indent = indentMatch ? indentMatch[1] : " ";
|
|
4275
|
+
newImport = `${prefix}{\n${keptSpecifiers.map((s) => `${indent}${s},`).join("\n")}\n} ${fromClause};`;
|
|
4276
|
+
} else newImport = `${prefix}{ ${keptSpecifiers.join(", ")} } ${fromClause};`;
|
|
4277
|
+
lines[span[0]] = newImport;
|
|
4278
|
+
for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
|
|
4279
|
+
};
|
|
4280
|
+
const rewritePyImportLine = (lines, lineIdx, syms, unusedNames) => {
|
|
4281
|
+
const fromMatch = lines[lineIdx].match(/^(\s*from\s+[\w.]+\s+import\s+)(.+)$/);
|
|
4282
|
+
if (!fromMatch) return;
|
|
4283
|
+
const prefix = fromMatch[1];
|
|
4284
|
+
const importPart = fromMatch[2].replace(/#.*$/, "").trim();
|
|
4285
|
+
const hasParen = importPart.startsWith("(");
|
|
4286
|
+
const keptSpecifiers = importPart.replace(/[()]/g, "").split(",").map((s) => s.trim()).filter((spec) => {
|
|
4287
|
+
const parts = spec.split(/\s+as\s+/);
|
|
4288
|
+
const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim();
|
|
4289
|
+
return !unusedNames.has(localName);
|
|
4290
|
+
});
|
|
4291
|
+
if (keptSpecifiers.length === 0) return;
|
|
4292
|
+
const joined = keptSpecifiers.join(", ");
|
|
4293
|
+
lines[lineIdx] = hasParen ? `${prefix}(${joined})` : `${prefix}${joined}`;
|
|
4294
|
+
};
|
|
4295
|
+
|
|
4296
|
+
//#endregion
|
|
4297
|
+
//#region src/engines/lint/expo-doctor.ts
|
|
4298
|
+
var expo_doctor_exports = /* @__PURE__ */ __exportAll({ runExpoDoctor: () => runExpoDoctor });
|
|
4299
|
+
const esmRequire = createRequire(import.meta.url);
|
|
4300
|
+
const ISSUE_PREFIX = "✖ ";
|
|
4301
|
+
const resolveExpoDoctorScript = () => {
|
|
4302
|
+
try {
|
|
4303
|
+
const packageJsonPath = esmRequire.resolve("expo-doctor/package.json");
|
|
4304
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
4305
|
+
const binRelativePath = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.["expo-doctor"];
|
|
4306
|
+
if (!binRelativePath) return null;
|
|
4307
|
+
return path.join(path.dirname(packageJsonPath), binRelativePath);
|
|
4308
|
+
} catch {
|
|
4309
|
+
return null;
|
|
4310
|
+
}
|
|
4311
|
+
};
|
|
4312
|
+
const toRuleSuffix = (title) => {
|
|
4313
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
4314
|
+
return slug.length > 0 ? slug : "issue";
|
|
4315
|
+
};
|
|
4316
|
+
const parseIssues = (output) => {
|
|
4317
|
+
const lines = output.split("\n").map((line) => line.trimEnd());
|
|
4318
|
+
const startIndex = lines.findIndex((line) => line.includes("Possible issues detected:"));
|
|
4319
|
+
if (startIndex < 0) return [];
|
|
4320
|
+
const issues = [];
|
|
4321
|
+
let current = null;
|
|
4322
|
+
let inAdvice = false;
|
|
4323
|
+
for (let i = startIndex + 1; i < lines.length; i += 1) {
|
|
4324
|
+
const line = lines[i].trim();
|
|
4325
|
+
if (/^\d+\s+checks failed/.test(line)) break;
|
|
4326
|
+
if (line.length === 0) continue;
|
|
4327
|
+
if (line.startsWith(ISSUE_PREFIX)) {
|
|
4328
|
+
if (current) issues.push(current);
|
|
4329
|
+
current = {
|
|
4330
|
+
title: line.slice(2).trim(),
|
|
4331
|
+
details: [],
|
|
4332
|
+
advice: []
|
|
4333
|
+
};
|
|
4334
|
+
inAdvice = false;
|
|
4335
|
+
continue;
|
|
4336
|
+
}
|
|
4337
|
+
if (!current) continue;
|
|
4338
|
+
if (line === "Advice:") {
|
|
4339
|
+
inAdvice = true;
|
|
4340
|
+
continue;
|
|
4341
|
+
}
|
|
4342
|
+
if (inAdvice) current.advice.push(line);
|
|
4343
|
+
else current.details.push(line);
|
|
4344
|
+
}
|
|
4345
|
+
if (current) issues.push(current);
|
|
4346
|
+
return issues;
|
|
4347
|
+
};
|
|
4348
|
+
const parseConfigError = (output) => {
|
|
4349
|
+
const line = output.split("\n").find((candidate) => candidate.trim().startsWith("ConfigError:"));
|
|
4350
|
+
return line ? line.trim() : null;
|
|
4351
|
+
};
|
|
4352
|
+
const toDiagnostics = (issues) => issues.map((issue) => {
|
|
4353
|
+
const helpParts = [issue.details.join(" ").trim(), issue.advice.join(" ").trim()].filter((part) => part.length > 0);
|
|
4354
|
+
return {
|
|
4355
|
+
filePath: "package.json",
|
|
4356
|
+
engine: "lint",
|
|
4357
|
+
rule: `expo-doctor/${toRuleSuffix(issue.title)}`,
|
|
4358
|
+
severity: "warning",
|
|
4359
|
+
message: `Expo Doctor: ${issue.title}`,
|
|
4360
|
+
help: helpParts.join(" "),
|
|
4361
|
+
line: 0,
|
|
4362
|
+
column: 0,
|
|
4363
|
+
category: "Expo",
|
|
4364
|
+
fixable: false
|
|
4365
|
+
};
|
|
4366
|
+
});
|
|
4367
|
+
const runExpoDoctor = async (context) => {
|
|
4368
|
+
const scriptPath = resolveExpoDoctorScript();
|
|
4369
|
+
let stdout = "";
|
|
4370
|
+
let stderr = "";
|
|
4371
|
+
try {
|
|
4372
|
+
if (scriptPath) {
|
|
4373
|
+
const result = await runSubprocess(process.execPath, [
|
|
4374
|
+
scriptPath,
|
|
4375
|
+
context.rootDirectory,
|
|
4376
|
+
"--verbose"
|
|
4377
|
+
], {
|
|
4378
|
+
cwd: context.rootDirectory,
|
|
4379
|
+
timeout: 12e4
|
|
4380
|
+
});
|
|
4381
|
+
stdout = result.stdout;
|
|
4382
|
+
stderr = result.stderr;
|
|
4383
|
+
} else {
|
|
4384
|
+
const result = await runSubprocess("npx", [
|
|
4385
|
+
"--yes",
|
|
4386
|
+
"expo-doctor",
|
|
4387
|
+
context.rootDirectory,
|
|
4388
|
+
"--verbose"
|
|
4389
|
+
], {
|
|
4390
|
+
cwd: context.rootDirectory,
|
|
4391
|
+
timeout: 12e4
|
|
4392
|
+
});
|
|
4393
|
+
stdout = result.stdout;
|
|
4394
|
+
stderr = result.stderr;
|
|
4395
|
+
}
|
|
4396
|
+
} catch {
|
|
4397
|
+
return [];
|
|
4398
|
+
}
|
|
4399
|
+
const output = [stdout, stderr].filter(Boolean).join("\n");
|
|
4400
|
+
if (!output) return [];
|
|
4401
|
+
const configError = parseConfigError(output);
|
|
4402
|
+
if (configError) return [{
|
|
4403
|
+
filePath: "package.json",
|
|
4404
|
+
engine: "lint",
|
|
4405
|
+
rule: "expo-doctor/config-error",
|
|
4406
|
+
severity: "warning",
|
|
4407
|
+
message: configError,
|
|
4408
|
+
help: "Install project dependencies, then re-run `aislop scan`.",
|
|
4409
|
+
line: 0,
|
|
4410
|
+
column: 0,
|
|
4411
|
+
category: "Expo",
|
|
4412
|
+
fixable: false
|
|
4413
|
+
}];
|
|
4414
|
+
return toDiagnostics(parseIssues(output));
|
|
4415
|
+
};
|
|
4416
|
+
|
|
4417
|
+
//#endregion
|
|
4418
|
+
//#region src/output/fix-progress.ts
|
|
4419
|
+
const SPINNER_FRAMES = [
|
|
4420
|
+
"⠋",
|
|
4421
|
+
"⠙",
|
|
4422
|
+
"⠹",
|
|
4423
|
+
"⠸",
|
|
4424
|
+
"⠼",
|
|
4425
|
+
"⠴",
|
|
4426
|
+
"⠦",
|
|
4427
|
+
"⠧",
|
|
4428
|
+
"⠇",
|
|
4429
|
+
"⠏"
|
|
4430
|
+
];
|
|
4431
|
+
const shouldRenderLive = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
|
|
4432
|
+
const formatElapsed = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
|
|
4433
|
+
const getStepSummary = (result) => {
|
|
4434
|
+
if (result.failed) return `Failed (${result.afterIssues} remain)`;
|
|
4435
|
+
if (result.beforeIssues === 0) return `0 issues, ${formatElapsed(result.elapsedMs)}`;
|
|
4436
|
+
if (result.afterIssues === 0) return `${result.resolvedIssues} resolved, ${formatElapsed(result.elapsedMs)}`;
|
|
4437
|
+
if (result.resolvedIssues > 0) return `${result.resolvedIssues} resolved, ${result.afterIssues} remaining, ${formatElapsed(result.elapsedMs)}`;
|
|
4438
|
+
return `no changes, ${result.afterIssues} issue${result.afterIssues === 1 ? "" : "s"}, ${formatElapsed(result.elapsedMs)}`;
|
|
4439
|
+
};
|
|
4440
|
+
const getStatusParts = (state, frameIndex) => {
|
|
4441
|
+
if (state.status === "running") return {
|
|
4442
|
+
icon: highlighter.info(SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length]),
|
|
4443
|
+
detail: highlighter.info("Running")
|
|
4444
|
+
};
|
|
4445
|
+
if (state.status === "done" && state.result) return {
|
|
4446
|
+
icon: state.result.afterIssues > 0 ? highlighter.warn("!") : highlighter.success("✓"),
|
|
4447
|
+
detail: highlighter.dim(`Done (${getStepSummary(state.result)})`)
|
|
4448
|
+
};
|
|
4449
|
+
if (state.status === "failed" && state.result) return {
|
|
4450
|
+
icon: highlighter.error("✗"),
|
|
4451
|
+
detail: highlighter.dim(`Failed (${getStepSummary(state.result)})`)
|
|
4452
|
+
};
|
|
4453
|
+
return {
|
|
4454
|
+
icon: highlighter.dim("○"),
|
|
4455
|
+
detail: highlighter.dim("Waiting")
|
|
4456
|
+
};
|
|
4457
|
+
};
|
|
4458
|
+
const clearRenderedLines$1 = (lineCount) => {
|
|
4459
|
+
if (lineCount === 0) return;
|
|
4460
|
+
process.stderr.write(`\u001B[${lineCount}F`);
|
|
4461
|
+
for (let index = 0; index < lineCount; index += 1) {
|
|
4462
|
+
process.stderr.write("\x1B[2K");
|
|
4463
|
+
if (index < lineCount - 1) process.stderr.write("\x1B[1E");
|
|
4464
|
+
}
|
|
4465
|
+
if (lineCount > 1) process.stderr.write(`\u001B[${lineCount - 1}F`);
|
|
4466
|
+
};
|
|
4467
|
+
const renderFixProgressBlock = (states, frameIndex) => {
|
|
4468
|
+
if (states.length === 0) return ` ${highlighter.bold("Fixes 0/0")} ${highlighter.dim("nothing to run")}\n`;
|
|
4469
|
+
const completedCount = states.filter((s) => s.status === "done" || s.status === "failed").length;
|
|
4470
|
+
const runningCount = states.filter((s) => s.status === "running").length;
|
|
4471
|
+
const labelWidth = Math.max(...states.map((s) => s.name.length));
|
|
4472
|
+
const headingStatus = completedCount === states.length ? highlighter.dim("complete") : runningCount > 0 ? highlighter.dim(`${runningCount} running`) : highlighter.dim("starting");
|
|
4473
|
+
return `${[` ${highlighter.bold(`Fixes ${completedCount}/${states.length}`)} ${headingStatus}`, ...states.map((state) => {
|
|
4474
|
+
const label = state.name.padEnd(labelWidth, " ");
|
|
4475
|
+
const { icon, detail } = getStatusParts(state, frameIndex);
|
|
4476
|
+
return ` ${icon} ${label} ${detail}`;
|
|
4477
|
+
})].join("\n")}\n`;
|
|
4478
|
+
};
|
|
4479
|
+
var FixProgressRenderer = class {
|
|
4480
|
+
states;
|
|
4481
|
+
previousLineCount = 0;
|
|
4482
|
+
frameIndex = 0;
|
|
4483
|
+
timer;
|
|
4484
|
+
live;
|
|
4485
|
+
constructor(stepNames) {
|
|
4486
|
+
this.states = stepNames.map((name) => ({
|
|
4487
|
+
name,
|
|
4488
|
+
status: "pending"
|
|
4489
|
+
}));
|
|
4490
|
+
this.live = shouldRenderLive();
|
|
4491
|
+
}
|
|
4492
|
+
isLive() {
|
|
4493
|
+
return this.live;
|
|
4494
|
+
}
|
|
4495
|
+
start() {
|
|
4496
|
+
if (!this.live) return;
|
|
4497
|
+
this.render();
|
|
4498
|
+
this.timer = setInterval(() => {
|
|
4499
|
+
this.frameIndex += 1;
|
|
4500
|
+
this.render();
|
|
4501
|
+
}, 100);
|
|
4502
|
+
this.timer.unref();
|
|
4503
|
+
}
|
|
4504
|
+
markStarted(name) {
|
|
4505
|
+
const state = this.states.find((s) => s.name === name);
|
|
4506
|
+
if (!state) return;
|
|
4507
|
+
state.status = "running";
|
|
4508
|
+
this.render();
|
|
4509
|
+
}
|
|
4510
|
+
markComplete(name, result) {
|
|
4511
|
+
const state = this.states.find((s) => s.name === name);
|
|
4512
|
+
if (!state) return;
|
|
4513
|
+
state.status = result.failed ? "failed" : "done";
|
|
4514
|
+
state.result = result;
|
|
4515
|
+
this.render();
|
|
4516
|
+
}
|
|
4517
|
+
stop() {
|
|
4518
|
+
if (this.timer) {
|
|
4519
|
+
clearInterval(this.timer);
|
|
4520
|
+
this.timer = void 0;
|
|
4521
|
+
}
|
|
4522
|
+
if (!this.live) return;
|
|
4523
|
+
this.render();
|
|
4524
|
+
}
|
|
4525
|
+
render() {
|
|
4526
|
+
if (!this.live) return;
|
|
4527
|
+
if (this.previousLineCount > 0) clearRenderedLines$1(this.previousLineCount);
|
|
4528
|
+
const output = renderFixProgressBlock(this.states, this.frameIndex);
|
|
4529
|
+
process.stderr.write(output);
|
|
4530
|
+
this.previousLineCount = output.split("\n").length - 1;
|
|
4531
|
+
}
|
|
4532
|
+
};
|
|
4533
|
+
|
|
4534
|
+
//#endregion
|
|
4535
|
+
//#region src/commands/fix-force.ts
|
|
4536
|
+
const getJsAuditFixCommand = (rootDirectory) => {
|
|
4537
|
+
if (fs.existsSync(path.join(rootDirectory, "pnpm-lock.yaml"))) return {
|
|
4538
|
+
command: "pnpm",
|
|
4539
|
+
args: ["audit", "--fix"]
|
|
4540
|
+
};
|
|
4541
|
+
if (fs.existsSync(path.join(rootDirectory, "package-lock.json")) || fs.existsSync(path.join(rootDirectory, "package.json"))) return {
|
|
4542
|
+
command: "npm",
|
|
4543
|
+
args: ["audit", "fix"]
|
|
4544
|
+
};
|
|
4545
|
+
return null;
|
|
4546
|
+
};
|
|
4547
|
+
const fixDependencyAudit = async (context) => {
|
|
4548
|
+
const auditFix = getJsAuditFixCommand(context.rootDirectory);
|
|
4549
|
+
if (!auditFix) return;
|
|
4550
|
+
const result = await runSubprocess(auditFix.command, auditFix.args, {
|
|
4551
|
+
cwd: context.rootDirectory,
|
|
4552
|
+
timeout: 18e4
|
|
4553
|
+
});
|
|
4554
|
+
if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `${auditFix.command} audit fix failed`);
|
|
4555
|
+
};
|
|
4556
|
+
const fixExpoDependencies = async (context) => {
|
|
4557
|
+
if ((await runSubprocess("npx", [
|
|
4558
|
+
"--yes",
|
|
4559
|
+
"expo",
|
|
4560
|
+
"install",
|
|
4561
|
+
"--fix"
|
|
4562
|
+
], {
|
|
4563
|
+
cwd: context.rootDirectory,
|
|
4564
|
+
timeout: 18e4
|
|
4565
|
+
})).exitCode === 0) return;
|
|
4566
|
+
const checkResult = await runSubprocess("npx", [
|
|
4567
|
+
"--yes",
|
|
4568
|
+
"expo",
|
|
4569
|
+
"install",
|
|
4570
|
+
"--check"
|
|
4571
|
+
], {
|
|
4572
|
+
cwd: context.rootDirectory,
|
|
4573
|
+
timeout: 18e4
|
|
4574
|
+
});
|
|
4575
|
+
if (checkResult.exitCode !== 0) throw new Error(checkResult.stderr || checkResult.stdout || "expo dependency check failed");
|
|
4576
|
+
};
|
|
4577
|
+
|
|
4578
|
+
//#endregion
|
|
4579
|
+
//#region src/commands/fix-step.ts
|
|
4042
4580
|
const uniqueFiles = (diagnostics) => [...new Set(diagnostics.map((d) => d.filePath))];
|
|
4043
4581
|
const uniqueFileCount = (diagnostics) => uniqueFiles(diagnostics).length;
|
|
4044
4582
|
const getFilePreviewLines = (title, files, verbose) => {
|
|
@@ -4062,7 +4600,8 @@ const getStepStatusLine = (result, name, elapsedLabel) => {
|
|
|
4062
4600
|
if (result.resolvedIssues > 0) return highlighter.warn(` ! ${name}: done (${result.resolvedIssues} resolved, ${result.afterIssues} remaining, ${elapsedLabel})`);
|
|
4063
4601
|
return highlighter.warn(` ! ${name}: done (no auto-fix changes, ${result.afterIssues} issue${result.afterIssues === 1 ? "" : "s"}, ${elapsedLabel})`);
|
|
4064
4602
|
};
|
|
4065
|
-
const runFixStep = async (name, detect, applyFix, options) => {
|
|
4603
|
+
const runFixStep = async (name, detect, applyFix, options, progress) => {
|
|
4604
|
+
progress.markStarted(name);
|
|
4066
4605
|
const stepStart = performance.now();
|
|
4067
4606
|
const before = await detect();
|
|
4068
4607
|
let applyError = null;
|
|
@@ -4082,28 +4621,21 @@ const runFixStep = async (name, detect, applyFix, options) => {
|
|
|
4082
4621
|
failed: applyError !== null && before.length === after.length,
|
|
4083
4622
|
elapsedMs
|
|
4084
4623
|
};
|
|
4085
|
-
|
|
4086
|
-
if (
|
|
4087
|
-
const
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4624
|
+
progress.markComplete(name, result);
|
|
4625
|
+
if (!progress.isLive()) {
|
|
4626
|
+
const lines = [getStepStatusLine(result, name, formatElapsed$2(result.elapsedMs))];
|
|
4627
|
+
if (applyError) {
|
|
4628
|
+
const reasonLines = getReasonLines(applyError instanceof Error ? applyError.message : String(applyError));
|
|
4629
|
+
const reasonToPrint = options.verbose ? reasonLines.printable : reasonLines.firstLine;
|
|
4630
|
+
for (const line of reasonToPrint.split("\n")) lines.push(highlighter.dim(` ${line}`));
|
|
4631
|
+
if (!options.verbose && reasonLines.printable !== reasonToPrint) lines.push(highlighter.dim(" Re-run with -d for full tool output."));
|
|
4632
|
+
}
|
|
4633
|
+
lines.push(...getFilePreviewLines("Affected", uniqueFiles(before), options.verbose));
|
|
4634
|
+
if (after.length > 0) lines.push(...getFilePreviewLines("Remaining", uniqueFiles(after), options.verbose));
|
|
4635
|
+
process.stdout.write(`${lines.join("\n")}\n\n`);
|
|
4636
|
+
}
|
|
4095
4637
|
return result;
|
|
4096
4638
|
};
|
|
4097
|
-
const createEngineContext = (rootDirectory, projectInfo, config) => ({
|
|
4098
|
-
rootDirectory,
|
|
4099
|
-
languages: projectInfo.languages,
|
|
4100
|
-
frameworks: projectInfo.frameworks,
|
|
4101
|
-
installedTools: projectInfo.installedTools,
|
|
4102
|
-
config: {
|
|
4103
|
-
quality: config.quality,
|
|
4104
|
-
security: config.security
|
|
4105
|
-
}
|
|
4106
|
-
});
|
|
4107
4639
|
const summarizeFixRun = (steps) => {
|
|
4108
4640
|
const totals = steps.reduce((acc, step) => {
|
|
4109
4641
|
acc.beforeIssues += step.beforeIssues;
|
|
@@ -4123,6 +4655,19 @@ const summarizeFixRun = (steps) => {
|
|
|
4123
4655
|
} else logger.log(` Fix summary: checked ${steps.length} step(s), resolved ${totals.resolvedIssues} issue(s), remaining ${totals.afterIssues}.`);
|
|
4124
4656
|
if (totals.failedSteps === 0 && totals.beforeIssues > 0 && totals.resolvedIssues === 0) logger.dim(" No auto-fixable changes were applied. Current findings are likely manual-fix categories.");
|
|
4125
4657
|
};
|
|
4658
|
+
|
|
4659
|
+
//#endregion
|
|
4660
|
+
//#region src/commands/fix.ts
|
|
4661
|
+
const createEngineContext = (rootDirectory, projectInfo, config) => ({
|
|
4662
|
+
rootDirectory,
|
|
4663
|
+
languages: projectInfo.languages,
|
|
4664
|
+
frameworks: projectInfo.frameworks,
|
|
4665
|
+
installedTools: projectInfo.installedTools,
|
|
4666
|
+
config: {
|
|
4667
|
+
quality: config.quality,
|
|
4668
|
+
security: config.security
|
|
4669
|
+
}
|
|
4670
|
+
});
|
|
4126
4671
|
const fixCommand = async (directory, config, options = {
|
|
4127
4672
|
verbose: false,
|
|
4128
4673
|
showHeader: true
|
|
@@ -4134,37 +4679,98 @@ const fixCommand = async (directory, config, options = {
|
|
|
4134
4679
|
printProjectMetadata(projectInfo);
|
|
4135
4680
|
const context = createEngineContext(resolvedDir, projectInfo, config);
|
|
4136
4681
|
const steps = [];
|
|
4682
|
+
const stepNames = [];
|
|
4683
|
+
if (config.engines["ai-slop"]) {
|
|
4684
|
+
stepNames.push("Unused imports");
|
|
4685
|
+
stepNames.push("Dead code & comments");
|
|
4686
|
+
}
|
|
4687
|
+
if (config.engines.lint) {
|
|
4688
|
+
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) stepNames.push("JS/TS lint fixes");
|
|
4689
|
+
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) stepNames.push("Python lint fixes");
|
|
4690
|
+
}
|
|
4691
|
+
if (config.engines["code-quality"]) {
|
|
4692
|
+
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) stepNames.push("Unused dependencies");
|
|
4693
|
+
}
|
|
4137
4694
|
if (config.engines.format) {
|
|
4138
|
-
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript"))
|
|
4139
|
-
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff)
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4695
|
+
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) stepNames.push("JS/TS formatting");
|
|
4696
|
+
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) stepNames.push("Python formatting");
|
|
4697
|
+
if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) stepNames.push("Go formatting");
|
|
4698
|
+
}
|
|
4699
|
+
if (options.force) {
|
|
4700
|
+
if (config.engines.security) stepNames.push("Dependency audit fixes");
|
|
4701
|
+
if (projectInfo.frameworks.includes("expo")) stepNames.push("Expo dependency alignment");
|
|
4702
|
+
}
|
|
4703
|
+
const progress = new FixProgressRenderer(stepNames);
|
|
4704
|
+
progress.start();
|
|
4705
|
+
if (config.engines["ai-slop"]) {
|
|
4706
|
+
steps.push(await runFixStep("Unused imports", () => detectUnusedImports(context), () => fixUnusedImports(context), options, progress));
|
|
4707
|
+
const detectFixableSlop = async () => {
|
|
4708
|
+
const [comments, dead] = await Promise.all([detectTrivialComments(context), detectDeadPatterns(context)]);
|
|
4709
|
+
return [...comments, ...dead].filter((d) => d.fixable);
|
|
4710
|
+
};
|
|
4711
|
+
steps.push(await runFixStep("Dead code & comments", detectFixableSlop, () => fixDeadPatterns(context), options, progress));
|
|
4143
4712
|
}
|
|
4144
4713
|
if (config.engines.lint) {
|
|
4145
|
-
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS lint fixes", () => runOxlint(context), () => fixOxlint(context), options));
|
|
4146
|
-
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => fixRuffLint(resolvedDir), options));
|
|
4714
|
+
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS lint fixes", () => runOxlint(context), () => options.force ? fixOxlintForce(context) : fixOxlint(context), options, progress));
|
|
4715
|
+
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => options.force ? fixRuffLintForce(resolvedDir) : fixRuffLint(resolvedDir), options, progress));
|
|
4147
4716
|
else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python lint fixes.");
|
|
4148
4717
|
}
|
|
4149
4718
|
if (config.engines["code-quality"]) {
|
|
4150
|
-
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("Unused dependencies", () => runKnipDependencyCheck(resolvedDir), () => fixUnusedDependencies(resolvedDir), options));
|
|
4719
|
+
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("Unused dependencies", () => runKnipDependencyCheck(resolvedDir), () => fixUnusedDependencies(resolvedDir), options, progress));
|
|
4720
|
+
}
|
|
4721
|
+
if (config.engines.format) {
|
|
4722
|
+
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS formatting", () => runBiomeFormat(context), () => fixBiomeFormat(context), options, progress));
|
|
4723
|
+
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python formatting", () => runRuffFormat(context), () => fixRuffFormat(resolvedDir), options, progress));
|
|
4724
|
+
else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python formatting fixes.");
|
|
4725
|
+
if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) steps.push(await runFixStep("Go formatting", () => runGofmt(context), () => fixGofmt(resolvedDir), options, progress));
|
|
4726
|
+
else if (projectInfo.languages.includes("go")) logger.warn(" Go detected but gofmt is not installed; skipping Go formatting fixes.");
|
|
4151
4727
|
}
|
|
4728
|
+
if (options.force) {
|
|
4729
|
+
if (config.engines.security) steps.push(await runFixStep("Dependency audit fixes", () => runDependencyAudit(context), () => fixDependencyAudit(context), options, progress));
|
|
4730
|
+
if (projectInfo.frameworks.includes("expo")) steps.push(await runFixStep("Expo dependency alignment", () => runExpoDoctor(context), () => fixExpoDependencies(context), options, progress));
|
|
4731
|
+
}
|
|
4732
|
+
progress.stop();
|
|
4733
|
+
const totalResolved = steps.reduce((sum, s) => sum + s.resolvedIssues, 0);
|
|
4152
4734
|
if (steps.length === 0) logger.dim(" No applicable auto-fixers found for this project.");
|
|
4153
4735
|
else {
|
|
4154
4736
|
logger.break();
|
|
4155
4737
|
summarizeFixRun(steps);
|
|
4156
4738
|
}
|
|
4157
|
-
if (!isTelemetryDisabled(config.telemetry?.enabled)) {
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
fixResolved: totalResolved
|
|
4164
|
-
});
|
|
4165
|
-
}
|
|
4739
|
+
if (!isTelemetryDisabled(config.telemetry?.enabled)) trackEvent({
|
|
4740
|
+
command: "fix",
|
|
4741
|
+
languages: projectInfo.languages,
|
|
4742
|
+
fixSteps: steps.length,
|
|
4743
|
+
fixResolved: totalResolved
|
|
4744
|
+
});
|
|
4166
4745
|
logger.break();
|
|
4167
|
-
|
|
4746
|
+
const configDir = findConfigDir(resolvedDir);
|
|
4747
|
+
const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
|
|
4748
|
+
const engineConfig = {
|
|
4749
|
+
quality: config.quality,
|
|
4750
|
+
security: config.security,
|
|
4751
|
+
architectureRulesPath: config.engines.architecture ? rulesPath : void 0
|
|
4752
|
+
};
|
|
4753
|
+
const allDiagnostics = (await runEngines({
|
|
4754
|
+
rootDirectory: resolvedDir,
|
|
4755
|
+
languages: projectInfo.languages,
|
|
4756
|
+
frameworks: projectInfo.frameworks,
|
|
4757
|
+
installedTools: projectInfo.installedTools,
|
|
4758
|
+
config: engineConfig
|
|
4759
|
+
}, config.engines, () => {}, () => {})).flatMap((r) => r.diagnostics);
|
|
4760
|
+
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
|
|
4761
|
+
const errors = allDiagnostics.filter((d) => d.severity === "error").length;
|
|
4762
|
+
const warnings = allDiagnostics.filter((d) => d.severity === "warning").length;
|
|
4763
|
+
const fixable = allDiagnostics.filter((d) => d.fixable).length;
|
|
4764
|
+
const manual = errors + warnings - fixable;
|
|
4765
|
+
const scoreColor = scoreResult.score >= config.scoring.thresholds.good ? highlighter.success : scoreResult.score >= config.scoring.thresholds.ok ? highlighter.warn : highlighter.error;
|
|
4766
|
+
logger.log(highlighter.dim("------------------------------------------------------------"));
|
|
4767
|
+
logger.log(highlighter.bold("Result"));
|
|
4768
|
+
logger.log(` Score: ${scoreColor(`${scoreResult.score}/100`)} ${scoreColor(`(${scoreResult.label})`)}`);
|
|
4769
|
+
logger.log(` Resolved: ${highlighter.success(String(totalResolved))} issue${totalResolved === 1 ? "" : "s"}`);
|
|
4770
|
+
logger.log(` Remaining: ${errors + warnings > 0 ? highlighter.warn(String(errors + warnings)) : highlighter.success("0")} (${errors} error${errors === 1 ? "" : "s"}, ${warnings} warning${warnings === 1 ? "" : "s"})`);
|
|
4771
|
+
if (fixable > 0) logger.log(` Auto-fixable: ${highlighter.info(String(fixable))}`);
|
|
4772
|
+
if (manual > 0) logger.log(` Manual effort: ${highlighter.dim(String(manual))}`);
|
|
4773
|
+
logger.log(highlighter.dim("------------------------------------------------------------"));
|
|
4168
4774
|
logger.break();
|
|
4169
4775
|
};
|
|
4170
4776
|
|
|
@@ -4289,7 +4895,7 @@ const rulesCommand = async (directory) => {
|
|
|
4289
4895
|
lines.push("");
|
|
4290
4896
|
}
|
|
4291
4897
|
}
|
|
4292
|
-
|
|
4898
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
4293
4899
|
};
|
|
4294
4900
|
|
|
4295
4901
|
//#endregion
|
|
@@ -4525,7 +5131,7 @@ const program = new Command().name("aislop").description("The unified code quali
|
|
|
4525
5131
|
}).addHelpText("after", `
|
|
4526
5132
|
${highlighter.dim("Commands:")}
|
|
4527
5133
|
aislop scan [dir] Full code quality scan
|
|
4528
|
-
aislop fix [dir] Auto-fix
|
|
5134
|
+
aislop fix [dir] Auto-fix ai slop in codebase
|
|
4529
5135
|
aislop init [dir] Initialize aislop config
|
|
4530
5136
|
aislop doctor [dir] Check installed tools
|
|
4531
5137
|
aislop ci [dir] CI-friendly JSON output
|
|
@@ -4537,7 +5143,8 @@ ${highlighter.dim("Examples:")}
|
|
|
4537
5143
|
aislop scan -d Scan with file/line details
|
|
4538
5144
|
aislop scan --changes Scan only changed files
|
|
4539
5145
|
aislop scan --staged Scan only staged files (for hooks)
|
|
4540
|
-
aislop fix Auto-fix
|
|
5146
|
+
aislop fix Auto-fix ai slop in codebase
|
|
5147
|
+
aislop fix --force Run aggressive fixes (includes audit and dependency alignment)
|
|
4541
5148
|
aislop ci JSON output for CI pipelines
|
|
4542
5149
|
`);
|
|
4543
5150
|
program.command("scan [directory]").description("Run full code quality scan").option("--changes", "only scan changed files").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON").action(async (directory = ".", _flags, command) => {
|
|
@@ -4553,9 +5160,12 @@ program.command("scan [directory]").description("Run full code quality scan").op
|
|
|
4553
5160
|
process.exit(exitCode);
|
|
4554
5161
|
}
|
|
4555
5162
|
});
|
|
4556
|
-
program.command("fix [directory]").description("Auto-fix
|
|
5163
|
+
program.command("fix [directory]").description("Auto-fix ai slop in codebase").option("-d, --verbose", "show detailed fix progress").option("-f, --force", "run aggressive fixes (audit and framework dependency alignment)").action(async (directory = ".", _flags, command) => {
|
|
4557
5164
|
const flags = command.optsWithGlobals();
|
|
4558
|
-
await fixCommand(directory, loadConfig(directory), {
|
|
5165
|
+
await fixCommand(directory, loadConfig(directory), {
|
|
5166
|
+
verbose: Boolean(flags.verbose),
|
|
5167
|
+
force: Boolean(flags.force)
|
|
5168
|
+
});
|
|
4559
5169
|
});
|
|
4560
5170
|
program.command("init [directory]").description("Initialize aislop config in project").action(async (directory = ".") => {
|
|
4561
5171
|
await initCommand(directory);
|
|
@@ -4580,4 +5190,4 @@ const main = async () => {
|
|
|
4580
5190
|
main();
|
|
4581
5191
|
|
|
4582
5192
|
//#endregion
|
|
4583
|
-
export { ENGINE_INFO as n,
|
|
5193
|
+
export { ENGINE_INFO as n, APP_VERSION as t };
|