code-gauge 5.1.0 → 7.0.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 +208 -80
- package/dist/checkCommand.cjs +5 -0
- package/dist/checkCommand.cjs.map +1 -0
- package/dist/checkCommand.d.ts +12 -0
- package/dist/checkCommand.js +5 -0
- package/dist/checkCommand.js.map +1 -0
- package/dist/cli.cjs +3 -3
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/cliConfig.cjs +1 -1
- package/dist/cliConfig.cjs.map +1 -1
- package/dist/cliConfig.d.ts +3 -11
- package/dist/cliConfig.js +1 -1
- package/dist/cliConfig.js.map +1 -1
- package/dist/exclusion.cjs +1 -1
- package/dist/exclusion.cjs.map +1 -1
- package/dist/exclusion.d.ts +2 -2
- package/dist/exclusion.js +1 -1
- package/dist/exclusion.js.map +1 -1
- package/dist/git.cjs +1 -1
- package/dist/git.cjs.map +1 -1
- package/dist/git.d.ts +13 -12
- package/dist/git.js +1 -1
- package/dist/git.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.js +1 -1
- package/dist/metrics.cjs +1 -1
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.ts +1 -2
- package/dist/metrics.js +1 -1
- package/dist/metrics.js.map +1 -1
- package/dist/nativeMetrics.cjs +1 -1
- package/dist/nativeMetrics.cjs.map +1 -1
- package/dist/nativeMetrics.js +1 -1
- package/dist/nativeMetrics.js.map +1 -1
- package/dist/scan.cjs +1 -1
- package/dist/scan.cjs.map +1 -1
- package/dist/scan.d.ts +10 -25
- package/dist/scan.js +1 -1
- package/dist/scan.js.map +1 -1
- package/dist/thresholdCheck.cjs +2 -0
- package/dist/thresholdCheck.cjs.map +1 -0
- package/dist/thresholdCheck.d.ts +36 -0
- package/dist/thresholdCheck.js +2 -0
- package/dist/thresholdCheck.js.map +1 -0
- package/dist/thresholds.cjs +2 -0
- package/dist/thresholds.cjs.map +1 -0
- package/dist/thresholds.d.ts +38 -0
- package/dist/thresholds.js +2 -0
- package/dist/thresholds.js.map +1 -0
- package/dist/types.d.ts +8 -4
- package/dist/worker.cjs +1 -1
- package/dist/worker.cjs.map +1 -1
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/package.json +9 -12
- package/dist/diffCommand.cjs +0 -5
- package/dist/diffCommand.cjs.map +0 -1
- package/dist/diffCommand.d.ts +0 -17
- package/dist/diffCommand.js +0 -5
- package/dist/diffCommand.js.map +0 -1
- package/dist/newDuplication.cjs +0 -2
- package/dist/newDuplication.cjs.map +0 -1
- package/dist/newDuplication.d.ts +0 -40
- package/dist/newDuplication.js +0 -2
- package/dist/newDuplication.js.map +0 -1
- package/dist/regressionGate.cjs +0 -2
- package/dist/regressionGate.cjs.map +0 -1
- package/dist/regressionGate.d.ts +0 -108
- package/dist/regressionGate.js +0 -2
- package/dist/regressionGate.js.map +0 -1
- package/scripts/installNative.mjs +0 -96
package/README.md
CHANGED
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
[](https://github.com/semantic-release/semantic-release)
|
|
8
8
|
[](https://github.com/WillBooster/shared/tree/main/packages/wbfy)
|
|
9
9
|
|
|
10
|
-
A command-line tool that ranks the files of a project by refactoring priority and
|
|
11
|
-
against metric
|
|
12
|
-
repository" runs `code-gauge` and starts from the top of the list,
|
|
13
|
-
|
|
10
|
+
A command-line tool that ranks the files of a project by refactoring priority and checks code
|
|
11
|
+
against metric thresholds, built for AI-agent workflows: an agent asked to "refactor this
|
|
12
|
+
repository" runs `code-gauge` and starts from the top of the list, `code-gauge check` lists every
|
|
13
|
+
function, file, and duplicated block over a threshold, and a PR pipeline runs
|
|
14
|
+
`code-gauge check --base main` to make the agent fix the violations in the code its change touches.
|
|
14
15
|
Measurement uses tree-sitter, and the output is deliberately small — only the metrics that tell an
|
|
15
16
|
agent _what to change_ are measured and reported, so nothing in the output anchors an agent toward
|
|
16
17
|
out-of-scope "improvements". A [programmatic API](#programmatic-api) is also available.
|
|
@@ -50,13 +51,16 @@ over three dimensions:
|
|
|
50
51
|
- **File NCSS** — non-commenting source statements, a comment- and formatting-independent size
|
|
51
52
|
measure calibrated against PMD's `NcssCount`.
|
|
52
53
|
|
|
53
|
-
Ranking is relative to the scanned project
|
|
54
|
-
|
|
54
|
+
Ranking is relative to the scanned project and ignores the
|
|
55
|
+
[thresholds](#threshold-check-code-gauge-check): the top of the list is worth refactoring first
|
|
56
|
+
regardless of where any cutoff would sit. Each reported file carries
|
|
55
57
|
the concrete evidence (worst function with location, duplication with partner files, file size) so
|
|
56
58
|
an agent can act on it directly.
|
|
57
59
|
|
|
58
60
|
## Options
|
|
59
61
|
|
|
62
|
+
The ranking command (`code-gauge [target]`, or spelled out, `code-gauge rank [target]`) accepts:
|
|
63
|
+
|
|
60
64
|
| Option | Description |
|
|
61
65
|
| ------------------------------------------ | ------------------------------------------------------------------------------- |
|
|
62
66
|
| `--config <path>` | Use this config file instead of the auto-detected `code-gauge.config.json`. |
|
|
@@ -68,51 +72,162 @@ an agent can act on it directly.
|
|
|
68
72
|
| `--duplication-max-gap-tokens <n>` | Maximum token gap merged into one gapped clone group; 0 disables (default 30). |
|
|
69
73
|
| `--duplication-min-similarity-percent <n>` | Minimum similarity percent for near-miss clones; 100 = exact only (default 70). |
|
|
70
74
|
|
|
71
|
-
##
|
|
75
|
+
## Threshold check (`code-gauge check`)
|
|
72
76
|
|
|
73
77
|
```sh
|
|
74
|
-
code-gauge
|
|
75
|
-
code-gauge
|
|
78
|
+
code-gauge check # every violation in the current directory
|
|
79
|
+
code-gauge check --base main # only violations in what the working tree changed since main
|
|
80
|
+
code-gauge check --base main src/api # the same, for the changed files under src/api
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`code-gauge check [target]` reports every violation of the thresholds below in the files under
|
|
84
|
+
the target. Inside a git repository it measures the repository's git-visible files (tracked, or
|
|
85
|
+
untracked and not ignored), so a block copied from a file outside the target is reported with that
|
|
86
|
+
file as its partner, and a copy that exists only in a git-ignored file is not duplication. The
|
|
87
|
+
built-in skipped directory names (`vendor`, `fixtures`, test directories, ...) apply below the
|
|
88
|
+
target, as in the ranking command, so `code-gauge check vendor` checks what is in `vendor`. A
|
|
89
|
+
git-ignored file is checked only when it is the target itself, and a directory git ignores (itself or
|
|
90
|
+
through a parent directory) is an error as the target (exit code 2). A directory git does not
|
|
91
|
+
ignore passes as a check of zero files when it holds no git-visible source file, for example when
|
|
92
|
+
its only sources are ignored build output. Outside a git repository it walks the target directory
|
|
93
|
+
like the ranking command. The thresholds:
|
|
94
|
+
|
|
95
|
+
| Config key | Violation | Default |
|
|
96
|
+
| --------------------------------- | ------------------------------------------------- | ------- |
|
|
97
|
+
| `maxFunctionCognitiveComplexity` | a function's cognitive complexity is above it | 15 |
|
|
98
|
+
| `maxFunctionCyclomaticComplexity` | a function's cyclomatic complexity is above it | 10 |
|
|
99
|
+
| `maxFunctionNcss` | a function's NCSS is above it | 60 |
|
|
100
|
+
| `maxFunctionNestingDepth` | a function's nesting depth is above it | 4 |
|
|
101
|
+
| `maxFunctionParameterCount` | a function's parameter count is above it | 7 |
|
|
102
|
+
| `maxFunctionHalsteadVolume` | a function's Halstead volume is above it | 2000 |
|
|
103
|
+
| `maxFunctionHalsteadDifficulty` | a function's Halstead difficulty is above it | 20 |
|
|
104
|
+
| `maxFunctionHalsteadEffort` | a function's Halstead effort is above it | 30000 |
|
|
105
|
+
| `maxFunctionDepDegree` | a function's DepDegree is above it | 50 |
|
|
106
|
+
| `maxFileNcss` | a file's NCSS is above it | 500 |
|
|
107
|
+
| `minDuplicateLines` | a duplicated block spans at least this many lines | 10 |
|
|
108
|
+
|
|
109
|
+
A duplicated block is one occurrence of a within-file or cross-file clone, found with the
|
|
110
|
+
[duplication detection settings](#duplication-detection-settings); its span runs from its first to
|
|
111
|
+
its last line. Occurrences of at least `minDuplicateLines` lines that overlap in a file are
|
|
112
|
+
reported as one block covering all of them.
|
|
113
|
+
|
|
114
|
+
Each threshold is set in the [`thresholds` config section](#configuration), where `null` disables
|
|
115
|
+
it and `languages` overrides thresholds for the files of one language, or with the command-line
|
|
116
|
+
option of the same name (`--max-function-ncss 80`, `--min-duplicate-lines off`), where `off`
|
|
117
|
+
disables it. The command line wins over the whole config file, per-language overrides included.
|
|
118
|
+
|
|
119
|
+
With `--base <ref>`, the check covers only what the working tree changed since the merge-base of
|
|
120
|
+
`<ref>` and `HEAD` (committed, staged, unstaged, and untracked changes; nothing is checked out):
|
|
121
|
+
|
|
122
|
+
- a function, when a diff hunk touches its span; deleting lines inside the span counts, and so does
|
|
123
|
+
deleting lines after the last line of a Python function (blank and comment-only lines aside), which may have been its
|
|
124
|
+
tail;
|
|
125
|
+
- a file-level threshold, when the change adds or deletes lines of the file (a rename or a mode
|
|
126
|
+
change alone does not count);
|
|
127
|
+
- a duplicated block, when it overlaps lines the change added.
|
|
128
|
+
|
|
129
|
+
`--base` only filters the report: the files measured and the duplication found are those of the
|
|
130
|
+
check without it, so code pasted from an unchanged file is reported where it was pasted. A
|
|
131
|
+
violation the change did not introduce is reported too once the change touches its function, file,
|
|
132
|
+
or duplicated block. `--base` requires a git repository.
|
|
133
|
+
|
|
134
|
+
When nothing violates a threshold, `check` prints a single line. Otherwise it prints a header with
|
|
135
|
+
the counts, one line per violation ordered by path and line, and one remediation hint per violated
|
|
136
|
+
metric:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
Threshold violations: 3 violations (1 functions, 0 files, 2 duplicated blocks) (4 files, 5 functions checked).
|
|
140
|
+
src/legacy.ts:1-21 decide: cognitive complexity 24 (<= 15), cyclomatic complexity 15 (<= 10), nesting depth 5 (<= 4)
|
|
141
|
+
src/report.ts:1-12: duplicated lines 12 (< 10), also at src/summary.ts:1-12
|
|
142
|
+
src/summary.ts:1-12: duplicated lines 12 (< 10), also at src/report.ts:1-12
|
|
143
|
+
|
|
144
|
+
How to fix:
|
|
145
|
+
- cognitive complexity: flatten nested branching with early returns and extract nested blocks into named functions.
|
|
146
|
+
- cyclomatic complexity: the function has too many independent paths; split it by decision or replace condition chains with a lookup table.
|
|
147
|
+
- nesting depth: replace nested conditions with guard clauses or move inner blocks into functions.
|
|
148
|
+
- duplicated lines: extract the repeated code into one shared function or module and call it from every location.
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
A function gets one line listing every threshold it exceeds, a file-level violation prints the path
|
|
152
|
+
without a line span, and a duplicated block lists up to three of its other copies. Paths are
|
|
153
|
+
relative to the repository root, or to the target directory outside a git repository.
|
|
154
|
+
|
|
155
|
+
| Exit code | Meaning |
|
|
156
|
+
| --------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
157
|
+
| 0 | No violations. |
|
|
158
|
+
| 1 | Violations. Whether they are warnings or errors is up to the caller. |
|
|
159
|
+
| 2 | The check is incomplete: a file it covers could not be measured, or the arguments, config file, target, or base ref are invalid. |
|
|
160
|
+
|
|
161
|
+
A file that cannot be measured causes exit code 2 only when the check covers it: a file under the
|
|
162
|
+
target or, with `--base`, a changed file under the target. Other unmeasured files of the
|
|
163
|
+
repository are warnings on stderr.
|
|
164
|
+
|
|
165
|
+
`check` accepts `--config`, `--include-tests`, `--json`, and the `--duplication-*` options of the
|
|
166
|
+
ranking command.
|
|
167
|
+
|
|
168
|
+
### JSON report
|
|
169
|
+
|
|
170
|
+
`code-gauge check --json` prints:
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{
|
|
174
|
+
"passed": false,
|
|
175
|
+
"base": "main",
|
|
176
|
+
"mergeBase": "9f624517156db3290e189b9fd0b8ed38de39a04c",
|
|
177
|
+
"summary": {
|
|
178
|
+
"violationCount": 2,
|
|
179
|
+
"functionViolationCount": 1,
|
|
180
|
+
"fileViolationCount": 0,
|
|
181
|
+
"duplicationViolationCount": 1,
|
|
182
|
+
"checkedFileCount": 2,
|
|
183
|
+
"checkedFunctionCount": 2
|
|
184
|
+
},
|
|
185
|
+
"violations": [
|
|
186
|
+
{
|
|
187
|
+
"kind": "function",
|
|
188
|
+
"file": "src/legacy.ts",
|
|
189
|
+
"startLine": 1,
|
|
190
|
+
"endLine": 21,
|
|
191
|
+
"name": "decide",
|
|
192
|
+
"exceeded": [
|
|
193
|
+
{ "metric": "functionCognitiveComplexity", "value": 24, "limit": 15 },
|
|
194
|
+
{ "metric": "functionCyclomaticComplexity", "value": 15, "limit": 10 },
|
|
195
|
+
{ "metric": "functionNestingDepth", "value": 5, "limit": 4 }
|
|
196
|
+
]
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"kind": "duplication",
|
|
200
|
+
"file": "src/pasted.ts",
|
|
201
|
+
"startLine": 1,
|
|
202
|
+
"endLine": 12,
|
|
203
|
+
"exceeded": [{ "metric": "duplicateLines", "value": 12, "limit": 10 }],
|
|
204
|
+
"partners": [{ "file": "src/report.ts", "startLine": 1, "endLine": 12 }]
|
|
205
|
+
}
|
|
206
|
+
],
|
|
207
|
+
"errors": [],
|
|
208
|
+
"warnings": []
|
|
209
|
+
}
|
|
76
210
|
```
|
|
77
211
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
-
|
|
96
|
-
|
|
97
|
-
pasted). Only added lines count, and only those whose normalized tokens a partner file repeats:
|
|
98
|
-
a file whose untouched code gained a copy elsewhere is not blamed for it, and an edit between
|
|
99
|
-
lines already duplicated with the same files, a clone rewritten in place against the same
|
|
100
|
-
files, or a clone moved elsewhere in the change is not new duplication, while code pasted next
|
|
101
|
-
to an existing clone, or replacing one with a copy of unrelated code, is. Added lines come from
|
|
102
|
-
`git diff`, like the hunks reviewers see.
|
|
103
|
-
- **Anti-gaming backstops.** Splitting a function resets its entity identity and could hide a
|
|
104
|
-
worsening behind the laxer new-code thresholds, so when a removed named function's content
|
|
105
|
-
partially reappears in unmatched new code the file's max cognitive complexity and total NCSS
|
|
106
|
-
ratchet too; purely additive changes and unrelated remove-plus-add changes stay ungated.
|
|
107
|
-
|
|
108
|
-
`--full` additionally prints the base → head values of every checked function; `--json` prints a
|
|
109
|
-
machine-readable report. The duplication flags and `--include-tests` work like the ranking command.
|
|
110
|
-
The duplication universes contain only git-visible files (tracked or untracked non-ignored), so
|
|
111
|
-
local ignored artifacts cannot skew the counts. [Excluded files](#excluded-files) are neither gated
|
|
112
|
-
nor part of the universes; a base blob is judged by the git attributes of the merge-base, so code
|
|
113
|
-
that stops being generated gates as new code. The Halstead volume allowance scales with the
|
|
114
|
-
base value (25%, floored at the configured tolerance) so it admits the same ~5-statement edit at
|
|
115
|
-
every function size.
|
|
212
|
+
- `passed`: `true` when there are no violations and no errors (exit code 0).
|
|
213
|
+
- `base`, `mergeBase`: the `--base` ref and the commit compared against; absent without `--base`.
|
|
214
|
+
- `summary`: the violation counts in total and per kind, and the number of files and functions
|
|
215
|
+
checked: those under the target or, with `--base`, the changed files under it and the functions
|
|
216
|
+
the change touches.
|
|
217
|
+
- `violations`: ordered by `file`, then `startLine`. Each has
|
|
218
|
+
- `kind`: `function`, `file`, or `duplication`;
|
|
219
|
+
- `file`, `startLine`, `endLine`: the 1-based line span of the function or duplicated block; for
|
|
220
|
+
kind `file`, the first and last line of the file;
|
|
221
|
+
- `name`: only for kind `function`; `<anonymous>` for a function without a name;
|
|
222
|
+
- `exceeded`: every violated threshold with its `metric` (the config key without its `max` or
|
|
223
|
+
`min` prefix, e.g. `functionNcss` for `maxFunctionNcss`), the measured `value`, and the `limit`
|
|
224
|
+
in force. A value violates when it is above the limit; `duplicateLines` violates from the limit
|
|
225
|
+
on;
|
|
226
|
+
- `partners`: only for kind `duplication`; every other copy of the block, ordered by `file`, then
|
|
227
|
+
`startLine`.
|
|
228
|
+
- `errors`: the files the check covers that could not be measured (exit code 2).
|
|
229
|
+
- `warnings`: files measured without cross-file duplication data, and unmeasured files of the
|
|
230
|
+
repository the check does not cover.
|
|
116
231
|
|
|
117
232
|
## Configuration
|
|
118
233
|
|
|
@@ -127,22 +242,19 @@ with `--config`). The following config reproduces every built-in default:
|
|
|
127
242
|
"minSimilarityPercent": 70
|
|
128
243
|
},
|
|
129
244
|
"rank": { "top": 10 },
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
"
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
"duplicateLines": 0
|
|
144
|
-
},
|
|
145
|
-
"matchSimilarityPercent": 70
|
|
245
|
+
"thresholds": {
|
|
246
|
+
"maxFunctionCognitiveComplexity": 15,
|
|
247
|
+
"maxFunctionCyclomaticComplexity": 10,
|
|
248
|
+
"maxFunctionNcss": 60,
|
|
249
|
+
"maxFunctionNestingDepth": 4,
|
|
250
|
+
"maxFunctionParameterCount": 7,
|
|
251
|
+
"maxFunctionHalsteadVolume": 2000,
|
|
252
|
+
"maxFunctionHalsteadDifficulty": 20,
|
|
253
|
+
"maxFunctionHalsteadEffort": 30000,
|
|
254
|
+
"maxFunctionDepDegree": 50,
|
|
255
|
+
"maxFileNcss": 500,
|
|
256
|
+
"minDuplicateLines": 10,
|
|
257
|
+
"languages": {}
|
|
146
258
|
},
|
|
147
259
|
"exclude": [],
|
|
148
260
|
"includeTests": false,
|
|
@@ -151,11 +263,29 @@ with `--config`). The following config reproduces every built-in default:
|
|
|
151
263
|
```
|
|
152
264
|
|
|
153
265
|
The command line wins over the config file, which wins over the defaults. Unknown settings are
|
|
154
|
-
rejected so stale configuration fails loudly.
|
|
266
|
+
rejected so stale configuration fails loudly. `rank.top` and `failOnError` apply to the ranking
|
|
267
|
+
command only.
|
|
268
|
+
|
|
269
|
+
`thresholds.languages` maps a language name (`javascript`, `jsx`, `typescript`, `tsx`, `python`,
|
|
270
|
+
`go`, `rust`, `java`, `ruby`, `c`, `cpp`, `csharp`, `kotlin`) to the thresholds that differ for its
|
|
271
|
+
files; a threshold it leaves out keeps the value set outside `languages`:
|
|
272
|
+
|
|
273
|
+
```json
|
|
274
|
+
{
|
|
275
|
+
"thresholds": {
|
|
276
|
+
"maxFunctionNcss": 60,
|
|
277
|
+
"maxFunctionHalsteadVolume": null,
|
|
278
|
+
"languages": {
|
|
279
|
+
"tsx": { "maxFunctionNcss": 100 },
|
|
280
|
+
"python": { "maxFunctionParameterCount": null }
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
155
285
|
|
|
156
286
|
### Excluded files
|
|
157
287
|
|
|
158
|
-
|
|
288
|
+
Every command skips, besides the built-in directories and file names:
|
|
159
289
|
|
|
160
290
|
- files matching an `exclude` glob pattern (e.g. `"exclude": ["src/legacy/**", "**/*.pb.ts"]`),
|
|
161
291
|
relative to the config file's directory and matched with Node's `path.posix.matchesGlob` (forward slashes on every platform);
|
|
@@ -166,6 +296,9 @@ Directory scans and the regression gate skip, besides the built-in directories a
|
|
|
166
296
|
`// Code generated by <tool>. DO NOT EDIT.`). Mark a hand-written file `-linguist-generated` to
|
|
167
297
|
keep it measured despite such a line.
|
|
168
298
|
|
|
299
|
+
Inside a git repository, `code-gauge check` additionally skips the files git ignores (see
|
|
300
|
+
[Threshold check](#threshold-check-code-gauge-check)).
|
|
301
|
+
|
|
169
302
|
A file passed as the explicit target is always measured.
|
|
170
303
|
|
|
171
304
|
### Duplication detection settings
|
|
@@ -209,8 +342,7 @@ The `duplication` section tunes how clones are detected:
|
|
|
209
342
|
- Per-function cyclomatic complexity (McCabe, own body only), counted as NIST SP 500-235 defines it:
|
|
210
343
|
every decision and short-circuit operator adds one, one per case-labelled statement, plus the
|
|
211
344
|
file total over McCabe's components (every function, every initializer block, decisions outside
|
|
212
|
-
functions, and the module body of a file that runs top-level code)
|
|
213
|
-
ranking and the regression gate ignore
|
|
345
|
+
functions, and the module body of a file that runs top-level code)
|
|
214
346
|
- Per-function parameter counts and locations (name, node type, line span)
|
|
215
347
|
- Within-file duplication: copy-pasted blocks matched on normalized tokens (identifiers anonymized
|
|
216
348
|
consistently, literals by kind, and literal-dense data tables excluded unless their values also
|
|
@@ -221,16 +353,16 @@ The `duplication` section tunes how clones are detected:
|
|
|
221
353
|
- Cross-file duplication (via `measureCrossFileDuplication`): copy-pasted blocks shared between
|
|
222
354
|
files, matched with the same normalization (exact, gapped, and near-miss clones) and reported as
|
|
223
355
|
groups with their file locations
|
|
224
|
-
- Halstead base counts, vocabulary, length, volume,
|
|
225
|
-
|
|
356
|
+
- Halstead base counts, vocabulary, length, volume, difficulty (half the distinct operators times
|
|
357
|
+
the total operands per distinct operand; 0 without operands), and effort (difficulty times
|
|
358
|
+
volume), per function and per file
|
|
226
359
|
- Per-function DepDegree (Beyer & Fararooy 2010), approximated as the number of variable reads
|
|
227
360
|
with a preceding same-name definition (declaration, assignment, or parameter) in the function —
|
|
228
|
-
a file-local single-assignment approximation
|
|
361
|
+
a file-local single-assignment approximation
|
|
229
362
|
|
|
230
363
|
Metrics that the validation literature shows to be weakly grounded or that invite misdirected
|
|
231
364
|
"improvements" (call-graph fan-in/fan-out, coupling and cohesion counts, maintainability index,
|
|
232
|
-
and similar) are intentionally not measured
|
|
233
|
-
output for the same reason; see
|
|
365
|
+
and similar) are intentionally not measured; see
|
|
234
366
|
[issue #44](https://github.com/WillBooster/code-gauge/issues/44) for the rationale and references.
|
|
235
367
|
|
|
236
368
|
## Supported languages
|
|
@@ -243,14 +375,10 @@ supporting arbitrary grammars would mean shipping incomplete metrics for them.
|
|
|
243
375
|
|
|
244
376
|
Parsing and every metric pass run in a Rust addon (tree-sitter); the thin TypeScript layer handles
|
|
245
377
|
the CLI, cross-file matching, and the Halstead float derivations. Releases include prebuilt addons
|
|
246
|
-
for Linux x64/arm64 (glibc and musl), macOS x64/arm64, and Windows x64
|
|
247
|
-
|
|
248
|
-
[Rust toolchain](https://rustup.rs)
|
|
249
|
-
|
|
250
|
-
If a prebuilt addon is unavailable, package managers that block dependency install scripts by
|
|
251
|
-
default (recent npm versions, and Bun unless `code-gauge` is listed in `trustedDependencies`) skip
|
|
252
|
-
the fallback build. Either approve `code-gauge`'s install script, or build the addon manually
|
|
253
|
-
inside the installed package (the runtime error message points here too):
|
|
378
|
+
for Linux x64/arm64 (glibc and musl), macOS x64/arm64, and Windows x64, and a release that
|
|
379
|
+
includes them runs no install script. On other platforms, build the addon from the bundled sources inside the installed
|
|
380
|
+
package, which requires a [Rust toolchain](https://rustup.rs) (the runtime error message points
|
|
381
|
+
here too):
|
|
254
382
|
|
|
255
383
|
```sh
|
|
256
384
|
node node_modules/code-gauge/scripts/buildNative.mjs
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./thresholds.cjs"),n=require("./cliConfig.cjs"),r=require("./git.cjs"),i=require("./exclusion.cjs"),a=require("./scan.cjs"),o=require("./thresholdCheck.cjs");let s=require("@willbooster/shared-lib"),c=require("node:fs/promises"),l=require("node:path");l=e.__toESM(l,1);let u=require("node:os");u=e.__toESM(u,1);async function d(e,r,i){try{let s=a.resolveTarget(e),c=await n.loadConfig(r.config,await a.configSearchDirectory(s)),l=n.resolveOptions(r,c),u=await p(s,r.base,l),d=o.checkThresholds(u.files,u.crossFileDuplication,t.resolveLimits(i,c.config.thresholds));await f(d.violations,u.root),l.json?E(r,u,d):b(r,u,d),u.errors.length>0?process.exitCode=2:d.violations.length>0&&(process.exitCode=1)}catch(e){a.writeStderr(`Error: ${(0,s.getErrorMessage)(e)}\n`),process.exitCode=2}}async function f(e,t){for(let n of e.filter(({kind:e})=>e===`file`)){let e=await(0,c.readFile)(l.default.join(t,n.file),`utf8`);/[\n\r]$/u.test(e)&&(n.endLine=Math.max(n.endLine-1,1))}}async function p(e,t,n){let i=await(0,c.realpath)(e),o=(await(0,c.stat)(i)).isFile()?i:void 0;if(o&&!a.getLanguage(o,n,!0))throw Error(`${l.default.basename(o)}: unsupported file type`);let s;try{s=await(0,c.realpath)(await r.resolveRepoRoot(o?l.default.dirname(i):i))}catch(e){if(t!==void 0)throw e;return await m(i,n)}return await h({repoRoot:s,canonicalTarget:i,targetFile:o},t,n)}async function m(e,t){let n=await a.configSearchDirectory(e),r=await a.scanTarget(e,{...t,loadExclusion:e=>i.loadExclusion(n,t.exclude,e)}),o=y(r,t);return{files:r.files.map(({file:e,metrics:t})=>({file:a.formatPath(e,r.displayRoot),metrics:t})),crossFileDuplication:o,errors:r.errors,warnings:r.warnings,root:r.displayRoot}}async function h({repoRoot:e,canonicalTarget:t,targetFile:n},o,s){if(n===void 0&&t!==e&&await r.isIgnored(e,t))throw Error(`${a.formatPath(t,e)}: git ignores this directory, so no file in it is checked; target a file to check it regardless`);let c=await r.listRepositoryFiles(e),u=new Set(n===void 0?[]:[n]),d=n!==void 0&&!c.has(a.formatPath(n,e))?a.formatPath(n,e):void 0;d!==void 0&&c.add(d);let f=i.keepPaths(await i.loadRepositoryExclusion(e,c,s.exclude),u),p=a.formatPath(t,e),m=await a.scanListedFiles(e,c,{...s,loadExclusion:()=>Promise.resolve(f)},u,n===void 0&&t!==e?p:``),h=y(m,s),v=n=>a.isWithinDirectory(l.default.join(e,n),t),b=m.files.map(({file:t,metrics:n})=>({file:a.formatPath(t,e),metrics:n})),x=b.filter(({file:e})=>v(e)),S=n=>t===e||n.startsWith(`${p}/`)||n.startsWith(`${p}:`),C;if(o!==void 0){C=await r.resolveMergeBase(e,o);let t=await r.listChangedFiles(e,C);d!==void 0&&t.push({status:`added`,headPath:d});let n=t.filter(({status:e,headPath:t})=>e!==`deleted`&&v(t));x=await _(b,n,e,C),S=e=>n.some(({headPath:t})=>e.startsWith(`${t}:`))}return{files:x,crossFileDuplication:h,...g(m,S),root:e,mergeBase:C}}function g(e,t){return{errors:e.errors.filter(e=>t(e)),warnings:[...e.warnings,...e.errors.filter(e=>!t(e))]}}async function _(e,t,n,i){let a=new Map(e.map(({file:e,metrics:t})=>[e,t]));return(await(0,s.mapConcurrently)(t,u.default.availableParallelism()*2,async e=>{let t=a.get(e.headPath);if(t===void 0)return[];let o={headStart:1,headCount:t.lines.total},s=e.basePath===void 0?[o]:await r.listLineHunks(n,i,e.basePath,e.headPath);return t.language===`python`&&s.some(({headCount:e})=>e===0)&&v(s,await(0,c.readFile)(l.default.join(n,e.headPath),`utf8`)),[{file:e.headPath,metrics:t,hunks:s}]})).flat()}function v(e,t){let n=t.split(/\r\n|\n|\r/u);for(let t of e)if(!(t.headCount>0))for(;t.headStart>0&&/^\s*(?:#.*)?$/u.test(n[t.headStart-1]??``);)--t.headStart}function y(e,t){if(e.fatalError)throw Error(e.fatalError);return a.addCrossFileDuplication(e,t),e.crossFileDuplication}function b(e,n,r){let{violations:i}=r,o=`${n.files.length} ${e.base===void 0?``:`changed `}files, ${r.checkedFunctionCount} functions checked${x(e,n)}`;n.errors.length>0?a.writeStdout(`Check could not complete: ${n.errors.length} measurement failures (details on stderr); ${S(i)} in the measured files (${o}).\n`):i.length===0?a.writeStdout(`No threshold violations: ${o}.\n`):a.writeStdout(`Threshold violations: ${S(i)} (${o}).\n`);for(let e of i)a.writeStdout(`${w(e)}\n`);let s=new Set(i.flatMap(({exceeded:e})=>e.map(({metric:e})=>e))),c=t.thresholds.filter(e=>s.has(t.metricNameOf(e)));if(c.length>0){a.writeStdout(`
|
|
2
|
+
How to fix:
|
|
3
|
+
`);for(let{label:e,hint:t}of c)a.writeStdout(`- ${e}: ${t}\n`)}for(let e of n.warnings)a.writeStderr(`Warning: ${e}\n`);for(let e of n.errors)a.writeStderr(`Error: ${e}\n`)}function x(e,t){return t.mergeBase===void 0?``:`; base ${e.base}, merge-base ${t.mergeBase.slice(0,12)}`}function S(e){let t=C(e);return`${t.violationCount} violations (${t.functionViolationCount} functions, ${t.fileViolationCount} files, ${t.duplicationViolationCount} duplicated blocks)`}function C(e){let t=t=>e.filter(e=>e.kind===t).length;return{violationCount:e.length,functionViolationCount:t(`function`),fileViolationCount:t(`file`),duplicationViolationCount:t(`duplication`)}}function w(e){let n=new Map(t.thresholds.map(e=>[t.metricNameOf(e),e.label])),r=e.exceeded.map(({metric:t,value:r,limit:i})=>`${n.get(t)} ${Math.ceil(r*10)/10} (${e.kind===`duplication`?`<`:`<=`} ${i})`).join(`, `);switch(e.kind){case`file`:return`${e.file}: ${r}`;case`function`:return`${T(e)} ${e.name?.replaceAll(/\s*[\n\r]\s*/gu,` `)}: ${r}`;case`duplication`:{let t=(e.partners??[]).map(e=>T(e)),n=[...t.slice(0,3),...t.length>3?[`...`]:[]];return`${T(e)}: ${r}${n.length>0?`, also at ${n.join(`, `)}`:``}`}}}function T({file:e,startLine:t,endLine:n}){return`${e}:${t}-${n}`}function E(e,t,n){a.writeStdout(JSON.stringify({passed:n.violations.length===0&&t.errors.length===0,base:e.base,mergeBase:t.mergeBase,summary:{...C(n.violations),checkedFileCount:t.files.length,checkedFunctionCount:n.checkedFunctionCount},violations:n.violations,errors:t.errors,warnings:t.warnings},void 0,2)+`
|
|
4
|
+
`)}exports.runCheckCommand=d;
|
|
5
|
+
//# sourceMappingURL=checkCommand.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkCommand.cjs","names":["resolveTarget","loadConfig","configSearchDirectory","resolveOptions","checkThresholds","resolveLimits","getErrorMessage","readFile","path","realpath","stat","getLanguage","resolveRepoRoot","scanTarget","loadExclusion","formatPath","isIgnored","listRepositoryFiles","keepPaths","loadRepositoryExclusion","scanListedFiles","isWithinDirectory","resolveMergeBase","listChangedFiles","mapConcurrently","os","listLineHunks","thresholds","metricNameOf"],"sources":["../src/checkCommand.ts"],"sourcesContent":["import { readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { getErrorMessage, mapConcurrently } from '@willbooster/shared-lib';\nimport { loadConfig, resolveOptions, type CliOptions, type ResolvedOptions } from './cliConfig.js';\nimport type { CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport { keepPaths, loadExclusion, loadRepositoryExclusion } from './exclusion.js';\nimport {\n isIgnored,\n listChangedFiles,\n listLineHunks,\n listRepositoryFiles,\n resolveMergeBase,\n resolveRepoRoot,\n type ChangedFile,\n type LineHunk,\n} from './git.js';\nimport {\n addCrossFileDuplication,\n configSearchDirectory,\n formatPath,\n getLanguage,\n isWithinDirectory,\n resolveTarget,\n scanListedFiles,\n scanTarget,\n writeStderr,\n writeStdout,\n type ScanResult,\n} from './scan.js';\nimport {\n checkThresholds,\n type BlockLocation,\n type CheckedFile,\n type CheckResult,\n type Violation,\n} from './thresholdCheck.js';\nimport { metricNameOf, resolveLimits, thresholds, type Limits } from './thresholds.js';\n\n/** Raw options of the `check` subcommand; every field is undefined unless the flag was passed. */\nexport interface CheckCliOptions extends CliOptions {\n base?: string;\n}\n\n/** The measured files a check covers and the duplication they take part in. */\ninterface CheckScope {\n files: CheckedFile[];\n crossFileDuplication?: CrossFileDuplicationMetrics;\n /** Measurement failures of files the check covers. */\n errors: string[];\n warnings: string[];\n /** The directory the files' paths are relative to. */\n root: string;\n mergeBase?: string;\n}\n\n/** Caps how many partner locations a duplicated block lists so the report stays scannable. */\nconst maxListedPartners = 3;\n\n/**\n * Reports every exceeded threshold of the target or, with `base`, of what the working tree changed\n * under it since the merge-base with that ref. Exit codes: 0 no violations, 1 violations, 2 files the check\n * covers could not be measured.\n */\nexport async function runCheckCommand(target: string, cliOptions: CheckCliOptions, cliLimits: Limits): Promise<void> {\n try {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const scope = await scanScope(resolvedTarget, cliOptions.base, options);\n const result = checkThresholds(\n scope.files,\n scope.crossFileDuplication,\n resolveLimits(cliLimits, config.config.thresholds)\n );\n await endFileViolationsAtLastLine(result.violations, scope.root);\n\n if (options.json) {\n printJsonReport(cliOptions, scope, result);\n } else {\n printTextReport(cliOptions, scope, result);\n }\n if (scope.errors.length > 0) {\n process.exitCode = 2;\n } else if (result.violations.length > 0) {\n process.exitCode = 1;\n }\n } catch (error) {\n writeStderr(`Error: ${getErrorMessage(error)}\\n`);\n process.exitCode = 2;\n }\n}\n\n/**\n * A file's measured line count includes the empty line after a final line terminator, which is\n * not a line of the file, so file-level violations end at the last line that exists.\n */\nasync function endFileViolationsAtLastLine(violations: readonly Violation[], root: string): Promise<void> {\n for (const violation of violations.filter(({ kind }) => kind === 'file')) {\n const content = await readFile(path.join(root, violation.file), 'utf8');\n if (/[\\n\\r]$/u.test(content)) {\n violation.endLine = Math.max(violation.endLine - 1, 1);\n }\n }\n}\n\n/**\n * Inside a git repository, the scope is the repository's git-visible files under the target, and\n * duplication is detected against all of them; `base` only narrows the scope to what changed.\n * Outside a repository, where there is no change to narrow to, the target directory is walked.\n */\nasync function scanScope(\n resolvedTarget: string,\n base: string | undefined,\n options: ResolvedOptions\n): Promise<CheckScope> {\n const canonicalTarget = await realpath(resolvedTarget);\n const targetStat = await stat(canonicalTarget);\n const targetFile = targetStat.isFile() ? canonicalTarget : undefined;\n if (targetFile && !getLanguage(targetFile, options, true)) {\n throw new Error(`${path.basename(targetFile)}: unsupported file type`);\n }\n let repoRoot;\n try {\n repoRoot = await realpath(await resolveRepoRoot(targetFile ? path.dirname(canonicalTarget) : canonicalTarget));\n } catch (error) {\n if (base !== undefined) {\n throw error;\n }\n return await scanDirectoryWalk(canonicalTarget, options);\n }\n return await scanRepository({ repoRoot, canonicalTarget, targetFile }, base, options);\n}\n\nasync function scanDirectoryWalk(canonicalTarget: string, options: ResolvedOptions): Promise<CheckScope> {\n const searchDirectory = await configSearchDirectory(canonicalTarget);\n const scan = await scanTarget(canonicalTarget, {\n ...options,\n loadExclusion: (absolutePaths) => loadExclusion(searchDirectory, options.exclude, absolutePaths),\n });\n const crossFileDuplication = measureDuplication(scan, options);\n return {\n files: scan.files.map(({ file, metrics }) => ({ file: formatPath(file, scan.displayRoot), metrics })),\n crossFileDuplication,\n errors: scan.errors,\n warnings: scan.warnings,\n root: scan.displayRoot,\n };\n}\n\ninterface RepositoryTarget {\n repoRoot: string;\n canonicalTarget: string;\n /** Set when the target is a file, which is measured even when git ignores it or it is excluded. */\n targetFile?: string;\n}\n\n/**\n * Measures every git-visible file (tracked or untracked non-ignored) of the repository, so that\n * code copied from outside the target is detected as duplication, and keeps the files under the\n * target: all of them or, with `base`, the changed ones with their line hunks.\n */\nasync function scanRepository(\n { repoRoot, canonicalTarget, targetFile }: RepositoryTarget,\n base: string | undefined,\n options: ResolvedOptions\n): Promise<CheckScope> {\n // Only git-visible files are measured, so a directory git ignores would pass as a check of no\n // files. A directory git lists but whose sources it ignores is not told apart from a repository\n // or package whose only sources are ignored build output, which must pass.\n if (targetFile === undefined && canonicalTarget !== repoRoot && (await isIgnored(repoRoot, canonicalTarget))) {\n throw new Error(\n `${formatPath(canonicalTarget, repoRoot)}: git ignores this directory, so no file in it is checked; target a file to check it regardless`\n );\n }\n const repositoryFiles = await listRepositoryFiles(repoRoot);\n const explicitFiles = new Set(targetFile === undefined ? [] : [targetFile]);\n const unlistedTarget =\n targetFile !== undefined && !repositoryFiles.has(formatPath(targetFile, repoRoot))\n ? formatPath(targetFile, repoRoot)\n : undefined;\n if (unlistedTarget !== undefined) {\n repositoryFiles.add(unlistedTarget);\n }\n const exclusion = keepPaths(await loadRepositoryExclusion(repoRoot, repositoryFiles, options.exclude), explicitFiles);\n // Scan errors start with the file or directory they concern.\n const targetPath = formatPath(canonicalTarget, repoRoot);\n const scan = await scanListedFiles(\n repoRoot,\n repositoryFiles,\n { ...options, loadExclusion: () => Promise.resolve(exclusion) },\n explicitFiles,\n targetFile === undefined && canonicalTarget !== repoRoot ? targetPath : ''\n );\n const crossFileDuplication = measureDuplication(scan, options);\n const isInTarget = (relativePath: string): boolean =>\n isWithinDirectory(path.join(repoRoot, relativePath), canonicalTarget);\n const measuredFiles = scan.files.map(({ file, metrics }) => ({ file: formatPath(file, repoRoot), metrics }));\n let files = measuredFiles.filter(({ file }) => isInTarget(file));\n let isCovered = (error: string): boolean =>\n canonicalTarget === repoRoot || error.startsWith(`${targetPath}/`) || error.startsWith(`${targetPath}:`);\n let mergeBase;\n if (base !== undefined) {\n mergeBase = await resolveMergeBase(repoRoot, base);\n const changedFiles = await listChangedFiles(repoRoot, mergeBase);\n // A targeted file git does not list has no diff; all of it counts as added.\n if (unlistedTarget !== undefined) {\n changedFiles.push({ status: 'added', headPath: unlistedTarget });\n }\n const changesInTarget = changedFiles.filter(({ status, headPath }) => status !== 'deleted' && isInTarget(headPath));\n files = await attachHunks(measuredFiles, changesInTarget, repoRoot, mergeBase);\n isCovered = (error) => changesInTarget.some(({ headPath }) => error.startsWith(`${headPath}:`));\n }\n return { files, crossFileDuplication, ...partitionErrors(scan, isCovered), root: repoRoot, mergeBase };\n}\n\n/** Only a failure on a file the check covers leaves it incomplete; the others are warnings. */\nfunction partitionErrors(\n scan: ScanResult,\n isCovered: (error: string) => boolean\n): Pick<CheckScope, 'errors' | 'warnings'> {\n return {\n errors: scan.errors.filter((error) => isCovered(error)),\n warnings: [...scan.warnings, ...scan.errors.filter((error) => !isCovered(error))],\n };\n}\n\n/** The measured changed files with their line hunks against the merge-base. */\nasync function attachHunks(\n measuredFiles: readonly CheckedFile[],\n changedFiles: readonly ChangedFile[],\n repoRoot: string,\n mergeBase: string\n): Promise<CheckedFile[]> {\n const metricsByPath = new Map(measuredFiles.map(({ file, metrics }) => [file, metrics]));\n const files = await mapConcurrently(changedFiles, os.availableParallelism() * 2, async (changed) => {\n const metrics = metricsByPath.get(changed.headPath);\n // A changed file the scan left out (unsupported, excluded, or failed) has nothing to check.\n if (metrics === undefined) {\n return [];\n }\n const wholeFile: LineHunk = { headStart: 1, headCount: metrics.lines.total };\n const hunks =\n changed.basePath === undefined\n ? [wholeFile]\n : await listLineHunks(repoRoot, mergeBase, changed.basePath, changed.headPath);\n if (metrics.language === 'python' && hunks.some(({ headCount }) => headCount === 0)) {\n anchorDeletionsToCode(hunks, await readFile(path.join(repoRoot, changed.headPath), 'utf8'));\n }\n return [{ file: changed.headPath, metrics, hunks }];\n });\n return files.flat();\n}\n\n/**\n * Moves each pure deletion up to the last code line before it. A Python function ends at its last\n * statement, so the blank and comment-only lines left above a deleted tail lie outside the span the\n * deletion shortened; anchored to that statement, the deletion counts as touching the function.\n */\nfunction anchorDeletionsToCode(hunks: LineHunk[], content: string): void {\n const lines = content.split(/\\r\\n|\\n|\\r/u);\n for (const hunk of hunks) {\n if (hunk.headCount > 0) continue;\n while (hunk.headStart > 0 && /^\\s*(?:#.*)?$/u.test(lines[hunk.headStart - 1] ?? '')) {\n hunk.headStart -= 1;\n }\n }\n}\n\n/** A run-wide failure (a missing target or native addon) leaves nothing to check. */\nfunction measureDuplication(scan: ScanResult, options: ResolvedOptions): CrossFileDuplicationMetrics | undefined {\n if (scan.fatalError) {\n throw new Error(scan.fatalError);\n }\n addCrossFileDuplication(scan, options);\n return scan.crossFileDuplication;\n}\n\nfunction printTextReport(cliOptions: CheckCliOptions, scope: CheckScope, result: CheckResult): void {\n const { violations } = result;\n const checked = `${scope.files.length} ${cliOptions.base === undefined ? '' : 'changed '}files, ${result.checkedFunctionCount} functions checked${describeBase(cliOptions, scope)}`;\n if (scope.errors.length > 0) {\n // Unmeasured files were not checked, so \"no violations\" would be vacuous; never claim a pass.\n writeStdout(\n `Check could not complete: ${scope.errors.length} measurement failures (details on stderr); ${describeCounts(violations)} in the measured files (${checked}).\\n`\n );\n } else if (violations.length === 0) {\n writeStdout(`No threshold violations: ${checked}.\\n`);\n } else {\n writeStdout(`Threshold violations: ${describeCounts(violations)} (${checked}).\\n`);\n }\n for (const violation of violations) {\n writeStdout(`${formatViolation(violation)}\\n`);\n }\n\n const violatedMetrics = new Set(violations.flatMap(({ exceeded }) => exceeded.map(({ metric }) => metric)));\n const violatedThresholds = thresholds.filter((threshold) => violatedMetrics.has(metricNameOf(threshold)));\n if (violatedThresholds.length > 0) {\n writeStdout('\\nHow to fix:\\n');\n for (const { label, hint } of violatedThresholds) {\n writeStdout(`- ${label}: ${hint}\\n`);\n }\n }\n\n for (const warning of scope.warnings) {\n writeStderr(`Warning: ${warning}\\n`);\n }\n for (const error of scope.errors) {\n writeStderr(`Error: ${error}\\n`);\n }\n}\n\nfunction describeBase(cliOptions: CheckCliOptions, scope: CheckScope): string {\n return scope.mergeBase === undefined ? '' : `; base ${cliOptions.base}, merge-base ${scope.mergeBase.slice(0, 12)}`;\n}\n\nfunction describeCounts(violations: readonly Violation[]): string {\n const counts = countViolations(violations);\n return `${counts.violationCount} violations (${counts.functionViolationCount} functions, ${counts.fileViolationCount} files, ${counts.duplicationViolationCount} duplicated blocks)`;\n}\n\nfunction countViolations(violations: readonly Violation[]): {\n violationCount: number;\n functionViolationCount: number;\n fileViolationCount: number;\n duplicationViolationCount: number;\n} {\n const count = (kind: Violation['kind']): number => violations.filter((violation) => violation.kind === kind).length;\n return {\n violationCount: violations.length,\n functionViolationCount: count('function'),\n fileViolationCount: count('file'),\n duplicationViolationCount: count('duplication'),\n };\n}\n\n/** One violation as a single line: the location, then every limit it exceeds. */\nfunction formatViolation(violation: Violation): string {\n const labelByMetric = new Map(thresholds.map((threshold) => [metricNameOf(threshold), threshold.label]));\n const exceeded = violation.exceeded\n .map(\n ({ metric, value, limit }) =>\n // Halstead values are fractional; rounding up keeps a violating value above the printed limit.\n `${labelByMetric.get(metric)} ${Math.ceil(value * 10) / 10} (${violation.kind === 'duplication' ? '<' : '<='} ${limit})`\n )\n .join(', ');\n switch (violation.kind) {\n case 'file': {\n return `${violation.file}: ${exceeded}`;\n }\n case 'function': {\n // A computed name can span lines in the source; the report keeps one line per function.\n return `${formatLocation(violation)} ${violation.name?.replaceAll(/\\s*[\\n\\r]\\s*/gu, ' ')}: ${exceeded}`;\n }\n case 'duplication': {\n const partners = (violation.partners ?? []).map((partner) => formatLocation(partner));\n const listed = [...partners.slice(0, maxListedPartners), ...(partners.length > maxListedPartners ? ['...'] : [])];\n return `${formatLocation(violation)}: ${exceeded}${listed.length > 0 ? `, also at ${listed.join(', ')}` : ''}`;\n }\n }\n}\n\nfunction formatLocation({ file, startLine, endLine }: BlockLocation): string {\n return `${file}:${startLine}-${endLine}`;\n}\n\nfunction printJsonReport(cliOptions: CheckCliOptions, scope: CheckScope, result: CheckResult): void {\n writeStdout(\n JSON.stringify(\n {\n passed: result.violations.length === 0 && scope.errors.length === 0,\n base: cliOptions.base,\n mergeBase: scope.mergeBase,\n summary: {\n ...countViolations(result.violations),\n checkedFileCount: scope.files.length,\n checkedFunctionCount: result.checkedFunctionCount,\n },\n violations: result.violations,\n errors: scope.errors,\n warnings: scope.warnings,\n },\n undefined,\n 2\n ) + '\\n'\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CliOptions } from './cliConfig.js';
|
|
2
|
+
import { type Limits } from './thresholds.js';
|
|
3
|
+
/** Raw options of the `check` subcommand; every field is undefined unless the flag was passed. */
|
|
4
|
+
export interface CheckCliOptions extends CliOptions {
|
|
5
|
+
base?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Reports every exceeded threshold of the target or, with `base`, of what the working tree changed
|
|
9
|
+
* under it since the merge-base with that ref. Exit codes: 0 no violations, 1 violations, 2 files the check
|
|
10
|
+
* covers could not be measured.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runCheckCommand(target: string, cliOptions: CheckCliOptions, cliLimits: Limits): Promise<void>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{metricNameOf as e,resolveLimits as t,thresholds as n}from"./thresholds.js";import{loadConfig as r,resolveOptions as i}from"./cliConfig.js";import{isIgnored as a,listChangedFiles as o,listLineHunks as s,listRepositoryFiles as c,resolveMergeBase as l,resolveRepoRoot as u}from"./git.js";import{keepPaths as d,loadExclusion as f,loadRepositoryExclusion as p}from"./exclusion.js";import{addCrossFileDuplication as m,configSearchDirectory as h,formatPath as g,getLanguage as _,isWithinDirectory as v,resolveTarget as y,scanListedFiles as b,scanTarget as x,writeStderr as S,writeStdout as C}from"./scan.js";import{checkThresholds as w}from"./thresholdCheck.js";import{getErrorMessage as T,mapConcurrently as E}from"@willbooster/shared-lib";import{readFile as D,realpath as O,stat as k}from"node:fs/promises";import A from"node:path";import j from"node:os";async function M(e,n,a){try{let o=y(e),s=await r(n.config,await h(o)),c=i(n,s),l=await P(o,n.base,c),u=w(l.files,l.crossFileDuplication,t(a,s.config.thresholds));await N(u.violations,l.root),c.json?q(n,l,u):V(n,l,u),l.errors.length>0?process.exitCode=2:u.violations.length>0&&(process.exitCode=1)}catch(e){S(`Error: ${T(e)}\n`),process.exitCode=2}}async function N(e,t){for(let n of e.filter(({kind:e})=>e===`file`)){let e=await D(A.join(t,n.file),`utf8`);/[\n\r]$/u.test(e)&&(n.endLine=Math.max(n.endLine-1,1))}}async function P(e,t,n){let r=await O(e),i=(await k(r)).isFile()?r:void 0;if(i&&!_(i,n,!0))throw Error(`${A.basename(i)}: unsupported file type`);let a;try{a=await O(await u(i?A.dirname(r):r))}catch(e){if(t!==void 0)throw e;return await F(r,n)}return await I({repoRoot:a,canonicalTarget:r,targetFile:i},t,n)}async function F(e,t){let n=await h(e),r=await x(e,{...t,loadExclusion:e=>f(n,t.exclude,e)}),i=B(r,t);return{files:r.files.map(({file:e,metrics:t})=>({file:g(e,r.displayRoot),metrics:t})),crossFileDuplication:i,errors:r.errors,warnings:r.warnings,root:r.displayRoot}}async function I({repoRoot:e,canonicalTarget:t,targetFile:n},r,i){if(n===void 0&&t!==e&&await a(e,t))throw Error(`${g(t,e)}: git ignores this directory, so no file in it is checked; target a file to check it regardless`);let s=await c(e),u=new Set(n===void 0?[]:[n]),f=n!==void 0&&!s.has(g(n,e))?g(n,e):void 0;f!==void 0&&s.add(f);let m=d(await p(e,s,i.exclude),u),h=g(t,e),_=await b(e,s,{...i,loadExclusion:()=>Promise.resolve(m)},u,n===void 0&&t!==e?h:``),y=B(_,i),x=n=>v(A.join(e,n),t),S=_.files.map(({file:t,metrics:n})=>({file:g(t,e),metrics:n})),C=S.filter(({file:e})=>x(e)),w=n=>t===e||n.startsWith(`${h}/`)||n.startsWith(`${h}:`),T;if(r!==void 0){T=await l(e,r);let t=await o(e,T);f!==void 0&&t.push({status:`added`,headPath:f});let n=t.filter(({status:e,headPath:t})=>e!==`deleted`&&x(t));C=await R(S,n,e,T),w=e=>n.some(({headPath:t})=>e.startsWith(`${t}:`))}return{files:C,crossFileDuplication:y,...L(_,w),root:e,mergeBase:T}}function L(e,t){return{errors:e.errors.filter(e=>t(e)),warnings:[...e.warnings,...e.errors.filter(e=>!t(e))]}}async function R(e,t,n,r){let i=new Map(e.map(({file:e,metrics:t})=>[e,t]));return(await E(t,j.availableParallelism()*2,async e=>{let t=i.get(e.headPath);if(t===void 0)return[];let a={headStart:1,headCount:t.lines.total},o=e.basePath===void 0?[a]:await s(n,r,e.basePath,e.headPath);return t.language===`python`&&o.some(({headCount:e})=>e===0)&&z(o,await D(A.join(n,e.headPath),`utf8`)),[{file:e.headPath,metrics:t,hunks:o}]})).flat()}function z(e,t){let n=t.split(/\r\n|\n|\r/u);for(let t of e)if(!(t.headCount>0))for(;t.headStart>0&&/^\s*(?:#.*)?$/u.test(n[t.headStart-1]??``);)--t.headStart}function B(e,t){if(e.fatalError)throw Error(e.fatalError);return m(e,t),e.crossFileDuplication}function V(t,r,i){let{violations:a}=i,o=`${r.files.length} ${t.base===void 0?``:`changed `}files, ${i.checkedFunctionCount} functions checked${H(t,r)}`;r.errors.length>0?C(`Check could not complete: ${r.errors.length} measurement failures (details on stderr); ${U(a)} in the measured files (${o}).\n`):a.length===0?C(`No threshold violations: ${o}.\n`):C(`Threshold violations: ${U(a)} (${o}).\n`);for(let e of a)C(`${G(e)}\n`);let s=new Set(a.flatMap(({exceeded:e})=>e.map(({metric:e})=>e))),c=n.filter(t=>s.has(e(t)));if(c.length>0){C(`
|
|
2
|
+
How to fix:
|
|
3
|
+
`);for(let{label:e,hint:t}of c)C(`- ${e}: ${t}\n`)}for(let e of r.warnings)S(`Warning: ${e}\n`);for(let e of r.errors)S(`Error: ${e}\n`)}function H(e,t){return t.mergeBase===void 0?``:`; base ${e.base}, merge-base ${t.mergeBase.slice(0,12)}`}function U(e){let t=W(e);return`${t.violationCount} violations (${t.functionViolationCount} functions, ${t.fileViolationCount} files, ${t.duplicationViolationCount} duplicated blocks)`}function W(e){let t=t=>e.filter(e=>e.kind===t).length;return{violationCount:e.length,functionViolationCount:t(`function`),fileViolationCount:t(`file`),duplicationViolationCount:t(`duplication`)}}function G(t){let r=new Map(n.map(t=>[e(t),t.label])),i=t.exceeded.map(({metric:e,value:n,limit:i})=>`${r.get(e)} ${Math.ceil(n*10)/10} (${t.kind===`duplication`?`<`:`<=`} ${i})`).join(`, `);switch(t.kind){case`file`:return`${t.file}: ${i}`;case`function`:return`${K(t)} ${t.name?.replaceAll(/\s*[\n\r]\s*/gu,` `)}: ${i}`;case`duplication`:{let e=(t.partners??[]).map(e=>K(e)),n=[...e.slice(0,3),...e.length>3?[`...`]:[]];return`${K(t)}: ${i}${n.length>0?`, also at ${n.join(`, `)}`:``}`}}}function K({file:e,startLine:t,endLine:n}){return`${e}:${t}-${n}`}function q(e,t,n){C(JSON.stringify({passed:n.violations.length===0&&t.errors.length===0,base:e.base,mergeBase:t.mergeBase,summary:{...W(n.violations),checkedFileCount:t.files.length,checkedFunctionCount:n.checkedFunctionCount},violations:n.violations,errors:t.errors,warnings:t.warnings},void 0,2)+`
|
|
4
|
+
`)}export{M as runCheckCommand};
|
|
5
|
+
//# sourceMappingURL=checkCommand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkCommand.js","names":[],"sources":["../src/checkCommand.ts"],"sourcesContent":["import { readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { getErrorMessage, mapConcurrently } from '@willbooster/shared-lib';\nimport { loadConfig, resolveOptions, type CliOptions, type ResolvedOptions } from './cliConfig.js';\nimport type { CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport { keepPaths, loadExclusion, loadRepositoryExclusion } from './exclusion.js';\nimport {\n isIgnored,\n listChangedFiles,\n listLineHunks,\n listRepositoryFiles,\n resolveMergeBase,\n resolveRepoRoot,\n type ChangedFile,\n type LineHunk,\n} from './git.js';\nimport {\n addCrossFileDuplication,\n configSearchDirectory,\n formatPath,\n getLanguage,\n isWithinDirectory,\n resolveTarget,\n scanListedFiles,\n scanTarget,\n writeStderr,\n writeStdout,\n type ScanResult,\n} from './scan.js';\nimport {\n checkThresholds,\n type BlockLocation,\n type CheckedFile,\n type CheckResult,\n type Violation,\n} from './thresholdCheck.js';\nimport { metricNameOf, resolveLimits, thresholds, type Limits } from './thresholds.js';\n\n/** Raw options of the `check` subcommand; every field is undefined unless the flag was passed. */\nexport interface CheckCliOptions extends CliOptions {\n base?: string;\n}\n\n/** The measured files a check covers and the duplication they take part in. */\ninterface CheckScope {\n files: CheckedFile[];\n crossFileDuplication?: CrossFileDuplicationMetrics;\n /** Measurement failures of files the check covers. */\n errors: string[];\n warnings: string[];\n /** The directory the files' paths are relative to. */\n root: string;\n mergeBase?: string;\n}\n\n/** Caps how many partner locations a duplicated block lists so the report stays scannable. */\nconst maxListedPartners = 3;\n\n/**\n * Reports every exceeded threshold of the target or, with `base`, of what the working tree changed\n * under it since the merge-base with that ref. Exit codes: 0 no violations, 1 violations, 2 files the check\n * covers could not be measured.\n */\nexport async function runCheckCommand(target: string, cliOptions: CheckCliOptions, cliLimits: Limits): Promise<void> {\n try {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const scope = await scanScope(resolvedTarget, cliOptions.base, options);\n const result = checkThresholds(\n scope.files,\n scope.crossFileDuplication,\n resolveLimits(cliLimits, config.config.thresholds)\n );\n await endFileViolationsAtLastLine(result.violations, scope.root);\n\n if (options.json) {\n printJsonReport(cliOptions, scope, result);\n } else {\n printTextReport(cliOptions, scope, result);\n }\n if (scope.errors.length > 0) {\n process.exitCode = 2;\n } else if (result.violations.length > 0) {\n process.exitCode = 1;\n }\n } catch (error) {\n writeStderr(`Error: ${getErrorMessage(error)}\\n`);\n process.exitCode = 2;\n }\n}\n\n/**\n * A file's measured line count includes the empty line after a final line terminator, which is\n * not a line of the file, so file-level violations end at the last line that exists.\n */\nasync function endFileViolationsAtLastLine(violations: readonly Violation[], root: string): Promise<void> {\n for (const violation of violations.filter(({ kind }) => kind === 'file')) {\n const content = await readFile(path.join(root, violation.file), 'utf8');\n if (/[\\n\\r]$/u.test(content)) {\n violation.endLine = Math.max(violation.endLine - 1, 1);\n }\n }\n}\n\n/**\n * Inside a git repository, the scope is the repository's git-visible files under the target, and\n * duplication is detected against all of them; `base` only narrows the scope to what changed.\n * Outside a repository, where there is no change to narrow to, the target directory is walked.\n */\nasync function scanScope(\n resolvedTarget: string,\n base: string | undefined,\n options: ResolvedOptions\n): Promise<CheckScope> {\n const canonicalTarget = await realpath(resolvedTarget);\n const targetStat = await stat(canonicalTarget);\n const targetFile = targetStat.isFile() ? canonicalTarget : undefined;\n if (targetFile && !getLanguage(targetFile, options, true)) {\n throw new Error(`${path.basename(targetFile)}: unsupported file type`);\n }\n let repoRoot;\n try {\n repoRoot = await realpath(await resolveRepoRoot(targetFile ? path.dirname(canonicalTarget) : canonicalTarget));\n } catch (error) {\n if (base !== undefined) {\n throw error;\n }\n return await scanDirectoryWalk(canonicalTarget, options);\n }\n return await scanRepository({ repoRoot, canonicalTarget, targetFile }, base, options);\n}\n\nasync function scanDirectoryWalk(canonicalTarget: string, options: ResolvedOptions): Promise<CheckScope> {\n const searchDirectory = await configSearchDirectory(canonicalTarget);\n const scan = await scanTarget(canonicalTarget, {\n ...options,\n loadExclusion: (absolutePaths) => loadExclusion(searchDirectory, options.exclude, absolutePaths),\n });\n const crossFileDuplication = measureDuplication(scan, options);\n return {\n files: scan.files.map(({ file, metrics }) => ({ file: formatPath(file, scan.displayRoot), metrics })),\n crossFileDuplication,\n errors: scan.errors,\n warnings: scan.warnings,\n root: scan.displayRoot,\n };\n}\n\ninterface RepositoryTarget {\n repoRoot: string;\n canonicalTarget: string;\n /** Set when the target is a file, which is measured even when git ignores it or it is excluded. */\n targetFile?: string;\n}\n\n/**\n * Measures every git-visible file (tracked or untracked non-ignored) of the repository, so that\n * code copied from outside the target is detected as duplication, and keeps the files under the\n * target: all of them or, with `base`, the changed ones with their line hunks.\n */\nasync function scanRepository(\n { repoRoot, canonicalTarget, targetFile }: RepositoryTarget,\n base: string | undefined,\n options: ResolvedOptions\n): Promise<CheckScope> {\n // Only git-visible files are measured, so a directory git ignores would pass as a check of no\n // files. A directory git lists but whose sources it ignores is not told apart from a repository\n // or package whose only sources are ignored build output, which must pass.\n if (targetFile === undefined && canonicalTarget !== repoRoot && (await isIgnored(repoRoot, canonicalTarget))) {\n throw new Error(\n `${formatPath(canonicalTarget, repoRoot)}: git ignores this directory, so no file in it is checked; target a file to check it regardless`\n );\n }\n const repositoryFiles = await listRepositoryFiles(repoRoot);\n const explicitFiles = new Set(targetFile === undefined ? [] : [targetFile]);\n const unlistedTarget =\n targetFile !== undefined && !repositoryFiles.has(formatPath(targetFile, repoRoot))\n ? formatPath(targetFile, repoRoot)\n : undefined;\n if (unlistedTarget !== undefined) {\n repositoryFiles.add(unlistedTarget);\n }\n const exclusion = keepPaths(await loadRepositoryExclusion(repoRoot, repositoryFiles, options.exclude), explicitFiles);\n // Scan errors start with the file or directory they concern.\n const targetPath = formatPath(canonicalTarget, repoRoot);\n const scan = await scanListedFiles(\n repoRoot,\n repositoryFiles,\n { ...options, loadExclusion: () => Promise.resolve(exclusion) },\n explicitFiles,\n targetFile === undefined && canonicalTarget !== repoRoot ? targetPath : ''\n );\n const crossFileDuplication = measureDuplication(scan, options);\n const isInTarget = (relativePath: string): boolean =>\n isWithinDirectory(path.join(repoRoot, relativePath), canonicalTarget);\n const measuredFiles = scan.files.map(({ file, metrics }) => ({ file: formatPath(file, repoRoot), metrics }));\n let files = measuredFiles.filter(({ file }) => isInTarget(file));\n let isCovered = (error: string): boolean =>\n canonicalTarget === repoRoot || error.startsWith(`${targetPath}/`) || error.startsWith(`${targetPath}:`);\n let mergeBase;\n if (base !== undefined) {\n mergeBase = await resolveMergeBase(repoRoot, base);\n const changedFiles = await listChangedFiles(repoRoot, mergeBase);\n // A targeted file git does not list has no diff; all of it counts as added.\n if (unlistedTarget !== undefined) {\n changedFiles.push({ status: 'added', headPath: unlistedTarget });\n }\n const changesInTarget = changedFiles.filter(({ status, headPath }) => status !== 'deleted' && isInTarget(headPath));\n files = await attachHunks(measuredFiles, changesInTarget, repoRoot, mergeBase);\n isCovered = (error) => changesInTarget.some(({ headPath }) => error.startsWith(`${headPath}:`));\n }\n return { files, crossFileDuplication, ...partitionErrors(scan, isCovered), root: repoRoot, mergeBase };\n}\n\n/** Only a failure on a file the check covers leaves it incomplete; the others are warnings. */\nfunction partitionErrors(\n scan: ScanResult,\n isCovered: (error: string) => boolean\n): Pick<CheckScope, 'errors' | 'warnings'> {\n return {\n errors: scan.errors.filter((error) => isCovered(error)),\n warnings: [...scan.warnings, ...scan.errors.filter((error) => !isCovered(error))],\n };\n}\n\n/** The measured changed files with their line hunks against the merge-base. */\nasync function attachHunks(\n measuredFiles: readonly CheckedFile[],\n changedFiles: readonly ChangedFile[],\n repoRoot: string,\n mergeBase: string\n): Promise<CheckedFile[]> {\n const metricsByPath = new Map(measuredFiles.map(({ file, metrics }) => [file, metrics]));\n const files = await mapConcurrently(changedFiles, os.availableParallelism() * 2, async (changed) => {\n const metrics = metricsByPath.get(changed.headPath);\n // A changed file the scan left out (unsupported, excluded, or failed) has nothing to check.\n if (metrics === undefined) {\n return [];\n }\n const wholeFile: LineHunk = { headStart: 1, headCount: metrics.lines.total };\n const hunks =\n changed.basePath === undefined\n ? [wholeFile]\n : await listLineHunks(repoRoot, mergeBase, changed.basePath, changed.headPath);\n if (metrics.language === 'python' && hunks.some(({ headCount }) => headCount === 0)) {\n anchorDeletionsToCode(hunks, await readFile(path.join(repoRoot, changed.headPath), 'utf8'));\n }\n return [{ file: changed.headPath, metrics, hunks }];\n });\n return files.flat();\n}\n\n/**\n * Moves each pure deletion up to the last code line before it. A Python function ends at its last\n * statement, so the blank and comment-only lines left above a deleted tail lie outside the span the\n * deletion shortened; anchored to that statement, the deletion counts as touching the function.\n */\nfunction anchorDeletionsToCode(hunks: LineHunk[], content: string): void {\n const lines = content.split(/\\r\\n|\\n|\\r/u);\n for (const hunk of hunks) {\n if (hunk.headCount > 0) continue;\n while (hunk.headStart > 0 && /^\\s*(?:#.*)?$/u.test(lines[hunk.headStart - 1] ?? '')) {\n hunk.headStart -= 1;\n }\n }\n}\n\n/** A run-wide failure (a missing target or native addon) leaves nothing to check. */\nfunction measureDuplication(scan: ScanResult, options: ResolvedOptions): CrossFileDuplicationMetrics | undefined {\n if (scan.fatalError) {\n throw new Error(scan.fatalError);\n }\n addCrossFileDuplication(scan, options);\n return scan.crossFileDuplication;\n}\n\nfunction printTextReport(cliOptions: CheckCliOptions, scope: CheckScope, result: CheckResult): void {\n const { violations } = result;\n const checked = `${scope.files.length} ${cliOptions.base === undefined ? '' : 'changed '}files, ${result.checkedFunctionCount} functions checked${describeBase(cliOptions, scope)}`;\n if (scope.errors.length > 0) {\n // Unmeasured files were not checked, so \"no violations\" would be vacuous; never claim a pass.\n writeStdout(\n `Check could not complete: ${scope.errors.length} measurement failures (details on stderr); ${describeCounts(violations)} in the measured files (${checked}).\\n`\n );\n } else if (violations.length === 0) {\n writeStdout(`No threshold violations: ${checked}.\\n`);\n } else {\n writeStdout(`Threshold violations: ${describeCounts(violations)} (${checked}).\\n`);\n }\n for (const violation of violations) {\n writeStdout(`${formatViolation(violation)}\\n`);\n }\n\n const violatedMetrics = new Set(violations.flatMap(({ exceeded }) => exceeded.map(({ metric }) => metric)));\n const violatedThresholds = thresholds.filter((threshold) => violatedMetrics.has(metricNameOf(threshold)));\n if (violatedThresholds.length > 0) {\n writeStdout('\\nHow to fix:\\n');\n for (const { label, hint } of violatedThresholds) {\n writeStdout(`- ${label}: ${hint}\\n`);\n }\n }\n\n for (const warning of scope.warnings) {\n writeStderr(`Warning: ${warning}\\n`);\n }\n for (const error of scope.errors) {\n writeStderr(`Error: ${error}\\n`);\n }\n}\n\nfunction describeBase(cliOptions: CheckCliOptions, scope: CheckScope): string {\n return scope.mergeBase === undefined ? '' : `; base ${cliOptions.base}, merge-base ${scope.mergeBase.slice(0, 12)}`;\n}\n\nfunction describeCounts(violations: readonly Violation[]): string {\n const counts = countViolations(violations);\n return `${counts.violationCount} violations (${counts.functionViolationCount} functions, ${counts.fileViolationCount} files, ${counts.duplicationViolationCount} duplicated blocks)`;\n}\n\nfunction countViolations(violations: readonly Violation[]): {\n violationCount: number;\n functionViolationCount: number;\n fileViolationCount: number;\n duplicationViolationCount: number;\n} {\n const count = (kind: Violation['kind']): number => violations.filter((violation) => violation.kind === kind).length;\n return {\n violationCount: violations.length,\n functionViolationCount: count('function'),\n fileViolationCount: count('file'),\n duplicationViolationCount: count('duplication'),\n };\n}\n\n/** One violation as a single line: the location, then every limit it exceeds. */\nfunction formatViolation(violation: Violation): string {\n const labelByMetric = new Map(thresholds.map((threshold) => [metricNameOf(threshold), threshold.label]));\n const exceeded = violation.exceeded\n .map(\n ({ metric, value, limit }) =>\n // Halstead values are fractional; rounding up keeps a violating value above the printed limit.\n `${labelByMetric.get(metric)} ${Math.ceil(value * 10) / 10} (${violation.kind === 'duplication' ? '<' : '<='} ${limit})`\n )\n .join(', ');\n switch (violation.kind) {\n case 'file': {\n return `${violation.file}: ${exceeded}`;\n }\n case 'function': {\n // A computed name can span lines in the source; the report keeps one line per function.\n return `${formatLocation(violation)} ${violation.name?.replaceAll(/\\s*[\\n\\r]\\s*/gu, ' ')}: ${exceeded}`;\n }\n case 'duplication': {\n const partners = (violation.partners ?? []).map((partner) => formatLocation(partner));\n const listed = [...partners.slice(0, maxListedPartners), ...(partners.length > maxListedPartners ? ['...'] : [])];\n return `${formatLocation(violation)}: ${exceeded}${listed.length > 0 ? `, also at ${listed.join(', ')}` : ''}`;\n }\n }\n}\n\nfunction formatLocation({ file, startLine, endLine }: BlockLocation): string {\n return `${file}:${startLine}-${endLine}`;\n}\n\nfunction printJsonReport(cliOptions: CheckCliOptions, scope: CheckScope, result: CheckResult): void {\n writeStdout(\n JSON.stringify(\n {\n passed: result.violations.length === 0 && scope.errors.length === 0,\n base: cliOptions.base,\n mergeBase: scope.mergeBase,\n summary: {\n ...countViolations(result.violations),\n checkedFileCount: scope.files.length,\n checkedFunctionCount: result.checkedFunctionCount,\n },\n violations: result.violations,\n errors: scope.errors,\n warnings: scope.warnings,\n },\n undefined,\n 2\n ) + '\\n'\n );\n}\n"],"mappings":"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"}
|
package/dist/cli.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";const e=require("./
|
|
3
|
-
`)}function
|
|
4
|
-
`);else{let e=
|
|
2
|
+
"use strict";const e=require("./thresholds.cjs"),t=require("./cliConfig.cjs"),n=require("./exclusion.cjs"),r=require("./scan.cjs"),i=require("./checkCommand.cjs");let a=require("@willbooster/shared-lib"),o=require("commander");s().catch(e=>{r.writeStderr(`Error: ${(0,a.getErrorMessage)(e)}\n`),process.exitCode=1});async function s(){let t=new o.Command().name(`code-gauge`).description(`Measure code metrics: rank refactoring candidates (the default command) or check thresholds.`);c(t.command(`rank`,{isDefault:!0}).description(`Rank the files of a project by refactoring priority`).argument(`[target]`,`file or directory to measure`,`.`)).option(`--top <number>`,`number of top-ranked files to report (default: 10)`,C).option(`--fail-on-error`,`exit with code 1 when files or directories cannot be scanned`).action(d);let n=c(t.command(`check`).description(`Report every exceeded threshold (exit 1 on violations, 2 when files cannot be measured); with --base, only in what the change touches`).argument(`[target]`,`file or directory to check`,`.`)).option(`--base <ref>`,`check only what the working tree changed since the merge-base of this git ref and HEAD`);for(let{key:t,defaultLimit:r}of e.thresholds)n.option(`--${u(t)} <number>`,`${l(t)} (default ${r}); "off" disables it`,b);n.exitOverride(e=>process.exit(e.exitCode===0?0:2)),n.action(async(t,n)=>{let r=Object.fromEntries(e.thresholds.flatMap(({key:e})=>n[e]===void 0?[]:[[e,n[e]]]));await i.runCheckCommand(t,n,r)}),await t.parseAsync()}function c(e){return e.option(`--config <path>`,`config file to use instead of the auto-detected ${t.configFileName}`).option(`--duplication-min-tokens <number>`,`minimum normalized token count for a duplicate region (default 40)`,C).option(`--duplication-max-gap-tokens <number>`,`maximum token gap merged into one gapped clone group; 0 disables merging (default 30)`,S).option(`--duplication-min-similarity-percent <number>`,`minimum similarity percent (1-100) for near-miss (Type-3) clone blocks; 100 reports exact matches only (default 70)`,x).option(`--include-tests`,`include test files and test directories`).option(`--json`,`print JSON output`)}function l(t){if(t===e.duplicationThreshold.key)return`line count from which a duplicated block is a violation`;let n=e.fileThresholds.find(e=>e.key===t);return n?`maximum ${n.label}`:`maximum ${e.functionThresholds.find(e=>e.key===t)?.label} of a function`}function u(e){return e.replaceAll(/[A-Z]/gu,e=>`-${e.toLowerCase()}`)}async function d(e,i){let a=r.resolveTarget(e),o=await r.configSearchDirectory(a),s=await t.loadConfig(i.config,o),c=t.resolveOptions(i,s),l=await r.scanTarget(a,{...c,loadExclusion:e=>n.loadExclusion(o,c.exclude,e)});r.addCrossFileDuplication(l,c);let u=f(l,c.top);c.json?g(l,u,c):_(a,l,u,c),(l.fatalError||c.failOnError&&l.errors.length>0)&&(process.exitCode=1)}function f(e,t){let n=e.files.map(({file:t,metrics:n})=>{let i=r.formatPath(t,e.displayRoot),a=r.collectDuplicatedLineNumbers(n,e.crossFileDuplication,i);return{file:i,worstFunction:h(n.functions),duplicatedLineCount:a.size,duplicatedLineRatio:n.lines.code===0?0:a.size/n.lines.code,crossFilePartners:[],ncss:n.ncssCount,codeLines:n.lines.code}}),i=m(n.map(e=>e.worstFunction?.cognitiveComplexity??0)),a=m(n.map(e=>e.duplicatedLineCount)),o=m(n.map(e=>e.ncss)),s=n.map(e=>({...e,score:i(e.worstFunction?.cognitiveComplexity??0)+a(e.duplicatedLineCount)+o(e.ncss)})).toSorted((e,t)=>t.score-e.score||t.ncss-e.ncss||e.file.localeCompare(t.file));return p(s.slice(0,t),e.crossFileDuplication),s}function p(e,t){if(!t)return;let n=new Map(e.map(e=>[e.file,/* @__PURE__ */ new Set]));for(let e of t.groups)for(let t of e.files){let r=n.get(t);if(r)for(let n of e.files)n!==t&&r.add(n)}for(let t of e)t.crossFilePartners=[...n.get(t.file)??[]].toSorted()}function m(e){let t=e.toSorted((e,t)=>e-t);return e=>{let n=0,r=t.length;for(;n<r;){let i=n+r>>1;t[i]<e?n=i+1:r=i}return t.length===0?0:n/t.length}}function h(e){let t;for(let n of e)(!t||n.cognitiveComplexity>t.cognitiveComplexity||n.cognitiveComplexity===t.cognitiveComplexity&&n.ncss>t.ncss)&&(t=n);if(t)return{name:t.name??`<anonymous>`,startLine:t.startLine,endLine:t.endLine,cognitiveComplexity:t.cognitiveComplexity,ncss:t.ncss,nestingDepth:t.nestingDepth}}function g(e,t,n){let i=t.slice(0,n.top);r.writeStdout(JSON.stringify({summary:y(e.files),totalRankedFiles:t.length,truncated:i.length<t.length,files:i,errors:e.errors,warnings:e.warnings},void 0,2)+`
|
|
3
|
+
`)}function _(e,t,n,i){if(t.fatalError){r.writeStderr(`Error: ${t.fatalError}\n`);return}let a=y(t.files);if(r.writeStdout(`Measured ${a.fileCount} files under ${e} (code LOC ${a.linesOfCode}, NCSS ${a.ncssCount}, functions ${a.functionCount})\n`),n.length===0)r.writeStdout(`No measurable files found.
|
|
4
|
+
`);else{let e=n.slice(0,i.top),t=n.length>e.length?` of ${n.length}`:``;r.writeStdout(`\nRefactoring candidates (top ${e.length}${t}):\n`);for(let[t,n]of e.entries())r.writeStdout(`${t+1}. ${v(n)}\n`)}if(t.warnings.length>0){r.writeStderr(`\nDegraded ${t.warnings.length} files (measured, but excluded from cross-file matching):\n`);for(let e of t.warnings.slice(0,10))r.writeStderr(`- ${e}\n`);t.warnings.length>10&&r.writeStderr(`- ... ${t.warnings.length-10} more\n`)}if(t.errors.length>0){r.writeStderr(`\nSkipped ${t.errors.length} files or directories:\n`);for(let e of t.errors.slice(0,10))r.writeStderr(`- ${e}\n`);t.errors.length>10&&r.writeStderr(`- ... ${t.errors.length-10} more\n`)}}function v(e){let t=[];if(e.worstFunction){let n=e.worstFunction;t.push(`worst function ${n.name} (L${n.startLine}-${n.endLine}) cognitive ${n.cognitiveComplexity}, NCSS ${n.ncss}, nesting ${n.nestingDepth}`)}if(e.duplicatedLineCount>0){let n=e.crossFilePartners.length>0?`, shared with ${e.crossFilePartners.slice(0,3).join(`, `)}${e.crossFilePartners.length>3?`, ...`:``}`:``;t.push(`duplicated lines ${e.duplicatedLineCount} (${Math.round(e.duplicatedLineRatio*100)}%${n})`)}return t.push(`file NCSS ${e.ncss}`),`${e.file} (score ${e.score.toFixed(2)}): ${t.join(`; `)}`}function y(e){let t=0,n=0,r=0,i=0;for(let a of e)t+=a.metrics.functions.length,n+=a.metrics.lines.code,r=Math.max(r,a.metrics.maxCognitiveComplexity),i+=a.metrics.ncssCount;return{fileCount:e.length,functionCount:t,linesOfCode:n,maxCognitiveComplexity:r,ncssCount:i}}function b(e){if(e===`off`)return 1/0;let t=e.trim()===``?NaN:Number(e);if(!Number.isFinite(t)||t<0)throw new o.InvalidArgumentError(`Expected a non-negative number or "off".`);return t}function x(e){let t=C(e);if(t>100)throw new o.InvalidArgumentError(`Expected an integer between 1 and 100.`);return t}function S(e){if(!/^\d+$/u.test(e))throw new o.InvalidArgumentError(`Expected a non-negative integer.`);let t=Number(e);if(!Number.isSafeInteger(t)||t<0)throw new o.InvalidArgumentError(`Expected a non-negative integer.`);return t}function C(e){let t=(0,a.parsePositiveInteger)(e);if(t===void 0)throw new o.InvalidArgumentError(`Expected a positive integer.`);return t}
|
|
5
5
|
//# sourceMappingURL=cli.cjs.map
|