code-gauge 8.0.1 → 10.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.
Files changed (43) hide show
  1. package/README.md +73 -46
  2. package/dist/checkCommand.cjs +3 -3
  3. package/dist/checkCommand.cjs.map +1 -1
  4. package/dist/checkCommand.d.ts +22 -1
  5. package/dist/checkCommand.js +3 -3
  6. package/dist/checkCommand.js.map +1 -1
  7. package/dist/cli.cjs +3 -3
  8. package/dist/cli.cjs.map +1 -1
  9. package/dist/cli.js +3 -3
  10. package/dist/cli.js.map +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/metrics.cjs +1 -1
  13. package/dist/metrics.cjs.map +1 -1
  14. package/dist/metrics.js +1 -1
  15. package/dist/metrics.js.map +1 -1
  16. package/dist/nativeMetrics.cjs +1 -1
  17. package/dist/nativeMetrics.cjs.map +1 -1
  18. package/dist/nativeMetrics.d.ts +4 -2
  19. package/dist/nativeMetrics.js +1 -1
  20. package/dist/nativeMetrics.js.map +1 -1
  21. package/dist/thresholdCheck.cjs +1 -1
  22. package/dist/thresholdCheck.cjs.map +1 -1
  23. package/dist/thresholdCheck.d.ts +18 -4
  24. package/dist/thresholdCheck.js +1 -1
  25. package/dist/thresholdCheck.js.map +1 -1
  26. package/dist/thresholds.cjs +1 -1
  27. package/dist/thresholds.cjs.map +1 -1
  28. package/dist/thresholds.d.ts +5 -3
  29. package/dist/thresholds.js +1 -1
  30. package/dist/thresholds.js.map +1 -1
  31. package/dist/types.d.ts +37 -0
  32. package/dist/wasmBinding.cjs +1 -1
  33. package/dist/wasmBinding.cjs.map +1 -1
  34. package/dist/wasmBinding.js +1 -1
  35. package/dist/wasmBinding.js.map +1 -1
  36. package/native/code-gauge.wasm +0 -0
  37. package/native/src/complexity.rs +173 -19
  38. package/native/src/container.rs +413 -0
  39. package/native/src/functions.rs +2 -5
  40. package/native/src/lib.rs +2 -1
  41. package/native/src/measure.rs +24 -8
  42. package/native/src/types.rs +21 -0
  43. package/package.json +9 -8
package/README.md CHANGED
@@ -35,7 +35,7 @@ C++ files. By default it skips generated, vendor, test, and tool directories as
35
35
  Measured 123 files under /path/to/project (code LOC 45678, NCSS 23456, functions 1789)
36
36
 
37
37
  Refactoring candidates (top 10 of 123):
38
- 1. src/metrics.ts (score 2.87): worst function measure (L120-310) cognitive 42, NCSS 220, nesting 6; duplicated lines 180 (20%, shared with src/other.ts); file NCSS 1240
38
+ 1. src/metrics.ts (score 2.87): worst function TreeMeasurer.measure (L120-310) cognitive 42, NCSS 220, nesting 6; duplicated lines 180 (20%, shared with src/other.ts); file NCSS 1240
39
39
  ...
40
40
  ```
41
41
 
@@ -55,7 +55,8 @@ Ranking is relative to the scanned project and ignores the
55
55
  [thresholds](#threshold-check-code-gauge-check): the top of the list is worth refactoring first
56
56
  regardless of where any cutoff would sit. Each reported file carries
57
57
  the concrete evidence (worst function with location, duplication with partner files, file size) so
58
- an agent can act on it directly.
58
+ an agent can act on it directly. A method is named with the type or module it belongs to
59
+ (`TreeMeasurer.measure`), as in the threshold check.
59
60
 
60
61
  ## Options
61
62
 
@@ -97,27 +98,31 @@ like the ranking command. The thresholds:
97
98
  | --------------------------------- | ------------------------------------------------- | ------- | ----- |
98
99
  | `maxFunctionCognitiveComplexity` | a function's cognitive complexity is above it | 15 | 30 |
99
100
  | `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 |
101
+ | `maxFunctionNcss` | a function's NCSS is above it | 30 | 60 |
102
+ | `maxFunctionNestingDepth` | a function's nesting depth is above it | off | off |
103
+ | `maxFunctionParameterCount` | a function's parameter count is above it | 6 | off |
103
104
  | `maxFunctionHalsteadVolume` | a function's Halstead volume is above it | off | off |
104
105
  | `maxFunctionHalsteadDifficulty` | a function's Halstead difficulty is above it | off | off |
105
106
  | `maxFunctionHalsteadEffort` | a function's Halstead effort is above it | off | off |
106
107
  | `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 |
108
+ | `maxFileNcss` | a file's NCSS is above it | 400 | 1000 |
109
+ | `minDuplicateLines` | a duplicated block holds at least this many lines | 15 | off |
109
110
 
110
111
  Every threshold has a limit per level, and a value is reported at the most severe level whose limit
111
112
  it violates. The warning limits mark code worth simplifying when it is touched; the error limits
112
113
  mark code to fix, and only errors fail the check (exit code 1).
113
114
 
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.
115
+ The defaults come from the 45,123 functions the WillBooster organization wrote in its repositories
116
+ (of 153,771 measured; copies of third-party projects and course material are left out), and from
117
+ ratings of 225 of those functions, 47 of their files, and 60 of their duplicated blocks: a warning limit sits where most rated items were worth changing or borderline, an error
118
+ limit where nearly all were worth changing. Cognitive complexity above 15 flags 3.1% of the
119
+ functions and above 30 flags 1.0%; NCSS above 30 flags 2.6% and above 60 flags 0.6%. Of the
120
+ thresholds that are off, nesting depth flags no function that cognitive complexity misses, and
121
+ cyclomatic complexity and Halstead difficulty flag functions rated like unflagged ones; Halstead
122
+ volume, Halstead effort, and DepDegree are high mostly where a function is long or takes many
123
+ parameters, which the limits above already flag.
124
+ [How the default thresholds were calibrated](docs/threshold-calibration.md) holds the corpus, the
125
+ distributions per language, the ratings, and the reasoning per threshold.
121
126
 
122
127
  Cognitive complexity, NCSS, Halstead volume, and DepDegree of a function cover the functions nested
123
128
  in it, so a limit of these that a function and a function nested in it both violate is reported
@@ -127,8 +132,10 @@ for each function.
127
132
 
128
133
  A duplicated block is one occurrence of a within-file or cross-file clone, found with the
129
134
  [duplication detection settings](#duplication-detection-settings); its span runs from its first to
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.
135
+ its last line, and its length is the number of lines of that span that are neither blank nor
136
+ comment-only, so that a copy has the same length whether or not it is commented. Occurrences of at least the warning or the error `minDuplicateLines` that overlap in a
137
+ file are reported as one block covering all of them, at the level the code lines of its whole span
138
+ reach.
132
139
 
133
140
  Each limit is set in the `warning` or `error` part of the
134
141
  [`thresholds` config section](#configuration), where `null` disables it and `languages` overrides
@@ -157,20 +164,27 @@ one remediation hint per violated metric:
157
164
 
158
165
  ```
159
166
  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 (warning 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
167
+ error: src/legacy.ts:1-48 Rules.decide: cognitive complexity 34 (max 30; largest parts L12-31 19, L33-40 isLegacy 8, L44 2), NCSS 36 (warning max 30)
168
+ warning: src/report.ts:1-18: duplicated lines 18 (max 14), also at src/summary.ts:1-18
169
+ warning: src/summary.ts:1-18: duplicated lines 18 (max 14), also at src/report.ts:1-18
163
170
 
164
171
  How to fix:
165
172
  - cognitive complexity: flatten nested branching with early returns and extract nested blocks into named functions.
166
- - nesting depth: replace nested conditions with guard clauses or move inner blocks into functions.
173
+ - NCSS: move each group of statements that forms one step into a named function.
167
174
  - duplicated lines: extract the repeated code into one shared function or module and call it from every location.
168
175
  ```
169
176
 
170
177
  A violation's level is that of its most severe limit. A function gets one line listing every
171
178
  threshold it exceeds, each with the largest value its violated limit allows and, when that limit is
172
- milder than the line's level, the limit's own level; a file-level violation
173
- prints the path without a line span, and a duplicated block lists up to three of its other copies.
179
+ milder than the line's level, the limit's own level; a method is named with the type or
180
+ module it belongs to (`Rules.decide`), while a function written as a value, such as a lambda
181
+ assigned to a field, keeps the bare name of what it is bound to. A cognitive-complexity violation also names up to three parts of the function
182
+ adding the most to it (none when all of it comes from operator sequences and jumps outside any
183
+ branching construct, which add to the score without being such a part), largest first, each with its lines, its name when it is a nested function,
184
+ and what it adds: the outermost branching constructs and nested functions of the body or, where one
185
+ of them merely wraps most of the function (a loop or an unnamed callback around the whole body),
186
+ the ones inside it. A file-level violation prints the path without a line span, and a duplicated block lists
187
+ up to three of its other copies, those with the most code lines first.
174
188
  Paths are relative to the repository root, or to the target directory outside a git repository.
175
189
 
176
190
  | Exit code | Meaning |
@@ -211,21 +225,26 @@ ranking command.
211
225
  "level": "error",
212
226
  "file": "src/legacy.ts",
213
227
  "startLine": 1,
214
- "endLine": 21,
228
+ "endLine": 48,
215
229
  "exceeded": [
216
230
  { "metric": "functionCognitiveComplexity", "value": 34, "level": "error", "limit": 30 },
217
- { "metric": "functionNestingDepth", "value": 5, "level": "warning", "limit": 4 }
231
+ { "metric": "functionNcss", "value": 36, "level": "warning", "limit": 30 }
218
232
  ],
219
- "name": "decide"
233
+ "name": "Rules.decide",
234
+ "largestBlocks": [
235
+ { "startLine": 12, "endLine": 31, "cognitiveComplexity": 19 },
236
+ { "startLine": 33, "endLine": 40, "cognitiveComplexity": 8, "name": "isLegacy" },
237
+ { "startLine": 44, "endLine": 44, "cognitiveComplexity": 2 }
238
+ ]
220
239
  },
221
240
  {
222
241
  "kind": "duplication",
223
242
  "level": "warning",
224
243
  "file": "src/pasted.ts",
225
244
  "startLine": 1,
226
- "endLine": 12,
227
- "exceeded": [{ "metric": "duplicateLines", "value": 12, "level": "warning", "limit": 10 }],
228
- "partners": [{ "file": "src/report.ts", "startLine": 1, "endLine": 12 }]
245
+ "endLine": 18,
246
+ "exceeded": [{ "metric": "duplicateLines", "value": 18, "level": "warning", "limit": 15 }],
247
+ "partners": [{ "file": "src/report.ts", "startLine": 1, "endLine": 18 }]
229
248
  }
230
249
  ],
231
250
  "errors": [],
@@ -243,13 +262,20 @@ ranking command.
243
262
  - `level`: `error` when any of its `exceeded` limits is an error, otherwise `warning`;
244
263
  - `file`, `startLine`, `endLine`: the 1-based line span of the function or duplicated block; for
245
264
  kind `file`, the first and last line of the file;
246
- - `name`: only for kind `function`; `<anonymous>` for a function without a name;
265
+ - `name`: only for kind `function`; a method's name follows the type or module it belongs to (`Rules.decide`), and a
266
+ function without a name is `<anonymous>`;
247
267
  - `exceeded`: every violated threshold with its `metric` (the config key without its `max` or
248
268
  `min` prefix, e.g. `functionNcss` for `maxFunctionNcss`), the measured `value`, the most severe
249
269
  `level` whose limit the value violates, and that `limit`. A value violates when it is above the
250
- limit; `duplicateLines` violates from the limit on;
251
- - `partners`: only for kind `duplication`; every other copy of the block, ordered by `file`, then
252
- `startLine`.
270
+ limit; `duplicateLines`, the code lines of the block, violates from the limit on;
271
+ - `largestBlocks`: only for a function exceeding a cognitive-complexity limit that has a
272
+ branching construct or a nested function adding to it; the parts the
273
+ text report names, each with its line span, the `cognitiveComplexity` it adds to the function,
274
+ and its `name` when it is a named nested function;
275
+ - `partners`: only for kind `duplication`; every other copy of the block, most code lines
276
+ first: the
277
+ block merges every clone overlapping it, so its copies range from whole copies of it to a few
278
+ lines matching one fragment.
253
279
  - `errors`: the files the check covers that could not be measured (exit code 2); unrelated to the
254
280
  `error` level of a violation.
255
281
  - `warnings`: files measured without cross-file duplication data, and unmeasured files of the
@@ -272,29 +298,29 @@ with `--config`). The following config reproduces every built-in default:
272
298
  "warning": {
273
299
  "maxFunctionCognitiveComplexity": 15,
274
300
  "maxFunctionCyclomaticComplexity": null,
275
- "maxFunctionNcss": 60,
276
- "maxFunctionNestingDepth": 4,
277
- "maxFunctionParameterCount": 7,
301
+ "maxFunctionNcss": 30,
302
+ "maxFunctionNestingDepth": null,
303
+ "maxFunctionParameterCount": 6,
278
304
  "maxFunctionHalsteadVolume": null,
279
305
  "maxFunctionHalsteadDifficulty": null,
280
306
  "maxFunctionHalsteadEffort": null,
281
307
  "maxFunctionDepDegree": null,
282
- "maxFileNcss": 500,
283
- "minDuplicateLines": 10,
308
+ "maxFileNcss": 400,
309
+ "minDuplicateLines": 15,
284
310
  "languages": {}
285
311
  },
286
312
  "error": {
287
313
  "maxFunctionCognitiveComplexity": 30,
288
314
  "maxFunctionCyclomaticComplexity": null,
289
- "maxFunctionNcss": 100,
290
- "maxFunctionNestingDepth": 5,
315
+ "maxFunctionNcss": 60,
316
+ "maxFunctionNestingDepth": null,
291
317
  "maxFunctionParameterCount": null,
292
318
  "maxFunctionHalsteadVolume": null,
293
319
  "maxFunctionHalsteadDifficulty": null,
294
320
  "maxFunctionHalsteadEffort": null,
295
321
  "maxFunctionDepDegree": null,
296
322
  "maxFileNcss": 1000,
297
- "minDuplicateLines": 20,
323
+ "minDuplicateLines": null,
298
324
  "languages": {}
299
325
  }
300
326
  },
@@ -316,10 +342,10 @@ differ for its files; a limit it leaves out keeps the value set outside `languag
316
342
  {
317
343
  "thresholds": {
318
344
  "warning": {
319
- "maxFunctionNcss": 60,
345
+ "maxFunctionNcss": 30,
320
346
  "maxFunctionParameterCount": 5,
321
347
  "languages": {
322
- "tsx": { "maxFunctionNcss": 100 },
348
+ "tsx": { "maxFunctionNcss": 50 },
323
349
  "python": { "maxFunctionParameterCount": null }
324
350
  }
325
351
  }
@@ -374,10 +400,10 @@ The `duplication` section tunes how clones are detected:
374
400
 
375
401
  `measureCode` reports, per file:
376
402
 
377
- - Physical LOC, code lines, comment-only lines, and blank lines
403
+ - Physical LOC, code lines (with their line numbers), comment-only lines, and blank lines
378
404
  - Per-function cognitive complexity (following the SonarSource specification, except its recursion
379
- increment, which is not counted; cross-validated against PMD's Java rules), plus the file-level
380
- total and maximum
405
+ increment, which is not counted; cross-validated against PMD's Java rules) with the up to three
406
+ parts of the function adding the most to it, plus the file-level total and maximum
381
407
  - Per-function and per-file NCSS (non-commenting source statements), calibrated against PMD's
382
408
  `NcssCount` rule for Java and generalized to every supported language; unlike PMD, package and
383
409
  import declarations count, and statement-shaped content is counted uniformly in expression
@@ -387,7 +413,8 @@ The `duplication` section tunes how clones are detected:
387
413
  every decision and short-circuit operator adds one, one per case-labelled statement, plus the
388
414
  file total over McCabe's components (every function, every initializer block, decisions outside
389
415
  functions, and the module body of a file that runs top-level code)
390
- - Per-function parameter counts and locations (name, node type, line span)
416
+ - Per-function parameter counts and locations (name, the type or module a declared function is a
417
+ member of, node type, line span)
391
418
  - Within-file duplication: copy-pasted blocks matched on normalized tokens (identifiers anonymized
392
419
  consistently, literals by kind, and literal-dense data tables excluded unless their values also
393
420
  match; dependency declarations such as imports, package clauses, `#include`s, re-exports, and
@@ -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 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(`
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),u.codeLineNumbersByFile);await p(d.violations,u.root),l.json?j(r,u,d):S(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=x(r,t);return{files:r.files.map(({file:e,metrics:t})=>({file:a.formatPath(e,r.displayRoot),metrics:t})),codeLineNumbersByFile:_(r,r.displayRoot),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=x(m,s),g=n=>a.isWithinDirectory(l.default.join(e,n),t),b=m.files.map(({file:t,metrics:n})=>({file:a.formatPath(t,e),metrics:n})),S=b.filter(({file:e})=>g(e)),C=n=>t===e||n.startsWith(`${p}/`)||n.startsWith(`${p}:`),w;if(o!==void 0){w=await r.resolveMergeBase(e,o);let t=await r.listChangedFiles(e,w);d!==void 0&&t.push({status:`added`,headPath:d});let n=t.filter(({status:e,headPath:t})=>e!==`deleted`&&g(t));S=await y(b,n,e,w),C=e=>n.some(({headPath:t})=>e.startsWith(`${t}:`))}return{files:S,crossFileDuplication:h,codeLineNumbersByFile:_(m,e),...v(m,C),root:e,mergeBase:w}}function _(e,t){return new Map(e.files.map(({file:e,metrics:n})=>[a.formatPath(e,t),new Set(n.codeLineNumbers)]))}function v(e,t){return{errors:e.errors.filter(e=>t(e)),warnings:[...e.warnings,...e.errors.filter(e=>!t(e))]}}async function y(e,t,n,i){let a=new Map(e.map(e=>[e.file,e]));return(await(0,s.mapConcurrently)(t,u.default.availableParallelism()*2,async e=>{let t=a.get(e.headPath);if(t===void 0)return[];let{metrics:o}=t,s={headStart:1,headCount:o.lines.total},u=e.basePath===void 0?[s]:await r.listLineHunks(n,i,e.basePath,e.headPath);return o.language===`python`&&u.some(({headCount:e})=>e===0)&&b(u,await(0,c.readFile)(l.default.join(n,e.headPath),`utf8`)),[{...t,hunks:u}]})).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 x(e,t){if(e.fatalError)throw Error(e.fatalError);return a.addCrossFileDuplication(e,t),e.crossFileDuplication}function S(e,n,r){let{violations:i}=r,o=`${n.files.length} ${e.base===void 0?``:`changed `}files, ${r.checkedFunctionCount} functions checked${C(e,n)}`;n.errors.length>0?a.writeStdout(`Check could not complete: ${n.errors.length} measurement failures (details on stderr); ${w(i)} in the measured files (${o}).\n`):i.length===0?a.writeStdout(`No threshold violations: ${o}.\n`):a.writeStdout(`Threshold violations: ${w(i)} (${o}).\n`);for(let e of i)a.writeStdout(`${E(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 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,level:i,limit:a})=>{let o=Math.ceil(r*10)/10,s=i===e.level?``:`${i} `,c=e.kind===`duplication`?Math.max(Math.ceil(a)-1,0):a;return`${n.get(t)} ${o} (${s}max ${c})`}).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
- `)}exports.runCheckCommand=d;
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 C(e,t){return t.mergeBase===void 0?``:`; base ${e.base}, merge-base ${t.mergeBase.slice(0,12)}`}function w(e){let t=T(e);return`${t.errorViolationCount} errors, ${t.warningViolationCount} warnings (${t.functionViolationCount} functions, ${t.fileViolationCount} files, ${t.duplicationViolationCount} duplicated blocks)`}function T(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 E(e){let n=new Map(t.thresholds.map(e=>[t.metricNameOf(e),e.label])),r=e.exceeded.map(({metric:t,value:r,level:i,limit:a})=>{let o=Math.ceil(r*10)/10,s=i===e.level?``:`${i} `,c=e.kind===`duplication`?Math.max(Math.ceil(a)-1,0):a,l=t===`functionCognitiveComplexity`?D(e):``;return`${n.get(t)} ${o} (${s}max ${c}${l})`}).join(`, `);return`${e.level}: ${O(e,r)}`}function D({largestBlocks:e=[]}){let t=e.map(({startLine:e,endLine:t,name:n,cognitiveComplexity:r})=>`L${e}${t===e?``:`-${t}`}${n?` ${k(n)}`:``} ${r}`);return t.length>0?`; largest parts ${t.join(`, `)}`:``}function O(e,t){switch(e.kind){case`file`:return`${e.file}: ${t}`;case`function`:return`${A(e)} ${k(e.name??``)}: ${t}`;case`duplication`:{let n=(e.partners??[]).map(e=>A(e)),r=[...n.slice(0,3),...n.length>3?[`...`]:[]];return`${A(e)}: ${t}${r.length>0?`, also at ${r.join(`, `)}`:``}`}}}function k(e){return e.replaceAll(/\s*[\n\r]\s*/gu,` `)}function A({file:e,startLine:t,endLine:n}){return`${e}:${t}-${n}`}function j(e,t,n){a.writeStdout(JSON.stringify({passed:!f(n.violations)&&t.errors.length===0,base:e.base,mergeBase:t.mergeBase,summary:{...T(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,exports.scanCheckScope=m;
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 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(({ metric, value, level, limit }) => {\n // Halstead values are fractional; rounding up keeps a violating value above the printed maximum.\n const roundedValue = Math.ceil(value * 10) / 10;\n const levelPrefix = level === violation.level ? '' : `${level} `;\n // A duplicated block violates from its limit on, so the largest allowed span is the last whole\n // line count below it; a limit of 0 allows none.\n const maxAllowed = violation.kind === 'duplication' ? Math.max(Math.ceil(limit) - 1, 0) : limit;\n return `${labelByMetric.get(metric)} ${roundedValue} (${levelPrefix}max ${maxAllowed})`;\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":"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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 CodeLineNumbersByFile,\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. */\nexport interface CheckScope {\n files: CheckedFile[];\n crossFileDuplication?: CrossFileDuplicationMetrics;\n /** Of every measured file, so that a copy outside the files the check covers is sized like one inside. */\n codeLineNumbersByFile: CodeLineNumbersByFile;\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 scanCheckScope(resolvedTarget, cliOptions.base, options);\n const result = checkThresholds(\n scope.files,\n scope.crossFileDuplication,\n resolveLimits(cliLimits, config.config.thresholds),\n scope.codeLineNumbersByFile\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 */\nexport async function scanCheckScope(\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 codeLineNumbersByFile: indexCodeLineNumbers(scan, scan.displayRoot),\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 {\n files,\n crossFileDuplication,\n codeLineNumbersByFile: indexCodeLineNumbers(scan, repoRoot),\n ...partitionErrors(scan, isCovered),\n root: repoRoot,\n mergeBase,\n };\n}\n\nfunction indexCodeLineNumbers(scan: ScanResult, root: string): CodeLineNumbersByFile {\n return new Map(scan.files.map(({ file, metrics }) => [formatPath(file, root), new Set(metrics.codeLineNumbers)]));\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 measuredByPath = new Map(measuredFiles.map((measured) => [measured.file, measured]));\n const files = await mapConcurrently(changedFiles, os.availableParallelism() * 2, async (changed) => {\n const measured = measuredByPath.get(changed.headPath);\n // A changed file the scan left out (unsupported, excluded, or failed) has nothing to check.\n if (measured === undefined) {\n return [];\n }\n const { metrics } = measured;\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 [{ ...measured, 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(({ metric, value, level, limit }) => {\n // Halstead values are fractional; rounding up keeps a violating value above the printed maximum.\n const roundedValue = Math.ceil(value * 10) / 10;\n const levelPrefix = level === violation.level ? '' : `${level} `;\n // A duplicated block violates from its limit on, so the largest allowed code-line count is\n // the last whole number below it; a limit of 0 allows none.\n const maxAllowed = violation.kind === 'duplication' ? Math.max(Math.ceil(limit) - 1, 0) : limit;\n const blocks = metric === 'functionCognitiveComplexity' ? describeLargestBlocks(violation) : '';\n return `${labelByMetric.get(metric)} ${roundedValue} (${levelPrefix}max ${maxAllowed}${blocks})`;\n })\n .join(', ');\n return `${violation.level}: ${describeViolation(violation, exceeded)}`;\n}\n\nfunction describeLargestBlocks({ largestBlocks = [] }: Violation): string {\n const blocks = largestBlocks.map(\n ({ startLine, endLine, name, cognitiveComplexity }) =>\n `L${startLine}${endLine === startLine ? '' : `-${endLine}`}${name ? ` ${toOneLine(name)}` : ''} ${cognitiveComplexity}`\n );\n return blocks.length > 0 ? `; largest parts ${blocks.join(', ')}` : '';\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 return `${formatLocation(violation)} ${toOneLine(violation.name ?? '')}: ${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\n/** A computed name can span lines in the source; the report keeps one line per function. */\nfunction toOneLine(name: string): string {\n return name.replaceAll(/\\s*[\\n\\r]\\s*/gu, ' ');\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":"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"}
@@ -1,12 +1,33 @@
1
- import { type CliOptions } from './cliConfig.js';
1
+ import { type CliOptions, type ResolvedOptions } from './cliConfig.js';
2
+ import type { CrossFileDuplicationMetrics } from './crossFileDuplication.js';
3
+ import { type CheckedFile, type CodeLineNumbersByFile } from './thresholdCheck.js';
2
4
  import { type LimitsByLevel } from './thresholds.js';
3
5
  /** Raw options of the `check` subcommand; every field is undefined unless the flag was passed. */
4
6
  export interface CheckCliOptions extends CliOptions {
5
7
  base?: string;
6
8
  }
9
+ /** The measured files a check covers and the duplication they take part in. */
10
+ export interface CheckScope {
11
+ files: CheckedFile[];
12
+ crossFileDuplication?: CrossFileDuplicationMetrics;
13
+ /** Of every measured file, so that a copy outside the files the check covers is sized like one inside. */
14
+ codeLineNumbersByFile: CodeLineNumbersByFile;
15
+ /** Measurement failures of files the check covers. */
16
+ errors: string[];
17
+ warnings: string[];
18
+ /** The directory the files' paths are relative to. */
19
+ root: string;
20
+ mergeBase?: string;
21
+ }
7
22
  /**
8
23
  * Reports every exceeded threshold of the target or, with `base`, of what the working tree changed
9
24
  * under it since the merge-base with that ref. Exit codes: 0 no error-level violations, 1 error-level
10
25
  * violations, 2 files the check covers could not be measured.
11
26
  */
12
27
  export declare function runCheckCommand(target: string, cliOptions: CheckCliOptions, cliLimits: LimitsByLevel): Promise<void>;
28
+ /**
29
+ * Inside a git repository, the scope is the repository's git-visible files under the target, and
30
+ * duplication is detected against all of them; `base` only narrows the scope to what changed.
31
+ * Outside a repository, where there is no change to narrow to, the target directory is walked.
32
+ */
33
+ export declare function scanCheckScope(resolvedTarget: string, base: string | undefined, options: ResolvedOptions): Promise<CheckScope>;
@@ -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 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(`
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),l.codeLineNumbersByFile);await P(u.violations,l.root),c.json?Q(n,l,u):U(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=H(r,t);return{files:r.files.map(({file:e,metrics:t})=>({file:g(e,r.displayRoot),metrics:t})),codeLineNumbersByFile:R(r,r.displayRoot),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=H(_,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 B(S,n,e,T),w=e=>n.some(({headPath:t})=>e.startsWith(`${t}:`))}return{files:C,crossFileDuplication:y,codeLineNumbersByFile:R(_,e),...z(_,w),root:e,mergeBase:T}}function R(e,t){return new Map(e.files.map(({file:e,metrics:n})=>[g(e,t),new Set(n.codeLineNumbers)]))}function z(e,t){return{errors:e.errors.filter(e=>t(e)),warnings:[...e.warnings,...e.errors.filter(e=>!t(e))]}}async function B(e,t,n,r){let i=new Map(e.map(e=>[e.file,e]));return(await E(t,j.availableParallelism()*2,async e=>{let t=i.get(e.headPath);if(t===void 0)return[];let{metrics:a}=t,o={headStart:1,headCount:a.lines.total},c=e.basePath===void 0?[o]:await s(n,r,e.basePath,e.headPath);return a.language===`python`&&c.some(({headCount:e})=>e===0)&&V(c,await D(A.join(n,e.headPath),`utf8`)),[{...t,hunks:c}]})).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 H(e,t){if(e.fatalError)throw Error(e.fatalError);return m(e,t),e.crossFileDuplication}function U(t,r,i){let{violations:a}=i,o=`${r.files.length} ${t.base===void 0?``:`changed `}files, ${i.checkedFunctionCount} functions checked${W(t,r)}`;r.errors.length>0?C(`Check could not complete: ${r.errors.length} measurement failures (details on stderr); ${G(a)} in the measured files (${o}).\n`):a.length===0?C(`No threshold violations: ${o}.\n`):C(`Threshold violations: ${G(a)} (${o}).\n`);for(let e of a)C(`${q(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 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,level:i,limit:a})=>{let o=Math.ceil(n*10)/10,s=i===t.level?``:`${i} `,c=t.kind===`duplication`?Math.max(Math.ceil(a)-1,0):a;return`${r.get(e)} ${o} (${s}max ${c})`}).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
- `)}export{M as runCheckCommand};
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 W(e,t){return t.mergeBase===void 0?``:`; base ${e.base}, merge-base ${t.mergeBase.slice(0,12)}`}function G(e){let t=K(e);return`${t.errorViolationCount} errors, ${t.warningViolationCount} warnings (${t.functionViolationCount} functions, ${t.fileViolationCount} files, ${t.duplicationViolationCount} duplicated blocks)`}function K(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 q(t){let r=new Map(n.map(t=>[e(t),t.label])),i=t.exceeded.map(({metric:e,value:n,level:i,limit:a})=>{let o=Math.ceil(n*10)/10,s=i===t.level?``:`${i} `,c=t.kind===`duplication`?Math.max(Math.ceil(a)-1,0):a,l=e===`functionCognitiveComplexity`?J(t):``;return`${r.get(e)} ${o} (${s}max ${c}${l})`}).join(`, `);return`${t.level}: ${Y(t,i)}`}function J({largestBlocks:e=[]}){let t=e.map(({startLine:e,endLine:t,name:n,cognitiveComplexity:r})=>`L${e}${t===e?``:`-${t}`}${n?` ${X(n)}`:``} ${r}`);return t.length>0?`; largest parts ${t.join(`, `)}`:``}function Y(e,t){switch(e.kind){case`file`:return`${e.file}: ${t}`;case`function`:return`${Z(e)} ${X(e.name??``)}: ${t}`;case`duplication`:{let n=(e.partners??[]).map(e=>Z(e)),r=[...n.slice(0,3),...n.length>3?[`...`]:[]];return`${Z(e)}: ${t}${r.length>0?`, also at ${r.join(`, `)}`:``}`}}}function X(e){return e.replaceAll(/\s*[\n\r]\s*/gu,` `)}function Z({file:e,startLine:t,endLine:n}){return`${e}:${t}-${n}`}function Q(e,t,n){C(JSON.stringify({passed:!N(n.violations)&&t.errors.length===0,base:e.base,mergeBase:t.mergeBase,summary:{...K(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,F as scanCheckScope};
5
5
  //# sourceMappingURL=checkCommand.js.map