code-gauge 10.1.0 → 10.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +33 -14
  2. package/THIRD-PARTY-NOTICES.txt +8 -8
  3. package/dist/checkCommand.cjs +1 -1
  4. package/dist/checkCommand.cjs.map +1 -1
  5. package/dist/checkCommand.js +1 -1
  6. package/dist/checkCommand.js.map +1 -1
  7. package/dist/crossFileDuplication.cjs +1 -1
  8. package/dist/crossFileDuplication.cjs.map +1 -1
  9. package/dist/crossFileDuplication.d.ts +3 -2
  10. package/dist/crossFileDuplication.js +1 -1
  11. package/dist/crossFileDuplication.js.map +1 -1
  12. package/dist/crossFileNearMiss.cjs +1 -1
  13. package/dist/crossFileNearMiss.cjs.map +1 -1
  14. package/dist/crossFileNearMiss.d.ts +6 -4
  15. package/dist/crossFileNearMiss.js +1 -1
  16. package/dist/crossFileNearMiss.js.map +1 -1
  17. package/dist/duplication.cjs +1 -1
  18. package/dist/duplication.cjs.map +1 -1
  19. package/dist/duplication.d.ts +22 -7
  20. package/dist/duplication.js +1 -1
  21. package/dist/duplication.js.map +1 -1
  22. package/dist/thresholdCheck.cjs +1 -1
  23. package/dist/thresholdCheck.cjs.map +1 -1
  24. package/dist/thresholdCheck.d.ts +2 -0
  25. package/dist/thresholdCheck.js +1 -1
  26. package/dist/thresholdCheck.js.map +1 -1
  27. package/dist/thresholds.cjs.map +1 -1
  28. package/dist/thresholds.d.ts +2 -3
  29. package/dist/thresholds.js.map +1 -1
  30. package/dist/types.d.ts +19 -8
  31. package/native/Cargo.lock +16 -16
  32. package/native/Cargo.toml +8 -8
  33. package/native/code-gauge.wasm +0 -0
  34. package/native/src/complexity.rs +10 -1
  35. package/native/src/container.rs +158 -7
  36. package/native/src/dep_degree.rs +13 -14
  37. package/native/src/duplication.rs +188 -75
  38. package/native/src/functions.rs +112 -24
  39. package/native/src/languages.rs +5 -0
  40. package/native/src/measure.rs +9 -4
  41. package/native/src/near_miss.rs +303 -52
  42. package/package.json +8 -8
package/README.md CHANGED
@@ -134,11 +134,13 @@ A duplicated block is one occurrence of a within-file or cross-file clone, found
134
134
  [duplication detection settings](#duplication-detection-settings); its span runs from its first to
135
135
  its last line, and its length is the number of lines of that span matched in another copy. Lines
136
136
  that are blank or comment-only do not count, so that a copy has the same length whether or not it
137
- is commented, and neither do the lines between the matched parts of an edited copy, which the copies do not
138
- share. A block that resembles another as a whole (a near-miss clone, at least
139
- `minSimilarityPercent` similar) counts all its code lines, its edited ones included. Occurrences of at least the warning or the error
137
+ is commented, and neither do the lines the copies do not share: those between the matched parts of
138
+ an edited copy, and those a block that resembles another as a whole (a near-miss clone, at least
139
+ `minSimilarityPercent` similar) adds or rewrites. A line of such a block counts when its copies
140
+ match more than half of the line's tokens between them. Occurrences of at least the warning or the error
140
141
  `minDuplicateLines` that overlap in a file are reported as one block covering all of them, at the
141
- level its duplicated lines reach, each line counted once.
142
+ level its duplicated lines reach, each line counted once; copies of each other that merely share a
143
+ line (the branches of an `else if` chain) are reported as blocks of their own.
142
144
 
143
145
  Each limit is set in the `warning` or `error` part of the
144
146
  [`thresholds` config section](#configuration), where `null` disables it and `languages` overrides
@@ -180,8 +182,9 @@ How to fix:
180
182
  A violation's level is that of its most severe limit. A function gets one line listing every
181
183
  threshold it exceeds, each with the largest value its violated limit allows and, when that limit is
182
184
  milder than the line's level, the limit's own level; a method is named with the type or
183
- module it belongs to (`Rules.decide`), while a function written as a value, such as a lambda
184
- 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
185
+ module it belongs to (`Rules.decide`), as is a function written as the value of a field or of a
186
+ constant of a namespace, and a member of a JavaScript or TypeScript object literal with the path
187
+ the object is bound to (`api.load` for `const api = { load() {} }`). A cognitive-complexity violation also names up to three parts of the function
185
188
  adding the most to it (none when all of it comes from operator sequences and jumps outside any
186
189
  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,
187
190
  and what it adds: the outermost branching constructs and nested functions of the body or, where one
@@ -230,8 +233,14 @@ ranking command.
230
233
  "startLine": 1,
231
234
  "endLine": 48,
232
235
  "exceeded": [
233
- { "metric": "functionCognitiveComplexity", "value": 34, "level": "error", "limit": 30 },
234
- { "metric": "functionNcss", "value": 36, "level": "warning", "limit": 30 }
236
+ {
237
+ "metric": "functionCognitiveComplexity",
238
+ "label": "cognitive complexity",
239
+ "value": 34,
240
+ "level": "error",
241
+ "limit": 30
242
+ },
243
+ { "metric": "functionNcss", "label": "NCSS", "value": 36, "level": "warning", "limit": 30 }
235
244
  ],
236
245
  "name": "Rules.decide",
237
246
  "largestBlocks": [
@@ -246,7 +255,15 @@ ranking command.
246
255
  "file": "src/pasted.ts",
247
256
  "startLine": 1,
248
257
  "endLine": 18,
249
- "exceeded": [{ "metric": "duplicateLines", "value": 18, "level": "warning", "limit": 15 }],
258
+ "exceeded": [
259
+ {
260
+ "metric": "duplicateLines",
261
+ "label": "duplicated lines",
262
+ "value": 18,
263
+ "level": "warning",
264
+ "limit": 15
265
+ }
266
+ ],
250
267
  "partners": [{ "file": "src/report.ts", "startLine": 1, "endLine": 18 }]
251
268
  }
252
269
  ],
@@ -265,10 +282,11 @@ ranking command.
265
282
  - `level`: `error` when any of its `exceeded` limits is an error, otherwise `warning`;
266
283
  - `file`, `startLine`, `endLine`: the 1-based line span of the function or duplicated block; for
267
284
  kind `file`, the first and last line of the file;
268
- - `name`: only for kind `function`; a method's name follows the type or module it belongs to (`Rules.decide`), and a
269
- function without a name is `<anonymous>`;
285
+ - `name`: only for kind `function`; a member's name follows the type, module, or object it
286
+ belongs to (`Rules.decide`), and a function without a name is `<anonymous>`;
270
287
  - `exceeded`: every violated threshold with its `metric` (the config key without its `max` or
271
- `min` prefix, e.g. `functionNcss` for `maxFunctionNcss`), the measured `value`, the most severe
288
+ `min` prefix, e.g. `functionNcss` for `maxFunctionNcss`), the `label` the text report prints
289
+ for it, the measured `value`, the most severe
272
290
  `level` whose limit the value violates, and that `limit`. A value violates when it is above the
273
291
  limit; `duplicateLines`, the lines of the block matched in another copy, violates from the limit on;
274
292
  - `largestBlocks`: only for a function exceeding a cognitive-complexity limit that has a
@@ -277,7 +295,8 @@ ranking command.
277
295
  and its `name` when it is a named nested function;
278
296
  - `partners`: only for kind `duplication`; every other copy of the block, largest first. A
279
297
  place repeating only a part of the block (less than `minSimilarityPercent` of its tokens) is
280
- not a copy of it, and is listed only when no copy is known.
298
+ not a copy of it, and is listed only for a block that no place copies as a whole, its parts
299
+ being repeated in different places.
281
300
  - `errors`: the files the check covers that could not be measured (exit code 2); unrelated to the
282
301
  `error` level of a violation.
283
302
  - `warnings`: files measured without cross-file duplication data, and unmeasured files of the
@@ -415,7 +434,7 @@ The `duplication` section tunes how clones are detected:
415
434
  every decision and short-circuit operator adds one, one per case-labelled statement, plus the
416
435
  file total over McCabe's components (every function, every initializer block, decisions outside
417
436
  functions, and the module body of a file that runs top-level code)
418
- - Per-function parameter counts and locations (name, the type or module a declared function is a
437
+ - Per-function parameter counts and locations (name, the type, module, or object a function is a
419
438
  member of, node type, line span)
420
439
  - Within-file duplication: copy-pasted blocks matched on normalized tokens (identifiers anonymized
421
440
  consistently, literals by kind, and literal-dense data tables excluded unless their values also
@@ -8432,7 +8432,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
8432
8432
  DEALINGS IN THE SOFTWARE.
8433
8433
 
8434
8434
  ====================================================================================================
8435
- willbooster-tree-sitter 1.0.5 — MIT — https://github.com/WillBooster/tree-sitter
8435
+ willbooster-tree-sitter 1.4.3 — MIT — https://github.com/WillBooster/tree-sitter
8436
8436
  ====================================================================================================
8437
8437
  ----- LICENSE -----
8438
8438
  The MIT License (MIT)
@@ -9070,7 +9070,7 @@ permissive licensing, and of not having licensing issues being an
9070
9070
  obstacle to adoption, that text has been removed.
9071
9071
 
9072
9072
  ====================================================================================================
9073
- willbooster-tree-sitter-c 1.1.0 — MIT — https://github.com/WillBooster/tree-sitter-c
9073
+ willbooster-tree-sitter-c 4.0.2 — MIT — https://github.com/WillBooster/tree-sitter-c
9074
9074
  ====================================================================================================
9075
9075
  ----- LICENSE -----
9076
9076
  The MIT License (MIT)
@@ -9097,7 +9097,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
9097
9097
  SOFTWARE.
9098
9098
 
9099
9099
  ====================================================================================================
9100
- willbooster-tree-sitter-c-sharp 1.0.3 — MIT — https://github.com/WillBooster/tree-sitter-c-sharp
9100
+ willbooster-tree-sitter-c-sharp 3.0.1 — MIT — https://github.com/WillBooster/tree-sitter-c-sharp
9101
9101
  ====================================================================================================
9102
9102
  ----- LICENSE -----
9103
9103
  The MIT License (MIT)
@@ -9124,7 +9124,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
9124
9124
  SOFTWARE.
9125
9125
 
9126
9126
  ====================================================================================================
9127
- willbooster-tree-sitter-cpp 1.1.1 — MIT — https://github.com/WillBooster/tree-sitter-cpp
9127
+ willbooster-tree-sitter-cpp 2.0.2 — MIT — https://github.com/WillBooster/tree-sitter-cpp
9128
9128
  ====================================================================================================
9129
9129
  ----- LICENSE -----
9130
9130
  The MIT License (MIT)
@@ -9151,7 +9151,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
9151
9151
  SOFTWARE.
9152
9152
 
9153
9153
  ====================================================================================================
9154
- willbooster-tree-sitter-javascript 1.0.3 — MIT — https://github.com/WillBooster/tree-sitter-javascript
9154
+ willbooster-tree-sitter-javascript 4.0.2 — MIT — https://github.com/WillBooster/tree-sitter-javascript
9155
9155
  ====================================================================================================
9156
9156
  ----- LICENSE -----
9157
9157
  The MIT License (MIT)
@@ -9178,7 +9178,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
9178
9178
  SOFTWARE.
9179
9179
 
9180
9180
  ====================================================================================================
9181
- willbooster-tree-sitter-kotlin 1.4.0 — MIT — https://github.com/WillBooster/tree-sitter-kotlin
9181
+ willbooster-tree-sitter-kotlin 4.0.2 — MIT — https://github.com/WillBooster/tree-sitter-kotlin
9182
9182
  ====================================================================================================
9183
9183
  ----- LICENSE -----
9184
9184
  The MIT License (MIT)
@@ -9205,7 +9205,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
9205
9205
  SOFTWARE.
9206
9206
 
9207
9207
  ====================================================================================================
9208
- willbooster-tree-sitter-rust 1.1.1 — MIT — https://github.com/WillBooster/tree-sitter-rust
9208
+ willbooster-tree-sitter-rust 2.0.2 — MIT — https://github.com/WillBooster/tree-sitter-rust
9209
9209
  ====================================================================================================
9210
9210
  ----- LICENSE -----
9211
9211
  The MIT License (MIT)
@@ -9232,7 +9232,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
9232
9232
  SOFTWARE.
9233
9233
 
9234
9234
  ====================================================================================================
9235
- willbooster-tree-sitter-typescript 1.1.1 — MIT — https://github.com/WillBooster/tree-sitter-typescript
9235
+ willbooster-tree-sitter-typescript 7.0.0 — MIT — https://github.com/WillBooster/tree-sitter-typescript
9236
9236
  ====================================================================================================
9237
9237
  ----- LICENSE -----
9238
9238
  The MIT License (MIT)
@@ -1,5 +1,5 @@
1
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.duplication,t.resolveLimits(i,c.config.thresholds));await p(d.violations,u.root),l.json?A(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)});return{files:r.files.map(({file:e,metrics:t})=>({file:a.formatPath(e,r.displayRoot),metrics:t})),duplication:b(r,t),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,duplication: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(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)&&y(u,await(0,c.readFile)(l.default.join(n,e.headPath),`utf8`)),[{...t,hunks:u}]})).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),{crossFile:e.crossFileDuplication,minSimilarityPercent:t.duplication.minSimilarityPercent}}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 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,l=t===`functionCognitiveComplexity`?E(e):``;return`${n.get(t)} ${o} (${s}max ${c}${l})`}).join(`, `);return`${e.level}: ${D(e,r)}`}function E({largestBlocks:e=[]}){let t=e.map(({startLine:e,endLine:t,name:n,cognitiveComplexity:r})=>`L${e}${t===e?``:`-${t}`}${n?` ${O(n)}`:``} ${r}`);return t.length>0?`; largest parts ${t.join(`, `)}`:``}function D(e,t){switch(e.kind){case`file`:return`${e.file}: ${t}`;case`function`:return`${k(e)} ${O(e.name??``)}: ${t}`;case`duplication`:{let n=(e.partners??[]).map(e=>k(e)),r=[...n.slice(0,3),...n.length>3?[`...`]:[]];return`${k(e)}: ${t}${r.length>0?`, also at ${r.join(`, `)}`:``}`}}}function O(e){return e.replaceAll(/\s*[\n\r]\s*/gu,` `)}function k({file:e,startLine:t,endLine:n}){return`${e}:${t}-${n}`}function A(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)+`
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 t=e.exceeded.map(({metric:t,label:n,value:r,level:i,limit:a})=>`${n} ${Math.ceil(r*10)/10} (${i===e.level?``:`${i} `}max ${e.kind===`duplication`?Math.max(Math.ceil(a)-1,0):a}${t===`functionCognitiveComplexity`?E(e):``})`).join(`, `);return`${e.level}: ${D(e,t)}`}function E({largestBlocks:e=[]}){let t=e.map(({startLine:e,endLine:t,name:n,cognitiveComplexity:r})=>`L${e}${t===e?``:`-${t}`}${n?` ${O(n)}`:``} ${r}`);return t.length>0?`; largest parts ${t.join(`, `)}`:``}function D(e,t){switch(e.kind){case`file`:return`${e.file}: ${t}`;case`function`:return`${k(e)} ${O(e.name??``)}: ${t}`;case`duplication`:{let n=(e.partners??[]).map(e=>k(e)),r=[...n.slice(0,3),...n.length>3?[`...`]:[]];return`${k(e)}: ${t}${r.length>0?`, also at ${r.join(`, `)}`:``}`}}}function O(e){return e.replaceAll(/\s*[\n\r]\s*/gu,` `)}function k({file:e,startLine:t,endLine:n}){return`${e}:${t}-${n}`}function A(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,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 { 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 CheckedDuplication,\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 duplication: CheckedDuplication;\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(scope.files, scope.duplication, resolveLimits(cliLimits, config.config.thresholds));\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 return {\n files: scan.files.map(({ file, metrics }) => ({ file: formatPath(file, scan.displayRoot), metrics })),\n duplication: measureDuplication(scan, options),\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 duplication = 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 duplication,\n ...partitionErrors(scan, isCovered),\n root: repoRoot,\n mergeBase,\n };\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): CheckedDuplication {\n if (scan.fatalError) {\n throw new Error(scan.fatalError);\n }\n addCrossFileDuplication(scan, options);\n return {\n crossFile: scan.crossFileDuplication,\n minSimilarityPercent: options.duplication.minSimilarityPercent,\n };\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
+ {"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 { 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 CheckedDuplication,\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 duplication: CheckedDuplication;\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(scope.files, scope.duplication, resolveLimits(cliLimits, config.config.thresholds));\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 return {\n files: scan.files.map(({ file, metrics }) => ({ file: formatPath(file, scan.displayRoot), metrics })),\n duplication: measureDuplication(scan, options),\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 duplication = 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 duplication,\n ...partitionErrors(scan, isCovered),\n root: repoRoot,\n mergeBase,\n };\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): CheckedDuplication {\n if (scan.fatalError) {\n throw new Error(scan.fatalError);\n }\n addCrossFileDuplication(scan, options);\n return {\n crossFile: scan.crossFileDuplication,\n minSimilarityPercent: options.duplication.minSimilarityPercent,\n };\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 exceeded = violation.exceeded\n .map(({ metric, label, 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 `${label} ${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,5 +1,5 @@
1
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.duplication,t(a,s.config.thresholds));await P(u.violations,l.root),c.json?Z(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)});return{files:r.files.map(({file:e,metrics:t})=>({file:g(e,r.displayRoot),metrics:t})),duplication:V(r,t),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,duplication: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(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)&&B(c,await D(A.join(n,e.headPath),`utf8`)),[{...t,hunks:c}]})).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),{crossFile:e.crossFileDuplication,minSimilarityPercent:t.duplication.minSimilarityPercent}}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 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,l=e===`functionCognitiveComplexity`?q(t):``;return`${r.get(e)} ${o} (${s}max ${c}${l})`}).join(`, `);return`${t.level}: ${J(t,i)}`}function q({largestBlocks:e=[]}){let t=e.map(({startLine:e,endLine:t,name:n,cognitiveComplexity:r})=>`L${e}${t===e?``:`-${t}`}${n?` ${Y(n)}`:``} ${r}`);return t.length>0?`; largest parts ${t.join(`, `)}`:``}function J(e,t){switch(e.kind){case`file`:return`${e.file}: ${t}`;case`function`:return`${X(e)} ${Y(e.name??``)}: ${t}`;case`duplication`:{let n=(e.partners??[]).map(e=>X(e)),r=[...n.slice(0,3),...n.length>3?[`...`]:[]];return`${X(e)}: ${t}${r.length>0?`, also at ${r.join(`, `)}`:``}`}}}function Y(e){return e.replaceAll(/\s*[\n\r]\s*/gu,` `)}function X({file:e,startLine:t,endLine:n}){return`${e}:${t}-${n}`}function Z(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)+`
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(e){let t=e.exceeded.map(({metric:t,label:n,value:r,level:i,limit:a})=>`${n} ${Math.ceil(r*10)/10} (${i===e.level?``:`${i} `}max ${e.kind===`duplication`?Math.max(Math.ceil(a)-1,0):a}${t===`functionCognitiveComplexity`?q(e):``})`).join(`, `);return`${e.level}: ${J(e,t)}`}function q({largestBlocks:e=[]}){let t=e.map(({startLine:e,endLine:t,name:n,cognitiveComplexity:r})=>`L${e}${t===e?``:`-${t}`}${n?` ${Y(n)}`:``} ${r}`);return t.length>0?`; largest parts ${t.join(`, `)}`:``}function J(e,t){switch(e.kind){case`file`:return`${e.file}: ${t}`;case`function`:return`${X(e)} ${Y(e.name??``)}: ${t}`;case`duplication`:{let n=(e.partners??[]).map(e=>X(e)),r=[...n.slice(0,3),...n.length>3?[`...`]:[]];return`${X(e)}: ${t}${r.length>0?`, also at ${r.join(`, `)}`:``}`}}}function Y(e){return e.replaceAll(/\s*[\n\r]\s*/gu,` `)}function X({file:e,startLine:t,endLine:n}){return`${e}:${t}-${n}`}function Z(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,F as scanCheckScope};
5
5
  //# sourceMappingURL=checkCommand.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"checkCommand.js","names":[],"sources":["../src/checkCommand.ts"],"sourcesContent":["import { readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { getErrorMessage, mapConcurrently } from '@willbooster/shared-lib';\nimport { loadConfig, resolveOptions, type CliOptions, type ResolvedOptions } from './cliConfig.js';\nimport { 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 CheckedDuplication,\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 duplication: CheckedDuplication;\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(scope.files, scope.duplication, resolveLimits(cliLimits, config.config.thresholds));\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 return {\n files: scan.files.map(({ file, metrics }) => ({ file: formatPath(file, scan.displayRoot), metrics })),\n duplication: measureDuplication(scan, options),\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 duplication = 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 duplication,\n ...partitionErrors(scan, isCovered),\n root: repoRoot,\n mergeBase,\n };\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): CheckedDuplication {\n if (scan.fatalError) {\n throw new Error(scan.fatalError);\n }\n addCrossFileDuplication(scan, options);\n return {\n crossFile: scan.crossFileDuplication,\n minSimilarityPercent: options.duplication.minSimilarityPercent,\n };\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
+ {"version":3,"file":"checkCommand.js","names":[],"sources":["../src/checkCommand.ts"],"sourcesContent":["import { readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { getErrorMessage, mapConcurrently } from '@willbooster/shared-lib';\nimport { loadConfig, resolveOptions, type CliOptions, type ResolvedOptions } from './cliConfig.js';\nimport { 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 CheckedDuplication,\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 duplication: CheckedDuplication;\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(scope.files, scope.duplication, resolveLimits(cliLimits, config.config.thresholds));\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 return {\n files: scan.files.map(({ file, metrics }) => ({ file: formatPath(file, scan.displayRoot), metrics })),\n duplication: measureDuplication(scan, options),\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 duplication = 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 duplication,\n ...partitionErrors(scan, isCovered),\n root: repoRoot,\n mergeBase,\n };\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): CheckedDuplication {\n if (scan.fatalError) {\n throw new Error(scan.fatalError);\n }\n addCrossFileDuplication(scan, options);\n return {\n crossFile: scan.crossFileDuplication,\n minSimilarityPercent: options.duplication.minSimilarityPercent,\n };\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 exceeded = violation.exceeded\n .map(({ metric, label, 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 `${label} ${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,2 +1,2 @@
1
- "use strict";const e=require("./duplication.cjs"),t=require("./crossFileNearMiss.cjs"),n=require("./duplicateSelection.cjs");function r(t,r){let{minTokens:o,maxGapTokens:d,minSimilarityPercent:f}=e.resolveDuplicationOptions(r),p=t.flatMap(({file:e,candidates:t},n)=>t.map(t=>({...t,regionBucket:n,file:e})));for(let e of a(t,o))p.push(e);let m=n.selectMaximalGroups(p,s,(e,t)=>e.regionBucket-t.regionBucket||e.startIndex-t.startIndex),h=c(t,d),g=l([...m.values()],h,d);for(let e of i(t,g,h,o,f))g.push(e);return u(g,t,h)}function i(e,n,r,i,a){let o=e.map(()=>[]);for(let{fileIndex:e,startTokenIndex:t,endTokenIndex:i}of n.flat()){let n=r[e]??0;o[e]?.push({startTokenIndex:t-n,endTokenIndex:i-n})}return t.collectCrossFileNearMissGroups(e,o,i,a).map(t=>t.map(t=>{let n=r[t.fileIndex]??0;return{...t,file:e[t.fileIndex]?.file??``,segments:t.segments.map(e=>({startTokenIndex:e.startTokenIndex+n,endTokenIndex:e.endTokenIndex+n})),startTokenIndex:t.startTokenIndex+n,endTokenIndex:t.endTokenIndex+n}}))}function a(t,n){let r=[],i=[];for(let[n,{tokens:a,containerStatements:o}]of t.entries())a&&o&&(r.push(n),i.push({tokens:a,literalCountPrefix:e.buildLiteralCountPrefix(a),containers:o}));return i.length<2?[]:e.collectSequenceWindowCandidates(i,n,!0).flatMap(({candidate:e,contextIndex:n})=>{let i=r[n],a=i===void 0?void 0:t[i];return i===void 0||a===void 0?[]:[{...e,regionBucket:i,file:a.file}]})}function o(e){return new Set(e.map(e=>e.file)).size>=2}function s(e){return e.length>=2&&new Set(e.map(e=>e.regionBucket)).size>=2}function c(e,t){let n=[],r=0;for(let{tokens:i,candidates:a}of e){n.push(r);let e=i?.length??0;if(!i)for(let t of a)e=Math.max(e,t.endTokenIndex);r+=e+t+1}return n}function l(t,n,r){let i=t.map(e=>e.map(e=>{let t=e.startTokenIndex+(n[e.regionBucket]??0),r=e.endTokenIndex+(n[e.regionBucket]??0);return{file:e.file,fileIndex:e.regionBucket,spanCountedElsewhere:e.nestedInLargerGroup,nestedInLargerGroup:e.nestedInLargerGroup,segments:[{startTokenIndex:t,endTokenIndex:r}],tokenCount:e.tokenCount,startTokenIndex:t,endTokenIndex:r,startIndex:e.startIndex,endIndex:e.endIndex,startLine:e.startLine,endLine:e.endLine}}).toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex));return e.mergeAdjacentGroups(i,r,o)}function u(t,n,r){let i=[],a=/* @__PURE__ */ new Map,o=new Map(n.map((e,t)=>[e.file,{tokens:e.tokens,codeLineNumbers:e.codeLineNumbers,offset:r[t]??0}])),s=/* @__PURE__ */ new Map,c=0;for(let n of t){c+=e.countRedundantFragments(n);let t=n.map(e=>{let{file:t,startLine:n,endLine:r,tokenCount:i}=e,a=d(e,o,s);return{file:t,startLine:n,endLine:r,...a&&{lineNumbers:a},tokenCount:i}}).toSorted((e,t)=>e.file.localeCompare(t.file)||e.startLine-t.startLine),r=[...new Set(t.map(({file:e})=>e))];for(let e of r)a.set(e,(a.get(e)??0)+1);i.push({files:r,occurrences:t,tokenCount:Math.min(...n.map(({tokenCount:e})=>e))})}return i.sort((e,t)=>t.tokenCount-e.tokenCount||(e.occurrences[0]?.file??``).localeCompare(t.occurrences[0]?.file??``)||(e.occurrences[0]?.startLine??0)-(t.occurrences[0]?.startLine??0)),{duplicateBlockCount:c,duplicateBlockGroupCountByFile:Object.fromEntries(a),duplicateLineNumbersByFile:Object.fromEntries([...s].map(([e,t])=>[e,[...t].toSorted((e,t)=>e-t)])),groups:i}}function d(t,n,r){let i=n.get(t.file);if(!i?.tokens)return;let a=r.get(t.file);a||(a=/* @__PURE__ */ new Set,r.set(t.file,a));let o=/* @__PURE__ */ new Set;for(let n of t.segments)e.collectSegmentLines({startTokenIndex:n.startTokenIndex-i.offset,endTokenIndex:n.endTokenIndex-i.offset},i.tokens,i.codeLineNumbers,o);for(let e of o)a.add(e);return[...o].toSorted((e,t)=>e-t)}exports.measureCrossFileDuplication=r;
1
+ "use strict";const e=require("./duplication.cjs"),t=require("./crossFileNearMiss.cjs"),n=require("./duplicateSelection.cjs");function r(t,r){let{minTokens:a,maxGapTokens:s,minSimilarityPercent:f}=e.resolveDuplicationOptions(r),p=t.flatMap(({file:e,candidates:t},n)=>t.map(t=>({...t,regionBucket:n,file:e})));for(let e of o(t,a))p.push(e);let m=n.selectMaximalGroups(p,c,(e,t)=>e.regionBucket-t.regionBucket||e.startIndex-t.startIndex),h=l(t,s),g=u([...m.values()],h,s);for(let e of i(t,g,h,a,f))g.push(e);return d(g,t,h)}function i(e,n,r,i,o){let s=e.map(()=>[]);for(let{fileIndex:e,startTokenIndex:t,endTokenIndex:i}of n.flat()){let n=r[e]??0;s[e]?.push({startTokenIndex:t-n,endTokenIndex:i-n})}return t.collectCrossFileNearMissGroups(e,s,i,o).map(t=>t.map(t=>{let n=r[t.fileIndex]??0;return{...t,file:e[t.fileIndex]?.file??``,segments:t.segments.map(e=>a(e,n)),matchedRuns:t.matchedRuns?.map(e=>a(e,n)),startTokenIndex:t.startTokenIndex+n,endTokenIndex:t.endTokenIndex+n}}))}function a({startTokenIndex:e,endTokenIndex:t},n){return{startTokenIndex:e+n,endTokenIndex:t+n}}function o(t,n){let r=[],i=[];for(let[n,{tokens:a,containerStatements:o}]of t.entries())a&&o&&(r.push(n),i.push({tokens:a,literalCountPrefix:e.buildLiteralCountPrefix(a),containers:o}));return i.length<2?[]:e.collectSequenceWindowCandidates(i,n,!0).flatMap(({candidate:e,contextIndex:n})=>{let i=r[n],a=i===void 0?void 0:t[i];return i===void 0||a===void 0?[]:[{...e,regionBucket:i,file:a.file}]})}function s(e){return new Set(e.map(e=>e.file)).size>=2}function c(e){return e.length>=2&&new Set(e.map(e=>e.regionBucket)).size>=2}function l(e,t){let n=[],r=0;for(let{tokens:i,candidates:a}of e){n.push(r);let e=i?.length??0;if(!i)for(let t of a)e=Math.max(e,t.endTokenIndex);r+=e+t+1}return n}function u(t,n,r){let i=t.map(e=>e.map(e=>{let t=e.startTokenIndex+(n[e.regionBucket]??0),r=e.endTokenIndex+(n[e.regionBucket]??0);return{file:e.file,fileIndex:e.regionBucket,spanCountedElsewhere:e.nestedInLargerGroup,nestedInLargerGroup:e.nestedInLargerGroup,segments:[{startTokenIndex:t,endTokenIndex:r}],tokenCount:e.tokenCount,startTokenIndex:t,endTokenIndex:r,startIndex:e.startIndex,endIndex:e.endIndex,startLine:e.startLine,endLine:e.endLine}}).toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex));return e.mergeAdjacentGroups(i,r,s)}function d(t,n,r){let i=[],a=/* @__PURE__ */ new Map,o=new Map(n.map((e,t)=>[e.file,{tokens:e.tokens,codeLineNumbers:e.codeLineNumbers,offset:r[t]??0}])),s=/* @__PURE__ */ new Map,c=0;for(let n of t){c+=e.countRedundantFragments(n);let t=n.map(e=>{let{file:t,startLine:n,endLine:r,tokenCount:i}=e,a=f(e,o,s);return{file:t,startLine:n,endLine:r,...a&&{lineNumbers:a},tokenCount:i}}).toSorted((e,t)=>e.file.localeCompare(t.file)||e.startLine-t.startLine),r=[...new Set(t.map(({file:e})=>e))];for(let e of r)a.set(e,(a.get(e)??0)+1);i.push({files:r,occurrences:t,tokenCount:Math.min(...n.map(({tokenCount:e})=>e))})}return i.sort((e,t)=>t.tokenCount-e.tokenCount||(e.occurrences[0]?.file??``).localeCompare(t.occurrences[0]?.file??``)||(e.occurrences[0]?.startLine??0)-(t.occurrences[0]?.startLine??0)),{duplicateBlockCount:c,duplicateBlockGroupCountByFile:Object.fromEntries(a),duplicateLineNumbersByFile:Object.fromEntries([...s].map(([e,t])=>[e,[...t].toSorted((e,t)=>e-t)])),groups:i}}function f(t,n,r){let i=n.get(t.file);if(!i?.tokens)return;let o=r.get(t.file);o||(o=/* @__PURE__ */ new Set,r.set(t.file,o));let s=e.collectMatchedLines(t.segments.map(e=>a(e,-i.offset)),t.matchedRuns?.map(e=>a(e,-i.offset)),i.tokens,i.codeLineNumbers);for(let e of s)o.add(e);return[...s].toSorted((e,t)=>e-t)}exports.measureCrossFileDuplication=r;
2
2
  //# sourceMappingURL=crossFileDuplication.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"crossFileDuplication.cjs","names":["resolveDuplicationOptions","selectMaximalGroups","collectCrossFileNearMissGroups","buildLiteralCountPrefix","collectSequenceWindowCandidates","mergeAdjacentGroups","countRedundantFragments"],"sources":["../src/crossFileDuplication.ts"],"sourcesContent":["import { collectCrossFileNearMissGroups } from './crossFileNearMiss.js';\nimport { selectMaximalGroups, type SelectableRegion } from './duplicateSelection.js';\nimport {\n buildLiteralCountPrefix,\n collectSegmentLines,\n collectSequenceWindowCandidates,\n countRedundantFragments,\n mergeAdjacentGroups,\n resolveDuplicationOptions,\n type CountedOccurrence,\n type CrossFileDuplicateCandidate,\n type CrossFileDuplicationFileData,\n type SequenceWindowContext,\n} from './duplication.js';\nimport type { DuplicationOptions } from './types.js';\n\nexport interface CrossFileDuplicationSourceFile extends Partial<CrossFileDuplicationFileData> {\n file: string;\n candidates: CrossFileDuplicateCandidate[];\n}\n\nexport interface CrossFileDuplicateOccurrence {\n endLine: number;\n file: string;\n /**\n * The code lines carrying the occurrence's matched tokens (see\n * DuplicateBlockOccurrence.lineNumbers). Absent for a file that supplied only candidates (no\n * `tokens`), whose matched lines are unknowable.\n */\n lineNumbers?: number[];\n startLine: number;\n /**\n * The tokens of its matched runs, or of a near-miss block or its matched cores with the edited\n * tokens among them; the group's `tokenCount` is the smallest of its occurrences'.\n */\n tokenCount: number;\n}\n\nexport interface CrossFileDuplicateBlockGroup {\n files: string[];\n occurrences: CrossFileDuplicateOccurrence[];\n /**\n * Token count of the smallest occurrence. For exact and gapped groups it is the matched token\n * count every occurrence shares (gaps are not counted); for near-miss (Type-3) groups it is the\n * token count of the smallest whole block or set of matched cores, edited tokens included.\n */\n tokenCount: number;\n}\n\nexport interface CrossFileDuplicationMetrics {\n /** Number of redundant copies across all groups, counted per matched fragment like within-file. */\n duplicateBlockCount: number;\n /** Groups the file participates in, keyed by the file name passed in. */\n duplicateBlockGroupCountByFile: Record<string, number>;\n /**\n * Per file, the 1-based code lines covered by the tokens of its cross-file occurrences, sorted\n * ascending: the matched tokens of exact and gapped occurrences, and every token of a near-miss\n * block or its matched cores, edited ones included (like within-file near-miss coverage). The unmatched gap of a merged\n * clone and comment/blank lines inside an occurrence's bounding range are excluded (blank rows\n * inside multi-row tokens only when the file supplied codeLineNumbers). A file that supplied\n * only candidates (no `tokens`) has no entry — without its token stream the covered lines are\n * unknowable, and an approximate bounding range would break this field's exactness.\n */\n duplicateLineNumbersByFile: Record<string, number[]>;\n groups: CrossFileDuplicateBlockGroup[];\n}\n\ninterface SelectableCandidate extends CrossFileDuplicateCandidate, SelectableRegion {\n regionBucket: number;\n file: string;\n}\n\n/** A cross-file occurrence: a within-file occurrence in the project-wide token index space. */\ninterface CrossFileOccurrence extends CountedOccurrence {\n file: string;\n fileIndex: number;\n}\n\n/**\n * Detects code regions duplicated across files. Per-file candidates (whole block subtrees and full\n * container runs, fingerprinted with the same normalization as within-file duplication) are joined\n * by a project-level window index over per-statement fingerprint sequences (CPD-style), so a\n * copy-pasted partial statement run embedded in different surrounding code is matched even though\n * no single file can know it repeats elsewhere. Candidates are grouped by fingerprint, and only\n * maximal, non-overlapping regions whose group spans at least two files are counted. Groups that\n * shrink to a single file during selection are shed — a within-file repeat is already reported by\n * that file's own duplication metrics. A copy nested inside a larger group's region (two files share\n * a whole function, a third file only a block of it) is reported with its group, so the third\n * file's copy still shows what it duplicates. Groups separated by a small token gap within each file then\n * merge into gapped (Type-3) clone groups under `maxGapTokens`, exactly like within-file merging.\n * Finally, blocks of files that supplied `nearMissBlocks` are compared across files for near-miss\n * (Type-3) clones under `minSimilarityPercent` (see crossFileNearMiss.ts).\n */\nexport function measureCrossFileDuplication(\n files: CrossFileDuplicationSourceFile[],\n options?: DuplicationOptions\n): CrossFileDuplicationMetrics {\n const { minTokens, maxGapTokens, minSimilarityPercent } = resolveDuplicationOptions(options);\n const candidates: SelectableCandidate[] = files.flatMap(({ file, candidates }, fileIndex) =>\n candidates.map((candidate) => ({ ...candidate, regionBucket: fileIndex, file }))\n );\n // Pushed one by one: spreading the project-scale window-candidate array as call arguments\n // overflows V8's argument limit (~124k) and crashes on Node, though Bun/JSC tolerates it.\n for (const candidate of collectWindowCandidates(files, minTokens)) {\n candidates.push(candidate);\n }\n const counted = selectMaximalGroups(\n candidates,\n spansMultipleFiles,\n // File index and position break coverage ties deterministically.\n (left, right) => left.regionBucket - right.regionBucket || left.startIndex - right.startIndex\n );\n const tokenOffsets = computeTokenOffsets(files, maxGapTokens);\n const groups = mergeGapAdjacentGroups([...counted.values()], tokenOffsets, maxGapTokens);\n for (const group of collectNearMissGroups(files, groups, tokenOffsets, minTokens, minSimilarityPercent)) {\n groups.push(group);\n }\n return summarize(groups, files, tokenOffsets);\n}\n\n/** Near-miss groups among the blocks the exact groups leave unreported, in the project token space. */\nfunction collectNearMissGroups(\n files: CrossFileDuplicationSourceFile[],\n exactGroups: CrossFileOccurrence[][],\n tokenOffsets: number[],\n minTokens: number,\n minSimilarityPercent: number\n): CrossFileOccurrence[][] {\n const reportedSpansByFile: { startTokenIndex: number; endTokenIndex: number }[][] = files.map(() => []);\n for (const { fileIndex, startTokenIndex, endTokenIndex } of exactGroups.flat()) {\n const offset = tokenOffsets[fileIndex] ?? 0;\n reportedSpansByFile[fileIndex]?.push({\n startTokenIndex: startTokenIndex - offset,\n endTokenIndex: endTokenIndex - offset,\n });\n }\n return collectCrossFileNearMissGroups(files, reportedSpansByFile, minTokens, minSimilarityPercent).map((group) =>\n group.map((occurrence) => {\n const offset = tokenOffsets[occurrence.fileIndex] ?? 0;\n return {\n ...occurrence,\n file: files[occurrence.fileIndex]?.file ?? '',\n segments: occurrence.segments.map((segment) => ({\n startTokenIndex: segment.startTokenIndex + offset,\n endTokenIndex: segment.endTokenIndex + offset,\n })),\n startTokenIndex: occurrence.startTokenIndex + offset,\n endTokenIndex: occurrence.endTokenIndex + offset,\n };\n })\n );\n}\n\n/** Repeated sub-windows of sibling statements matched across the whole project's files. */\nfunction collectWindowCandidates(files: CrossFileDuplicationSourceFile[], minTokens: number): SelectableCandidate[] {\n const fileIndexByContext: number[] = [];\n const contexts: SequenceWindowContext[] = [];\n for (const [fileIndex, { tokens, containerStatements }] of files.entries()) {\n if (tokens && containerStatements) {\n fileIndexByContext.push(fileIndex);\n contexts.push({ tokens, literalCountPrefix: buildLiteralCountPrefix(tokens), containers: containerStatements });\n }\n }\n if (contexts.length < 2) {\n return [];\n }\n return collectSequenceWindowCandidates(contexts, minTokens, true).flatMap(({ candidate, contextIndex }) => {\n const fileIndex = fileIndexByContext[contextIndex];\n const file = fileIndex === undefined ? undefined : files[fileIndex];\n return fileIndex === undefined || file === undefined\n ? []\n : [{ ...candidate, regionBucket: fileIndex, file: file.file }];\n });\n}\n\n/** A merged group is reported only while it still covers more than one file. */\nfunction spansMultipleFilesAfterMerge(group: CrossFileOccurrence[]): boolean {\n return new Set(group.map((occurrence) => occurrence.file)).size >= 2;\n}\n\nfunction spansMultipleFiles(group: SelectableCandidate[]): boolean {\n return group.length >= 2 && new Set(group.map((candidate) => candidate.regionBucket)).size >= 2;\n}\n\n/**\n * Per-file token offsets that map every file into one project-wide token index space: each file's\n * tokens are offset by more than `maxGapTokens` past the previous file's, so occurrences in\n * different files are never gap-adjacent and merged pairs always stay within one file.\n */\nfunction computeTokenOffsets(files: CrossFileDuplicationSourceFile[], maxGapTokens: number): number[] {\n const tokenOffsets: number[] = [];\n let offset = 0;\n for (const { tokens, candidates } of files) {\n tokenOffsets.push(offset);\n // Accumulated in a loop: spreading a project-scale candidate array as call arguments would\n // overflow V8's argument limit (~124k) and crash on Node.\n let tokenCount = tokens?.length ?? 0;\n if (!tokens) {\n for (const candidate of candidates) {\n tokenCount = Math.max(tokenCount, candidate.endTokenIndex);\n }\n }\n offset += tokenCount + maxGapTokens + 1;\n }\n return tokenOffsets;\n}\n\n/** Reuses the within-file gapped (Type-3) merging in the project-wide token index space. */\nfunction mergeGapAdjacentGroups(\n groups: SelectableCandidate[][],\n tokenOffsets: number[],\n maxGapTokens: number\n): CrossFileOccurrence[][] {\n const occurrenceGroups = groups.map((group) =>\n group\n .map((candidate): CrossFileOccurrence => {\n const start = candidate.startTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n const end = candidate.endTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n return {\n file: candidate.file,\n fileIndex: candidate.regionBucket,\n spanCountedElsewhere: candidate.nestedInLargerGroup,\n nestedInLargerGroup: candidate.nestedInLargerGroup,\n segments: [{ startTokenIndex: start, endTokenIndex: end }],\n tokenCount: candidate.tokenCount,\n startTokenIndex: start,\n endTokenIndex: end,\n startIndex: candidate.startIndex,\n endIndex: candidate.endIndex,\n startLine: candidate.startLine,\n endLine: candidate.endLine,\n };\n })\n .toSorted((left, right) => left.startTokenIndex - right.startTokenIndex)\n );\n return mergeAdjacentGroups(occurrenceGroups, maxGapTokens, spansMultipleFilesAfterMerge);\n}\n\nfunction summarize(\n groups: CrossFileOccurrence[][],\n files: CrossFileDuplicationSourceFile[],\n tokenOffsets: number[]\n): CrossFileDuplicationMetrics {\n const reported: CrossFileDuplicateBlockGroup[] = [];\n // Accumulated in Maps: file names are arbitrary strings, and a plain object would read\n // inherited properties for names like \"constructor\".\n const groupCountByFile = new Map<string, number>();\n const fileDataByName = new Map(\n files.map((file, index) => [\n file.file,\n { tokens: file.tokens, codeLineNumbers: file.codeLineNumbers, offset: tokenOffsets[index] ?? 0 },\n ])\n );\n const lineNumbersByFile = new Map<string, Set<number>>();\n let duplicateBlockCount = 0;\n for (const group of groups) {\n // Mirrors within-file counting: each redundant occurrence contributes one count per matched\n // fragment, gapped merging consolidates the grouping without halving the count, and spans a\n // partial merge shares between a retained group and the merged group count once.\n duplicateBlockCount += countRedundantFragments(group);\n const occurrences = group\n .map((occurrence) => {\n const { file, startLine, endLine, tokenCount } = occurrence;\n const lineNumbers = collectOccurrenceLines(occurrence, fileDataByName, lineNumbersByFile);\n return { file, startLine, endLine, ...(lineNumbers && { lineNumbers }), tokenCount };\n })\n .toSorted((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n const files = [...new Set(occurrences.map(({ file }) => file))];\n for (const file of files) {\n groupCountByFile.set(file, (groupCountByFile.get(file) ?? 0) + 1);\n }\n reported.push({ files, occurrences, tokenCount: Math.min(...group.map(({ tokenCount }) => tokenCount)) });\n }\n reported.sort(\n (left, right) =>\n right.tokenCount - left.tokenCount ||\n (left.occurrences[0]?.file ?? '').localeCompare(right.occurrences[0]?.file ?? '') ||\n (left.occurrences[0]?.startLine ?? 0) - (right.occurrences[0]?.startLine ?? 0)\n );\n return {\n duplicateBlockCount,\n duplicateBlockGroupCountByFile: Object.fromEntries(groupCountByFile),\n duplicateLineNumbersByFile: Object.fromEntries(\n [...lineNumbersByFile].map(([file, lines]) => [file, [...lines].toSorted((left, right) => left - right)])\n ),\n groups: reported,\n };\n}\n\n/**\n * Adds the code lines an occurrence's segment tokens cover (matched tokens of an exact or gapped\n * occurrence, the whole block or matched cores of a near-miss one) to its file's line set, mapping the\n * project-wide token segments back into the file's own token stream, and returns them in order.\n * A file that supplied only candidates (no token stream) is skipped rather than approximated from\n * the bounding line range, which would include gap and comment/blank lines and break the field's\n * exactness contract.\n */\nfunction collectOccurrenceLines(\n occurrence: CrossFileOccurrence,\n fileDataByName: Map<\n string,\n { tokens?: CrossFileDuplicationSourceFile['tokens']; codeLineNumbers?: Set<number>; offset: number }\n >,\n lineNumbersByFile: Map<string, Set<number>>\n): number[] | undefined {\n const fileData = fileDataByName.get(occurrence.file);\n if (!fileData?.tokens) {\n return undefined;\n }\n let fileLines = lineNumbersByFile.get(occurrence.file);\n if (!fileLines) {\n fileLines = new Set();\n lineNumbersByFile.set(occurrence.file, fileLines);\n }\n const lines = new Set<number>();\n for (const segment of occurrence.segments) {\n collectSegmentLines(\n {\n startTokenIndex: segment.startTokenIndex - fileData.offset,\n endTokenIndex: segment.endTokenIndex - fileData.offset,\n },\n fileData.tokens,\n fileData.codeLineNumbers,\n lines\n );\n }\n for (const line of lines) {\n fileLines.add(line);\n }\n return [...lines].toSorted((left, right) => left - right);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"crossFileDuplication.cjs","names":["resolveDuplicationOptions","selectMaximalGroups","collectCrossFileNearMissGroups","buildLiteralCountPrefix","collectSequenceWindowCandidates","mergeAdjacentGroups","countRedundantFragments","collectMatchedLines"],"sources":["../src/crossFileDuplication.ts"],"sourcesContent":["import { collectCrossFileNearMissGroups } from './crossFileNearMiss.js';\nimport { selectMaximalGroups, type SelectableRegion } from './duplicateSelection.js';\nimport {\n buildLiteralCountPrefix,\n collectMatchedLines,\n collectSequenceWindowCandidates,\n countRedundantFragments,\n mergeAdjacentGroups,\n resolveDuplicationOptions,\n type CountedOccurrence,\n type CrossFileDuplicateCandidate,\n type CrossFileDuplicationFileData,\n type SequenceWindowContext,\n type TokenSegment,\n} from './duplication.js';\nimport type { DuplicationOptions } from './types.js';\n\nexport interface CrossFileDuplicationSourceFile extends Partial<CrossFileDuplicationFileData> {\n file: string;\n candidates: CrossFileDuplicateCandidate[];\n}\n\nexport interface CrossFileDuplicateOccurrence {\n endLine: number;\n file: string;\n /**\n * The code lines carrying the occurrence's matched tokens (see\n * DuplicateBlockOccurrence.lineNumbers). Absent for a file that supplied only candidates (no\n * `tokens`), whose matched lines are unknowable.\n */\n lineNumbers?: number[];\n startLine: number;\n /**\n * The tokens of its matched runs, or of a near-miss block or its matched cores with the edited\n * tokens among them; the group's `tokenCount` is the smallest of its occurrences'.\n */\n tokenCount: number;\n}\n\nexport interface CrossFileDuplicateBlockGroup {\n files: string[];\n occurrences: CrossFileDuplicateOccurrence[];\n /**\n * Token count of the smallest occurrence. For exact and gapped groups it is the matched token\n * count every occurrence shares (gaps are not counted); for near-miss (Type-3) groups it is the\n * token count of the smallest whole block or set of matched cores, edited tokens included.\n */\n tokenCount: number;\n}\n\nexport interface CrossFileDuplicationMetrics {\n /** Number of redundant copies across all groups, counted per matched fragment like within-file. */\n duplicateBlockCount: number;\n /** Groups the file participates in, keyed by the file name passed in. */\n duplicateBlockGroupCountByFile: Record<string, number>;\n /**\n * Per file, the 1-based code lines covered by the tokens of its cross-file occurrences, sorted\n * ascending: the matched tokens of exact and gapped occurrences, and the lines of a near-miss\n * block or its matched cores more than half of whose tokens its partners match (like within-file\n * near-miss coverage). The unmatched gap of a merged\n * clone and comment/blank lines inside an occurrence's bounding range are excluded (blank rows\n * inside multi-row tokens only when the file supplied codeLineNumbers). A file that supplied\n * only candidates (no `tokens`) has no entry — without its token stream the covered lines are\n * unknowable, and an approximate bounding range would break this field's exactness.\n */\n duplicateLineNumbersByFile: Record<string, number[]>;\n groups: CrossFileDuplicateBlockGroup[];\n}\n\ninterface SelectableCandidate extends CrossFileDuplicateCandidate, SelectableRegion {\n regionBucket: number;\n file: string;\n}\n\n/** A cross-file occurrence: a within-file occurrence in the project-wide token index space. */\ninterface CrossFileOccurrence extends CountedOccurrence {\n file: string;\n fileIndex: number;\n}\n\n/**\n * Detects code regions duplicated across files. Per-file candidates (whole block subtrees and full\n * container runs, fingerprinted with the same normalization as within-file duplication) are joined\n * by a project-level window index over per-statement fingerprint sequences (CPD-style), so a\n * copy-pasted partial statement run embedded in different surrounding code is matched even though\n * no single file can know it repeats elsewhere. Candidates are grouped by fingerprint, and only\n * maximal, non-overlapping regions whose group spans at least two files are counted. Groups that\n * shrink to a single file during selection are shed — a within-file repeat is already reported by\n * that file's own duplication metrics. A copy nested inside a larger group's region (two files share\n * a whole function, a third file only a block of it) is reported with its group, so the third\n * file's copy still shows what it duplicates. Groups separated by a small token gap within each file then\n * merge into gapped (Type-3) clone groups under `maxGapTokens`, exactly like within-file merging.\n * Finally, blocks of files that supplied `nearMissBlocks` are compared across files for near-miss\n * (Type-3) clones under `minSimilarityPercent` (see crossFileNearMiss.ts).\n */\nexport function measureCrossFileDuplication(\n files: CrossFileDuplicationSourceFile[],\n options?: DuplicationOptions\n): CrossFileDuplicationMetrics {\n const { minTokens, maxGapTokens, minSimilarityPercent } = resolveDuplicationOptions(options);\n const candidates: SelectableCandidate[] = files.flatMap(({ file, candidates }, fileIndex) =>\n candidates.map((candidate) => ({ ...candidate, regionBucket: fileIndex, file }))\n );\n // Pushed one by one: spreading the project-scale window-candidate array as call arguments\n // overflows V8's argument limit (~124k) and crashes on Node, though Bun/JSC tolerates it.\n for (const candidate of collectWindowCandidates(files, minTokens)) {\n candidates.push(candidate);\n }\n const counted = selectMaximalGroups(\n candidates,\n spansMultipleFiles,\n // File index and position break coverage ties deterministically.\n (left, right) => left.regionBucket - right.regionBucket || left.startIndex - right.startIndex\n );\n const tokenOffsets = computeTokenOffsets(files, maxGapTokens);\n const groups = mergeGapAdjacentGroups([...counted.values()], tokenOffsets, maxGapTokens);\n for (const group of collectNearMissGroups(files, groups, tokenOffsets, minTokens, minSimilarityPercent)) {\n groups.push(group);\n }\n return summarize(groups, files, tokenOffsets);\n}\n\n/** Near-miss groups among the blocks the exact groups leave unreported, in the project token space. */\nfunction collectNearMissGroups(\n files: CrossFileDuplicationSourceFile[],\n exactGroups: CrossFileOccurrence[][],\n tokenOffsets: number[],\n minTokens: number,\n minSimilarityPercent: number\n): CrossFileOccurrence[][] {\n const reportedSpansByFile: { startTokenIndex: number; endTokenIndex: number }[][] = files.map(() => []);\n for (const { fileIndex, startTokenIndex, endTokenIndex } of exactGroups.flat()) {\n const offset = tokenOffsets[fileIndex] ?? 0;\n reportedSpansByFile[fileIndex]?.push({\n startTokenIndex: startTokenIndex - offset,\n endTokenIndex: endTokenIndex - offset,\n });\n }\n return collectCrossFileNearMissGroups(files, reportedSpansByFile, minTokens, minSimilarityPercent).map((group) =>\n group.map((occurrence) => {\n const offset = tokenOffsets[occurrence.fileIndex] ?? 0;\n return {\n ...occurrence,\n file: files[occurrence.fileIndex]?.file ?? '',\n segments: occurrence.segments.map((segment) => shift(segment, offset)),\n matchedRuns: occurrence.matchedRuns?.map((run) => shift(run, offset)),\n startTokenIndex: occurrence.startTokenIndex + offset,\n endTokenIndex: occurrence.endTokenIndex + offset,\n };\n })\n );\n}\n\nfunction shift({ startTokenIndex, endTokenIndex }: TokenSegment, offset: number): TokenSegment {\n return { startTokenIndex: startTokenIndex + offset, endTokenIndex: endTokenIndex + offset };\n}\n\n/** Repeated sub-windows of sibling statements matched across the whole project's files. */\nfunction collectWindowCandidates(files: CrossFileDuplicationSourceFile[], minTokens: number): SelectableCandidate[] {\n const fileIndexByContext: number[] = [];\n const contexts: SequenceWindowContext[] = [];\n for (const [fileIndex, { tokens, containerStatements }] of files.entries()) {\n if (tokens && containerStatements) {\n fileIndexByContext.push(fileIndex);\n contexts.push({ tokens, literalCountPrefix: buildLiteralCountPrefix(tokens), containers: containerStatements });\n }\n }\n if (contexts.length < 2) {\n return [];\n }\n return collectSequenceWindowCandidates(contexts, minTokens, true).flatMap(({ candidate, contextIndex }) => {\n const fileIndex = fileIndexByContext[contextIndex];\n const file = fileIndex === undefined ? undefined : files[fileIndex];\n return fileIndex === undefined || file === undefined\n ? []\n : [{ ...candidate, regionBucket: fileIndex, file: file.file }];\n });\n}\n\n/** A merged group is reported only while it still covers more than one file. */\nfunction spansMultipleFilesAfterMerge(group: CrossFileOccurrence[]): boolean {\n return new Set(group.map((occurrence) => occurrence.file)).size >= 2;\n}\n\nfunction spansMultipleFiles(group: SelectableCandidate[]): boolean {\n return group.length >= 2 && new Set(group.map((candidate) => candidate.regionBucket)).size >= 2;\n}\n\n/**\n * Per-file token offsets that map every file into one project-wide token index space: each file's\n * tokens are offset by more than `maxGapTokens` past the previous file's, so occurrences in\n * different files are never gap-adjacent and merged pairs always stay within one file.\n */\nfunction computeTokenOffsets(files: CrossFileDuplicationSourceFile[], maxGapTokens: number): number[] {\n const tokenOffsets: number[] = [];\n let offset = 0;\n for (const { tokens, candidates } of files) {\n tokenOffsets.push(offset);\n // Accumulated in a loop: spreading a project-scale candidate array as call arguments would\n // overflow V8's argument limit (~124k) and crash on Node.\n let tokenCount = tokens?.length ?? 0;\n if (!tokens) {\n for (const candidate of candidates) {\n tokenCount = Math.max(tokenCount, candidate.endTokenIndex);\n }\n }\n offset += tokenCount + maxGapTokens + 1;\n }\n return tokenOffsets;\n}\n\n/** Reuses the within-file gapped (Type-3) merging in the project-wide token index space. */\nfunction mergeGapAdjacentGroups(\n groups: SelectableCandidate[][],\n tokenOffsets: number[],\n maxGapTokens: number\n): CrossFileOccurrence[][] {\n const occurrenceGroups = groups.map((group) =>\n group\n .map((candidate): CrossFileOccurrence => {\n const start = candidate.startTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n const end = candidate.endTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n return {\n file: candidate.file,\n fileIndex: candidate.regionBucket,\n spanCountedElsewhere: candidate.nestedInLargerGroup,\n nestedInLargerGroup: candidate.nestedInLargerGroup,\n segments: [{ startTokenIndex: start, endTokenIndex: end }],\n tokenCount: candidate.tokenCount,\n startTokenIndex: start,\n endTokenIndex: end,\n startIndex: candidate.startIndex,\n endIndex: candidate.endIndex,\n startLine: candidate.startLine,\n endLine: candidate.endLine,\n };\n })\n .toSorted((left, right) => left.startTokenIndex - right.startTokenIndex)\n );\n return mergeAdjacentGroups(occurrenceGroups, maxGapTokens, spansMultipleFilesAfterMerge);\n}\n\nfunction summarize(\n groups: CrossFileOccurrence[][],\n files: CrossFileDuplicationSourceFile[],\n tokenOffsets: number[]\n): CrossFileDuplicationMetrics {\n const reported: CrossFileDuplicateBlockGroup[] = [];\n // Accumulated in Maps: file names are arbitrary strings, and a plain object would read\n // inherited properties for names like \"constructor\".\n const groupCountByFile = new Map<string, number>();\n const fileDataByName = new Map(\n files.map((file, index) => [\n file.file,\n { tokens: file.tokens, codeLineNumbers: file.codeLineNumbers, offset: tokenOffsets[index] ?? 0 },\n ])\n );\n const lineNumbersByFile = new Map<string, Set<number>>();\n let duplicateBlockCount = 0;\n for (const group of groups) {\n // Mirrors within-file counting: each redundant occurrence contributes one count per matched\n // fragment, gapped merging consolidates the grouping without halving the count, and spans a\n // partial merge shares between a retained group and the merged group count once.\n duplicateBlockCount += countRedundantFragments(group);\n const occurrences = group\n .map((occurrence) => {\n const { file, startLine, endLine, tokenCount } = occurrence;\n const lineNumbers = collectOccurrenceLines(occurrence, fileDataByName, lineNumbersByFile);\n return { file, startLine, endLine, ...(lineNumbers && { lineNumbers }), tokenCount };\n })\n .toSorted((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n const files = [...new Set(occurrences.map(({ file }) => file))];\n for (const file of files) {\n groupCountByFile.set(file, (groupCountByFile.get(file) ?? 0) + 1);\n }\n reported.push({ files, occurrences, tokenCount: Math.min(...group.map(({ tokenCount }) => tokenCount)) });\n }\n reported.sort(\n (left, right) =>\n right.tokenCount - left.tokenCount ||\n (left.occurrences[0]?.file ?? '').localeCompare(right.occurrences[0]?.file ?? '') ||\n (left.occurrences[0]?.startLine ?? 0) - (right.occurrences[0]?.startLine ?? 0)\n );\n return {\n duplicateBlockCount,\n duplicateBlockGroupCountByFile: Object.fromEntries(groupCountByFile),\n duplicateLineNumbersByFile: Object.fromEntries(\n [...lineNumbersByFile].map(([file, lines]) => [file, [...lines].toSorted((left, right) => left - right)])\n ),\n groups: reported,\n };\n}\n\n/**\n * Adds the code lines an occurrence's matched tokens cover (see collectMatchedLines) to its file's\n * line set, mapping the project-wide token segments back into the file's own token stream, and\n * returns them in order.\n * A file that supplied only candidates (no token stream) is skipped rather than approximated from\n * the bounding line range, which would include gap and comment/blank lines and break the field's\n * exactness contract.\n */\nfunction collectOccurrenceLines(\n occurrence: CrossFileOccurrence,\n fileDataByName: Map<\n string,\n { tokens?: CrossFileDuplicationSourceFile['tokens']; codeLineNumbers?: Set<number>; offset: number }\n >,\n lineNumbersByFile: Map<string, Set<number>>\n): number[] | undefined {\n const fileData = fileDataByName.get(occurrence.file);\n if (!fileData?.tokens) {\n return undefined;\n }\n let fileLines = lineNumbersByFile.get(occurrence.file);\n if (!fileLines) {\n fileLines = new Set();\n lineNumbersByFile.set(occurrence.file, fileLines);\n }\n const lines = collectMatchedLines(\n occurrence.segments.map((segment) => shift(segment, -fileData.offset)),\n occurrence.matchedRuns?.map((run) => shift(run, -fileData.offset)),\n fileData.tokens,\n fileData.codeLineNumbers\n );\n for (const line of lines) {\n fileLines.add(line);\n }\n return [...lines].toSorted((left, right) => left - right);\n}\n"],"mappings":"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"}