aislop 0.1.2 → 0.2.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 +90 -264
- package/dist/cli.js +128 -24
- package/dist/{engine-info-B4Eq4giL.js → engine-info-DpU0WTTj.js} +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +195 -117
- package/dist/{json-BMSa_G7o.js → json-UG8l_sLC.js} +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,55 +3,38 @@
|
|
|
3
3
|
**Stop AI slop from shipping.**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/aislop)
|
|
6
|
+
[](https://www.npmjs.com/package/aislop)
|
|
6
7
|
[](https://github.com/heavykenny/aislop/actions/workflows/ci.yml)
|
|
7
8
|
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](https://nodejs.org)
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
`aislop` is a unified code-quality CLI for multi-language repositories. It runs formatting, linting, code-quality, AI-pattern, architecture, and security checks behind a single command and summarizes everything as one score out of 100.
|
|
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
13
|
```
|
|
14
14
|
$ npx aislop scan
|
|
15
15
|
|
|
16
|
-
aislop Scan v0.
|
|
16
|
+
aislop Scan v0.2.0
|
|
17
17
|
|
|
18
18
|
✓ Project my-app (typescript)
|
|
19
19
|
Source files: 142
|
|
20
20
|
|
|
21
|
-
✓ Formatting:
|
|
22
|
-
✓ Linting:
|
|
23
|
-
✓ Code Quality:
|
|
24
|
-
! Maintainability:
|
|
25
|
-
✓ Security:
|
|
21
|
+
✓ Formatting: done (0 issues)
|
|
22
|
+
✓ Linting: done (2 warnings)
|
|
23
|
+
✓ Code Quality: done (1 warning)
|
|
24
|
+
! Maintainability: done (4 warnings)
|
|
25
|
+
✓ Security: done (0 issues)
|
|
26
26
|
|
|
27
27
|
Score: 80/100 (Healthy)
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
---
|
|
31
31
|
|
|
32
|
-
## Why aislop?
|
|
33
|
-
|
|
34
|
-
Code generated by AI assistants often looks correct at first glance but is full of quality issues that pass review because they are spread across dozens of files. No single existing linter catches all of them. `aislop` was built to be the one tool that does:
|
|
35
|
-
|
|
36
|
-
- **Catches AI-specific patterns** that ESLint and Prettier ignore: trivial comments (`// Import React`), thin wrappers, generic names (`data2`, `helper_1`), swallowed exceptions
|
|
37
|
-
- **Multi-language** -- TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP in one CLI
|
|
38
|
-
- **Single score** -- one number to gate PRs, track in CI, and trend over time
|
|
39
|
-
- **Zero config to start** -- run `npx aislop scan` and get results immediately
|
|
40
|
-
- **Batteries included** -- ships with oxlint, biome, and knip; downloads ruff and golangci-lint automatically on install
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
32
|
## Installation
|
|
45
33
|
|
|
46
|
-
### Run without installing
|
|
47
|
-
|
|
48
34
|
```bash
|
|
35
|
+
# Run without installing
|
|
49
36
|
npx aislop scan
|
|
50
|
-
```
|
|
51
37
|
|
|
52
|
-
### Install as a dev dependency
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
38
|
# npm
|
|
56
39
|
npm install --save-dev aislop
|
|
57
40
|
|
|
@@ -60,300 +43,143 @@ yarn add --dev aislop
|
|
|
60
43
|
|
|
61
44
|
# pnpm
|
|
62
45
|
pnpm add -D aislop
|
|
63
|
-
```
|
|
64
46
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
```bash
|
|
47
|
+
# Global
|
|
68
48
|
npm install -g aislop
|
|
69
|
-
aislop scan
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
`aislop` ships with Node-based tooling (oxlint, biome, knip) as package dependencies. On install it also downloads bundled binaries for **ruff** and **golangci-lint**. To skip those downloads:
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
AISLOP_SKIP_TOOL_DOWNLOAD=1 npm install
|
|
76
49
|
```
|
|
77
50
|
|
|
78
|
-
|
|
51
|
+
Also available as [`@heavykenny/aislop`](docs/installation.md) on GitHub Packages.
|
|
79
52
|
|
|
80
53
|
---
|
|
81
54
|
|
|
82
|
-
##
|
|
55
|
+
## Usage
|
|
83
56
|
|
|
84
|
-
|
|
85
|
-
# Scan the current directory
|
|
86
|
-
npx aislop scan
|
|
87
|
-
|
|
88
|
-
# Auto-fix formatting and lint issues
|
|
89
|
-
npx aislop fix
|
|
90
|
-
|
|
91
|
-
# Scan only changed files (great for pre-commit)
|
|
92
|
-
npx aislop scan --changes
|
|
93
|
-
|
|
94
|
-
# Scan only staged files
|
|
95
|
-
npx aislop scan --staged
|
|
57
|
+
### Scan your project
|
|
96
58
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
# Interactive menu
|
|
104
|
-
npx aislop
|
|
59
|
+
```bash
|
|
60
|
+
aislop scan # scan current directory
|
|
61
|
+
aislop scan ./src # scan a specific directory
|
|
62
|
+
aislop scan --changes # only files changed from HEAD
|
|
63
|
+
aislop scan --staged # only staged files (pre-commit)
|
|
64
|
+
aislop scan --json # output JSON
|
|
105
65
|
```
|
|
106
66
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
## Commands
|
|
110
|
-
|
|
111
|
-
| Command | What it does |
|
|
112
|
-
|---|---|
|
|
113
|
-
| `aislop` | Interactive TTY menu (falls back to `scan` in non-TTY) |
|
|
114
|
-
| `aislop scan [dir]` | Run all enabled engines and print a scored report |
|
|
115
|
-
| `aislop fix [dir]` | Apply safe auto-fixes (formatting + lint) |
|
|
116
|
-
| `aislop ci [dir]` | Output JSON for CI pipelines |
|
|
117
|
-
| `aislop init [dir]` | Create `.aislop/config.yml` and `.aislop/rules.yml` |
|
|
118
|
-
| `aislop doctor [dir]` | Report which tools are installed and available |
|
|
119
|
-
| `aislop rules [dir]` | List all built-in and configured rules |
|
|
120
|
-
|
|
121
|
-
### Flags
|
|
122
|
-
|
|
123
|
-
| Flag | Description |
|
|
124
|
-
|---|---|
|
|
125
|
-
| `--changes` | Only scan files changed from `HEAD` |
|
|
126
|
-
| `--staged` | Only scan staged files |
|
|
127
|
-
| `-d, --verbose` | Show detailed per-file output |
|
|
128
|
-
| `--json` | Output JSON instead of terminal UI |
|
|
129
|
-
| `-v, --version` | Print version |
|
|
130
|
-
|
|
131
|
-
---
|
|
132
|
-
|
|
133
|
-
## What it catches
|
|
134
|
-
|
|
135
|
-
`aislop` groups checks into six engines. Each engine runs in parallel for speed.
|
|
136
|
-
|
|
137
|
-
### Formatting
|
|
138
|
-
|
|
139
|
-
Enforces consistent formatting using the best tool for each language.
|
|
140
|
-
|
|
141
|
-
| Language | Tool |
|
|
142
|
-
|---|---|
|
|
143
|
-
| TypeScript / JavaScript | Biome |
|
|
144
|
-
| Python | ruff format |
|
|
145
|
-
| Go | gofmt |
|
|
146
|
-
| Rust | cargo fmt |
|
|
147
|
-
| Ruby | rubocop |
|
|
148
|
-
| PHP | php-cs-fixer |
|
|
149
|
-
|
|
150
|
-
### Linting
|
|
151
|
-
|
|
152
|
-
Catches bugs and bad practices.
|
|
153
|
-
|
|
154
|
-
| Language | Tool |
|
|
155
|
-
|---|---|
|
|
156
|
-
| TypeScript / JavaScript | oxlint (bundled, with React/Next.js awareness) |
|
|
157
|
-
| Python | ruff |
|
|
158
|
-
| Go | golangci-lint |
|
|
159
|
-
| Rust | clippy |
|
|
160
|
-
| Ruby | rubocop |
|
|
67
|
+
### Fix issues automatically
|
|
161
68
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
| Rule | What it checks |
|
|
167
|
-
|---|---|
|
|
168
|
-
| `complexity/function-too-long` | Functions exceeding configurable line limit (default: 80) |
|
|
169
|
-
| `complexity/file-too-large` | Files exceeding configurable line limit (default: 400) |
|
|
170
|
-
| `complexity/deep-nesting` | Control-flow nesting beyond threshold (default: 5) |
|
|
171
|
-
| `complexity/too-many-params` | Functions with too many parameters (default: 6) |
|
|
172
|
-
| `duplication/block` | Cross-file duplicate code blocks (12+ lines) |
|
|
173
|
-
| `knip/files`, `knip/exports`, `knip/types` | Dead code via knip (JS/TS) |
|
|
174
|
-
|
|
175
|
-
### AI slop detection (Maintainability)
|
|
176
|
-
|
|
177
|
-
The rules that make aislop unique. These catch the patterns AI assistants leave behind.
|
|
178
|
-
|
|
179
|
-
| Rule | Severity | What it catches |
|
|
180
|
-
|---|---|---|
|
|
181
|
-
| `ai-slop/trivial-comment` | warning | Comments restating the code (`// Import React`, `// Return the value`) |
|
|
182
|
-
| `ai-slop/swallowed-exception` | error | Empty catch blocks, catch blocks that only log (JS/TS/Python/Go/Ruby/Java) |
|
|
183
|
-
| `ai-slop/thin-wrapper` | warning | Functions that only delegate to another function |
|
|
184
|
-
| `ai-slop/generic-naming` | info | AI-generated names: `helper_1`, `data2`, `temp1` |
|
|
185
|
-
| `ai-slop/unused-import` | warning | Unused imports (JS/TS and Python) |
|
|
186
|
-
| `ai-slop/console-leftover` | warning | `console.log`/`debug`/`info` left in production code |
|
|
187
|
-
| `ai-slop/todo-stub` | info | Unresolved TODO/FIXME/HACK comments |
|
|
188
|
-
| `ai-slop/unreachable-code` | warning | Code after `return`/`throw` statements |
|
|
189
|
-
| `ai-slop/constant-condition` | warning | `if (true)`, `if (false)`, `if (0)` |
|
|
190
|
-
| `ai-slop/empty-function` | info | Empty function bodies |
|
|
191
|
-
| `ai-slop/unsafe-type-assertion` | warning | `as any` in TypeScript |
|
|
192
|
-
| `ai-slop/double-type-assertion` | warning | `as unknown as X` pattern |
|
|
193
|
-
| `ai-slop/ts-directive` | info | `@ts-ignore` / `@ts-expect-error` usage |
|
|
194
|
-
|
|
195
|
-
### Security
|
|
196
|
-
|
|
197
|
-
Finds secrets, risky constructs, and vulnerable dependencies.
|
|
198
|
-
|
|
199
|
-
| Rule | What it catches |
|
|
200
|
-
|---|---|
|
|
201
|
-
| `security/hardcoded-secret` | API keys, AWS credentials, JWT tokens, database URLs, passwords |
|
|
202
|
-
| `security/eval` | `eval()` usage (JS/TS/Python/Ruby/PHP) |
|
|
203
|
-
| `security/innerhtml` | `.innerHTML` and `dangerouslySetInnerHTML` |
|
|
204
|
-
| `security/sql-injection` | String concatenation in SQL queries |
|
|
205
|
-
| `security/shell-injection` | User input in command execution |
|
|
206
|
-
| `security/vulnerable-dependency` | npm/pip/cargo/go dependency audit |
|
|
207
|
-
|
|
208
|
-
### Architecture (opt-in)
|
|
209
|
-
|
|
210
|
-
Custom import and path rules defined in `.aislop/rules.yml`.
|
|
211
|
-
|
|
212
|
-
| Rule type | Example |
|
|
213
|
-
|---|---|
|
|
214
|
-
| `forbid_import` | Ban `axios` project-wide |
|
|
215
|
-
| `forbid_import_from_path` | Controllers cannot import database modules |
|
|
216
|
-
| `require_pattern` | Require error handling in API routes |
|
|
217
|
-
|
|
218
|
-
---
|
|
69
|
+
```bash
|
|
70
|
+
aislop fix # auto-fix formatting + lint issues
|
|
71
|
+
```
|
|
219
72
|
|
|
220
|
-
|
|
73
|
+
### Use in CI pipelines
|
|
221
74
|
|
|
222
|
-
|
|
75
|
+
```bash
|
|
76
|
+
aislop ci # JSON output, exits 1 if score < threshold
|
|
77
|
+
```
|
|
223
78
|
|
|
224
|
-
|
|
225
|
-
|---|---|
|
|
226
|
-
| Error | 3.0 |
|
|
227
|
-
| Warning | 1.0 |
|
|
228
|
-
| Info | 0.25 |
|
|
79
|
+
### Other commands
|
|
229
80
|
|
|
230
|
-
|
|
81
|
+
```bash
|
|
82
|
+
aislop init # create .aislop/config.yml
|
|
83
|
+
aislop doctor # check which tools are available
|
|
84
|
+
aislop rules # list all built-in rules
|
|
85
|
+
aislop # interactive menu
|
|
86
|
+
```
|
|
231
87
|
|
|
232
|
-
|
|
233
|
-
|---|---|
|
|
234
|
-
| 75 -- 100 | Healthy |
|
|
235
|
-
| 50 -- 74 | Needs Work |
|
|
236
|
-
| 0 -- 49 | Critical |
|
|
88
|
+
See [all commands and flags](docs/commands.md).
|
|
237
89
|
|
|
238
90
|
---
|
|
239
91
|
|
|
240
|
-
##
|
|
92
|
+
## Use in your project
|
|
241
93
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
```yaml
|
|
245
|
-
version: 1
|
|
246
|
-
|
|
247
|
-
engines:
|
|
248
|
-
format: true
|
|
249
|
-
lint: true
|
|
250
|
-
code-quality: true
|
|
251
|
-
ai-slop: true
|
|
252
|
-
architecture: false # opt-in, needs rules.yml
|
|
253
|
-
security: true
|
|
254
|
-
|
|
255
|
-
quality:
|
|
256
|
-
maxFunctionLoc: 80
|
|
257
|
-
maxFileLoc: 400
|
|
258
|
-
maxNesting: 5
|
|
259
|
-
maxParams: 6
|
|
260
|
-
|
|
261
|
-
security:
|
|
262
|
-
audit: true
|
|
263
|
-
auditTimeout: 25000
|
|
264
|
-
|
|
265
|
-
scoring:
|
|
266
|
-
weights:
|
|
267
|
-
format: 0.5
|
|
268
|
-
lint: 1.0
|
|
269
|
-
code-quality: 1.5
|
|
270
|
-
ai-slop: 1.0
|
|
271
|
-
architecture: 1.0
|
|
272
|
-
security: 2.0
|
|
273
|
-
thresholds:
|
|
274
|
-
good: 75
|
|
275
|
-
ok: 50
|
|
94
|
+
### Pre-commit hook
|
|
276
95
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
format: json
|
|
96
|
+
```bash
|
|
97
|
+
npx aislop scan --staged
|
|
280
98
|
```
|
|
281
99
|
|
|
282
|
-
---
|
|
283
|
-
|
|
284
|
-
## CI / CD
|
|
285
|
-
|
|
286
100
|
### GitHub Actions
|
|
287
101
|
|
|
288
102
|
```yaml
|
|
289
|
-
- uses: actions/setup-node@
|
|
103
|
+
- uses: actions/setup-node@v6
|
|
290
104
|
with:
|
|
291
105
|
node-version: 20
|
|
292
|
-
|
|
293
106
|
- run: npx aislop ci
|
|
294
107
|
```
|
|
295
108
|
|
|
296
|
-
###
|
|
109
|
+
### Quality gate
|
|
297
110
|
|
|
298
|
-
Set
|
|
111
|
+
Set a minimum score in `.aislop/config.yml`:
|
|
299
112
|
|
|
300
113
|
```yaml
|
|
301
114
|
ci:
|
|
302
115
|
failBelow: 70
|
|
303
116
|
```
|
|
304
117
|
|
|
305
|
-
`aislop ci` exits with code 1 when the score
|
|
306
|
-
|
|
307
|
-
### Pre-commit hook
|
|
308
|
-
|
|
309
|
-
```bash
|
|
310
|
-
npx aislop scan --staged
|
|
311
|
-
```
|
|
118
|
+
`aislop ci` exits with code 1 when the score drops below the threshold. See [CI/CD docs](docs/ci.md) for more.
|
|
312
119
|
|
|
313
120
|
---
|
|
314
121
|
|
|
315
|
-
##
|
|
122
|
+
## Why aislop?
|
|
316
123
|
|
|
317
|
-
|
|
318
|
-
|---|---|---|---|---|---|
|
|
319
|
-
| TypeScript | Biome | oxlint | knip, complexity | All rules | All rules |
|
|
320
|
-
| JavaScript | Biome | oxlint | knip, complexity | All rules | All rules |
|
|
321
|
-
| Python | ruff | ruff | complexity | Imports, exceptions, comments | Secrets, audit |
|
|
322
|
-
| Go | gofmt | golangci-lint | complexity | Exceptions, comments | Secrets, audit |
|
|
323
|
-
| Rust | cargo fmt | clippy | complexity | Comments | Secrets, audit |
|
|
324
|
-
| Ruby | rubocop | rubocop | complexity | Exceptions, comments | Secrets |
|
|
325
|
-
| PHP | php-cs-fixer | -- | complexity | Comments | Secrets |
|
|
124
|
+
AI-generated code passes review because issues are spread across dozens of files. No single linter catches all of them. `aislop` does:
|
|
326
125
|
|
|
327
|
-
|
|
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
|
|
328
132
|
|
|
329
|
-
##
|
|
133
|
+
## What it catches
|
|
330
134
|
|
|
331
|
-
|
|
135
|
+
Six engines run in parallel: **Formatting**, **Linting**, **Code Quality**, **AI Slop Detection**, **Security**, and **Architecture** (opt-in).
|
|
332
136
|
|
|
333
|
-
|
|
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 |
|
|
334
145
|
|
|
335
|
-
|
|
146
|
+
See the full [rules reference](docs/rules.md) for all 30+ built-in rules.
|
|
336
147
|
|
|
337
|
-
|
|
338
|
-
# Environment variable (any of these)
|
|
339
|
-
AISLOP_NO_TELEMETRY=1 aislop scan
|
|
340
|
-
DO_NOT_TRACK=1 aislop scan
|
|
148
|
+
---
|
|
341
149
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
150
|
+
## Documentation
|
|
151
|
+
|
|
152
|
+
| Topic | Link |
|
|
153
|
+
|---|---|
|
|
154
|
+
| Installation | [docs/installation.md](docs/installation.md) |
|
|
155
|
+
| Commands & flags | [docs/commands.md](docs/commands.md) |
|
|
156
|
+
| Rules reference | [docs/rules.md](docs/rules.md) |
|
|
157
|
+
| Configuration | [docs/configuration.md](docs/configuration.md) |
|
|
158
|
+
| Scoring | [docs/scoring.md](docs/scoring.md) |
|
|
159
|
+
| CI / CD | [docs/ci.md](docs/ci.md) |
|
|
160
|
+
| Telemetry | [docs/telemetry.md](docs/telemetry.md) |
|
|
346
161
|
|
|
347
162
|
---
|
|
348
163
|
|
|
349
164
|
## Contributing
|
|
350
165
|
|
|
351
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup
|
|
166
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and how to add new rules. AI coding assistants can find project context in [AGENTS.md](AGENTS.md).
|
|
167
|
+
|
|
168
|
+
## Acknowledgments
|
|
169
|
+
|
|
170
|
+
`aislop` is built on top of excellent open-source projects:
|
|
171
|
+
|
|
172
|
+
- [Biome](https://biomejs.dev/) — formatting and linting for JS/TS
|
|
173
|
+
- [oxlint](https://oxc.rs/) — fast JavaScript/TypeScript linter
|
|
174
|
+
- [knip](https://knip.dev/) — unused files, exports, and dependencies
|
|
175
|
+
- [ruff](https://docs.astral.sh/ruff/) — Python linting and formatting
|
|
176
|
+
- [golangci-lint](https://golangci-lint.run/) — Go linting
|
|
177
|
+
- [expo-doctor](https://docs.expo.dev/) — Expo/React Native project health
|
|
352
178
|
|
|
353
|
-
##
|
|
179
|
+
## Contributors
|
|
354
180
|
|
|
355
|
-
|
|
181
|
+
[](https://github.com/heavykenny/aislop/graphs/contributors)
|
|
356
182
|
|
|
357
183
|
## License
|
|
358
184
|
|
|
359
|
-
[MIT](LICENSE)
|
|
185
|
+
[MIT](LICENSE)
|
package/dist/cli.js
CHANGED
|
@@ -46,7 +46,8 @@ const DEFAULT_CONFIG = {
|
|
|
46
46
|
thresholds: {
|
|
47
47
|
good: 75,
|
|
48
48
|
ok: 50
|
|
49
|
-
}
|
|
49
|
+
},
|
|
50
|
+
smoothing: 10
|
|
50
51
|
},
|
|
51
52
|
ci: {
|
|
52
53
|
failBelow: 0,
|
|
@@ -146,7 +147,8 @@ const ScoringSchema = z.object({
|
|
|
146
147
|
thresholds: ThresholdsSchema.default(() => ({
|
|
147
148
|
good: 75,
|
|
148
149
|
ok: 50
|
|
149
|
-
}))
|
|
150
|
+
})),
|
|
151
|
+
smoothing: z.number().nonnegative().default(10)
|
|
150
152
|
});
|
|
151
153
|
const CiSchema = z.object({
|
|
152
154
|
failBelow: z.number().default(0),
|
|
@@ -178,7 +180,8 @@ const AislopConfigSchema = z.object({
|
|
|
178
180
|
thresholds: {
|
|
179
181
|
good: 75,
|
|
180
182
|
ok: 50
|
|
181
|
-
}
|
|
183
|
+
},
|
|
184
|
+
smoothing: 10
|
|
182
185
|
})),
|
|
183
186
|
ci: CiSchema.default(() => ({
|
|
184
187
|
failBelow: 0,
|
|
@@ -1279,10 +1282,7 @@ const FUNCTION_PATTERNS = [
|
|
|
1279
1282
|
]
|
|
1280
1283
|
}
|
|
1281
1284
|
];
|
|
1282
|
-
const countParams = (
|
|
1283
|
-
if (!paramStr.trim()) return 0;
|
|
1284
|
-
return paramStr.split(",").length;
|
|
1285
|
-
};
|
|
1285
|
+
const countParams = (p) => p.trim() ? p.split(",").length : 0;
|
|
1286
1286
|
const matchFunctionOnLine = (line, ext) => {
|
|
1287
1287
|
for (let i = 0; i < FUNCTION_PATTERNS.length; i++) {
|
|
1288
1288
|
const pattern = FUNCTION_PATTERNS[i];
|
|
@@ -1643,13 +1643,41 @@ const isToolInstalled = async (tool) => {
|
|
|
1643
1643
|
//#region src/engines/code-quality/knip.ts
|
|
1644
1644
|
const KNIP_MESSAGE_MAP = {
|
|
1645
1645
|
files: "Unused file",
|
|
1646
|
+
dependencies: "Unused dependency",
|
|
1647
|
+
devDependencies: "Unused devDependency",
|
|
1648
|
+
unlisted: "Unlisted dependency",
|
|
1649
|
+
unresolved: "Unresolved import",
|
|
1650
|
+
binaries: "Unlisted binary",
|
|
1646
1651
|
exports: "Unused export",
|
|
1647
1652
|
types: "Unused type",
|
|
1648
1653
|
duplicates: "Duplicate export"
|
|
1649
1654
|
};
|
|
1655
|
+
const DEPENDENCY_TYPES = [
|
|
1656
|
+
"dependencies",
|
|
1657
|
+
"devDependencies",
|
|
1658
|
+
"unlisted",
|
|
1659
|
+
"unresolved",
|
|
1660
|
+
"binaries"
|
|
1661
|
+
];
|
|
1662
|
+
const isDependencyType = (type) => DEPENDENCY_TYPES.includes(type);
|
|
1663
|
+
const getIssueItems = (fileIssue, issueType) => {
|
|
1664
|
+
const items = fileIssue[issueType];
|
|
1665
|
+
return Array.isArray(items) ? items : [];
|
|
1666
|
+
};
|
|
1667
|
+
const DEPENDENCY_HELP = {
|
|
1668
|
+
dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
1669
|
+
devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
1670
|
+
unlisted: "This package is imported in code but not declared in package.json. Run `npm install` to add it.",
|
|
1671
|
+
unresolved: "This import cannot be resolved. Check for typos or missing packages.",
|
|
1672
|
+
binaries: "This binary is used but its package is not in package.json."
|
|
1673
|
+
};
|
|
1650
1674
|
const collectIssues = (fileIssue, issueType, rootDir, knipCwd) => {
|
|
1651
1675
|
const diagnostics = [];
|
|
1652
|
-
const issues =
|
|
1676
|
+
const issues = getIssueItems(fileIssue, issueType);
|
|
1677
|
+
const category = isDependencyType(issueType) ? "Dependencies" : "Dead Code";
|
|
1678
|
+
const severity = issueType === "unlisted" || issueType === "unresolved" ? "error" : "warning";
|
|
1679
|
+
const fixable = issueType === "dependencies" || issueType === "devDependencies";
|
|
1680
|
+
const help = DEPENDENCY_HELP[issueType] ?? "";
|
|
1653
1681
|
for (const issue of issues) {
|
|
1654
1682
|
const symbol = issue.name ?? issue.symbol ?? "unknown";
|
|
1655
1683
|
const absolutePath = path.resolve(knipCwd, fileIssue.file);
|
|
@@ -1657,13 +1685,13 @@ const collectIssues = (fileIssue, issueType, rootDir, knipCwd) => {
|
|
|
1657
1685
|
filePath: path.relative(rootDir, absolutePath),
|
|
1658
1686
|
engine: "code-quality",
|
|
1659
1687
|
rule: `knip/${issueType}`,
|
|
1660
|
-
severity
|
|
1688
|
+
severity,
|
|
1661
1689
|
message: `${KNIP_MESSAGE_MAP[issueType]}: ${symbol}`,
|
|
1662
|
-
help
|
|
1690
|
+
help,
|
|
1663
1691
|
line: issue.line ?? 0,
|
|
1664
1692
|
column: issue.col ?? 0,
|
|
1665
|
-
category
|
|
1666
|
-
fixable
|
|
1693
|
+
category,
|
|
1694
|
+
fixable
|
|
1667
1695
|
});
|
|
1668
1696
|
}
|
|
1669
1697
|
return diagnostics;
|
|
@@ -1697,6 +1725,38 @@ const findKnipBin = (rootDirectory, monorepoRoot) => {
|
|
|
1697
1725
|
}
|
|
1698
1726
|
return null;
|
|
1699
1727
|
};
|
|
1728
|
+
const runKnipDependencyCheck = async (rootDirectory) => {
|
|
1729
|
+
return (await runKnip(rootDirectory)).filter((d) => d.rule === "knip/dependencies" || d.rule === "knip/devDependencies");
|
|
1730
|
+
};
|
|
1731
|
+
const fixUnusedDependencies = async (rootDirectory) => {
|
|
1732
|
+
const diagnostics = await runKnipDependencyCheck(rootDirectory);
|
|
1733
|
+
if (diagnostics.length === 0) return;
|
|
1734
|
+
const pkgPath = path.join(rootDirectory, "package.json");
|
|
1735
|
+
if (!fs.existsSync(pkgPath)) return;
|
|
1736
|
+
const raw = fs.readFileSync(pkgPath, "utf-8");
|
|
1737
|
+
const pkg = JSON.parse(raw);
|
|
1738
|
+
const unusedDeps = /* @__PURE__ */ new Set();
|
|
1739
|
+
const unusedDevDeps = /* @__PURE__ */ new Set();
|
|
1740
|
+
for (const d of diagnostics) {
|
|
1741
|
+
const pkgName = d.message.replace(/^Unused (dev)?[Dd]ependency: /, "");
|
|
1742
|
+
if (d.rule === "knip/dependencies") unusedDeps.add(pkgName);
|
|
1743
|
+
if (d.rule === "knip/devDependencies") unusedDevDeps.add(pkgName);
|
|
1744
|
+
}
|
|
1745
|
+
let changed = false;
|
|
1746
|
+
if (pkg.dependencies) {
|
|
1747
|
+
for (const name of unusedDeps) if (name in pkg.dependencies) {
|
|
1748
|
+
delete pkg.dependencies[name];
|
|
1749
|
+
changed = true;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
if (pkg.devDependencies) {
|
|
1753
|
+
for (const name of unusedDevDeps) if (name in pkg.devDependencies) {
|
|
1754
|
+
delete pkg.devDependencies[name];
|
|
1755
|
+
changed = true;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
if (changed) fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, " ")}\n`);
|
|
1759
|
+
};
|
|
1700
1760
|
const runKnip = async (rootDirectory) => {
|
|
1701
1761
|
const knipRuntime = findKnipBin(rootDirectory, findMonorepoRoot(rootDirectory));
|
|
1702
1762
|
if (!knipRuntime) return [];
|
|
@@ -1730,11 +1790,13 @@ const runKnip = async (rootDirectory) => {
|
|
|
1730
1790
|
fixable: false
|
|
1731
1791
|
});
|
|
1732
1792
|
const issues = parsed.issues ?? [];
|
|
1733
|
-
|
|
1793
|
+
const issueTypes = [
|
|
1794
|
+
...DEPENDENCY_TYPES,
|
|
1734
1795
|
"exports",
|
|
1735
1796
|
"types",
|
|
1736
1797
|
"duplicates"
|
|
1737
|
-
]
|
|
1798
|
+
];
|
|
1799
|
+
for (const fileIssue of issues) for (const type of issueTypes) diagnostics.push(...collectIssues(fileIssue, type, rootDirectory, knipRuntime.cwd));
|
|
1738
1800
|
return diagnostics;
|
|
1739
1801
|
} catch {
|
|
1740
1802
|
return [];
|
|
@@ -3118,7 +3180,7 @@ const logger = {
|
|
|
3118
3180
|
* Application version — injected at build time by tsdown from package.json.
|
|
3119
3181
|
* The fallback should always match the "version" field in package.json.
|
|
3120
3182
|
*/
|
|
3121
|
-
const APP_VERSION = "0.
|
|
3183
|
+
const APP_VERSION = "0.2.0";
|
|
3122
3184
|
|
|
3123
3185
|
//#endregion
|
|
3124
3186
|
//#region src/output/layout.ts
|
|
@@ -3344,7 +3406,12 @@ var ScanProgressRenderer = class {
|
|
|
3344
3406
|
//#endregion
|
|
3345
3407
|
//#region src/scoring/index.ts
|
|
3346
3408
|
const PERFECT_SCORE$1 = 100;
|
|
3347
|
-
const
|
|
3409
|
+
const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
|
|
3410
|
+
if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
|
|
3411
|
+
const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
|
|
3412
|
+
return Math.max(1, filesWithDiagnostics);
|
|
3413
|
+
};
|
|
3414
|
+
const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoothing) => {
|
|
3348
3415
|
if (diagnostics.length === 0) return {
|
|
3349
3416
|
score: PERFECT_SCORE$1,
|
|
3350
3417
|
label: "Healthy"
|
|
@@ -3355,7 +3422,11 @@ const calculateScore = (diagnostics, weights, thresholds) => {
|
|
|
3355
3422
|
const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
|
|
3356
3423
|
deductions += severityPenalty * engineWeight;
|
|
3357
3424
|
}
|
|
3358
|
-
const
|
|
3425
|
+
const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
|
|
3426
|
+
const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
|
|
3427
|
+
const issueDensity = Math.min(1, diagnostics.length / (effectiveFileCount + smoothingConstant));
|
|
3428
|
+
const scaledDeductions = deductions * Math.sqrt(issueDensity);
|
|
3429
|
+
const score = Math.max(0, Math.round(PERFECT_SCORE$1 - PERFECT_SCORE$1 * Math.log1p(scaledDeductions) / Math.log1p(PERFECT_SCORE$1 + scaledDeductions)));
|
|
3359
3430
|
return {
|
|
3360
3431
|
score,
|
|
3361
3432
|
label: score >= thresholds.good ? "Healthy" : score >= thresholds.ok ? "Needs Work" : "Critical"
|
|
@@ -3683,9 +3754,13 @@ const getAnonymousId = () => {
|
|
|
3683
3754
|
for (let i = 0; i < raw.length; i++) hash = hash * 33 ^ raw.charCodeAt(i);
|
|
3684
3755
|
return `aislop_${(hash >>> 0).toString(36)}`;
|
|
3685
3756
|
};
|
|
3757
|
+
/** Pending telemetry request — kept alive so Node doesn't exit before it completes. */
|
|
3758
|
+
let pendingRequest = null;
|
|
3686
3759
|
/**
|
|
3687
3760
|
* Fire-and-forget telemetry event to PostHog.
|
|
3688
|
-
* Never throws, never blocks
|
|
3761
|
+
* Never throws, never blocks CLI output.
|
|
3762
|
+
* The request is kept alive via `flushTelemetry()` so Node doesn't
|
|
3763
|
+
* exit before it completes.
|
|
3689
3764
|
*/
|
|
3690
3765
|
const trackEvent = (event) => {
|
|
3691
3766
|
const payload = {
|
|
@@ -3708,12 +3783,23 @@ const trackEvent = (event) => {
|
|
|
3708
3783
|
},
|
|
3709
3784
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3710
3785
|
};
|
|
3711
|
-
fetch(`${POSTHOG_HOST}/capture/`, {
|
|
3786
|
+
pendingRequest = fetch(`${POSTHOG_HOST}/capture/`, {
|
|
3712
3787
|
method: "POST",
|
|
3713
3788
|
headers: { "Content-Type": "application/json" },
|
|
3714
3789
|
body: JSON.stringify(payload),
|
|
3715
3790
|
signal: AbortSignal.timeout(3e3)
|
|
3716
|
-
}).catch(() => {});
|
|
3791
|
+
}).then(() => {}).catch(() => {});
|
|
3792
|
+
};
|
|
3793
|
+
/**
|
|
3794
|
+
* Wait for any pending telemetry request to complete.
|
|
3795
|
+
* Call this before `process.exit()` to ensure the event is delivered.
|
|
3796
|
+
* Times out after 3 seconds so it never hangs the CLI.
|
|
3797
|
+
*/
|
|
3798
|
+
const flushTelemetry = async () => {
|
|
3799
|
+
if (pendingRequest) {
|
|
3800
|
+
await pendingRequest;
|
|
3801
|
+
pendingRequest = null;
|
|
3802
|
+
}
|
|
3717
3803
|
};
|
|
3718
3804
|
|
|
3719
3805
|
//#endregion
|
|
@@ -3771,7 +3857,7 @@ const scanCommand = async (directory, config, options) => {
|
|
|
3771
3857
|
progressRenderer?.stop();
|
|
3772
3858
|
const allDiagnostics = results.flatMap((r) => r.diagnostics);
|
|
3773
3859
|
const elapsedMs = performance.now() - startTime;
|
|
3774
|
-
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds);
|
|
3860
|
+
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
|
|
3775
3861
|
const exitCode = allDiagnostics.some((d) => d.severity === "error") || scoreResult.score < config.ci.failBelow ? 1 : 0;
|
|
3776
3862
|
if (!isTelemetryDisabled(config.telemetry?.enabled)) {
|
|
3777
3863
|
const engineIssues = {};
|
|
@@ -4060,6 +4146,9 @@ const fixCommand = async (directory, config, options = {
|
|
|
4060
4146
|
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => fixRuffLint(resolvedDir), options));
|
|
4061
4147
|
else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python lint fixes.");
|
|
4062
4148
|
}
|
|
4149
|
+
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));
|
|
4151
|
+
}
|
|
4063
4152
|
if (steps.length === 0) logger.dim(" No applicable auto-fixers found for this project.");
|
|
4064
4153
|
else {
|
|
4065
4154
|
logger.break();
|
|
@@ -4139,6 +4228,11 @@ const BUILTIN_RULES = [
|
|
|
4139
4228
|
engine: "code-quality",
|
|
4140
4229
|
rules: [
|
|
4141
4230
|
"knip/files",
|
|
4231
|
+
"knip/dependencies",
|
|
4232
|
+
"knip/devDependencies",
|
|
4233
|
+
"knip/unlisted",
|
|
4234
|
+
"knip/unresolved",
|
|
4235
|
+
"knip/binaries",
|
|
4142
4236
|
"knip/exports",
|
|
4143
4237
|
"knip/types",
|
|
4144
4238
|
"complexity/file-too-large",
|
|
@@ -4424,7 +4518,10 @@ const program = new Command().name("aislop").description("The unified code quali
|
|
|
4424
4518
|
verbose: Boolean(flags.verbose),
|
|
4425
4519
|
json: Boolean(flags.json)
|
|
4426
4520
|
});
|
|
4427
|
-
if (exitCode !== 0)
|
|
4521
|
+
if (exitCode !== 0) {
|
|
4522
|
+
await flushTelemetry();
|
|
4523
|
+
process.exit(exitCode);
|
|
4524
|
+
}
|
|
4428
4525
|
}).addHelpText("after", `
|
|
4429
4526
|
${highlighter.dim("Commands:")}
|
|
4430
4527
|
aislop scan [dir] Full code quality scan
|
|
@@ -4451,7 +4548,10 @@ program.command("scan [directory]").description("Run full code quality scan").op
|
|
|
4451
4548
|
verbose: Boolean(flags.verbose),
|
|
4452
4549
|
json: Boolean(flags.json)
|
|
4453
4550
|
});
|
|
4454
|
-
if (exitCode !== 0)
|
|
4551
|
+
if (exitCode !== 0) {
|
|
4552
|
+
await flushTelemetry();
|
|
4553
|
+
process.exit(exitCode);
|
|
4554
|
+
}
|
|
4455
4555
|
});
|
|
4456
4556
|
program.command("fix [directory]").description("Auto-fix formatting and lint issues").option("-d, --verbose", "show detailed fix progress").action(async (directory = ".", _flags, command) => {
|
|
4457
4557
|
const flags = command.optsWithGlobals();
|
|
@@ -4465,13 +4565,17 @@ program.command("doctor [directory]").description("Check installed tools and env
|
|
|
4465
4565
|
});
|
|
4466
4566
|
program.command("ci [directory]").description("CI-friendly JSON output with exit codes").action(async (directory = ".") => {
|
|
4467
4567
|
const { exitCode } = await ciCommand(directory, loadConfig(directory));
|
|
4468
|
-
if (exitCode !== 0)
|
|
4568
|
+
if (exitCode !== 0) {
|
|
4569
|
+
await flushTelemetry();
|
|
4570
|
+
process.exit(exitCode);
|
|
4571
|
+
}
|
|
4469
4572
|
});
|
|
4470
4573
|
program.command("rules [directory]").description("List all available rules").action(async (directory = ".") => {
|
|
4471
4574
|
await rulesCommand(directory);
|
|
4472
4575
|
});
|
|
4473
4576
|
const main = async () => {
|
|
4474
4577
|
await program.parseAsync();
|
|
4578
|
+
await flushTelemetry();
|
|
4475
4579
|
};
|
|
4476
4580
|
main();
|
|
4477
4581
|
|
|
@@ -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.
|
|
6
|
+
const APP_VERSION = "0.2.0";
|
|
7
7
|
|
|
8
8
|
//#endregion
|
|
9
9
|
//#region src/output/engine-info.ts
|
package/dist/index.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ declare const AislopConfigSchema: z.ZodObject<{
|
|
|
30
30
|
good: z.ZodDefault<z.ZodNumber>;
|
|
31
31
|
ok: z.ZodDefault<z.ZodNumber>;
|
|
32
32
|
}, z.core.$strip>>;
|
|
33
|
+
smoothing: z.ZodDefault<z.ZodNumber>;
|
|
33
34
|
}, z.core.$strip>>;
|
|
34
35
|
ci: z.ZodDefault<z.ZodObject<{
|
|
35
36
|
failBelow: z.ZodDefault<z.ZodNumber>;
|
|
@@ -114,6 +115,6 @@ interface ScoreResult {
|
|
|
114
115
|
declare const calculateScore: (diagnostics: Diagnostic[], weights: Record<string, number>, thresholds: {
|
|
115
116
|
good: number;
|
|
116
117
|
ok: number;
|
|
117
|
-
}) => ScoreResult;
|
|
118
|
+
}, sourceFileCount?: number, smoothing?: number) => ScoreResult;
|
|
118
119
|
//#endregion
|
|
119
120
|
export { type AislopConfig, type Diagnostic, type EngineName, type EngineResult, type Framework, type Language, type ProjectInfo, type ScoreResult, type Severity, calculateScore, discoverProject, doctorCommand, fixCommand, initCommand, loadConfig, scanCommand };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as getEngineLabel, r as APP_VERSION, t as ENGINE_INFO } from "./engine-info-
|
|
1
|
+
import { n as getEngineLabel, r as APP_VERSION, t as ENGINE_INFO } from "./engine-info-DpU0WTTj.js";
|
|
2
2
|
import { n as runSubprocess, t as isToolInstalled } from "./subprocess-99puEEGl.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import fs from "node:fs";
|
|
@@ -719,6 +719,170 @@ const fixRuffFormat = async (rootDirectory) => {
|
|
|
719
719
|
if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `ruff format exited with code ${result.exitCode}`);
|
|
720
720
|
};
|
|
721
721
|
|
|
722
|
+
//#endregion
|
|
723
|
+
//#region src/engines/code-quality/knip.ts
|
|
724
|
+
const KNIP_MESSAGE_MAP = {
|
|
725
|
+
files: "Unused file",
|
|
726
|
+
dependencies: "Unused dependency",
|
|
727
|
+
devDependencies: "Unused devDependency",
|
|
728
|
+
unlisted: "Unlisted dependency",
|
|
729
|
+
unresolved: "Unresolved import",
|
|
730
|
+
binaries: "Unlisted binary",
|
|
731
|
+
exports: "Unused export",
|
|
732
|
+
types: "Unused type",
|
|
733
|
+
duplicates: "Duplicate export"
|
|
734
|
+
};
|
|
735
|
+
const DEPENDENCY_TYPES = [
|
|
736
|
+
"dependencies",
|
|
737
|
+
"devDependencies",
|
|
738
|
+
"unlisted",
|
|
739
|
+
"unresolved",
|
|
740
|
+
"binaries"
|
|
741
|
+
];
|
|
742
|
+
const isDependencyType = (type) => DEPENDENCY_TYPES.includes(type);
|
|
743
|
+
const getIssueItems = (fileIssue, issueType) => {
|
|
744
|
+
const items = fileIssue[issueType];
|
|
745
|
+
return Array.isArray(items) ? items : [];
|
|
746
|
+
};
|
|
747
|
+
const DEPENDENCY_HELP = {
|
|
748
|
+
dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
749
|
+
devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
750
|
+
unlisted: "This package is imported in code but not declared in package.json. Run `npm install` to add it.",
|
|
751
|
+
unresolved: "This import cannot be resolved. Check for typos or missing packages.",
|
|
752
|
+
binaries: "This binary is used but its package is not in package.json."
|
|
753
|
+
};
|
|
754
|
+
const collectIssues = (fileIssue, issueType, rootDir, knipCwd) => {
|
|
755
|
+
const diagnostics = [];
|
|
756
|
+
const issues = getIssueItems(fileIssue, issueType);
|
|
757
|
+
const category = isDependencyType(issueType) ? "Dependencies" : "Dead Code";
|
|
758
|
+
const severity = issueType === "unlisted" || issueType === "unresolved" ? "error" : "warning";
|
|
759
|
+
const fixable = issueType === "dependencies" || issueType === "devDependencies";
|
|
760
|
+
const help = DEPENDENCY_HELP[issueType] ?? "";
|
|
761
|
+
for (const issue of issues) {
|
|
762
|
+
const symbol = issue.name ?? issue.symbol ?? "unknown";
|
|
763
|
+
const absolutePath = path.resolve(knipCwd, fileIssue.file);
|
|
764
|
+
diagnostics.push({
|
|
765
|
+
filePath: path.relative(rootDir, absolutePath),
|
|
766
|
+
engine: "code-quality",
|
|
767
|
+
rule: `knip/${issueType}`,
|
|
768
|
+
severity,
|
|
769
|
+
message: `${KNIP_MESSAGE_MAP[issueType]}: ${symbol}`,
|
|
770
|
+
help,
|
|
771
|
+
line: issue.line ?? 0,
|
|
772
|
+
column: issue.col ?? 0,
|
|
773
|
+
category,
|
|
774
|
+
fixable
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
return diagnostics;
|
|
778
|
+
};
|
|
779
|
+
const findMonorepoRoot = (directory) => {
|
|
780
|
+
let current = path.dirname(directory);
|
|
781
|
+
while (current !== path.dirname(current)) {
|
|
782
|
+
if (fs.existsSync(path.join(current, "pnpm-workspace.yaml")) || (() => {
|
|
783
|
+
const pkgPath = path.join(current, "package.json");
|
|
784
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
785
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
786
|
+
return Array.isArray(pkg.workspaces) || pkg.workspaces?.packages;
|
|
787
|
+
})()) return current;
|
|
788
|
+
current = path.dirname(current);
|
|
789
|
+
}
|
|
790
|
+
return null;
|
|
791
|
+
};
|
|
792
|
+
const KNIP_RELATIVE_BIN = path.join("node_modules", "knip", "bin", "knip.js");
|
|
793
|
+
const findKnipBin = (rootDirectory, monorepoRoot) => {
|
|
794
|
+
const localPath = path.join(rootDirectory, KNIP_RELATIVE_BIN);
|
|
795
|
+
if (fs.existsSync(localPath)) return {
|
|
796
|
+
binPath: localPath,
|
|
797
|
+
cwd: rootDirectory
|
|
798
|
+
};
|
|
799
|
+
if (monorepoRoot) {
|
|
800
|
+
const monorepoPath = path.join(monorepoRoot, KNIP_RELATIVE_BIN);
|
|
801
|
+
if (fs.existsSync(monorepoPath)) return {
|
|
802
|
+
binPath: monorepoPath,
|
|
803
|
+
cwd: monorepoRoot
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
return null;
|
|
807
|
+
};
|
|
808
|
+
const runKnipDependencyCheck = async (rootDirectory) => {
|
|
809
|
+
return (await runKnip(rootDirectory)).filter((d) => d.rule === "knip/dependencies" || d.rule === "knip/devDependencies");
|
|
810
|
+
};
|
|
811
|
+
const fixUnusedDependencies = async (rootDirectory) => {
|
|
812
|
+
const diagnostics = await runKnipDependencyCheck(rootDirectory);
|
|
813
|
+
if (diagnostics.length === 0) return;
|
|
814
|
+
const pkgPath = path.join(rootDirectory, "package.json");
|
|
815
|
+
if (!fs.existsSync(pkgPath)) return;
|
|
816
|
+
const raw = fs.readFileSync(pkgPath, "utf-8");
|
|
817
|
+
const pkg = JSON.parse(raw);
|
|
818
|
+
const unusedDeps = /* @__PURE__ */ new Set();
|
|
819
|
+
const unusedDevDeps = /* @__PURE__ */ new Set();
|
|
820
|
+
for (const d of diagnostics) {
|
|
821
|
+
const pkgName = d.message.replace(/^Unused (dev)?[Dd]ependency: /, "");
|
|
822
|
+
if (d.rule === "knip/dependencies") unusedDeps.add(pkgName);
|
|
823
|
+
if (d.rule === "knip/devDependencies") unusedDevDeps.add(pkgName);
|
|
824
|
+
}
|
|
825
|
+
let changed = false;
|
|
826
|
+
if (pkg.dependencies) {
|
|
827
|
+
for (const name of unusedDeps) if (name in pkg.dependencies) {
|
|
828
|
+
delete pkg.dependencies[name];
|
|
829
|
+
changed = true;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (pkg.devDependencies) {
|
|
833
|
+
for (const name of unusedDevDeps) if (name in pkg.devDependencies) {
|
|
834
|
+
delete pkg.devDependencies[name];
|
|
835
|
+
changed = true;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (changed) fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, " ")}\n`);
|
|
839
|
+
};
|
|
840
|
+
const runKnip = async (rootDirectory) => {
|
|
841
|
+
const knipRuntime = findKnipBin(rootDirectory, findMonorepoRoot(rootDirectory));
|
|
842
|
+
if (!knipRuntime) return [];
|
|
843
|
+
try {
|
|
844
|
+
const args = [
|
|
845
|
+
knipRuntime.binPath,
|
|
846
|
+
"--no-progress",
|
|
847
|
+
"--reporter",
|
|
848
|
+
"json",
|
|
849
|
+
"--no-exit-code"
|
|
850
|
+
];
|
|
851
|
+
const result = await runSubprocess(process.execPath, args, {
|
|
852
|
+
cwd: knipRuntime.cwd,
|
|
853
|
+
timeout: 2e4,
|
|
854
|
+
env: { FORCE_COLOR: "0" }
|
|
855
|
+
});
|
|
856
|
+
if (!result.stdout) return [];
|
|
857
|
+
const parsed = JSON.parse(result.stdout);
|
|
858
|
+
const diagnostics = [];
|
|
859
|
+
const files = parsed.files ?? [];
|
|
860
|
+
for (const unusedFile of files) diagnostics.push({
|
|
861
|
+
filePath: path.relative(rootDirectory, path.resolve(knipRuntime.cwd, unusedFile)),
|
|
862
|
+
engine: "code-quality",
|
|
863
|
+
rule: "knip/files",
|
|
864
|
+
severity: "warning",
|
|
865
|
+
message: KNIP_MESSAGE_MAP.files,
|
|
866
|
+
help: "This file is not imported by any other file in the project.",
|
|
867
|
+
line: 0,
|
|
868
|
+
column: 0,
|
|
869
|
+
category: "Dead Code",
|
|
870
|
+
fixable: false
|
|
871
|
+
});
|
|
872
|
+
const issues = parsed.issues ?? [];
|
|
873
|
+
const issueTypes = [
|
|
874
|
+
...DEPENDENCY_TYPES,
|
|
875
|
+
"exports",
|
|
876
|
+
"types",
|
|
877
|
+
"duplicates"
|
|
878
|
+
];
|
|
879
|
+
for (const fileIssue of issues) for (const type of issueTypes) diagnostics.push(...collectIssues(fileIssue, type, rootDirectory, knipRuntime.cwd));
|
|
880
|
+
return diagnostics;
|
|
881
|
+
} catch {
|
|
882
|
+
return [];
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
|
|
722
886
|
//#endregion
|
|
723
887
|
//#region src/engines/lint/oxlint-config.ts
|
|
724
888
|
const createOxlintConfig = (options) => {
|
|
@@ -1078,9 +1242,13 @@ const getAnonymousId = () => {
|
|
|
1078
1242
|
for (let i = 0; i < raw.length; i++) hash = hash * 33 ^ raw.charCodeAt(i);
|
|
1079
1243
|
return `aislop_${(hash >>> 0).toString(36)}`;
|
|
1080
1244
|
};
|
|
1245
|
+
/** Pending telemetry request — kept alive so Node doesn't exit before it completes. */
|
|
1246
|
+
let pendingRequest = null;
|
|
1081
1247
|
/**
|
|
1082
1248
|
* Fire-and-forget telemetry event to PostHog.
|
|
1083
|
-
* Never throws, never blocks
|
|
1249
|
+
* Never throws, never blocks CLI output.
|
|
1250
|
+
* The request is kept alive via `flushTelemetry()` so Node doesn't
|
|
1251
|
+
* exit before it completes.
|
|
1084
1252
|
*/
|
|
1085
1253
|
const trackEvent = (event) => {
|
|
1086
1254
|
const payload = {
|
|
@@ -1103,12 +1271,12 @@ const trackEvent = (event) => {
|
|
|
1103
1271
|
},
|
|
1104
1272
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1105
1273
|
};
|
|
1106
|
-
fetch(`${POSTHOG_HOST}/capture/`, {
|
|
1274
|
+
pendingRequest = fetch(`${POSTHOG_HOST}/capture/`, {
|
|
1107
1275
|
method: "POST",
|
|
1108
1276
|
headers: { "Content-Type": "application/json" },
|
|
1109
1277
|
body: JSON.stringify(payload),
|
|
1110
1278
|
signal: AbortSignal.timeout(3e3)
|
|
1111
|
-
}).catch(() => {});
|
|
1279
|
+
}).then(() => {}).catch(() => {});
|
|
1112
1280
|
};
|
|
1113
1281
|
|
|
1114
1282
|
//#endregion
|
|
@@ -1220,6 +1388,9 @@ const fixCommand = async (directory, config, options = {
|
|
|
1220
1388
|
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => fixRuffLint(resolvedDir), options));
|
|
1221
1389
|
else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python lint fixes.");
|
|
1222
1390
|
}
|
|
1391
|
+
if (config.engines["code-quality"]) {
|
|
1392
|
+
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("Unused dependencies", () => runKnipDependencyCheck(resolvedDir), () => fixUnusedDependencies(resolvedDir), options));
|
|
1393
|
+
}
|
|
1223
1394
|
if (steps.length === 0) logger.dim(" No applicable auto-fixers found for this project.");
|
|
1224
1395
|
else {
|
|
1225
1396
|
logger.break();
|
|
@@ -1273,7 +1444,8 @@ const DEFAULT_CONFIG = {
|
|
|
1273
1444
|
thresholds: {
|
|
1274
1445
|
good: 75,
|
|
1275
1446
|
ok: 50
|
|
1276
|
-
}
|
|
1447
|
+
},
|
|
1448
|
+
smoothing: 10
|
|
1277
1449
|
},
|
|
1278
1450
|
ci: {
|
|
1279
1451
|
failBelow: 0,
|
|
@@ -1373,7 +1545,8 @@ const ScoringSchema = z.object({
|
|
|
1373
1545
|
thresholds: ThresholdsSchema.default(() => ({
|
|
1374
1546
|
good: 75,
|
|
1375
1547
|
ok: 50
|
|
1376
|
-
}))
|
|
1548
|
+
})),
|
|
1549
|
+
smoothing: z.number().nonnegative().default(10)
|
|
1377
1550
|
});
|
|
1378
1551
|
const CiSchema = z.object({
|
|
1379
1552
|
failBelow: z.number().default(0),
|
|
@@ -1405,7 +1578,8 @@ const AislopConfigSchema = z.object({
|
|
|
1405
1578
|
thresholds: {
|
|
1406
1579
|
good: 75,
|
|
1407
1580
|
ok: 50
|
|
1408
|
-
}
|
|
1581
|
+
},
|
|
1582
|
+
smoothing: 10
|
|
1409
1583
|
})),
|
|
1410
1584
|
ci: CiSchema.default(() => ({
|
|
1411
1585
|
failBelow: 0,
|
|
@@ -2392,10 +2566,7 @@ const FUNCTION_PATTERNS = [
|
|
|
2392
2566
|
]
|
|
2393
2567
|
}
|
|
2394
2568
|
];
|
|
2395
|
-
const countParams = (
|
|
2396
|
-
if (!paramStr.trim()) return 0;
|
|
2397
|
-
return paramStr.split(",").length;
|
|
2398
|
-
};
|
|
2569
|
+
const countParams = (p) => p.trim() ? p.split(",").length : 0;
|
|
2399
2570
|
const matchFunctionOnLine = (line, ext) => {
|
|
2400
2571
|
for (let i = 0; i < FUNCTION_PATTERNS.length; i++) {
|
|
2401
2572
|
const pattern = FUNCTION_PATTERNS[i];
|
|
@@ -2696,108 +2867,6 @@ const checkDuplication = async (context) => {
|
|
|
2696
2867
|
return diagnostics;
|
|
2697
2868
|
};
|
|
2698
2869
|
|
|
2699
|
-
//#endregion
|
|
2700
|
-
//#region src/engines/code-quality/knip.ts
|
|
2701
|
-
const KNIP_MESSAGE_MAP = {
|
|
2702
|
-
files: "Unused file",
|
|
2703
|
-
exports: "Unused export",
|
|
2704
|
-
types: "Unused type",
|
|
2705
|
-
duplicates: "Duplicate export"
|
|
2706
|
-
};
|
|
2707
|
-
const collectIssues = (fileIssue, issueType, rootDir, knipCwd) => {
|
|
2708
|
-
const diagnostics = [];
|
|
2709
|
-
const issues = issueType === "exports" ? fileIssue.exports ?? [] : issueType === "types" ? fileIssue.types ?? [] : fileIssue.duplicates ?? [];
|
|
2710
|
-
for (const issue of issues) {
|
|
2711
|
-
const symbol = issue.name ?? issue.symbol ?? "unknown";
|
|
2712
|
-
const absolutePath = path.resolve(knipCwd, fileIssue.file);
|
|
2713
|
-
diagnostics.push({
|
|
2714
|
-
filePath: path.relative(rootDir, absolutePath),
|
|
2715
|
-
engine: "code-quality",
|
|
2716
|
-
rule: `knip/${issueType}`,
|
|
2717
|
-
severity: "warning",
|
|
2718
|
-
message: `${KNIP_MESSAGE_MAP[issueType]}: ${symbol}`,
|
|
2719
|
-
help: "",
|
|
2720
|
-
line: issue.line ?? 0,
|
|
2721
|
-
column: issue.col ?? 0,
|
|
2722
|
-
category: "Dead Code",
|
|
2723
|
-
fixable: false
|
|
2724
|
-
});
|
|
2725
|
-
}
|
|
2726
|
-
return diagnostics;
|
|
2727
|
-
};
|
|
2728
|
-
const findMonorepoRoot = (directory) => {
|
|
2729
|
-
let current = path.dirname(directory);
|
|
2730
|
-
while (current !== path.dirname(current)) {
|
|
2731
|
-
if (fs.existsSync(path.join(current, "pnpm-workspace.yaml")) || (() => {
|
|
2732
|
-
const pkgPath = path.join(current, "package.json");
|
|
2733
|
-
if (!fs.existsSync(pkgPath)) return false;
|
|
2734
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
2735
|
-
return Array.isArray(pkg.workspaces) || pkg.workspaces?.packages;
|
|
2736
|
-
})()) return current;
|
|
2737
|
-
current = path.dirname(current);
|
|
2738
|
-
}
|
|
2739
|
-
return null;
|
|
2740
|
-
};
|
|
2741
|
-
const KNIP_RELATIVE_BIN = path.join("node_modules", "knip", "bin", "knip.js");
|
|
2742
|
-
const findKnipBin = (rootDirectory, monorepoRoot) => {
|
|
2743
|
-
const localPath = path.join(rootDirectory, KNIP_RELATIVE_BIN);
|
|
2744
|
-
if (fs.existsSync(localPath)) return {
|
|
2745
|
-
binPath: localPath,
|
|
2746
|
-
cwd: rootDirectory
|
|
2747
|
-
};
|
|
2748
|
-
if (monorepoRoot) {
|
|
2749
|
-
const monorepoPath = path.join(monorepoRoot, KNIP_RELATIVE_BIN);
|
|
2750
|
-
if (fs.existsSync(monorepoPath)) return {
|
|
2751
|
-
binPath: monorepoPath,
|
|
2752
|
-
cwd: monorepoRoot
|
|
2753
|
-
};
|
|
2754
|
-
}
|
|
2755
|
-
return null;
|
|
2756
|
-
};
|
|
2757
|
-
const runKnip = async (rootDirectory) => {
|
|
2758
|
-
const knipRuntime = findKnipBin(rootDirectory, findMonorepoRoot(rootDirectory));
|
|
2759
|
-
if (!knipRuntime) return [];
|
|
2760
|
-
try {
|
|
2761
|
-
const args = [
|
|
2762
|
-
knipRuntime.binPath,
|
|
2763
|
-
"--no-progress",
|
|
2764
|
-
"--reporter",
|
|
2765
|
-
"json",
|
|
2766
|
-
"--no-exit-code"
|
|
2767
|
-
];
|
|
2768
|
-
const result = await runSubprocess(process.execPath, args, {
|
|
2769
|
-
cwd: knipRuntime.cwd,
|
|
2770
|
-
timeout: 2e4,
|
|
2771
|
-
env: { FORCE_COLOR: "0" }
|
|
2772
|
-
});
|
|
2773
|
-
if (!result.stdout) return [];
|
|
2774
|
-
const parsed = JSON.parse(result.stdout);
|
|
2775
|
-
const diagnostics = [];
|
|
2776
|
-
const files = parsed.files ?? [];
|
|
2777
|
-
for (const unusedFile of files) diagnostics.push({
|
|
2778
|
-
filePath: path.relative(rootDirectory, path.resolve(knipRuntime.cwd, unusedFile)),
|
|
2779
|
-
engine: "code-quality",
|
|
2780
|
-
rule: "knip/files",
|
|
2781
|
-
severity: "warning",
|
|
2782
|
-
message: KNIP_MESSAGE_MAP.files,
|
|
2783
|
-
help: "This file is not imported by any other file in the project.",
|
|
2784
|
-
line: 0,
|
|
2785
|
-
column: 0,
|
|
2786
|
-
category: "Dead Code",
|
|
2787
|
-
fixable: false
|
|
2788
|
-
});
|
|
2789
|
-
const issues = parsed.issues ?? [];
|
|
2790
|
-
for (const fileIssue of issues) for (const type of [
|
|
2791
|
-
"exports",
|
|
2792
|
-
"types",
|
|
2793
|
-
"duplicates"
|
|
2794
|
-
]) diagnostics.push(...collectIssues(fileIssue, type, rootDirectory, knipRuntime.cwd));
|
|
2795
|
-
return diagnostics;
|
|
2796
|
-
} catch {
|
|
2797
|
-
return [];
|
|
2798
|
-
}
|
|
2799
|
-
};
|
|
2800
|
-
|
|
2801
2870
|
//#endregion
|
|
2802
2871
|
//#region src/engines/code-quality/index.ts
|
|
2803
2872
|
const codeQualityEngine = {
|
|
@@ -3776,7 +3845,12 @@ var ScanProgressRenderer = class {
|
|
|
3776
3845
|
//#endregion
|
|
3777
3846
|
//#region src/scoring/index.ts
|
|
3778
3847
|
const PERFECT_SCORE$1 = 100;
|
|
3779
|
-
const
|
|
3848
|
+
const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
|
|
3849
|
+
if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
|
|
3850
|
+
const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
|
|
3851
|
+
return Math.max(1, filesWithDiagnostics);
|
|
3852
|
+
};
|
|
3853
|
+
const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoothing) => {
|
|
3780
3854
|
if (diagnostics.length === 0) return {
|
|
3781
3855
|
score: PERFECT_SCORE$1,
|
|
3782
3856
|
label: "Healthy"
|
|
@@ -3787,7 +3861,11 @@ const calculateScore = (diagnostics, weights, thresholds) => {
|
|
|
3787
3861
|
const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
|
|
3788
3862
|
deductions += severityPenalty * engineWeight;
|
|
3789
3863
|
}
|
|
3790
|
-
const
|
|
3864
|
+
const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
|
|
3865
|
+
const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
|
|
3866
|
+
const issueDensity = Math.min(1, diagnostics.length / (effectiveFileCount + smoothingConstant));
|
|
3867
|
+
const scaledDeductions = deductions * Math.sqrt(issueDensity);
|
|
3868
|
+
const score = Math.max(0, Math.round(PERFECT_SCORE$1 - PERFECT_SCORE$1 * Math.log1p(scaledDeductions) / Math.log1p(PERFECT_SCORE$1 + scaledDeductions)));
|
|
3791
3869
|
return {
|
|
3792
3870
|
score,
|
|
3793
3871
|
label: score >= thresholds.good ? "Healthy" : score >= thresholds.ok ? "Needs Work" : "Critical"
|
|
@@ -3971,7 +4049,7 @@ const scanCommand = async (directory, config, options) => {
|
|
|
3971
4049
|
progressRenderer?.stop();
|
|
3972
4050
|
const allDiagnostics = results.flatMap((r) => r.diagnostics);
|
|
3973
4051
|
const elapsedMs = performance.now() - startTime;
|
|
3974
|
-
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds);
|
|
4052
|
+
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
|
|
3975
4053
|
const exitCode = allDiagnostics.some((d) => d.severity === "error") || scoreResult.score < config.ci.failBelow ? 1 : 0;
|
|
3976
4054
|
if (!isTelemetryDisabled(config.telemetry?.enabled)) {
|
|
3977
4055
|
const engineIssues = {};
|
|
@@ -3991,7 +4069,7 @@ const scanCommand = async (directory, config, options) => {
|
|
|
3991
4069
|
});
|
|
3992
4070
|
}
|
|
3993
4071
|
if (options.json) {
|
|
3994
|
-
const { buildJsonOutput } = await import("./json-
|
|
4072
|
+
const { buildJsonOutput } = await import("./json-UG8l_sLC.js");
|
|
3995
4073
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
3996
4074
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
3997
4075
|
return { exitCode };
|