devkit-quality 0.1.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/LICENSE +21 -0
- package/README.md +312 -0
- package/bin/devkit.js +3 -0
- package/dist/ast/comments.js +28 -0
- package/dist/ast/parse.js +56 -0
- package/dist/ast/walk.js +69 -0
- package/dist/cli.js +167 -0
- package/dist/cliLogic.js +36 -0
- package/dist/config.js +57 -0
- package/dist/context.js +2 -0
- package/dist/discovery.js +99 -0
- package/dist/finding.js +20 -0
- package/dist/glob.js +35 -0
- package/dist/ignore.js +28 -0
- package/dist/moduleGraph.js +311 -0
- package/dist/moduleResolution.js +64 -0
- package/dist/reporters.js +258 -0
- package/dist/rules/architecture.js +57 -0
- package/dist/rules/complexity.js +230 -0
- package/dist/rules/deadCode.js +252 -0
- package/dist/rules/dependencies.js +137 -0
- package/dist/rules/duplication.js +138 -0
- package/dist/rules/errorHandling.js +120 -0
- package/dist/rules/hygiene.js +102 -0
- package/dist/rules/javascript.js +69 -0
- package/dist/rules/redundancy.js +75 -0
- package/dist/rules/secretPatterns.js +24 -0
- package/dist/rules/security.js +318 -0
- package/dist/rules/typescript.js +124 -0
- package/dist/rules.js +546 -0
- package/dist/scanner.js +150 -0
- package/dist/scoring.js +68 -0
- package/dist/suppressions.js +21 -0
- package/dist/terminal.js +135 -0
- package/dist/types.js +2 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Karan Chourasia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# DevKit
|
|
2
|
+
|
|
3
|
+
DevKit is a deterministic, offline code-quality scanner for JavaScript and TypeScript repositories. It walks a project, parses every source file with the TypeScript compiler, and reports a 0-10 "code quality" score backed by concrete, file-and-line findings — dead code, unused dependencies, excessive complexity, duplication, unsafe error handling, unsafe TypeScript, security anti-patterns, architecture violations, and repository hygiene issues.
|
|
4
|
+
|
|
5
|
+
It does not call an LLM, an API, or the network. Every finding is reproducible from the source tree alone.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx devkit scan
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
DevKit Repository Scan
|
|
13
|
+
/path/to/project
|
|
14
|
+
|
|
15
|
+
7.8/10 Good
|
|
16
|
+
[███████████████░░░░]
|
|
17
|
+
|
|
18
|
+
Dead Code 8.4
|
|
19
|
+
Dependencies 7.1
|
|
20
|
+
Complexity 6.8
|
|
21
|
+
...
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Table of contents
|
|
25
|
+
|
|
26
|
+
- [How it works](#how-it-works)
|
|
27
|
+
- [Architecture](#architecture)
|
|
28
|
+
- [Project layout](#project-layout)
|
|
29
|
+
- [Setup](#setup)
|
|
30
|
+
- [Usage](#usage)
|
|
31
|
+
- [Configuration](#configuration)
|
|
32
|
+
- [Scoring model](#scoring-model)
|
|
33
|
+
- [Output formats](#output-formats)
|
|
34
|
+
- [CI/CD integration](#cicd-integration)
|
|
35
|
+
- [Development](#development)
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
A scan runs in a single pass over the repository:
|
|
40
|
+
|
|
41
|
+
1. **Discovery** — walk the directory tree, apply configured include/exclude and ignore patterns, skip generated files, and select `.js`/`.jsx`/`.ts`/`.tsx`/`.mjs`/`.cjs` files.
|
|
42
|
+
2. **Parse** — build one `ts.Program` (via the TypeScript Compiler API) covering every discovered file, with `noUnusedLocals`/`noUnusedParameters`/`noImplicitAny` enabled so the compiler itself surfaces dead bindings and implicit `any`.
|
|
43
|
+
3. **Module graph** — resolve imports, `require()`, dynamic `import()`, TypeScript path aliases, and re-exports into an internal dependency graph (edges, reverse edges, entry points, re-export targets).
|
|
44
|
+
4. **Rules** — each rule module receives a shared `RuleContext` (files, program, module graph, config, `package.json`) and returns `Finding[]`.
|
|
45
|
+
5. **Scoring** — findings are weighted by severity × confidence, normalized against repository size, and rolled up into per-category and overall scores.
|
|
46
|
+
6. **Reporting** — the same `ScanSummary` is rendered as colored terminal output, JSON, Markdown, or SARIF.
|
|
47
|
+
|
|
48
|
+
Design principle carried over from the project's PRD (`devkit-slop-scanner-final-prd.md`): never use an LLM to answer a question static analysis can answer deterministically. DevKit only measures *observable* characteristics of the code (unused, duplicated, overly complex, unsafe) — it makes no claim about authorship or subjective quality.
|
|
49
|
+
|
|
50
|
+
## Architecture
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
devkit scan
|
|
54
|
+
|
|
|
55
|
+
v
|
|
56
|
+
scanRepository() scanner.ts
|
|
57
|
+
|
|
|
58
|
+
+-------------+--------------+
|
|
59
|
+
| | |
|
|
60
|
+
walkDirectory createProgram buildModuleGraph
|
|
61
|
+
(discovery.ts) (ast/parse.ts) (moduleGraph.ts)
|
|
62
|
+
| | |
|
|
63
|
+
+-------------+--------------+
|
|
64
|
+
|
|
|
65
|
+
v
|
|
66
|
+
RuleContext context.ts
|
|
67
|
+
|
|
|
68
|
+
+----+----+----+----+----+----+----+----+----+----+
|
|
69
|
+
| | | | | | | | | | |
|
|
70
|
+
deadCode|complexity|duplication|errorHandling|redundancy
|
|
71
|
+
dependencies typescript javascript security
|
|
72
|
+
architecture hygiene
|
|
73
|
+
(src/rules/*.ts, definitions in rules.ts)
|
|
74
|
+
+----+----+----+----+----+----+----+----+----+----+
|
|
75
|
+
|
|
|
76
|
+
v
|
|
77
|
+
Finding[] finding.ts / types.ts
|
|
78
|
+
|
|
|
79
|
+
v
|
|
80
|
+
computeScores() scoring.ts
|
|
81
|
+
|
|
|
82
|
+
v
|
|
83
|
+
ScanSummary
|
|
84
|
+
|
|
|
85
|
+
+-------------+-------------+-------------+
|
|
86
|
+
| | | |
|
|
87
|
+
formatTerminal formatJson formatMarkdown formatSarif
|
|
88
|
+
(reporters.ts)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Core modules
|
|
92
|
+
|
|
93
|
+
| Module | Responsibility |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| [`src/cli.ts`](src/cli.ts) | Commander-based CLI entry point; wires commands to the scanner and reporters. |
|
|
96
|
+
| [`src/scanner.ts`](src/scanner.ts) | Orchestrates a full scan: discovery → program → module graph → rules → metrics → scoring. |
|
|
97
|
+
| [`src/discovery.ts`](src/discovery.ts) | Directory walking, file filtering, generated/test-file detection, line counting. |
|
|
98
|
+
| [`src/ignore.ts`](src/ignore.ts) | Loads `.gitignore` and `.devkitignore` patterns for scan filtering. |
|
|
99
|
+
| [`src/ast/parse.ts`](src/ast/parse.ts) | Creates the shared `ts.Program` (compiler options, script-kind detection). |
|
|
100
|
+
| [`src/ast/walk.ts`](src/ast/walk.ts) | AST traversal helpers (`forEachNode`, function-like detection, line/column lookup). |
|
|
101
|
+
| [`src/ast/comments.ts`](src/ast/comments.ts) | Collects and caches comment ranges for hygiene checks and suppressions. |
|
|
102
|
+
| [`src/moduleResolution.ts`](src/moduleResolution.ts) | Resolves modules using TypeScript config, path aliases, and a per-scan cache. |
|
|
103
|
+
| [`src/moduleGraph.ts`](src/moduleGraph.ts) | Resolves imports/exports/re-exports into an internal dependency graph; detects entry points and import cycles. |
|
|
104
|
+
| [`src/suppressions.ts`](src/suppressions.ts) | Applies `devkit-disable-next-line` directives to findings. |
|
|
105
|
+
| [`src/cliLogic.ts`](src/cliLogic.ts) | Pure finding-filter and quality-gate logic used by the CLI. |
|
|
106
|
+
| [`src/config.ts`](src/config.ts) | Loads/writes `.devkitrc.json`; rule enable/disable and threshold overrides. |
|
|
107
|
+
| [`src/context.ts`](src/context.ts) | `RuleContext` type shared by every rule module. |
|
|
108
|
+
| [`src/rules.ts`](src/rules.ts) | Declarative catalog of every rule's metadata (id, severity, confidence, docs) — consumed by `devkit rules`/`devkit explain`. |
|
|
109
|
+
| [`src/rules/*.ts`](src/rules) | One module per category; each exports a `run*Rules(context)` function that returns `Finding[]`. |
|
|
110
|
+
| [`src/finding.ts`](src/finding.ts) | `buildFinding()` constructs a `Finding` with a stable id. |
|
|
111
|
+
| [`src/scoring.ts`](src/scoring.ts) | Converts findings into category scores and an overall 0-10 score. |
|
|
112
|
+
| [`src/reporters.ts`](src/reporters.ts) | Terminal, JSON, Markdown, and SARIF renderers, plus baseline-compare output. |
|
|
113
|
+
| [`src/terminal.ts`](src/terminal.ts) | ANSI color helpers, score bar, code-frame rendering, spinner. |
|
|
114
|
+
| [`src/glob.ts`](src/glob.ts) | Minimal glob-to-regex matcher used for config include/exclude patterns. |
|
|
115
|
+
|
|
116
|
+
### Rule categories implemented
|
|
117
|
+
|
|
118
|
+
| Category | Rule IDs | File |
|
|
119
|
+
| --- | --- | --- |
|
|
120
|
+
| Dead code | `DEAD002`–`DEAD011` | [`src/rules/deadCode.ts`](src/rules/deadCode.ts) |
|
|
121
|
+
| Dependencies | `DEP001`–`DEP003` | [`src/rules/dependencies.ts`](src/rules/dependencies.ts) |
|
|
122
|
+
| Complexity | `COMPLEX001`–`COMPLEX005` | [`src/rules/complexity.ts`](src/rules/complexity.ts) |
|
|
123
|
+
| Duplication | `DUP001` | [`src/rules/duplication.ts`](src/rules/duplication.ts) |
|
|
124
|
+
| Error handling | `ERR001`–`ERR004` | [`src/rules/errorHandling.ts`](src/rules/errorHandling.ts) |
|
|
125
|
+
| Redundant logic | `REDUNDANT001`–`REDUNDANT002` | [`src/rules/redundancy.ts`](src/rules/redundancy.ts) |
|
|
126
|
+
| TypeScript safety | `TS001`–`TS003` | [`src/rules/typescript.ts`](src/rules/typescript.ts) |
|
|
127
|
+
| JavaScript hygiene | `JS001`–`JS002` | [`src/rules/javascript.ts`](src/rules/javascript.ts) |
|
|
128
|
+
| Security | `SEC001`–`SEC010` (SEC006 reserved) | [`src/rules/security.ts`](src/rules/security.ts) |
|
|
129
|
+
| Architecture | `ARCH001` | [`src/rules/architecture.ts`](src/rules/architecture.ts) |
|
|
130
|
+
| Hygiene | `HYGIENE001`–`HYGIENE004` | [`src/rules/hygiene.ts`](src/rules/hygiene.ts) |
|
|
131
|
+
|
|
132
|
+
Run `devkit rules` for the live list, or `devkit explain <RULE_ID>` for a rule's full writeup (why it matters, example, fix classification).
|
|
133
|
+
|
|
134
|
+
> **Note:** `devkit-slop-scanner-final-prd.md` in the repo root is the original product-requirements document and describes a larger target surface (incremental caching, monorepo-aware scoring, rule presets, a V2 LLM layer, and more). The table above reflects what is currently implemented in `src/`; treat the PRD as the roadmap, not the current feature set. Minimal `devkit-disable-next-line` suppressions are implemented.
|
|
135
|
+
|
|
136
|
+
## Project layout
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
cli-tool/
|
|
140
|
+
├── bin/devkit.js # Shebang entry point, requires dist/cli.js
|
|
141
|
+
├── src/
|
|
142
|
+
│ ├── cli.ts # Command definitions (scan, report, metrics, rules, explain, baseline, fix, init)
|
|
143
|
+
│ ├── scanner.ts # scanRepository() — the main pipeline
|
|
144
|
+
│ ├── discovery.ts # File walking/filtering
|
|
145
|
+
│ ├── moduleGraph.ts # Import/export graph + cycle detection
|
|
146
|
+
│ ├── config.ts # .devkitrc.json loading
|
|
147
|
+
│ ├── context.ts # RuleContext type
|
|
148
|
+
│ ├── rules.ts # Rule metadata catalog
|
|
149
|
+
│ ├── rules/ # One file per rule category (the detectors)
|
|
150
|
+
│ ├── ast/ # TypeScript Compiler API helpers
|
|
151
|
+
│ ├── finding.ts # Finding constructor
|
|
152
|
+
│ ├── scoring.ts # Score computation
|
|
153
|
+
│ ├── reporters.ts # terminal/json/markdown/sarif output
|
|
154
|
+
│ ├── terminal.ts # Color/formatting primitives
|
|
155
|
+
│ └── __tests__/ # Vitest suite, one file per rule category + e2e
|
|
156
|
+
├── dist/ # Compiled output (tsc), what bin/devkit.js actually runs
|
|
157
|
+
├── tsconfig.json
|
|
158
|
+
└── package.json
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Setup
|
|
162
|
+
|
|
163
|
+
Requires Node.js (for the TypeScript compiler API and `fs`/`path` usage — any reasonably current LTS version works).
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# install dependencies
|
|
167
|
+
npm install
|
|
168
|
+
|
|
169
|
+
# build (compiles src/ -> dist/, which bin/devkit.js requires)
|
|
170
|
+
npm run build
|
|
171
|
+
|
|
172
|
+
# run against the current directory
|
|
173
|
+
node dist/cli.js scan
|
|
174
|
+
# or, once linked/installed as a package:
|
|
175
|
+
devkit scan
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
For local development without a build step, run the CLI directly through `tsx`:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
npm run dev -- scan # tsx src/cli.ts scan
|
|
182
|
+
npm run scan # shortcut for the above
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
To use the `devkit` command globally from this checkout:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
npm run build
|
|
189
|
+
npm link # exposes `devkit` on PATH via the "bin" entry in package.json
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Usage
|
|
193
|
+
|
|
194
|
+
Initialize a config file in a target project:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
devkit init
|
|
198
|
+
# writes .devkitrc.json with default include/exclude globs
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Run a full scan (defaults to colored terminal output):
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
devkit scan
|
|
205
|
+
devkit scan --json # machine-readable JSON (ScanSummary)
|
|
206
|
+
devkit scan --format markdown # Markdown report
|
|
207
|
+
devkit scan --format sarif # SARIF, for code-scanning platforms
|
|
208
|
+
devkit scan --category dead-code # only one category
|
|
209
|
+
devkit scan --severity high # only one severity level
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Other commands:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
devkit metrics # repository metrics: LOC, function/class counts, largest files, etc.
|
|
216
|
+
devkit report # full Markdown report (same as `scan --format markdown`)
|
|
217
|
+
devkit rules # list every built-in rule (id, title, category)
|
|
218
|
+
devkit explain DEAD010 # full explanation of a single rule
|
|
219
|
+
devkit baseline create # snapshot the current score to .devkit/baseline.json
|
|
220
|
+
devkit baseline compare # compare current score against the stored baseline
|
|
221
|
+
devkit fix # preview findings marked as safe to fix
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Configuration
|
|
225
|
+
|
|
226
|
+
`devkit init` creates `.devkitrc.json` in the project root:
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"project": { "name": "my-project" },
|
|
231
|
+
"scan": {
|
|
232
|
+
"include": ["src/**", "packages/**"],
|
|
233
|
+
"exclude": ["node_modules/**", "dist/**", "coverage/**", ".git/**", ".devkit/**"]
|
|
234
|
+
},
|
|
235
|
+
"rules": {}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Per-rule overrides live under `rules`:
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{
|
|
243
|
+
"rules": {
|
|
244
|
+
"COMPLEX001": { "max": 15 },
|
|
245
|
+
"JS001": { "enabled": false }
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Architecture-layer rules (`ARCH001`) are opt-in and only produce findings once configured:
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
{
|
|
254
|
+
"architecture": {
|
|
255
|
+
"layers": {
|
|
256
|
+
"controllers": { "match": ["src/controllers/**"], "cannotImport": ["database"] },
|
|
257
|
+
"database": { "match": ["src/database/**"], "cannotImport": [] }
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Scoring model
|
|
264
|
+
|
|
265
|
+
Each finding is weighted by `severity × confidence` ([`src/scoring.ts`](src/scoring.ts)) and normalized against source LOC (in 500-line chunks), so a single low-confidence finding in a large repository barely moves the score. Category scores are combined into the overall score using fixed weights:
|
|
266
|
+
|
|
267
|
+
| Category | Weight |
|
|
268
|
+
| --- | ---: |
|
|
269
|
+
| Dead code | 20% |
|
|
270
|
+
| Complexity | 15% |
|
|
271
|
+
| Dependencies | 10% |
|
|
272
|
+
| Duplication | 10% |
|
|
273
|
+
| Redundant logic | 10% |
|
|
274
|
+
| Error handling | 10% |
|
|
275
|
+
| Type safety | 10% |
|
|
276
|
+
| Architecture | 10% |
|
|
277
|
+
| Hygiene | 5% |
|
|
278
|
+
|
|
279
|
+
Security findings are scored and reported separately rather than folded into the overall score.
|
|
280
|
+
|
|
281
|
+
## Output formats
|
|
282
|
+
|
|
283
|
+
- **Terminal** (default) — colored, with a score bar, per-category breakdown, and code frames for the top finding per category.
|
|
284
|
+
- **JSON** (`--json` / `--format json`) — the full `ScanSummary` object: score, category scores, every finding, repository metrics.
|
|
285
|
+
- **Markdown** (`--format markdown` / `devkit report`) — category table, top deductions, and a flat findings list, suitable for pasting into a PR description.
|
|
286
|
+
- **SARIF** (`--format sarif`) — standard SARIF 2.1.0, for GitHub code scanning and similar tools.
|
|
287
|
+
|
|
288
|
+
## Current limitations
|
|
289
|
+
|
|
290
|
+
- Framework entry-point detection is limited to package metadata, tests, and a basic Next.js convention check. Other React setups such as Vite may need entry files specified through package metadata or imports.
|
|
291
|
+
- `.gitignore` and `.devkitignore` negation patterns (`!pattern`) are skipped; full Git ignore semantics are not implemented.
|
|
292
|
+
- `devkit fix` only previews findings marked as safe. It does not modify files.
|
|
293
|
+
- `devkit baseline compare` compares the overall score only; it does not report newly added or resolved findings.
|
|
294
|
+
- DevKit has no rule presets, React-specific or test-quality rules, configuration analysis, incremental cache, or monorepo-aware scoring.
|
|
295
|
+
|
|
296
|
+
## CI/CD integration
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
devkit scan --min-score 7 # exit 1 if the overall score falls below 7
|
|
300
|
+
devkit scan --fail-on high # exit 1 if any HIGH or CRITICAL finding exists
|
|
301
|
+
devkit baseline compare # compare against a previously stored baseline
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Development
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
npm run build # tsc -p tsconfig.json
|
|
308
|
+
npm test # vitest run
|
|
309
|
+
npm run dev # tsx src/cli.ts (no build step)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Tests live in [`src/__tests__/`](src/__tests__), one file per rule category plus an end-to-end scan test (`e2e.test.ts`) and a scoring test. Each rule test typically builds a small in-memory `RuleContext` (see [`testUtils.ts`](src/__tests__/testUtils.ts)) and asserts on the findings a rule produces for known-good and known-bad snippets.
|
package/bin/devkit.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.collectCommentRanges = collectCommentRanges;
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
const COMMENT_RANGE_CACHE = new WeakMap();
|
|
9
|
+
function collectCommentRanges(sourceFile) {
|
|
10
|
+
const cached = COMMENT_RANGE_CACHE.get(sourceFile);
|
|
11
|
+
if (cached)
|
|
12
|
+
return cached;
|
|
13
|
+
const ranges = new Map();
|
|
14
|
+
const addAt = (position) => {
|
|
15
|
+
for (const range of typescript_1.default.getLeadingCommentRanges(sourceFile.text, position) ?? []) {
|
|
16
|
+
ranges.set(`${range.pos}:${range.end}`, range);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
addAt(sourceFile.getFullStart());
|
|
20
|
+
const visit = (node) => {
|
|
21
|
+
addAt(node.getFullStart());
|
|
22
|
+
typescript_1.default.forEachChild(node, visit);
|
|
23
|
+
};
|
|
24
|
+
visit(sourceFile);
|
|
25
|
+
const result = [...ranges.values()].sort((a, b) => a.pos - b.pos);
|
|
26
|
+
COMMENT_RANGE_CACHE.set(sourceFile, result);
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PROGRAM_COMPILER_OPTIONS = void 0;
|
|
7
|
+
exports.parseSourceFile = parseSourceFile;
|
|
8
|
+
exports.createProgram = createProgram;
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
11
|
+
function scriptKindFor(filePath) {
|
|
12
|
+
const ext = node_path_1.default.extname(filePath).toLowerCase();
|
|
13
|
+
switch (ext) {
|
|
14
|
+
case '.tsx':
|
|
15
|
+
return typescript_1.default.ScriptKind.TSX;
|
|
16
|
+
case '.jsx':
|
|
17
|
+
return typescript_1.default.ScriptKind.JSX;
|
|
18
|
+
case '.ts':
|
|
19
|
+
return typescript_1.default.ScriptKind.TS;
|
|
20
|
+
case '.mjs':
|
|
21
|
+
case '.cjs':
|
|
22
|
+
case '.js':
|
|
23
|
+
return typescript_1.default.ScriptKind.JS;
|
|
24
|
+
default:
|
|
25
|
+
return typescript_1.default.ScriptKind.Unknown;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function parseSourceFile(filePath, text) {
|
|
29
|
+
return typescript_1.default.createSourceFile(filePath, text, typescript_1.default.ScriptTarget.ES2022, true, scriptKindFor(filePath));
|
|
30
|
+
}
|
|
31
|
+
exports.PROGRAM_COMPILER_OPTIONS = {
|
|
32
|
+
target: typescript_1.default.ScriptTarget.ES2022,
|
|
33
|
+
module: typescript_1.default.ModuleKind.ESNext,
|
|
34
|
+
moduleResolution: typescript_1.default.ModuleResolutionKind.Bundler,
|
|
35
|
+
jsx: typescript_1.default.JsxEmit.ReactJSX,
|
|
36
|
+
allowJs: true,
|
|
37
|
+
checkJs: true,
|
|
38
|
+
noEmit: true,
|
|
39
|
+
skipLibCheck: true,
|
|
40
|
+
noUnusedLocals: true,
|
|
41
|
+
noUnusedParameters: true,
|
|
42
|
+
noImplicitAny: true,
|
|
43
|
+
strictNullChecks: true,
|
|
44
|
+
strict: false
|
|
45
|
+
};
|
|
46
|
+
function createProgram(files) {
|
|
47
|
+
// The default host created implicitly by ts.createProgram does not set parent pointers on
|
|
48
|
+
// parsed nodes, which breaks node.getStart()/getEnd()/getText() used throughout the rule
|
|
49
|
+
// engine. Build the host explicitly with setParentNodes enabled.
|
|
50
|
+
const host = typescript_1.default.createCompilerHost(exports.PROGRAM_COMPILER_OPTIONS, true);
|
|
51
|
+
return typescript_1.default.createProgram({
|
|
52
|
+
rootNames: files,
|
|
53
|
+
options: exports.PROGRAM_COMPILER_OPTIONS,
|
|
54
|
+
host
|
|
55
|
+
});
|
|
56
|
+
}
|
package/dist/ast/walk.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.forEachNode = forEachNode;
|
|
7
|
+
exports.isFunctionLike = isFunctionLike;
|
|
8
|
+
exports.getFunctionName = getFunctionName;
|
|
9
|
+
exports.lineAndColumn = lineAndColumn;
|
|
10
|
+
exports.findNodeAtPosition = findNodeAtPosition;
|
|
11
|
+
exports.isStatementContainer = isStatementContainer;
|
|
12
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
13
|
+
function forEachNode(root, visitor) {
|
|
14
|
+
visitor(root);
|
|
15
|
+
typescript_1.default.forEachChild(root, (child) => forEachNode(child, visitor));
|
|
16
|
+
}
|
|
17
|
+
function isFunctionLike(node) {
|
|
18
|
+
return (typescript_1.default.isFunctionDeclaration(node) ||
|
|
19
|
+
typescript_1.default.isFunctionExpression(node) ||
|
|
20
|
+
typescript_1.default.isArrowFunction(node) ||
|
|
21
|
+
typescript_1.default.isMethodDeclaration(node) ||
|
|
22
|
+
typescript_1.default.isConstructorDeclaration(node) ||
|
|
23
|
+
typescript_1.default.isGetAccessorDeclaration(node) ||
|
|
24
|
+
typescript_1.default.isSetAccessorDeclaration(node));
|
|
25
|
+
}
|
|
26
|
+
function getFunctionName(node) {
|
|
27
|
+
if (node.name && typescript_1.default.isIdentifier(node.name)) {
|
|
28
|
+
return node.name.text;
|
|
29
|
+
}
|
|
30
|
+
if (typescript_1.default.isConstructorDeclaration(node)) {
|
|
31
|
+
return 'constructor';
|
|
32
|
+
}
|
|
33
|
+
const parent = node.parent;
|
|
34
|
+
if (parent) {
|
|
35
|
+
if (typescript_1.default.isVariableDeclaration(parent) && typescript_1.default.isIdentifier(parent.name)) {
|
|
36
|
+
return parent.name.text;
|
|
37
|
+
}
|
|
38
|
+
if (typescript_1.default.isPropertyAssignment(parent) && typescript_1.default.isIdentifier(parent.name)) {
|
|
39
|
+
return parent.name.text;
|
|
40
|
+
}
|
|
41
|
+
if (typescript_1.default.isPropertyDeclaration(parent) && typescript_1.default.isIdentifier(parent.name)) {
|
|
42
|
+
return parent.name.text;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return 'anonymous';
|
|
46
|
+
}
|
|
47
|
+
function lineAndColumn(sourceFile, pos) {
|
|
48
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos);
|
|
49
|
+
return { line: line + 1, column: character + 1 };
|
|
50
|
+
}
|
|
51
|
+
function findNodeAtPosition(root, pos) {
|
|
52
|
+
let best = root;
|
|
53
|
+
const visit = (node) => {
|
|
54
|
+
if (pos < node.getFullStart() || pos >= node.getEnd()) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
best = node;
|
|
58
|
+
typescript_1.default.forEachChild(node, visit);
|
|
59
|
+
};
|
|
60
|
+
visit(root);
|
|
61
|
+
return best;
|
|
62
|
+
}
|
|
63
|
+
function isStatementContainer(node) {
|
|
64
|
+
return (typescript_1.default.isBlock(node) ||
|
|
65
|
+
typescript_1.default.isSourceFile(node) ||
|
|
66
|
+
typescript_1.default.isCaseClause(node) ||
|
|
67
|
+
typescript_1.default.isDefaultClause(node) ||
|
|
68
|
+
typescript_1.default.isModuleBlock(node));
|
|
69
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const commander_1 = require("commander");
|
|
10
|
+
const config_1 = require("./config");
|
|
11
|
+
const rules_1 = require("./rules");
|
|
12
|
+
const reporters_1 = require("./reporters");
|
|
13
|
+
const scanner_1 = require("./scanner");
|
|
14
|
+
const scoring_1 = require("./scoring");
|
|
15
|
+
const terminal_1 = require("./terminal");
|
|
16
|
+
const cliLogic_1 = require("./cliLogic");
|
|
17
|
+
async function runWithSpinner(text, task) {
|
|
18
|
+
const spinner = new terminal_1.Spinner(text);
|
|
19
|
+
spinner.start();
|
|
20
|
+
await (0, terminal_1.waitForNextTick)();
|
|
21
|
+
try {
|
|
22
|
+
return task();
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
spinner.stop();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const program = new commander_1.Command();
|
|
29
|
+
program.name('devkit').description('Deterministic repository-quality scanner for JavaScript and TypeScript projects').version('0.1.0');
|
|
30
|
+
program
|
|
31
|
+
.command('init')
|
|
32
|
+
.description('Create a default DevKit config file in the current project')
|
|
33
|
+
.action(() => {
|
|
34
|
+
const configPath = (0, config_1.writeConfig)(process.cwd());
|
|
35
|
+
console.log(`Created config at ${configPath}`);
|
|
36
|
+
});
|
|
37
|
+
async function runScan(options) {
|
|
38
|
+
const summary = await runWithSpinner('Scanning repository...', () => (0, scanner_1.scanRepository)(process.cwd()));
|
|
39
|
+
const filteredFindings = (0, cliLogic_1.filterFindings)(summary.findings, options);
|
|
40
|
+
const filteredSummary = filteredFindings.length === summary.findings.length
|
|
41
|
+
? summary
|
|
42
|
+
: {
|
|
43
|
+
...summary,
|
|
44
|
+
findings: filteredFindings,
|
|
45
|
+
...(0, scoring_1.computeScores)(filteredFindings, summary.metrics.sourceLOC)
|
|
46
|
+
};
|
|
47
|
+
const outputType = options.json ? 'json' : (options.format ?? 'terminal');
|
|
48
|
+
const output = outputType === 'json'
|
|
49
|
+
? (0, reporters_1.formatJson)(filteredSummary)
|
|
50
|
+
: outputType === 'markdown'
|
|
51
|
+
? (0, reporters_1.formatMarkdown)(filteredSummary)
|
|
52
|
+
: outputType === 'sarif'
|
|
53
|
+
? (0, reporters_1.formatSarif)(filteredSummary)
|
|
54
|
+
: (0, reporters_1.formatTerminal)(filteredSummary);
|
|
55
|
+
console.log(output);
|
|
56
|
+
if (options.failOn && !(0, cliLogic_1.isKnownSeverity)(options.failOn))
|
|
57
|
+
console.error(`Unknown severity: ${options.failOn}`);
|
|
58
|
+
return (0, cliLogic_1.determineExitFailure)(filteredSummary, options);
|
|
59
|
+
}
|
|
60
|
+
program
|
|
61
|
+
.command('scan')
|
|
62
|
+
.description('Scan the current repository for code-quality issues')
|
|
63
|
+
.option('--json', 'Output JSON instead of terminal text')
|
|
64
|
+
.option('--format <type>', 'Output format: terminal, json, markdown, sarif')
|
|
65
|
+
.option('--category <name>', 'Filter by category')
|
|
66
|
+
.option('--severity <level>', 'Filter by severity')
|
|
67
|
+
.option('--min-score <score>', 'Fail with exit code 1 when score falls below this threshold')
|
|
68
|
+
.option('--fail-on <severity>', 'Fail with exit code 1 when any finding at or above this severity exists')
|
|
69
|
+
.action(async (options) => {
|
|
70
|
+
if (await runScan(options)) {
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
program
|
|
75
|
+
.command('report')
|
|
76
|
+
.description('Generate a detailed Markdown report of the current repository')
|
|
77
|
+
.action(async () => {
|
|
78
|
+
const summary = await runWithSpinner('Scanning repository...', () => (0, scanner_1.scanRepository)(process.cwd()));
|
|
79
|
+
console.log((0, reporters_1.formatMarkdown)(summary));
|
|
80
|
+
});
|
|
81
|
+
program
|
|
82
|
+
.command('metrics')
|
|
83
|
+
.description('Print repository metrics for the current project')
|
|
84
|
+
.action(async () => {
|
|
85
|
+
const summary = await runWithSpinner('Scanning repository...', () => (0, scanner_1.scanRepository)(process.cwd()));
|
|
86
|
+
console.log((0, reporters_1.formatMetrics)(summary));
|
|
87
|
+
});
|
|
88
|
+
program
|
|
89
|
+
.command('rules')
|
|
90
|
+
.description('List the available built-in rules')
|
|
91
|
+
.action(() => {
|
|
92
|
+
for (const rule of (0, rules_1.listRules)()) {
|
|
93
|
+
console.log(`${rule.id} - ${rule.title} (${rule.category})`);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
program
|
|
97
|
+
.command('explain <ruleId>')
|
|
98
|
+
.description('Explain a rule by its ID')
|
|
99
|
+
.action((ruleId) => {
|
|
100
|
+
const rule = (0, rules_1.getRuleById)(ruleId.toUpperCase());
|
|
101
|
+
if (!rule) {
|
|
102
|
+
console.error(`Unknown rule: ${ruleId}`);
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
console.log(`${rule.id}: ${rule.title}`);
|
|
107
|
+
console.log(`Category: ${rule.category}`);
|
|
108
|
+
console.log(`Severity: ${rule.severity}`);
|
|
109
|
+
console.log(`Confidence: ${rule.confidence}`);
|
|
110
|
+
console.log(`Description: ${rule.description}`);
|
|
111
|
+
console.log(`Why it matters: ${rule.why}`);
|
|
112
|
+
console.log(`Example: ${rule.example}`);
|
|
113
|
+
console.log(`Fix classification: ${rule.fixClassification}`);
|
|
114
|
+
if (rule.defaultThreshold !== undefined) {
|
|
115
|
+
console.log(`Default threshold: ${rule.defaultThreshold}`);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
program
|
|
119
|
+
.command('baseline <action>')
|
|
120
|
+
.description('Create or compare a baseline score stored under .devkit')
|
|
121
|
+
.action(async (action) => {
|
|
122
|
+
const root = process.cwd();
|
|
123
|
+
const dir = node_path_1.default.join(root, '.devkit');
|
|
124
|
+
const baselineFile = node_path_1.default.join(dir, 'baseline.json');
|
|
125
|
+
if (!node_fs_1.default.existsSync(dir)) {
|
|
126
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
127
|
+
}
|
|
128
|
+
if (action === 'create') {
|
|
129
|
+
const summary = await runWithSpinner('Scanning repository...', () => (0, scanner_1.scanRepository)(root));
|
|
130
|
+
node_fs_1.default.writeFileSync(baselineFile, JSON.stringify({ score: summary.score }, null, 2));
|
|
131
|
+
console.log(`Created baseline at ${baselineFile} with score ${summary.score.toFixed(1)}`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (action === 'compare') {
|
|
135
|
+
if (!node_fs_1.default.existsSync(baselineFile)) {
|
|
136
|
+
console.error('No baseline exists. Run "devkit baseline create" first.');
|
|
137
|
+
process.exitCode = 1;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const previousBaseline = JSON.parse(node_fs_1.default.readFileSync(baselineFile, 'utf8'));
|
|
141
|
+
const current = await runWithSpinner('Scanning repository...', () => (0, scanner_1.scanRepository)(root));
|
|
142
|
+
const previousScore = Number(previousBaseline.score ?? current.score);
|
|
143
|
+
console.log((0, reporters_1.formatBaselineCompare)(previousScore, current.score));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
console.error(`Unsupported baseline action: ${action}`);
|
|
147
|
+
process.exitCode = 1;
|
|
148
|
+
});
|
|
149
|
+
program
|
|
150
|
+
.command('fix')
|
|
151
|
+
.description('Preview findings marked as safe to fix')
|
|
152
|
+
.action(async () => {
|
|
153
|
+
const summary = await runWithSpinner('Scanning repository...', () => (0, scanner_1.scanRepository)(process.cwd()));
|
|
154
|
+
const safeFixes = summary.findings.filter((finding) => finding.fixAvailable);
|
|
155
|
+
console.log('Safe fix preview');
|
|
156
|
+
if (safeFixes.length === 0) {
|
|
157
|
+
console.log('No auto-fixable findings were detected.');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
for (const fix of safeFixes.slice(0, 10)) {
|
|
161
|
+
console.log(`- ${fix.ruleId}: ${fix.file}:${fix.line} - ${fix.message}`);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
165
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
166
|
+
process.exitCode = 1;
|
|
167
|
+
});
|