code-gauge 6.0.0 → 8.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 CHANGED
@@ -80,7 +80,8 @@ code-gauge check --base main # only violations in what the working tree
80
80
  code-gauge check --base main src/api # the same, for the changed files under src/api
81
81
  ```
82
82
 
83
- `code-gauge check [target]` reports every violation of the thresholds below in the files under
83
+ `code-gauge check [target]` reports every violation of the thresholds below, as a warning or as an
84
+ error, in the files under
84
85
  the target. Inside a git repository it measures the repository's git-visible files (tracked, or
85
86
  untracked and not ignored), so a block copied from a file outside the target is reported with that
86
87
  file as its partner, and a copy that exists only in a git-ignored file is not duplication. The
@@ -92,28 +93,47 @@ ignore passes as a check of zero files when it holds no git-visible source file,
92
93
  its only sources are ignored build output. Outside a git repository it walks the target directory
93
94
  like the ranking command. The thresholds:
94
95
 
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 |
96
+ | Config key | Violation | Warning | Error |
97
+ | --------------------------------- | ------------------------------------------------- | ------- | ----- |
98
+ | `maxFunctionCognitiveComplexity` | a function's cognitive complexity is above it | 15 | 30 |
99
+ | `maxFunctionCyclomaticComplexity` | a function's cyclomatic complexity is above it | off | off |
100
+ | `maxFunctionNcss` | a function's NCSS is above it | 60 | 100 |
101
+ | `maxFunctionNestingDepth` | a function's nesting depth is above it | 4 | 5 |
102
+ | `maxFunctionParameterCount` | a function's parameter count is above it | 7 | off |
103
+ | `maxFunctionHalsteadVolume` | a function's Halstead volume is above it | off | off |
104
+ | `maxFunctionHalsteadDifficulty` | a function's Halstead difficulty is above it | off | off |
105
+ | `maxFunctionHalsteadEffort` | a function's Halstead effort is above it | off | off |
106
+ | `maxFunctionDepDegree` | a function's DepDegree is above it | off | off |
107
+ | `maxFileNcss` | a file's NCSS is above it | 500 | 1000 |
108
+ | `minDuplicateLines` | a duplicated block spans at least this many lines | 10 | 20 |
109
+
110
+ Every threshold has a limit per level, and a value is reported at the most severe level whose limit
111
+ it violates. The warning limits mark code worth simplifying when it is touched; the error limits
112
+ mark code to fix, and only errors fail the check (exit code 1).
113
+
114
+ The warning limits follow common conventions. The error limits and the thresholds that are off
115
+ were set from the 2139 functions of [WillBooster/shared](https://github.com/WillBooster/shared)
116
+ (TypeScript, October 2026): a cognitive complexity above 15 flags 5% of them and one above 30 flags
117
+ 1.5%, and the functions sampled above 30 each had a part that reads better extracted, while those
118
+ between 15 and 30 were mixed. Cyclomatic complexity, the Halstead metrics, and DepDegree are off
119
+ because, at limits of 10, 2000, 20, 30000, and 50, all but 24 of the 107 functions they flagged
120
+ there also exceeded the cognitive-complexity limit.
121
+
122
+ Cognitive complexity, NCSS, Halstead volume, and DepDegree of a function cover the functions nested
123
+ in it, so a limit of these that a function and a function nested in it both violate is reported
124
+ for the outer function only. The other limits measure a function's own body (or, for Halstead
125
+ difficulty and effort, a ratio that can be higher for the nested function alone) and are reported
126
+ for each function.
108
127
 
109
128
  A duplicated block is one occurrence of a within-file or cross-file clone, found with the
110
129
  [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.
130
+ its last line. Occurrences of at least the warning or the error `minDuplicateLines` that overlap in a
131
+ file are reported as one block covering all of them, at the level its whole span reaches.
113
132
 
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`
133
+ Each limit is set in the `warning` or `error` part of the
134
+ [`thresholds` config section](#configuration), where `null` disables it and `languages` overrides
135
+ limits for the files of one language, or with the command-line option named after the level and
136
+ the config key (`--warning-max-function-ncss 80`, `--error-min-duplicate-lines off`), where `off`
117
137
  disables it. The command line wins over the whole config file, per-language overrides included.
118
138
 
119
139
  With `--base <ref>`, the check covers only what the working tree changed since the merge-base of
@@ -132,30 +152,30 @@ violation the change did not introduce is reported too once the change touches i
132
152
  or duplicated block. `--base` requires a git repository.
133
153
 
134
154
  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:
155
+ the counts, one line per violation with the errors first, each level ordered by path and line, and
156
+ one remediation hint per violated metric:
137
157
 
138
158
  ```
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
159
+ Threshold violations: 1 errors, 2 warnings (1 functions, 0 files, 2 duplicated blocks) (4 files, 5 functions checked).
160
+ error: src/legacy.ts:1-21 decide: cognitive complexity 34 (max 30), nesting depth 5 (max 4)
161
+ warning: src/report.ts:1-12: duplicated lines 12 (max 9), also at src/summary.ts:1-12
162
+ warning: src/summary.ts:1-12: duplicated lines 12 (max 9), also at src/report.ts:1-12
143
163
 
144
164
  How to fix:
145
165
  - 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
166
  - nesting depth: replace nested conditions with guard clauses or move inner blocks into functions.
148
167
  - duplicated lines: extract the repeated code into one shared function or module and call it from every location.
149
168
  ```
150
169
 
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.
170
+ A violation's level is that of its most severe limit. A function gets one line listing every
171
+ threshold it exceeds, each with the largest value its violated limit allows; a file-level violation
172
+ prints the path without a line span, and a duplicated block lists up to three of its other copies.
173
+ Paths are relative to the repository root, or to the target directory outside a git repository.
154
174
 
155
175
  | Exit code | Meaning |
156
176
  | --------- | -------------------------------------------------------------------------------------------------------------------------------- |
157
- | 0 | No violations. |
158
- | 1 | Violations. Whether they are warnings or errors is up to the caller. |
177
+ | 0 | No errors; warnings do not fail the check. |
178
+ | 1 | Errors. |
159
179
  | 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
180
 
161
181
  A file that cannot be measured causes exit code 2 only when the check covers it: a file under the
@@ -176,6 +196,8 @@ ranking command.
176
196
  "mergeBase": "9f624517156db3290e189b9fd0b8ed38de39a04c",
177
197
  "summary": {
178
198
  "violationCount": 2,
199
+ "errorViolationCount": 1,
200
+ "warningViolationCount": 1,
179
201
  "functionViolationCount": 1,
180
202
  "fileViolationCount": 0,
181
203
  "duplicationViolationCount": 1,
@@ -185,22 +207,23 @@ ranking command.
185
207
  "violations": [
186
208
  {
187
209
  "kind": "function",
210
+ "level": "error",
188
211
  "file": "src/legacy.ts",
189
212
  "startLine": 1,
190
213
  "endLine": 21,
191
- "name": "decide",
192
214
  "exceeded": [
193
- { "metric": "functionCognitiveComplexity", "value": 24, "limit": 15 },
194
- { "metric": "functionCyclomaticComplexity", "value": 15, "limit": 10 },
195
- { "metric": "functionNestingDepth", "value": 5, "limit": 4 }
196
- ]
215
+ { "metric": "functionCognitiveComplexity", "value": 34, "level": "error", "limit": 30 },
216
+ { "metric": "functionNestingDepth", "value": 5, "level": "warning", "limit": 4 }
217
+ ],
218
+ "name": "decide"
197
219
  },
198
220
  {
199
221
  "kind": "duplication",
222
+ "level": "warning",
200
223
  "file": "src/pasted.ts",
201
224
  "startLine": 1,
202
225
  "endLine": 12,
203
- "exceeded": [{ "metric": "duplicateLines", "value": 12, "limit": 10 }],
226
+ "exceeded": [{ "metric": "duplicateLines", "value": 12, "level": "warning", "limit": 10 }],
204
227
  "partners": [{ "file": "src/report.ts", "startLine": 1, "endLine": 12 }]
205
228
  }
206
229
  ],
@@ -209,23 +232,25 @@ ranking command.
209
232
  }
210
233
  ```
211
234
 
212
- - `passed`: `true` when there are no violations and no errors (exit code 0).
235
+ - `passed`: `true` when no violation is an error and every covered file was measured (exit code 0).
213
236
  - `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
237
+ - `summary`: the violation counts in total, per level, and per kind, and the number of files and
238
+ functions checked: those under the target or, with `--base`, the changed files under it and the
239
+ functions the change touches.
240
+ - `violations`: the errors first, each level ordered by `file`, then `startLine`. Each has
218
241
  - `kind`: `function`, `file`, or `duplication`;
242
+ - `level`: `error` when any of its `exceeded` limits is an error, otherwise `warning`;
219
243
  - `file`, `startLine`, `endLine`: the 1-based line span of the function or duplicated block; for
220
244
  kind `file`, the first and last line of the file;
221
245
  - `name`: only for kind `function`; `<anonymous>` for a function without a name;
222
246
  - `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;
247
+ `min` prefix, e.g. `functionNcss` for `maxFunctionNcss`), the measured `value`, the most severe
248
+ `level` whose limit the value violates, and that `limit`. A value violates when it is above the
249
+ limit; `duplicateLines` violates from the limit on;
226
250
  - `partners`: only for kind `duplication`; every other copy of the block, ordered by `file`, then
227
251
  `startLine`.
228
- - `errors`: the files the check covers that could not be measured (exit code 2).
252
+ - `errors`: the files the check covers that could not be measured (exit code 2); unrelated to the
253
+ `error` level of a violation.
229
254
  - `warnings`: files measured without cross-file duplication data, and unmeasured files of the
230
255
  repository the check does not cover.
231
256
 
@@ -243,18 +268,34 @@ with `--config`). The following config reproduces every built-in default:
243
268
  },
244
269
  "rank": { "top": 10 },
245
270
  "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": {}
271
+ "warning": {
272
+ "maxFunctionCognitiveComplexity": 15,
273
+ "maxFunctionCyclomaticComplexity": null,
274
+ "maxFunctionNcss": 60,
275
+ "maxFunctionNestingDepth": 4,
276
+ "maxFunctionParameterCount": 7,
277
+ "maxFunctionHalsteadVolume": null,
278
+ "maxFunctionHalsteadDifficulty": null,
279
+ "maxFunctionHalsteadEffort": null,
280
+ "maxFunctionDepDegree": null,
281
+ "maxFileNcss": 500,
282
+ "minDuplicateLines": 10,
283
+ "languages": {}
284
+ },
285
+ "error": {
286
+ "maxFunctionCognitiveComplexity": 30,
287
+ "maxFunctionCyclomaticComplexity": null,
288
+ "maxFunctionNcss": 100,
289
+ "maxFunctionNestingDepth": 5,
290
+ "maxFunctionParameterCount": null,
291
+ "maxFunctionHalsteadVolume": null,
292
+ "maxFunctionHalsteadDifficulty": null,
293
+ "maxFunctionHalsteadEffort": null,
294
+ "maxFunctionDepDegree": null,
295
+ "maxFileNcss": 1000,
296
+ "minDuplicateLines": 20,
297
+ "languages": {}
298
+ }
258
299
  },
259
300
  "exclude": [],
260
301
  "includeTests": false,
@@ -266,18 +307,20 @@ The command line wins over the config file, which wins over the defaults. Unknow
266
307
  rejected so stale configuration fails loudly. `rank.top` and `failOnError` apply to the ranking
267
308
  command only.
268
309
 
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`:
310
+ `languages` in a level of `thresholds` maps a language name (`javascript`, `jsx`, `typescript`,
311
+ `tsx`, `python`, `go`, `rust`, `java`, `ruby`, `c`, `cpp`, `csharp`, `kotlin`) to the limits that
312
+ differ for its files; a limit it leaves out keeps the value set outside `languages`:
272
313
 
273
314
  ```json
274
315
  {
275
316
  "thresholds": {
276
- "maxFunctionNcss": 60,
277
- "maxFunctionHalsteadVolume": null,
278
- "languages": {
279
- "tsx": { "maxFunctionNcss": 100 },
280
- "python": { "maxFunctionParameterCount": null }
317
+ "warning": {
318
+ "maxFunctionNcss": 60,
319
+ "maxFunctionParameterCount": 5,
320
+ "languages": {
321
+ "tsx": { "maxFunctionNcss": 100 },
322
+ "python": { "maxFunctionParameterCount": null }
323
+ }
281
324
  }
282
325
  }
283
326
  }
@@ -375,14 +418,10 @@ supporting arbitrary grammars would mean shipping incomplete metrics for them.
375
418
 
376
419
  Parsing and every metric pass run in a Rust addon (tree-sitter); the thin TypeScript layer handles
377
420
  the CLI, cross-file matching, and the Halstead float derivations. Releases include prebuilt addons
378
- for Linux x64/arm64 (glibc and musl), macOS x64/arm64, and Windows x64. On other platforms,
379
- `postinstall` builds the addon from the bundled sources, which requires a
380
- [Rust toolchain](https://rustup.rs).
381
-
382
- If a prebuilt addon is unavailable, package managers that block dependency install scripts by
383
- default (recent npm versions, and Bun unless `code-gauge` is listed in `trustedDependencies`) skip
384
- the fallback build. Either approve `code-gauge`'s install script, or build the addon manually
385
- inside the installed package (the runtime error message points here too):
421
+ for Linux x64/arm64 (glibc and musl), macOS x64/arm64, and Windows x64, and a release that
422
+ includes them runs no install script. On other platforms, build the addon from the bundled sources inside the installed
423
+ package, which requires a [Rust toolchain](https://rustup.rs) (the runtime error message points
424
+ here too):
386
425
 
387
426
  ```sh
388
427
  node node_modules/code-gauge/scripts/buildNative.mjs
@@ -1,5 +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(`
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 m(s,r.base,l),d=o.checkThresholds(u.files,u.crossFileDuplication,t.resolveLimits(i,c.config.thresholds));await p(d.violations,u.root),l.json?O(r,u,d):x(r,u,d),u.errors.length>0?process.exitCode=2:f(d.violations)&&(process.exitCode=1)}catch(e){a.writeStderr(`Error: ${(0,s.getErrorMessage)(e)}\n`),process.exitCode=2}}function f(e){return e.some(({level:e})=>e===`error`)}async function p(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 m(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 h(i,n)}return await g({repoRoot:s,canonicalTarget:i,targetFile:o},t,n)}async function h(e,t){let n=await a.configSearchDirectory(e),r=await a.scanTarget(e,{...t,loadExclusion:e=>i.loadExclusion(n,t.exclude,e)}),o=b(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 g({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=b(m,s),g=n=>a.isWithinDirectory(l.default.join(e,n),t),y=m.files.map(({file:t,metrics:n})=>({file:a.formatPath(t,e),metrics:n})),x=y.filter(({file:e})=>g(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`&&g(t));x=await v(y,n,e,C),S=e=>n.some(({headPath:t})=>e.startsWith(`${t}:`))}return{files:x,crossFileDuplication:h,..._(m,S),root:e,mergeBase:C}}function _(e,t){return{errors:e.errors.filter(e=>t(e)),warnings:[...e.warnings,...e.errors.filter(e=>!t(e))]}}async function v(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)&&y(s,await(0,c.readFile)(l.default.join(n,e.headPath),`utf8`)),[{file:e.headPath,metrics:t,hunks:s}]})).flat()}function y(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 a.addCrossFileDuplication(e,t),e.crossFileDuplication}function x(e,n,r){let{violations:i}=r,o=`${n.files.length} ${e.base===void 0?``:`changed `}files, ${r.checkedFunctionCount} functions checked${S(e,n)}`;n.errors.length>0?a.writeStdout(`Check could not complete: ${n.errors.length} measurement failures (details on stderr); ${C(i)} in the measured files (${o}).\n`):i.length===0?a.writeStdout(`No threshold violations: ${o}.\n`):a.writeStdout(`Threshold violations: ${C(i)} (${o}).\n`);for(let e of i)a.writeStdout(`${T(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
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)+`
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 S(e,t){return t.mergeBase===void 0?``:`; base ${e.base}, merge-base ${t.mergeBase.slice(0,12)}`}function C(e){let t=w(e);return`${t.errorViolationCount} errors, ${t.warningViolationCount} warnings (${t.functionViolationCount} functions, ${t.fileViolationCount} files, ${t.duplicationViolationCount} duplicated blocks)`}function w(e){let t=t=>e.filter(e=>e.kind===t).length,n=e.filter(({level:e})=>e===`error`).length;return{violationCount:e.length,errorViolationCount:n,warningViolationCount:e.length-n,functionViolationCount:t(`function`),fileViolationCount:t(`file`),duplicationViolationCount:t(`duplication`)}}function T(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} (max ${e.kind===`duplication`?Math.max(Math.ceil(i)-1,0):i})`).join(`, `);return`${e.level}: ${E(e,r)}`}function E(e,t){switch(e.kind){case`file`:return`${e.file}: ${t}`;case`function`:return`${D(e)} ${e.name?.replaceAll(/\s*[\n\r]\s*/gu,` `)}: ${t}`;case`duplication`:{let n=(e.partners??[]).map(e=>D(e)),r=[...n.slice(0,3),...n.length>3?[`...`]:[]];return`${D(e)}: ${t}${r.length>0?`, also at ${r.join(`, `)}`:``}`}}}function D({file:e,startLine:t,endLine:n}){return`${e}:${t}-${n}`}function O(e,t,n){a.writeStdout(JSON.stringify({passed:!f(n.violations)&&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
4
  `)}exports.runCheckCommand=d;
5
5
  //# sourceMappingURL=checkCommand.cjs.map
@@ -1 +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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"}
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 LimitsByLevel } 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 error-level violations, 1 error-level\n * violations, 2 files the check covers could not be measured.\n */\nexport async function runCheckCommand(\n target: string,\n cliOptions: CheckCliOptions,\n cliLimits: LimitsByLevel\n): 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 (hasErrors(result.violations)) {\n process.exitCode = 1;\n }\n } catch (error) {\n writeStderr(`Error: ${getErrorMessage(error)}\\n`);\n process.exitCode = 2;\n }\n}\n\nfunction hasErrors(violations: readonly Violation[]): boolean {\n return violations.some(({ level }) => level === 'error');\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.errorViolationCount} errors, ${counts.warningViolationCount} warnings (${counts.functionViolationCount} functions, ${counts.fileViolationCount} files, ${counts.duplicationViolationCount} duplicated blocks)`;\n}\n\nfunction countViolations(violations: readonly Violation[]): {\n violationCount: number;\n errorViolationCount: number;\n warningViolationCount: 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 const errorViolationCount = violations.filter(({ level }) => level === 'error').length;\n return {\n violationCount: violations.length,\n errorViolationCount,\n warningViolationCount: violations.length - errorViolationCount,\n functionViolationCount: count('function'),\n fileViolationCount: count('file'),\n duplicationViolationCount: count('duplication'),\n };\n}\n\n/** One violation as a single line: its level, 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 maximum.\n // A duplicated block violates from its limit on, so the largest allowed span is the last\n // whole line count below it; a limit of 0 allows none.\n `${labelByMetric.get(metric)} ${Math.ceil(value * 10) / 10} (max ${violation.kind === 'duplication' ? Math.max(Math.ceil(limit) - 1, 0) : limit})`\n )\n .join(', ');\n return `${violation.level}: ${describeViolation(violation, exceeded)}`;\n}\n\nfunction describeViolation(violation: Violation, exceeded: string): string {\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: !hasErrors(result.violations) && 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":"mYAgEA,eAAsB,EACpB,EACA,EACA,EACe,CACf,GAAI,CACF,IAAM,EAAiBA,EAAAA,cAAc,CAAM,EACrC,EAAS,MAAMC,EAAAA,WAAW,EAAW,OAAQ,MAAMC,EAAAA,sBAAsB,CAAc,CAAC,EACxF,EAAUC,EAAAA,eAAe,EAAY,CAAM,EAC3C,EAAQ,MAAM,EAAU,EAAgB,EAAW,KAAM,CAAO,EAChE,EAASC,EAAAA,gBACb,EAAM,MACN,EAAM,qBACNC,EAAAA,cAAc,EAAW,EAAO,OAAO,UAAU,CACnD,EACA,MAAM,EAA4B,EAAO,WAAY,EAAM,IAAI,EAE3D,EAAQ,KACV,EAAgB,EAAY,EAAO,CAAM,EAEzC,EAAgB,EAAY,EAAO,CAAM,EAEvC,EAAM,OAAO,OAAS,EACxB,QAAQ,SAAW,EACV,EAAU,EAAO,UAAU,IACpC,QAAQ,SAAW,EAEvB,OAAS,EAAO,CACd,EAAA,YAAY,WAAA,EAAUC,EAAAA,gBAAAA,CAAgB,CAAK,EAAE,GAAG,EAChD,QAAQ,SAAW,CACrB,CACF,CAEA,SAAS,EAAU,EAA2C,CAC5D,OAAO,EAAW,MAAM,CAAE,WAAY,IAAU,OAAO,CACzD,CAMA,eAAe,EAA4B,EAAkC,EAA6B,CACxG,IAAK,IAAM,KAAa,EAAW,QAAQ,CAAE,UAAW,IAAS,MAAM,EAAG,CACxE,IAAM,EAAU,MAAA,EAAMC,EAAAA,SAAAA,CAASC,EAAAA,QAAK,KAAK,EAAM,EAAU,IAAI,EAAG,MAAM,EAClE,WAAW,KAAK,CAAO,IACzB,EAAU,QAAU,KAAK,IAAI,EAAU,QAAU,EAAG,CAAC,EAEzD,CACF,CAOA,eAAe,EACb,EACA,EACA,EACqB,CACrB,IAAM,EAAkB,MAAA,EAAMC,EAAAA,SAAAA,CAAS,CAAc,EAE/C,GAAa,MAAA,EADMC,EAAAA,KAAAA,CAAK,CAAe,EAAA,CACf,OAAO,EAAI,EAAkB,IAAA,GAC3D,GAAI,GAAc,CAACC,EAAAA,YAAY,EAAY,EAAS,EAAI,EACtD,MAAU,MAAM,GAAGH,EAAAA,QAAK,SAAS,CAAU,EAAE,wBAAwB,EAEvE,IAAI,EACJ,GAAI,CACF,EAAW,MAAA,EAAMC,EAAAA,SAAAA,CAAS,MAAMG,EAAAA,gBAAgB,EAAaJ,EAAAA,QAAK,QAAQ,CAAe,EAAI,CAAe,CAAC,CAC/G,OAAS,EAAO,CACd,GAAI,IAAS,IAAA,GACX,MAAM,EAER,OAAO,MAAM,EAAkB,EAAiB,CAAO,CACzD,CACA,OAAO,MAAM,EAAe,CAAE,WAAU,kBAAiB,YAAW,EAAG,EAAM,CAAO,CACtF,CAEA,eAAe,EAAkB,EAAyB,EAA+C,CACvG,IAAM,EAAkB,MAAMN,EAAAA,sBAAsB,CAAe,EAC7D,EAAO,MAAMW,EAAAA,WAAW,EAAiB,CAC7C,GAAG,EACH,cAAgB,GAAkBC,EAAAA,cAAc,EAAiB,EAAQ,QAAS,CAAa,CACjG,CAAC,EACK,EAAuB,EAAmB,EAAM,CAAO,EAC7D,MAAO,CACL,MAAO,EAAK,MAAM,KAAK,CAAE,OAAM,cAAe,CAAE,KAAMC,EAAAA,WAAW,EAAM,EAAK,WAAW,EAAG,SAAQ,EAAE,EACpG,uBACA,OAAQ,EAAK,OACb,SAAU,EAAK,SACf,KAAM,EAAK,WACb,CACF,CAcA,eAAe,EACb,CAAE,WAAU,kBAAiB,cAC7B,EACA,EACqB,CAIrB,GAAI,IAAe,IAAA,IAAa,IAAoB,GAAa,MAAMC,EAAAA,UAAU,EAAU,CAAe,EACxG,MAAU,MACR,GAAGD,EAAAA,WAAW,EAAiB,CAAQ,EAAE,gGAC3C,EAEF,IAAM,EAAkB,MAAME,EAAAA,oBAAoB,CAAQ,EACpD,EAAgB,IAAI,IAAI,IAAe,IAAA,GAAY,CAAC,EAAI,CAAC,CAAU,CAAC,EACpE,EACJ,IAAe,IAAA,IAAa,CAAC,EAAgB,IAAIF,EAAAA,WAAW,EAAY,CAAQ,CAAC,EAC7EA,EAAAA,WAAW,EAAY,CAAQ,EAC/B,IAAA,GACF,IAAmB,IAAA,IACrB,EAAgB,IAAI,CAAc,EAEpC,IAAM,EAAYG,EAAAA,UAAU,MAAMC,EAAAA,wBAAwB,EAAU,EAAiB,EAAQ,OAAO,EAAG,CAAa,EAE9G,EAAaJ,EAAAA,WAAW,EAAiB,CAAQ,EACjD,EAAO,MAAMK,EAAAA,gBACjB,EACA,EACA,CAAE,GAAG,EAAS,kBAAqB,QAAQ,QAAQ,CAAS,CAAE,EAC9D,EACA,IAAe,IAAA,IAAa,IAAoB,EAAW,EAAa,EAC1E,EACM,EAAuB,EAAmB,EAAM,CAAO,EACvD,EAAc,GAClBC,EAAAA,kBAAkBb,EAAAA,QAAK,KAAK,EAAU,CAAY,EAAG,CAAe,EAChE,EAAgB,EAAK,MAAM,KAAK,CAAE,OAAM,cAAe,CAAE,KAAMO,EAAAA,WAAW,EAAM,CAAQ,EAAG,SAAQ,EAAE,EACvG,EAAQ,EAAc,QAAQ,CAAE,UAAW,EAAW,CAAI,CAAC,EAC3D,EAAa,GACf,IAAoB,GAAY,EAAM,WAAW,GAAG,EAAW,EAAE,GAAK,EAAM,WAAW,GAAG,EAAW,EAAE,EACrG,EACJ,GAAI,IAAS,IAAA,GAAW,CACtB,EAAY,MAAMO,EAAAA,iBAAiB,EAAU,CAAI,EACjD,IAAM,EAAe,MAAMC,EAAAA,iBAAiB,EAAU,CAAS,EAE3D,IAAmB,IAAA,IACrB,EAAa,KAAK,CAAE,OAAQ,QAAS,SAAU,CAAe,CAAC,EAEjE,IAAM,EAAkB,EAAa,QAAQ,CAAE,SAAQ,cAAe,IAAW,WAAa,EAAW,CAAQ,CAAC,EAClH,EAAQ,MAAM,EAAY,EAAe,EAAiB,EAAU,CAAS,EAC7E,EAAa,GAAU,EAAgB,MAAM,CAAE,cAAe,EAAM,WAAW,GAAG,EAAS,EAAE,CAAC,CAChG,CACA,MAAO,CAAE,QAAO,uBAAsB,GAAG,EAAgB,EAAM,CAAS,EAAG,KAAM,EAAU,WAAU,CACvG,CAGA,SAAS,EACP,EACA,EACyC,CACzC,MAAO,CACL,OAAQ,EAAK,OAAO,OAAQ,GAAU,EAAU,CAAK,CAAC,EACtD,SAAU,CAAC,GAAG,EAAK,SAAU,GAAG,EAAK,OAAO,OAAQ,GAAU,CAAC,EAAU,CAAK,CAAC,CAAC,CAClF,CACF,CAGA,eAAe,EACb,EACA,EACA,EACA,EACwB,CACxB,IAAM,EAAgB,IAAI,IAAI,EAAc,KAAK,CAAE,OAAM,aAAc,CAAC,EAAM,CAAO,CAAC,CAAC,EAiBvF,OAAO,MAAA,EAhBaC,EAAAA,gBAAAA,CAAgB,EAAcC,EAAAA,QAAG,qBAAqB,EAAI,EAAG,KAAO,IAAY,CAClG,IAAM,EAAU,EAAc,IAAI,EAAQ,QAAQ,EAElD,GAAI,IAAY,IAAA,GACd,MAAO,CAAC,EAEV,IAAM,EAAsB,CAAE,UAAW,EAAG,UAAW,EAAQ,MAAM,KAAM,EACrE,EACJ,EAAQ,WAAa,IAAA,GACjB,CAAC,CAAS,EACV,MAAMC,EAAAA,cAAc,EAAU,EAAW,EAAQ,SAAU,EAAQ,QAAQ,EAIjF,OAHI,EAAQ,WAAa,UAAY,EAAM,MAAM,CAAE,eAAgB,IAAc,CAAC,GAChF,EAAsB,EAAO,MAAA,EAAMnB,EAAAA,SAAAA,CAASC,EAAAA,QAAK,KAAK,EAAU,EAAQ,QAAQ,EAAG,MAAM,CAAC,EAErF,CAAC,CAAE,KAAM,EAAQ,SAAU,UAAS,OAAM,CAAC,CACpD,CAAC,EAAA,CACY,KAAK,CACpB,CAOA,SAAS,EAAsB,EAAmB,EAAuB,CACvE,IAAM,EAAQ,EAAQ,MAAM,aAAa,EACzC,IAAK,IAAM,KAAQ,EACb,OAAK,UAAY,GACrB,KAAO,EAAK,UAAY,GAAK,iBAAiB,KAAK,EAAM,EAAK,UAAY,IAAM,EAAE,GAChF,IAAK,SAGX,CAGA,SAAS,EAAmB,EAAkB,EAAmE,CAC/G,GAAI,EAAK,WACP,MAAU,MAAM,EAAK,UAAU,EAGjC,OADA,EAAA,wBAAwB,EAAM,CAAO,EAC9B,EAAK,oBACd,CAEA,SAAS,EAAgB,EAA6B,EAAmB,EAA2B,CAClG,GAAM,CAAE,cAAe,EACjB,EAAU,GAAG,EAAM,MAAM,OAAO,GAAG,EAAW,OAAS,IAAA,GAAY,GAAK,WAAW,SAAS,EAAO,qBAAqB,oBAAoB,EAAa,EAAY,CAAK,IAC5K,EAAM,OAAO,OAAS,EAExB,EAAA,YACE,6BAA6B,EAAM,OAAO,OAAO,6CAA6C,EAAe,CAAU,EAAE,0BAA0B,EAAQ,KAC7J,EACS,EAAW,SAAW,EAC/B,EAAA,YAAY,4BAA4B,EAAQ,IAAI,EAEpD,EAAA,YAAY,yBAAyB,EAAe,CAAU,EAAE,IAAI,EAAQ,KAAK,EAEnF,IAAK,IAAM,KAAa,EACtB,EAAA,YAAY,GAAG,EAAgB,CAAS,EAAE,GAAG,EAG/C,IAAM,EAAkB,IAAI,IAAI,EAAW,SAAS,CAAE,cAAe,EAAS,KAAK,CAAE,YAAa,CAAM,CAAC,CAAC,EACpG,EAAqBmB,EAAAA,WAAW,OAAQ,GAAc,EAAgB,IAAIC,EAAAA,aAAa,CAAS,CAAC,CAAC,EACxG,GAAI,EAAmB,OAAS,EAAG,CACjC,EAAA,YAAY;;CAAiB,EAC7B,IAAK,GAAM,CAAE,QAAO,UAAU,EAC5B,EAAA,YAAY,KAAK,EAAM,IAAI,EAAK,GAAG,CAEvC,CAEA,IAAK,IAAM,KAAW,EAAM,SAC1B,EAAA,YAAY,YAAY,EAAQ,GAAG,EAErC,IAAK,IAAM,KAAS,EAAM,OACxB,EAAA,YAAY,UAAU,EAAM,GAAG,CAEnC,CAEA,SAAS,EAAa,EAA6B,EAA2B,CAC5E,OAAO,EAAM,YAAc,IAAA,GAAY,GAAK,UAAU,EAAW,KAAK,eAAe,EAAM,UAAU,MAAM,EAAG,EAAE,GAClH,CAEA,SAAS,EAAe,EAA0C,CAChE,IAAM,EAAS,EAAgB,CAAU,EACzC,MAAO,GAAG,EAAO,oBAAoB,WAAW,EAAO,sBAAsB,aAAa,EAAO,uBAAuB,cAAc,EAAO,mBAAmB,UAAU,EAAO,0BAA0B,oBAC7M,CAEA,SAAS,EAAgB,EAOvB,CACA,IAAM,EAAS,GAAoC,EAAW,OAAQ,GAAc,EAAU,OAAS,CAAI,CAAC,CAAC,OACvG,EAAsB,EAAW,QAAQ,CAAE,WAAY,IAAU,OAAO,CAAC,CAAC,OAChF,MAAO,CACL,eAAgB,EAAW,OAC3B,sBACA,sBAAuB,EAAW,OAAS,EAC3C,uBAAwB,EAAM,UAAU,EACxC,mBAAoB,EAAM,MAAM,EAChC,0BAA2B,EAAM,aAAa,CAChD,CACF,CAGA,SAAS,EAAgB,EAA8B,CACrD,IAAM,EAAgB,IAAI,IAAID,EAAAA,WAAW,IAAK,GAAc,CAACC,EAAAA,aAAa,CAAS,EAAG,EAAU,KAAK,CAAC,CAAC,EACjG,EAAW,EAAU,SACxB,KACE,CAAE,SAAQ,QAAO,WAIhB,GAAG,EAAc,IAAI,CAAM,EAAE,GAAG,KAAK,KAAK,EAAQ,EAAE,EAAI,GAAG,QAAQ,EAAU,OAAS,cAAgB,KAAK,IAAI,KAAK,KAAK,CAAK,EAAI,EAAG,CAAC,EAAI,EAAM,EACpJ,CAAC,CACA,KAAK,IAAI,EACZ,MAAO,GAAG,EAAU,MAAM,IAAI,EAAkB,EAAW,CAAQ,GACrE,CAEA,SAAS,EAAkB,EAAsB,EAA0B,CACzE,OAAQ,EAAU,KAAlB,CACE,IAAK,OACH,MAAO,GAAG,EAAU,KAAK,IAAI,IAE/B,IAAK,WAEH,MAAO,GAAG,EAAe,CAAS,EAAE,GAAG,EAAU,MAAM,WAAW,iBAAkB,GAAG,EAAE,IAAI,IAE/F,IAAK,cAAe,CAClB,IAAM,GAAY,EAAU,UAAY,CAAC,EAAA,CAAG,IAAK,GAAY,EAAe,CAAO,CAAC,EAC9E,EAAS,CAAC,GAAG,EAAS,MAAM,EAAG,CAAiB,EAAG,GAAI,EAAS,OAAS,EAAoB,CAAC,KAAK,EAAI,CAAC,CAAE,EAChH,MAAO,GAAG,EAAe,CAAS,EAAE,IAAI,IAAW,EAAO,OAAS,EAAI,aAAa,EAAO,KAAK,IAAI,IAAM,IAC5G,CACF,CACF,CAEA,SAAS,EAAe,CAAE,OAAM,YAAW,WAAkC,CAC3E,MAAO,GAAG,EAAK,GAAG,EAAU,GAAG,GACjC,CAEA,SAAS,EAAgB,EAA6B,EAAmB,EAA2B,CAClG,EAAA,YACE,KAAK,UACH,CACE,OAAQ,CAAC,EAAU,EAAO,UAAU,GAAK,EAAM,OAAO,SAAW,EACjE,KAAM,EAAW,KACjB,UAAW,EAAM,UACjB,QAAS,CACP,GAAG,EAAgB,EAAO,UAAU,EACpC,iBAAkB,EAAM,MAAM,OAC9B,qBAAsB,EAAO,oBAC/B,EACA,WAAY,EAAO,WACnB,OAAQ,EAAM,OACd,SAAU,EAAM,QAClB,EACA,IAAA,GACA,CACF,EAAI;CACN,CACF"}
@@ -1,12 +1,12 @@
1
1
  import { type CliOptions } from './cliConfig.js';
2
- import { type Limits } from './thresholds.js';
2
+ import { type LimitsByLevel } from './thresholds.js';
3
3
  /** Raw options of the `check` subcommand; every field is undefined unless the flag was passed. */
4
4
  export interface CheckCliOptions extends CliOptions {
5
5
  base?: string;
6
6
  }
7
7
  /**
8
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.
9
+ * under it since the merge-base with that ref. Exit codes: 0 no error-level violations, 1 error-level
10
+ * violations, 2 files the check covers could not be measured.
11
11
  */
12
- export declare function runCheckCommand(target: string, cliOptions: CheckCliOptions, cliLimits: Limits): Promise<void>;
12
+ export declare function runCheckCommand(target: string, cliOptions: CheckCliOptions, cliLimits: LimitsByLevel): Promise<void>;
@@ -1,5 +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(`
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 F(o,n.base,c),u=w(l.files,l.crossFileDuplication,t(a,s.config.thresholds));await P(u.violations,l.root),c.json?Y(n,l,u):H(n,l,u),l.errors.length>0?process.exitCode=2:N(u.violations)&&(process.exitCode=1)}catch(e){S(`Error: ${T(e)}\n`),process.exitCode=2}}function N(e){return e.some(({level:e})=>e===`error`)}async function P(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 F(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 I(r,n)}return await L({repoRoot:a,canonicalTarget:r,targetFile:i},t,n)}async function I(e,t){let n=await h(e),r=await x(e,{...t,loadExclusion:e=>f(n,t.exclude,e)}),i=V(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 L({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=V(_,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 z(S,n,e,T),w=e=>n.some(({headPath:t})=>e.startsWith(`${t}:`))}return{files:C,crossFileDuplication:y,...R(_,w),root:e,mergeBase:T}}function R(e,t){return{errors:e.errors.filter(e=>t(e)),warnings:[...e.warnings,...e.errors.filter(e=>!t(e))]}}async function z(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)&&B(o,await D(A.join(n,e.headPath),`utf8`)),[{file:e.headPath,metrics:t,hunks:o}]})).flat()}function B(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 V(e,t){if(e.fatalError)throw Error(e.fatalError);return m(e,t),e.crossFileDuplication}function H(t,r,i){let{violations:a}=i,o=`${r.files.length} ${t.base===void 0?``:`changed `}files, ${i.checkedFunctionCount} functions checked${U(t,r)}`;r.errors.length>0?C(`Check could not complete: ${r.errors.length} measurement failures (details on stderr); ${W(a)} in the measured files (${o}).\n`):a.length===0?C(`No threshold violations: ${o}.\n`):C(`Threshold violations: ${W(a)} (${o}).\n`);for(let e of a)C(`${K(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
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)+`
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 U(e,t){return t.mergeBase===void 0?``:`; base ${e.base}, merge-base ${t.mergeBase.slice(0,12)}`}function W(e){let t=G(e);return`${t.errorViolationCount} errors, ${t.warningViolationCount} warnings (${t.functionViolationCount} functions, ${t.fileViolationCount} files, ${t.duplicationViolationCount} duplicated blocks)`}function G(e){let t=t=>e.filter(e=>e.kind===t).length,n=e.filter(({level:e})=>e===`error`).length;return{violationCount:e.length,errorViolationCount:n,warningViolationCount:e.length-n,functionViolationCount:t(`function`),fileViolationCount:t(`file`),duplicationViolationCount:t(`duplication`)}}function K(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} (max ${t.kind===`duplication`?Math.max(Math.ceil(i)-1,0):i})`).join(`, `);return`${t.level}: ${q(t,i)}`}function q(e,t){switch(e.kind){case`file`:return`${e.file}: ${t}`;case`function`:return`${J(e)} ${e.name?.replaceAll(/\s*[\n\r]\s*/gu,` `)}: ${t}`;case`duplication`:{let n=(e.partners??[]).map(e=>J(e)),r=[...n.slice(0,3),...n.length>3?[`...`]:[]];return`${J(e)}: ${t}${r.length>0?`, also at ${r.join(`, `)}`:``}`}}}function J({file:e,startLine:t,endLine:n}){return`${e}:${t}-${n}`}function Y(e,t,n){C(JSON.stringify({passed:!N(n.violations)&&t.errors.length===0,base:e.base,mergeBase:t.mergeBase,summary:{...G(n.violations),checkedFileCount:t.files.length,checkedFunctionCount:n.checkedFunctionCount},violations:n.violations,errors:t.errors,warnings:t.warnings},void 0,2)+`
4
4
  `)}export{M as runCheckCommand};
5
5
  //# sourceMappingURL=checkCommand.js.map