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/README.md
CHANGED
|
@@ -10,25 +10,87 @@
|
|
|
10
10
|
|
|
11
11
|
`aislop` is a unified code-quality CLI that catches the lazy patterns AI coding tools leave behind. One command, one score out of 100.
|
|
12
12
|
|
|
13
|
+
`aislop` helps teams review AI-assisted code faster by combining formatting, linting, maintainability, AI-pattern detection, architecture checks, and security checks into a single report.
|
|
14
|
+
|
|
15
|
+
## See it in action
|
|
16
|
+
|
|
17
|
+
### Scan
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
### Fix
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# scan your project
|
|
29
|
+
npx aislop scan
|
|
30
|
+
|
|
31
|
+
# auto-fix what can be fixed safely
|
|
32
|
+
npx aislop fix
|
|
33
|
+
|
|
34
|
+
# CI mode (JSON output + quality gate)
|
|
35
|
+
npx aislop ci
|
|
13
36
|
```
|
|
14
|
-
$ npx aislop scan
|
|
15
37
|
|
|
16
|
-
|
|
38
|
+
Sample output:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
aislop scan v0.2.0
|
|
17
42
|
|
|
18
43
|
✓ Project my-app (typescript)
|
|
19
44
|
Source files: 142
|
|
20
45
|
|
|
21
|
-
✓ Formatting:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
✓ Security:
|
|
26
|
-
|
|
27
|
-
|
|
46
|
+
✓ Formatting: done (0 issues)
|
|
47
|
+
! Linting: done (2 warnings)
|
|
48
|
+
! Code Quality: done (1 warning)
|
|
49
|
+
✓ Maintainability: done (0 issues)
|
|
50
|
+
✓ Security: done (0 issues)
|
|
51
|
+
|
|
52
|
+
------------------------------------------------------------
|
|
53
|
+
Summary
|
|
54
|
+
Score: 89/100 (Healthy)
|
|
55
|
+
Issues: 0 errors, 3 warnings
|
|
56
|
+
Auto-fixable: 2
|
|
57
|
+
Files: 142
|
|
58
|
+
Time: 2.3s
|
|
59
|
+
------------------------------------------------------------
|
|
28
60
|
```
|
|
29
61
|
|
|
30
62
|
---
|
|
31
63
|
|
|
64
|
+
## Why aislop
|
|
65
|
+
|
|
66
|
+
AI-generated changes often pass review because problems are spread across many files and many categories.
|
|
67
|
+
`aislop` gives you one view and one score.
|
|
68
|
+
|
|
69
|
+
- **One command, full picture**: formatting + lint + maintainability + AI slop + security (+ architecture)
|
|
70
|
+
- **Score-based quality gate**: use a single 0-100 score in CI and PR checks
|
|
71
|
+
- **Auto-fix support**: remove unused imports, apply lint suggestions, and format in one pass
|
|
72
|
+
- **Duplication visibility**: flag repeated blocks and encourage extraction into shared modules
|
|
73
|
+
- **Software engineering best practices**: enforce function/file size limits, nesting limits, dead code cleanup, and safer patterns
|
|
74
|
+
- **Works across stacks**: TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP, Expo/React Native
|
|
75
|
+
- **Zero-config start**: run `npx aislop scan` and get useful output immediately
|
|
76
|
+
|
|
77
|
+
## What it catches
|
|
78
|
+
|
|
79
|
+
Six engines run in parallel: **Formatting**, **Linting**, **Code Quality**, **AI Slop Detection**, **Security**, and **Architecture** (opt-in).
|
|
80
|
+
|
|
81
|
+
| Engine | Examples |
|
|
82
|
+
|---|---|
|
|
83
|
+
| Formatting | Biome, ruff, gofmt, cargo fmt, rubocop, php-cs-fixer |
|
|
84
|
+
| Linting | oxlint, ruff, golangci-lint, clippy, expo-doctor |
|
|
85
|
+
| Code Quality | Function/file size limits, deep nesting, duplication, dead code, unused dependencies (knip) |
|
|
86
|
+
| AI Slop | Trivial comments, swallowed exceptions, unused imports, console leftovers, type assertion abuse, TODO stubs |
|
|
87
|
+
| Security | Hardcoded secrets, eval, innerHTML, SQL/shell injection, dependency audits |
|
|
88
|
+
| Architecture | Custom import bans, layering rules, required patterns |
|
|
89
|
+
|
|
90
|
+
See the full [rules reference](docs/rules.md).
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
32
94
|
## Installation
|
|
33
95
|
|
|
34
96
|
```bash
|
|
@@ -67,7 +129,7 @@ aislop scan --json # output JSON
|
|
|
67
129
|
### Fix issues automatically
|
|
68
130
|
|
|
69
131
|
```bash
|
|
70
|
-
aislop fix # auto-fix formatting
|
|
132
|
+
aislop fix # auto-fix unused imports, formatting, and lint fixes
|
|
71
133
|
```
|
|
72
134
|
|
|
73
135
|
### Use in CI pipelines
|
|
@@ -76,6 +138,19 @@ aislop fix # auto-fix formatting + lint issues
|
|
|
76
138
|
aislop ci # JSON output, exits 1 if score < threshold
|
|
77
139
|
```
|
|
78
140
|
|
|
141
|
+
### Common workflow
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# before commit
|
|
145
|
+
aislop scan --staged
|
|
146
|
+
|
|
147
|
+
# during local cleanup
|
|
148
|
+
aislop fix
|
|
149
|
+
|
|
150
|
+
# full project check
|
|
151
|
+
aislop scan
|
|
152
|
+
```
|
|
153
|
+
|
|
79
154
|
### Other commands
|
|
80
155
|
|
|
81
156
|
```bash
|
|
@@ -103,6 +178,7 @@ npx aislop scan --staged
|
|
|
103
178
|
- uses: actions/setup-node@v6
|
|
104
179
|
with:
|
|
105
180
|
node-version: 20
|
|
181
|
+
- run: npm ci
|
|
106
182
|
- run: npx aislop ci
|
|
107
183
|
```
|
|
108
184
|
|
|
@@ -119,34 +195,6 @@ ci:
|
|
|
119
195
|
|
|
120
196
|
---
|
|
121
197
|
|
|
122
|
-
## Why aislop?
|
|
123
|
-
|
|
124
|
-
AI-generated code passes review because issues are spread across dozens of files. No single linter catches all of them. `aislop` does:
|
|
125
|
-
|
|
126
|
-
- **AI-specific pattern detection** — trivial comments, thin wrappers, generic names, swallowed exceptions, `as any` casts
|
|
127
|
-
- **Multi-language** — TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP, Expo/React Native
|
|
128
|
-
- **Single score** — one number to gate PRs, track in CI, and trend over time
|
|
129
|
-
- **Zero config** — run `npx aislop scan` and get results immediately
|
|
130
|
-
- **Framework-aware** — auto-detects Next.js, React, Expo, Vite, Remix, Django, Flask, FastAPI
|
|
131
|
-
- **Batteries included** — ships with oxlint, biome, knip; downloads ruff and golangci-lint on install
|
|
132
|
-
|
|
133
|
-
## What it catches
|
|
134
|
-
|
|
135
|
-
Six engines run in parallel: **Formatting**, **Linting**, **Code Quality**, **AI Slop Detection**, **Security**, and **Architecture** (opt-in).
|
|
136
|
-
|
|
137
|
-
| Engine | Examples |
|
|
138
|
-
|---|---|
|
|
139
|
-
| Formatting | Biome, ruff, gofmt, cargo fmt, rubocop, php-cs-fixer |
|
|
140
|
-
| Linting | oxlint, ruff, golangci-lint, clippy, expo-doctor |
|
|
141
|
-
| Code Quality | Function/file size limits, deep nesting, duplication, dead code, unused dependencies (knip) |
|
|
142
|
-
| AI Slop | Trivial comments, swallowed exceptions, unused imports, console leftovers, type assertion abuse, TODO stubs |
|
|
143
|
-
| Security | Hardcoded secrets, eval, innerHTML, SQL/shell injection, dependency audits |
|
|
144
|
-
| Architecture | Custom import bans, layering rules, required patterns |
|
|
145
|
-
|
|
146
|
-
See the full [rules reference](docs/rules.md) for all 30+ built-in rules.
|
|
147
|
-
|
|
148
|
-
---
|
|
149
|
-
|
|
150
198
|
## Documentation
|
|
151
199
|
|
|
152
200
|
| Topic | Link |
|
package/dist/cli.js
CHANGED
|
@@ -872,6 +872,7 @@ const JS_EXTENSIONS = new Set([
|
|
|
872
872
|
".cjs"
|
|
873
873
|
]);
|
|
874
874
|
const PY_EXTENSIONS = new Set([".py"]);
|
|
875
|
+
const REMOVE_MARKER = "\0__AISLOP_REMOVE__";
|
|
875
876
|
const extractJsImportedSymbols = (lines) => {
|
|
876
877
|
const symbols = [];
|
|
877
878
|
const importLines = /* @__PURE__ */ new Set();
|
|
@@ -980,32 +981,47 @@ const isSymbolUsed = (name, content, importLines, lines) => {
|
|
|
980
981
|
}
|
|
981
982
|
return false;
|
|
982
983
|
};
|
|
984
|
+
const analyzeFile = (filePath) => {
|
|
985
|
+
if (isAutoGenerated(filePath)) return null;
|
|
986
|
+
let content;
|
|
987
|
+
try {
|
|
988
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
989
|
+
} catch {
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
const ext = path.extname(filePath);
|
|
993
|
+
const lines = content.split("\n");
|
|
994
|
+
let symbols;
|
|
995
|
+
let importLines;
|
|
996
|
+
if (JS_EXTENSIONS.has(ext)) {
|
|
997
|
+
const result = extractJsImportedSymbols(lines);
|
|
998
|
+
symbols = result.symbols;
|
|
999
|
+
importLines = result.importLines;
|
|
1000
|
+
} else if (PY_EXTENSIONS.has(ext)) {
|
|
1001
|
+
const result = extractPyImportedSymbols(lines);
|
|
1002
|
+
symbols = result.symbols;
|
|
1003
|
+
importLines = result.importLines;
|
|
1004
|
+
} else return null;
|
|
1005
|
+
return {
|
|
1006
|
+
lines,
|
|
1007
|
+
symbols,
|
|
1008
|
+
importLines,
|
|
1009
|
+
ext
|
|
1010
|
+
};
|
|
1011
|
+
};
|
|
1012
|
+
const getUnusedSymbols = (lines, symbols, importLines) => {
|
|
1013
|
+
const content = lines.join("\n");
|
|
1014
|
+
return symbols.filter((symbol) => !isSymbolUsed(symbol.name, content, importLines, lines));
|
|
1015
|
+
};
|
|
983
1016
|
const detectUnusedImports = async (context) => {
|
|
984
1017
|
const files = getSourceFiles(context);
|
|
985
1018
|
const diagnostics = [];
|
|
986
1019
|
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);
|
|
1020
|
+
const analysis = analyzeFile(filePath);
|
|
1021
|
+
if (!analysis) continue;
|
|
995
1022
|
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({
|
|
1023
|
+
const unused = getUnusedSymbols(analysis.lines, analysis.symbols, analysis.importLines);
|
|
1024
|
+
for (const symbol of unused) diagnostics.push({
|
|
1009
1025
|
filePath: relativePath,
|
|
1010
1026
|
engine: "ai-slop",
|
|
1011
1027
|
rule: "ai-slop/unused-import",
|
|
@@ -1922,14 +1938,11 @@ const parseBiomeJsonOutput = (output, rootDir) => {
|
|
|
1922
1938
|
const fixBiomeFormat = async (context) => {
|
|
1923
1939
|
const targets = getBiomeTargets(context);
|
|
1924
1940
|
if (targets.length === 0) return;
|
|
1925
|
-
|
|
1926
|
-
"
|
|
1941
|
+
await runBiome([
|
|
1942
|
+
"format",
|
|
1927
1943
|
"--write",
|
|
1928
|
-
"--formatter-enabled=true",
|
|
1929
|
-
"--linter-enabled=false",
|
|
1930
1944
|
...targets
|
|
1931
1945
|
], context.rootDirectory, 6e4);
|
|
1932
|
-
if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `Biome exited with code ${result.exitCode}`);
|
|
1933
1946
|
};
|
|
1934
1947
|
|
|
1935
1948
|
//#endregion
|
|
@@ -2492,6 +2505,7 @@ const fixOxlint = async (context) => {
|
|
|
2492
2505
|
"-c",
|
|
2493
2506
|
configPath,
|
|
2494
2507
|
"--fix",
|
|
2508
|
+
"--fix-suggestions",
|
|
2495
2509
|
"."
|
|
2496
2510
|
];
|
|
2497
2511
|
const result = await runSubprocess(process.execPath, args, {
|
|
@@ -3180,14 +3194,13 @@ const logger = {
|
|
|
3180
3194
|
* Application version — injected at build time by tsdown from package.json.
|
|
3181
3195
|
* The fallback should always match the "version" field in package.json.
|
|
3182
3196
|
*/
|
|
3183
|
-
const APP_VERSION = "0.2.
|
|
3197
|
+
const APP_VERSION = "0.2.1";
|
|
3184
3198
|
|
|
3185
3199
|
//#endregion
|
|
3186
3200
|
//#region src/output/layout.ts
|
|
3187
3201
|
const formatElapsed$1 = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
|
|
3188
3202
|
const printCommandHeader = (commandName) => {
|
|
3189
|
-
logger.log(highlighter.bold(`aislop ${commandName}`));
|
|
3190
|
-
logger.log(highlighter.dim(`v${APP_VERSION}`));
|
|
3203
|
+
logger.log(`${highlighter.bold(`aislop ${commandName.toLowerCase()}`)} ${highlighter.dim(`v${APP_VERSION}`)}`);
|
|
3191
3204
|
logger.break();
|
|
3192
3205
|
};
|
|
3193
3206
|
const formatProjectSummary = (project) => `Project ${highlighter.info(project.projectName)} (${highlighter.info(project.languages.join(", "))})`;
|
|
@@ -3198,81 +3211,6 @@ const printProjectMetadata = (project) => {
|
|
|
3198
3211
|
logger.break();
|
|
3199
3212
|
};
|
|
3200
3213
|
|
|
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
3214
|
//#endregion
|
|
3277
3215
|
//#region src/output/scan-progress.ts
|
|
3278
3216
|
const SPINNER_FRAMES = [
|
|
@@ -3882,12 +3820,13 @@ const scanCommand = async (directory, config, options) => {
|
|
|
3882
3820
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
3883
3821
|
return { exitCode };
|
|
3884
3822
|
}
|
|
3885
|
-
|
|
3823
|
+
const output = [
|
|
3886
3824
|
"",
|
|
3887
3825
|
allDiagnostics.length === 0 ? `${highlighter.success(" ✓ No issues found.")}\n` : renderDiagnostics(allDiagnostics, options.verbose),
|
|
3888
3826
|
renderSummary(allDiagnostics, scoreResult, elapsedMs, projectInfo.sourceFileCount, config.scoring.thresholds),
|
|
3889
3827
|
""
|
|
3890
|
-
].join("\n")
|
|
3828
|
+
].join("\n");
|
|
3829
|
+
process.stdout.write(output);
|
|
3891
3830
|
return { exitCode };
|
|
3892
3831
|
};
|
|
3893
3832
|
|
|
@@ -4037,6 +3976,99 @@ const doctorCommand = async (directory) => {
|
|
|
4037
3976
|
printDoctorConclusion(isAllGood());
|
|
4038
3977
|
};
|
|
4039
3978
|
|
|
3979
|
+
//#endregion
|
|
3980
|
+
//#region src/engines/ai-slop/unused-imports-fix.ts
|
|
3981
|
+
const fixUnusedImports = async (context) => {
|
|
3982
|
+
const files = getSourceFiles(context);
|
|
3983
|
+
for (const filePath of files) {
|
|
3984
|
+
const analysis = analyzeFile(filePath);
|
|
3985
|
+
if (!analysis) continue;
|
|
3986
|
+
const unused = getUnusedSymbols(analysis.lines, analysis.symbols, analysis.importLines);
|
|
3987
|
+
if (unused.length === 0) continue;
|
|
3988
|
+
const unusedNames = new Set(unused.map((u) => u.name));
|
|
3989
|
+
const lines = [...analysis.lines];
|
|
3990
|
+
const symbolsByLine = /* @__PURE__ */ new Map();
|
|
3991
|
+
for (const sym of analysis.symbols) {
|
|
3992
|
+
const arr = symbolsByLine.get(sym.line) ?? [];
|
|
3993
|
+
arr.push(sym);
|
|
3994
|
+
symbolsByLine.set(sym.line, arr);
|
|
3995
|
+
}
|
|
3996
|
+
const linesToRemove = /* @__PURE__ */ new Set();
|
|
3997
|
+
for (const [lineNo, syms] of symbolsByLine) {
|
|
3998
|
+
const lineIdx = lineNo - 1;
|
|
3999
|
+
const allUnused = syms.every((s) => unusedNames.has(s.name));
|
|
4000
|
+
const importSpan = getImportSpan(lineIdx, analysis.importLines);
|
|
4001
|
+
if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
|
|
4002
|
+
else if (JS_EXTENSIONS.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
|
|
4003
|
+
else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, syms, unusedNames);
|
|
4004
|
+
}
|
|
4005
|
+
if (linesToRemove.size === 0 && unused.length === 0) continue;
|
|
4006
|
+
const sortedRemove = [...linesToRemove].sort((a, b) => b - a);
|
|
4007
|
+
for (const idx of sortedRemove) lines.splice(idx, 1);
|
|
4008
|
+
const filtered = lines.filter((l) => l !== REMOVE_MARKER);
|
|
4009
|
+
while (filtered.length > 0 && filtered[0].trim() === "") filtered.shift();
|
|
4010
|
+
fs.writeFileSync(filePath, filtered.join("\n"));
|
|
4011
|
+
}
|
|
4012
|
+
};
|
|
4013
|
+
const getImportSpan = (startIdx, importLines) => {
|
|
4014
|
+
const span = [startIdx];
|
|
4015
|
+
let idx = startIdx + 1;
|
|
4016
|
+
while (importLines.has(idx)) {
|
|
4017
|
+
span.push(idx);
|
|
4018
|
+
idx++;
|
|
4019
|
+
}
|
|
4020
|
+
return span;
|
|
4021
|
+
};
|
|
4022
|
+
const rewriteJsImportSpan = (lines, span, syms, unusedNames) => {
|
|
4023
|
+
const fullImport = span.map((i) => lines[i]).join("\n");
|
|
4024
|
+
const namedMatch = fullImport.match(/\{([^}]+)\}/s);
|
|
4025
|
+
if (!namedMatch) return;
|
|
4026
|
+
const unusedNamed = syms.filter((s) => !s.isDefault && !s.isNamespace && unusedNames.has(s.name));
|
|
4027
|
+
if (unusedNamed.length === 0) return;
|
|
4028
|
+
const unusedNamedSet = new Set(unusedNamed.map((s) => s.name));
|
|
4029
|
+
const keptSpecifiers = namedMatch[1].split(",").map((s) => s.trim()).filter(Boolean).filter((spec) => {
|
|
4030
|
+
const parts = spec.split(/\s+as\s+/);
|
|
4031
|
+
const localName = parts.length > 1 ? parts[1].trim().replace(/^type\s+/, "") : parts[0].trim().replace(/^type\s+/, "");
|
|
4032
|
+
return !unusedNamedSet.has(localName);
|
|
4033
|
+
});
|
|
4034
|
+
if (keptSpecifiers.length === 0) {
|
|
4035
|
+
if (syms.find((s) => s.isDefault && !unusedNames.has(s.name))) {
|
|
4036
|
+
const rewritten = fullImport.replace(/,\s*\{[^}]*\}/s, "").replace(/\{[^}]*\}\s*,?\s*/s, "");
|
|
4037
|
+
lines[span[0]] = rewritten.replace(/\n/g, " ").replace(/\s+/g, " ");
|
|
4038
|
+
for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
|
|
4039
|
+
}
|
|
4040
|
+
return;
|
|
4041
|
+
}
|
|
4042
|
+
const fromMatch = fullImport.match(/\}\s*(from\s+.+)$/s);
|
|
4043
|
+
const fromClause = fromMatch ? fromMatch[1].trim() : "";
|
|
4044
|
+
const importPrefix = fullImport.match(/^(import\s+(?:\w+\s*,\s*)?)/);
|
|
4045
|
+
const prefix = importPrefix ? importPrefix[1] : "import ";
|
|
4046
|
+
const wasMultiLine = span.length > 1;
|
|
4047
|
+
let newImport;
|
|
4048
|
+
if (wasMultiLine && keptSpecifiers.length > 2) {
|
|
4049
|
+
const indentMatch = lines[span[1]]?.match(/^(\s+)/);
|
|
4050
|
+
const indent = indentMatch ? indentMatch[1] : " ";
|
|
4051
|
+
newImport = `${prefix}{\n${keptSpecifiers.map((s) => `${indent}${s},`).join("\n")}\n} ${fromClause}`;
|
|
4052
|
+
} else newImport = `${prefix}{ ${keptSpecifiers.join(", ")} } ${fromClause}`;
|
|
4053
|
+
lines[span[0]] = newImport;
|
|
4054
|
+
for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
|
|
4055
|
+
};
|
|
4056
|
+
const rewritePyImportLine = (lines, lineIdx, syms, unusedNames) => {
|
|
4057
|
+
const fromMatch = lines[lineIdx].match(/^(\s*from\s+[\w.]+\s+import\s+)(.+)$/);
|
|
4058
|
+
if (!fromMatch) return;
|
|
4059
|
+
const prefix = fromMatch[1];
|
|
4060
|
+
const importPart = fromMatch[2].replace(/#.*$/, "").trim();
|
|
4061
|
+
const hasParen = importPart.startsWith("(");
|
|
4062
|
+
const keptSpecifiers = importPart.replace(/[()]/g, "").split(",").map((s) => s.trim()).filter((spec) => {
|
|
4063
|
+
const parts = spec.split(/\s+as\s+/);
|
|
4064
|
+
const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim();
|
|
4065
|
+
return !unusedNames.has(localName);
|
|
4066
|
+
});
|
|
4067
|
+
if (keptSpecifiers.length === 0) return;
|
|
4068
|
+
const joined = keptSpecifiers.join(", ");
|
|
4069
|
+
lines[lineIdx] = hasParen ? `${prefix}(${joined})` : `${prefix}${joined}`;
|
|
4070
|
+
};
|
|
4071
|
+
|
|
4040
4072
|
//#endregion
|
|
4041
4073
|
//#region src/commands/fix.ts
|
|
4042
4074
|
const uniqueFiles = (diagnostics) => [...new Set(diagnostics.map((d) => d.filePath))];
|
|
@@ -4091,7 +4123,7 @@ const runFixStep = async (name, detect, applyFix, options) => {
|
|
|
4091
4123
|
}
|
|
4092
4124
|
lines.push(...getFilePreviewLines("Affected", uniqueFiles(before), options.verbose));
|
|
4093
4125
|
if (after.length > 0) lines.push(...getFilePreviewLines("Remaining", uniqueFiles(after), options.verbose));
|
|
4094
|
-
|
|
4126
|
+
process.stdout.write(`${lines.join("\n")}\n\n`);
|
|
4095
4127
|
return result;
|
|
4096
4128
|
};
|
|
4097
4129
|
const createEngineContext = (rootDirectory, projectInfo, config) => ({
|
|
@@ -4134,13 +4166,7 @@ const fixCommand = async (directory, config, options = {
|
|
|
4134
4166
|
printProjectMetadata(projectInfo);
|
|
4135
4167
|
const context = createEngineContext(resolvedDir, projectInfo, config);
|
|
4136
4168
|
const steps = [];
|
|
4137
|
-
if (config.engines.
|
|
4138
|
-
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS formatting", () => runBiomeFormat(context), () => fixBiomeFormat(context), options));
|
|
4139
|
-
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python formatting", () => runRuffFormat(context), () => fixRuffFormat(resolvedDir), options));
|
|
4140
|
-
else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python formatting fixes.");
|
|
4141
|
-
if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) steps.push(await runFixStep("Go formatting", () => runGofmt(context), () => fixGofmt(resolvedDir), options));
|
|
4142
|
-
else if (projectInfo.languages.includes("go")) logger.warn(" Go detected but gofmt is not installed; skipping Go formatting fixes.");
|
|
4143
|
-
}
|
|
4169
|
+
if (config.engines["ai-slop"]) steps.push(await runFixStep("Unused imports", () => detectUnusedImports(context), () => fixUnusedImports(context), options));
|
|
4144
4170
|
if (config.engines.lint) {
|
|
4145
4171
|
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS lint fixes", () => runOxlint(context), () => fixOxlint(context), options));
|
|
4146
4172
|
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => fixRuffLint(resolvedDir), options));
|
|
@@ -4149,6 +4175,13 @@ const fixCommand = async (directory, config, options = {
|
|
|
4149
4175
|
if (config.engines["code-quality"]) {
|
|
4150
4176
|
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("Unused dependencies", () => runKnipDependencyCheck(resolvedDir), () => fixUnusedDependencies(resolvedDir), options));
|
|
4151
4177
|
}
|
|
4178
|
+
if (config.engines.format) {
|
|
4179
|
+
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS formatting", () => runBiomeFormat(context), () => fixBiomeFormat(context), options));
|
|
4180
|
+
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python formatting", () => runRuffFormat(context), () => fixRuffFormat(resolvedDir), options));
|
|
4181
|
+
else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python formatting fixes.");
|
|
4182
|
+
if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) steps.push(await runFixStep("Go formatting", () => runGofmt(context), () => fixGofmt(resolvedDir), options));
|
|
4183
|
+
else if (projectInfo.languages.includes("go")) logger.warn(" Go detected but gofmt is not installed; skipping Go formatting fixes.");
|
|
4184
|
+
}
|
|
4152
4185
|
if (steps.length === 0) logger.dim(" No applicable auto-fixers found for this project.");
|
|
4153
4186
|
else {
|
|
4154
4187
|
logger.break();
|
|
@@ -4289,7 +4322,7 @@ const rulesCommand = async (directory) => {
|
|
|
4289
4322
|
lines.push("");
|
|
4290
4323
|
}
|
|
4291
4324
|
}
|
|
4292
|
-
|
|
4325
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
4293
4326
|
};
|
|
4294
4327
|
|
|
4295
4328
|
//#endregion
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Application version — injected at build time by tsdown from package.json.
|
|
4
4
|
* The fallback should always match the "version" field in package.json.
|
|
5
5
|
*/
|
|
6
|
-
const APP_VERSION = "0.2.
|
|
6
|
+
const APP_VERSION = "0.2.1";
|
|
7
7
|
|
|
8
8
|
//#endregion
|
|
9
9
|
//#region src/output/engine-info.ts
|