code-gauge 10.2.1 → 10.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -445,7 +445,8 @@ The `duplication` section tunes how clones are detected:
445
445
  `require`s carry no tokens, since every module must spell out its own), with adjacent matches around a small edit merged into gapped (Type-3) clone groups and
446
446
  near-miss (Type-3) clones matched by token-LCS similarity (tolerating reordered statements and
447
447
  copies embedded in added code), each copy with its line span, its matched lines, and its matched token count,
448
- plus duplicated line count and ratio
448
+ plus duplicated line count and ratio; code standing on its own that copies only a part of larger
449
+ copies is listed with that part of each of them
449
450
  - Cross-file duplication (via `measureCrossFileDuplication`): copy-pasted blocks shared between
450
451
  files, matched with the same normalization (exact, gapped, and near-miss clones) and reported as
451
452
  groups with the file location, the matched lines, and the matched token count of each copy
@@ -510,7 +511,7 @@ does not skip generated files (e.g. `.d.ts`, `.min.js`) or test files.
510
511
  ### Cloudflare Workers
511
512
 
512
513
  Workers cannot load native addons, so the package's `workerd` export runs the same API on a
513
- WebAssembly build of the Rust engine (about 2.3 MB gzipped). Wrangler resolves that export
514
+ WebAssembly build of the Rust engine (about 3.5 MB gzipped). Wrangler resolves that export
514
515
  automatically, and no `nodejs_compat` flag is needed:
515
516
 
516
517
  ```ts
@@ -7859,39 +7859,6 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
7859
7859
  IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
7860
7860
  DEALINGS IN THE SOFTWARE.
7861
7861
 
7862
- ====================================================================================================
7863
- tree-sitter-go 0.25.0 — MIT — https://github.com/tree-sitter/tree-sitter-go
7864
- ====================================================================================================
7865
- ----- LICENSE -----
7866
- The MIT License (MIT)
7867
-
7868
- Copyright (c) 2014 Max Brunsfeld
7869
-
7870
- Permission is hereby granted, free of charge, to any person obtaining a copy
7871
- of this software and associated documentation files (the "Software"), to deal
7872
- in the Software without restriction, including without limitation the rights
7873
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7874
- copies of the Software, and to permit persons to whom the Software is
7875
- furnished to do so, subject to the following conditions:
7876
-
7877
- The above copyright notice and this permission notice shall be included in all
7878
- copies or substantial portions of the Software.
7879
-
7880
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
7881
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7882
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
7883
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
7884
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
7885
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
7886
- SOFTWARE.
7887
-
7888
- ====================================================================================================
7889
- tree-sitter-java 0.23.5 — MIT — https://github.com/tree-sitter/tree-sitter-java
7890
- ====================================================================================================
7891
- This crate's published sources bundle no root license file; the upstream notice is:
7892
- Copyright (c) 2017 Ayman Nadeem
7893
- The canonical text of the MIT license is reproduced in the appendix at the end of this document.
7894
-
7895
7862
  ====================================================================================================
7896
7863
  tree-sitter-language 0.1.8 — MIT — https://github.com/tree-sitter/tree-sitter
7897
7864
  ====================================================================================================
@@ -7918,39 +7885,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
7918
7885
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
7919
7886
  SOFTWARE.
7920
7887
 
7921
- ====================================================================================================
7922
- tree-sitter-python 0.25.0 — MIT — https://github.com/tree-sitter/tree-sitter-python
7923
- ====================================================================================================
7924
- ----- LICENSE -----
7925
- The MIT License (MIT)
7926
-
7927
- Copyright (c) 2016 Max Brunsfeld
7928
-
7929
- Permission is hereby granted, free of charge, to any person obtaining a copy
7930
- of this software and associated documentation files (the "Software"), to deal
7931
- in the Software without restriction, including without limitation the rights
7932
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7933
- copies of the Software, and to permit persons to whom the Software is
7934
- furnished to do so, subject to the following conditions:
7935
-
7936
- The above copyright notice and this permission notice shall be included in all
7937
- copies or substantial portions of the Software.
7938
-
7939
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
7940
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7941
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
7942
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
7943
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
7944
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
7945
- SOFTWARE.
7946
-
7947
- ====================================================================================================
7948
- tree-sitter-ruby 0.23.1 — MIT — https://github.com/tree-sitter/tree-sitter-ruby
7949
- ====================================================================================================
7950
- This crate's published sources bundle no root license file; the upstream notice is:
7951
- Copyright (c) 2016 Rob Rix
7952
- The canonical text of the MIT license is reproduced in the appendix at the end of this document.
7953
-
7954
7888
  ====================================================================================================
7955
7889
  unicode-ident 1.0.24 — (MIT OR Apache-2.0) AND Unicode-3.0 — https://github.com/dtolnay/unicode-ident
7956
7890
  ====================================================================================================
@@ -9150,6 +9084,60 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
9150
9084
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
9151
9085
  SOFTWARE.
9152
9086
 
9087
+ ====================================================================================================
9088
+ willbooster-tree-sitter-go 1.0.0 — MIT — https://github.com/WillBooster/tree-sitter-go
9089
+ ====================================================================================================
9090
+ ----- LICENSE -----
9091
+ The MIT License (MIT)
9092
+
9093
+ Copyright (c) 2014 Max Brunsfeld
9094
+ Copyright (c) 2026 WillBooster Inc.
9095
+
9096
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9097
+ of this software and associated documentation files (the "Software"), to deal
9098
+ in the Software without restriction, including without limitation the rights
9099
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9100
+ copies of the Software, and to permit persons to whom the Software is
9101
+ furnished to do so, subject to the following conditions:
9102
+
9103
+ The above copyright notice and this permission notice shall be included in all
9104
+ copies or substantial portions of the Software.
9105
+
9106
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
9107
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
9108
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
9109
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
9110
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
9111
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
9112
+ SOFTWARE.
9113
+
9114
+ ====================================================================================================
9115
+ willbooster-tree-sitter-java 1.0.0 — MIT — https://github.com/WillBooster/tree-sitter-java
9116
+ ====================================================================================================
9117
+ ----- LICENSE -----
9118
+ MIT License
9119
+
9120
+ Copyright (c) 2017 Ayman Nadeem
9121
+ Copyright (c) 2026 WillBooster Inc.
9122
+
9123
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9124
+ of this software and associated documentation files (the "Software"), to deal
9125
+ in the Software without restriction, including without limitation the rights
9126
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9127
+ copies of the Software, and to permit persons to whom the Software is
9128
+ furnished to do so, subject to the following conditions:
9129
+
9130
+ The above copyright notice and this permission notice shall be included in all
9131
+ copies or substantial portions of the Software.
9132
+
9133
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
9134
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
9135
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
9136
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
9137
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
9138
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
9139
+ SOFTWARE.
9140
+
9153
9141
  ====================================================================================================
9154
9142
  willbooster-tree-sitter-javascript 4.0.2 — MIT — https://github.com/WillBooster/tree-sitter-javascript
9155
9143
  ====================================================================================================
@@ -9204,6 +9192,60 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
9204
9192
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
9205
9193
  SOFTWARE.
9206
9194
 
9195
+ ====================================================================================================
9196
+ willbooster-tree-sitter-python 1.0.1 — MIT — https://github.com/WillBooster/tree-sitter-python
9197
+ ====================================================================================================
9198
+ ----- LICENSE -----
9199
+ The MIT License (MIT)
9200
+
9201
+ Copyright (c) 2016 Max Brunsfeld
9202
+ Copyright (c) 2026 WillBooster Inc.
9203
+
9204
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9205
+ of this software and associated documentation files (the "Software"), to deal
9206
+ in the Software without restriction, including without limitation the rights
9207
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9208
+ copies of the Software, and to permit persons to whom the Software is
9209
+ furnished to do so, subject to the following conditions:
9210
+
9211
+ The above copyright notice and this permission notice shall be included in all
9212
+ copies or substantial portions of the Software.
9213
+
9214
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
9215
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
9216
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
9217
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
9218
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
9219
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
9220
+ SOFTWARE.
9221
+
9222
+ ====================================================================================================
9223
+ willbooster-tree-sitter-ruby 1.0.0 — MIT — https://github.com/WillBooster/tree-sitter-ruby
9224
+ ====================================================================================================
9225
+ ----- LICENSE -----
9226
+ The MIT License (MIT)
9227
+
9228
+ Copyright (c) 2016 Rob Rix
9229
+ Copyright (c) 2026 WillBooster Inc.
9230
+
9231
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9232
+ of this software and associated documentation files (the "Software"), to deal
9233
+ in the Software without restriction, including without limitation the rights
9234
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9235
+ copies of the Software, and to permit persons to whom the Software is
9236
+ furnished to do so, subject to the following conditions:
9237
+
9238
+ The above copyright notice and this permission notice shall be included in all
9239
+ copies or substantial portions of the Software.
9240
+
9241
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
9242
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
9243
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
9244
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
9245
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
9246
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
9247
+ SOFTWARE.
9248
+
9207
9249
  ====================================================================================================
9208
9250
  willbooster-tree-sitter-rust 2.0.2 — MIT — https://github.com/WillBooster/tree-sitter-rust
9209
9251
  ====================================================================================================
@@ -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:a,maxGapTokens:o,minSimilarityPercent:s}=e.resolveDuplicationOptions(r),l=t.flatMap(({file:e,candidates:t},n)=>t.map(t=>({...t,regionBucket:n,file:e})));for(let e of u(t,a))l.push(e);let d=n.selectMaximalGroups(l,f,(e,t)=>e.regionBucket-t.regionBucket||e.startIndex-t.startIndex),g=p(t,o),_=m([...d.values()],g,o);return h(i(_,c(t,_,g,a,s),a),t,g)}function i(e,t,n){let r=/* @__PURE__ */ new Map;for(let[t,n]of e.entries())for(let{fileIndex:e}of n){let n=r.get(e)??/* @__PURE__ */ new Set;n.add(t),r.set(e,n)}let i=/* @__PURE__ */ new Set,s=/* @__PURE__ */ new Set,c=t.map(t=>{let c=e=>{let r=t.filter(t=>t.segments.some(t=>e.segments.some(e=>a(e,t)))),[i]=r;if(r.length===1&&i)return Math.max(0,i.startTokenIndex-e.startTokenIndex)+Math.max(0,e.endTokenIndex-i.endTokenIndex)<n?i:void 0},l=/* @__PURE__ */ new Map,u=/* @__PURE__ */ new Set;for(let e of t)for(let t of e.spanCountedElsewhere?r.get(e.fileIndex)??[]:[])u.add(t);for(let t of[...u].toSorted((e,t)=>e-t)){let n=e[t]??[],r=n.map(c);if(!(i.has(t)||r.includes(void 0)||new Set(r).size!==r.length)){i.add(t);for(let[e,t]of r.entries()){let r=n[e];if(t&&r){let e=l.get(t)??[];e.push(r),l.set(t,e)}}}}return t.map(e=>{let t=l.get(e);if(!t)return e;let n=o([e,...t]);return s.add(n),n})});for(let t of s)for(let n of r.get(t.fileIndex)??[])if(!i.has(n)&&e[n]?.some(e=>!e.spanCountedElsewhere&&t.segments.every(t=>e.segments.some(e=>e.startTokenIndex<=t.startTokenIndex&&t.endTokenIndex<=e.endTokenIndex)))){t.spanCountedElsewhere=!0;break}return[...e.filter((e,t)=>!i.has(t)),...c]}function a(e,t){return e.startTokenIndex<t.endTokenIndex&&t.startTokenIndex<e.endTokenIndex}function o(e){let t=s(e.flatMap(e=>e.segments));return{...e[0],spanCountedElsewhere:void 0,nestedInLargerGroup:void 0,segments:t,matchedRuns:s(e.flatMap(e=>e.matchedRuns??e.segments)),exactSegments:s(e.flatMap(e=>e.matchedRuns?e.exactSegments??[]:e.segments)),tokenCount:t.reduce((e,t)=>e+t.endTokenIndex-t.startTokenIndex,0),startTokenIndex:Math.min(...e.map(e=>e.startTokenIndex)),endTokenIndex:Math.max(...e.map(e=>e.endTokenIndex)),startIndex:Math.min(...e.map(e=>e.startIndex)),endIndex:Math.max(...e.map(e=>e.endIndex)),startLine:Math.min(...e.map(e=>e.startLine)),endLine:Math.max(...e.map(e=>e.endLine))}}function s(e){let t=[];for(let n of e.toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex)){let e=t.at(-1);e&&n.startTokenIndex<e.endTokenIndex?e.endTokenIndex=Math.max(e.endTokenIndex,n.endTokenIndex):t.push({...n})}return t}function c(e,n,r,i,a){let o=e.map(()=>[]);for(let{fileIndex:e,segments:t}of n.flat()){let n=r[e]??0;for(let r of t)o[e]?.push(l(r,-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=>l(e,n)),matchedRuns:t.matchedRuns?.map(e=>l(e,n)),startTokenIndex:t.startTokenIndex+n,endTokenIndex:t.endTokenIndex+n}}))}function l({startTokenIndex:e,endTokenIndex:t},n){return{startTokenIndex:e+n,endTokenIndex:t+n}}function u(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 d(e){return new Set(e.map(e=>e.file)).size>=2}function f(e){return e.length>=2&&new Set(e.map(e=>e.regionBucket)).size>=2}function p(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 m(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,d)}function h(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=g(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 g(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=e.collectMatchedLines(t.segments.map(e=>l(e,-i.offset)),t.matchedRuns?.map(e=>l(e,-i.offset)),i.tokens,i.codeLineNumbers,t.exactSegments?.map(e=>l(e,-i.offset)));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:o,minSimilarityPercent:s}=e.resolveDuplicationOptions(r),l=t.flatMap(({file:e,candidates:t},n)=>t.map(t=>({...t,regionBucket:n,file:e})));for(let e of u(t,a))l.push(e);let d=n.selectMaximalGroups(l,f,(e,t)=>e.regionBucket-t.regionBucket||e.startIndex-t.startIndex),g=p(t,o),_=m([...d.values()],g,o);return h(i(_,c(t,_,g,a,s),a),t,g)}function i(e,t,n){let r=/* @__PURE__ */ new Map;for(let[t,n]of e.entries())for(let{fileIndex:e}of n){let n=r.get(e)??/* @__PURE__ */ new Set;n.add(t),r.set(e,n)}let i=/* @__PURE__ */ new Set,s=/* @__PURE__ */ new Set,c=t.map(t=>{let c=e=>{let r=t.filter(t=>t.segments.some(t=>e.segments.some(e=>a(e,t)))),[i]=r;if(r.length===1&&i)return Math.max(0,i.startTokenIndex-e.startTokenIndex)+Math.max(0,e.endTokenIndex-i.endTokenIndex)<n?i:void 0},l=/* @__PURE__ */ new Map,u=/* @__PURE__ */ new Set;for(let e of t)for(let t of e.spanCountedElsewhere?r.get(e.fileIndex)??[]:[])u.add(t);for(let t of[...u].toSorted((e,t)=>e-t)){let n=e[t]??[],r=n.map(c);if(!(i.has(t)||r.includes(void 0)||new Set(r).size!==r.length)){i.add(t);for(let[e,t]of r.entries()){let r=n[e];if(t&&r){let e=l.get(t)??[];e.push(r),l.set(t,e)}}}}return t.map(e=>{let t=l.get(e);if(!t)return e;let n=o([e,...t]);return s.add(n),n})});for(let t of s)for(let n of r.get(t.fileIndex)??[])if(!i.has(n)&&e[n]?.some(e=>!e.spanCountedElsewhere&&t.segments.every(t=>e.segments.some(e=>e.startTokenIndex<=t.startTokenIndex&&t.endTokenIndex<=e.endTokenIndex)))){t.spanCountedElsewhere=!0;break}return[...e.filter((e,t)=>!i.has(t)),...c]}function a(e,t){return e.startTokenIndex<t.endTokenIndex&&t.startTokenIndex<e.endTokenIndex}function o(e){let t=s(e.flatMap(e=>e.segments));return{...e[0],spanCountedElsewhere:void 0,nestedInLargerGroup:void 0,segments:t,matchedRuns:s(e.flatMap(e=>e.matchedRuns??e.segments)),exactSegments:s(e.flatMap(e=>e.matchedRuns?e.exactSegments??[]:e.segments)),tokenCount:t.reduce((e,t)=>e+t.endTokenIndex-t.startTokenIndex,0),startTokenIndex:Math.min(...e.map(e=>e.startTokenIndex)),endTokenIndex:Math.max(...e.map(e=>e.endTokenIndex)),startIndex:Math.min(...e.map(e=>e.startIndex)),endIndex:Math.max(...e.map(e=>e.endIndex)),startLine:Math.min(...e.map(e=>e.startLine)),endLine:Math.max(...e.map(e=>e.endLine))}}function s(e){let t=[];for(let n of e.toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex)){let e=t.at(-1);e&&n.startTokenIndex<e.endTokenIndex?e.endTokenIndex=Math.max(e.endTokenIndex,n.endTokenIndex):t.push({...n})}return t}function c(e,n,r,i,a){let o=t=>{let n=e.map(()=>[]);for(let{fileIndex:e,segments:i}of t.flat()){let t=r[e]??0;for(let r of i)n[e]?.push(l(r,-t))}return n};return t.collectCrossFileNearMissGroups(e,o(n),o(n.filter(e=>!e.some(e=>e.nestedInLargerGroup))),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=>l(e,n)),matchedRuns:t.matchedRuns?.map(e=>l(e,n)),startTokenIndex:t.startTokenIndex+n,endTokenIndex:t.endTokenIndex+n}}))}function l({startTokenIndex:e,endTokenIndex:t},n){return{startTokenIndex:e+n,endTokenIndex:t+n}}function u(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 d(e){return new Set(e.map(e=>e.file)).size>=2}function f(e){return e.length>=2&&new Set(e.map(e=>e.regionBucket)).size>=2}function p(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 m(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,d)}function h(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=g(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 g(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=e.collectMatchedLines(t.segments.map(e=>l(e,-i.offset)),t.matchedRuns?.map(e=>l(e,-i.offset)),i.tokens,i.codeLineNumbers,t.exactSegments?.map(e=>l(e,-i.offset)));for(let e of o)a.add(e);return[...o].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","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 or which a part of it\n * matched exactly reaches (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 exactGroups = mergeGapAdjacentGroups([...counted.values()], tokenOffsets, maxGapTokens);\n const nearMissGroups = collectNearMissGroups(files, exactGroups, tokenOffsets, minTokens, minSimilarityPercent);\n return summarize(takeOverExactGroups(exactGroups, nearMissGroups, minTokens), files, tokenOffsets);\n}\n\n/**\n * Lets a near-miss group take over the exact groups standing for the same copies, as within a\n * file (collect_near_miss_groups in native/src/duplication.rs): every occurrence of such a group\n * overlaps exactly one copy of the near-miss group (one overlapping none reports content the copies\n * do not share, and one reaching into two would make one copy take over lines of the next), exceeds\n * its span by less than `minTokens` (a copy is listed at the size it matched, not at that of a\n * clone larger by a reportable part, which may run into code no copy matched), and no two overlap\n * the same copy (those repeat within the copy, not between the copies).\n * Each copy then becomes one occurrence with the exact fragments it holds, so the copies are\n * listed once.\n */\nfunction takeOverExactGroups(\n exactGroups: CrossFileOccurrence[][],\n nearMissGroups: CrossFileOccurrence[][],\n minTokens: number\n): CrossFileOccurrence[][] {\n const exactGroupIndexesByFile = new Map<number, Set<number>>();\n for (const [groupIndex, group] of exactGroups.entries()) {\n for (const { fileIndex } of group) {\n const indexes = exactGroupIndexesByFile.get(fileIndex) ?? new Set();\n indexes.add(groupIndex);\n exactGroupIndexesByFile.set(fileIndex, indexes);\n }\n }\n const taken = new Set<number>();\n const coalesced = new Set<CrossFileOccurrence>();\n const merged = nearMissGroups.map((copies) => {\n const copyOf = (occurrence: CrossFileOccurrence): CrossFileOccurrence | undefined => {\n const overlapped = copies.filter((copy) =>\n copy.segments.some((segment) => occurrence.segments.some((matched) => overlap(matched, segment)))\n );\n const [copy] = overlapped;\n if (overlapped.length !== 1 || !copy) {\n return undefined;\n }\n const excess =\n Math.max(0, copy.startTokenIndex - occurrence.startTokenIndex) +\n Math.max(0, occurrence.endTokenIndex - copy.endTokenIndex);\n return excess < minTokens ? copy : undefined;\n };\n const fragmentsByCopy = new Map<CrossFileOccurrence, CrossFileOccurrence[]>();\n const candidateIndexes = new Set<number>();\n for (const copy of copies) {\n for (const groupIndex of copy.spanCountedElsewhere ? (exactGroupIndexesByFile.get(copy.fileIndex) ?? []) : []) {\n candidateIndexes.add(groupIndex);\n }\n }\n for (const groupIndex of [...candidateIndexes].toSorted((left, right) => left - right)) {\n const group = exactGroups[groupIndex] ?? [];\n const owners = group.map(copyOf);\n if (taken.has(groupIndex) || owners.includes(undefined) || new Set(owners).size !== owners.length) {\n continue;\n }\n taken.add(groupIndex);\n for (const [index, owner] of owners.entries()) {\n const fragment = group[index];\n if (owner && fragment) {\n const fragments = fragmentsByCopy.get(owner) ?? [];\n fragments.push(fragment);\n fragmentsByCopy.set(owner, fragments);\n }\n }\n }\n return copies.map((copy): CrossFileOccurrence => {\n const fragments = fragmentsByCopy.get(copy);\n if (!fragments) {\n return copy;\n }\n const whole = coalesceOccurrences([copy, ...fragments]);\n coalesced.add(whole);\n return whole;\n });\n });\n // A copy that took its fragments over counts them itself, unless a group left in place still\n // counts all of it: a larger clone whose matched segments enclose those of the copy, which its\n // fragments were nested in. A smaller run left in place inside the copy counts its own repeats,\n // not the copy.\n for (const copy of coalesced) {\n for (const groupIndex of exactGroupIndexesByFile.get(copy.fileIndex) ?? []) {\n const enclosed =\n !taken.has(groupIndex) &&\n exactGroups[groupIndex]?.some(\n (occurrence) =>\n !occurrence.spanCountedElsewhere &&\n copy.segments.every((segment) =>\n occurrence.segments.some(\n (matched) =>\n matched.startTokenIndex <= segment.startTokenIndex && segment.endTokenIndex <= matched.endTokenIndex\n )\n )\n );\n if (enclosed) {\n copy.spanCountedElsewhere = true;\n break;\n }\n }\n }\n return [...exactGroups.filter((_, groupIndex) => !taken.has(groupIndex)), ...merged];\n}\n\nfunction overlap(left: TokenSegment, right: TokenSegment): boolean {\n return left.startTokenIndex < right.endTokenIndex && right.startTokenIndex < left.endTokenIndex;\n}\n\n/** The parts of one copy as one occurrence: an exact part is matched throughout, a near-miss part where its partners match it. */\nfunction coalesceOccurrences(parts: [CrossFileOccurrence, ...CrossFileOccurrence[]]): CrossFileOccurrence {\n const segments = mergeSegments(parts.flatMap((part) => part.segments));\n return {\n ...parts[0],\n spanCountedElsewhere: undefined,\n nestedInLargerGroup: undefined,\n segments,\n matchedRuns: mergeSegments(parts.flatMap((part) => part.matchedRuns ?? part.segments)),\n exactSegments: mergeSegments(\n parts.flatMap((part) => (part.matchedRuns ? (part.exactSegments ?? []) : part.segments))\n ),\n tokenCount: segments.reduce((sum, segment) => sum + segment.endTokenIndex - segment.startTokenIndex, 0),\n startTokenIndex: Math.min(...parts.map((part) => part.startTokenIndex)),\n endTokenIndex: Math.max(...parts.map((part) => part.endTokenIndex)),\n startIndex: Math.min(...parts.map((part) => part.startIndex)),\n endIndex: Math.max(...parts.map((part) => part.endIndex)),\n startLine: Math.min(...parts.map((part) => part.startLine)),\n endLine: Math.max(...parts.map((part) => part.endLine)),\n };\n}\n\n/** The unions of overlapping segments, in position order. */\nfunction mergeSegments(segments: TokenSegment[]): TokenSegment[] {\n const merged: TokenSegment[] = [];\n for (const segment of segments.toSorted((left, right) => left.startTokenIndex - right.startTokenIndex)) {\n const last = merged.at(-1);\n if (last && segment.startTokenIndex < last.endTokenIndex) {\n last.endTokenIndex = Math.max(last.endTokenIndex, segment.endTokenIndex);\n } else {\n merged.push({ ...segment });\n }\n }\n return merged;\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 // Segment by segment: the gap of a gapped clone is not reported content.\n for (const { fileIndex, segments } of exactGroups.flat()) {\n const offset = tokenOffsets[fileIndex] ?? 0;\n for (const segment of segments) {\n reportedSpansByFile[fileIndex]?.push(shift(segment, -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 occurrence.exactSegments?.map((segment) => shift(segment, -fileData.offset))\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 or which a part of it\n * matched exactly reaches (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 exactGroups = mergeGapAdjacentGroups([...counted.values()], tokenOffsets, maxGapTokens);\n const nearMissGroups = collectNearMissGroups(files, exactGroups, tokenOffsets, minTokens, minSimilarityPercent);\n return summarize(takeOverExactGroups(exactGroups, nearMissGroups, minTokens), files, tokenOffsets);\n}\n\n/**\n * Lets a near-miss group take over the exact groups standing for the same copies, as within a\n * file (collect_near_miss_groups in native/src/duplication.rs): every occurrence of such a group\n * overlaps exactly one copy of the near-miss group (one overlapping none reports content the copies\n * do not share, and one reaching into two would make one copy take over lines of the next), exceeds\n * its span by less than `minTokens` (a copy is listed at the size it matched, not at that of a\n * clone larger by a reportable part, which may run into code no copy matched), and no two overlap\n * the same copy (those repeat within the copy, not between the copies).\n * Each copy then becomes one occurrence with the exact fragments it holds, so the copies are\n * listed once.\n */\nfunction takeOverExactGroups(\n exactGroups: CrossFileOccurrence[][],\n nearMissGroups: CrossFileOccurrence[][],\n minTokens: number\n): CrossFileOccurrence[][] {\n const exactGroupIndexesByFile = new Map<number, Set<number>>();\n for (const [groupIndex, group] of exactGroups.entries()) {\n for (const { fileIndex } of group) {\n const indexes = exactGroupIndexesByFile.get(fileIndex) ?? new Set();\n indexes.add(groupIndex);\n exactGroupIndexesByFile.set(fileIndex, indexes);\n }\n }\n const taken = new Set<number>();\n const coalesced = new Set<CrossFileOccurrence>();\n const merged = nearMissGroups.map((copies) => {\n const copyOf = (occurrence: CrossFileOccurrence): CrossFileOccurrence | undefined => {\n const overlapped = copies.filter((copy) =>\n copy.segments.some((segment) => occurrence.segments.some((matched) => overlap(matched, segment)))\n );\n const [copy] = overlapped;\n if (overlapped.length !== 1 || !copy) {\n return undefined;\n }\n const excess =\n Math.max(0, copy.startTokenIndex - occurrence.startTokenIndex) +\n Math.max(0, occurrence.endTokenIndex - copy.endTokenIndex);\n return excess < minTokens ? copy : undefined;\n };\n const fragmentsByCopy = new Map<CrossFileOccurrence, CrossFileOccurrence[]>();\n const candidateIndexes = new Set<number>();\n for (const copy of copies) {\n for (const groupIndex of copy.spanCountedElsewhere ? (exactGroupIndexesByFile.get(copy.fileIndex) ?? []) : []) {\n candidateIndexes.add(groupIndex);\n }\n }\n for (const groupIndex of [...candidateIndexes].toSorted((left, right) => left - right)) {\n const group = exactGroups[groupIndex] ?? [];\n const owners = group.map(copyOf);\n if (taken.has(groupIndex) || owners.includes(undefined) || new Set(owners).size !== owners.length) {\n continue;\n }\n taken.add(groupIndex);\n for (const [index, owner] of owners.entries()) {\n const fragment = group[index];\n if (owner && fragment) {\n const fragments = fragmentsByCopy.get(owner) ?? [];\n fragments.push(fragment);\n fragmentsByCopy.set(owner, fragments);\n }\n }\n }\n return copies.map((copy): CrossFileOccurrence => {\n const fragments = fragmentsByCopy.get(copy);\n if (!fragments) {\n return copy;\n }\n const whole = coalesceOccurrences([copy, ...fragments]);\n coalesced.add(whole);\n return whole;\n });\n });\n // A copy that took its fragments over counts them itself, unless a group left in place still\n // counts all of it: a larger clone whose matched segments enclose those of the copy, which its\n // fragments were nested in. A smaller run left in place inside the copy counts its own repeats,\n // not the copy.\n for (const copy of coalesced) {\n for (const groupIndex of exactGroupIndexesByFile.get(copy.fileIndex) ?? []) {\n const enclosed =\n !taken.has(groupIndex) &&\n exactGroups[groupIndex]?.some(\n (occurrence) =>\n !occurrence.spanCountedElsewhere &&\n copy.segments.every((segment) =>\n occurrence.segments.some(\n (matched) =>\n matched.startTokenIndex <= segment.startTokenIndex && segment.endTokenIndex <= matched.endTokenIndex\n )\n )\n );\n if (enclosed) {\n copy.spanCountedElsewhere = true;\n break;\n }\n }\n }\n return [...exactGroups.filter((_, groupIndex) => !taken.has(groupIndex)), ...merged];\n}\n\nfunction overlap(left: TokenSegment, right: TokenSegment): boolean {\n return left.startTokenIndex < right.endTokenIndex && right.startTokenIndex < left.endTokenIndex;\n}\n\n/** The parts of one copy as one occurrence: an exact part is matched throughout, a near-miss part where its partners match it. */\nfunction coalesceOccurrences(parts: [CrossFileOccurrence, ...CrossFileOccurrence[]]): CrossFileOccurrence {\n const segments = mergeSegments(parts.flatMap((part) => part.segments));\n return {\n ...parts[0],\n spanCountedElsewhere: undefined,\n nestedInLargerGroup: undefined,\n segments,\n matchedRuns: mergeSegments(parts.flatMap((part) => part.matchedRuns ?? part.segments)),\n exactSegments: mergeSegments(\n parts.flatMap((part) => (part.matchedRuns ? (part.exactSegments ?? []) : part.segments))\n ),\n tokenCount: segments.reduce((sum, segment) => sum + segment.endTokenIndex - segment.startTokenIndex, 0),\n startTokenIndex: Math.min(...parts.map((part) => part.startTokenIndex)),\n endTokenIndex: Math.max(...parts.map((part) => part.endTokenIndex)),\n startIndex: Math.min(...parts.map((part) => part.startIndex)),\n endIndex: Math.max(...parts.map((part) => part.endIndex)),\n startLine: Math.min(...parts.map((part) => part.startLine)),\n endLine: Math.max(...parts.map((part) => part.endLine)),\n };\n}\n\n/** The unions of overlapping segments, in position order. */\nfunction mergeSegments(segments: TokenSegment[]): TokenSegment[] {\n const merged: TokenSegment[] = [];\n for (const segment of segments.toSorted((left, right) => left.startTokenIndex - right.startTokenIndex)) {\n const last = merged.at(-1);\n if (last && segment.startTokenIndex < last.endTokenIndex) {\n last.endTokenIndex = Math.max(last.endTokenIndex, segment.endTokenIndex);\n } else {\n merged.push({ ...segment });\n }\n }\n return merged;\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 // Segment by segment: the gap of a gapped clone is not reported content.\n const spansByFile = (groups: CrossFileOccurrence[][]): TokenSegment[][] => {\n const spans: TokenSegment[][] = files.map(() => []);\n for (const { fileIndex, segments } of groups.flat()) {\n const offset = tokenOffsets[fileIndex] ?? 0;\n for (const segment of segments) {\n spans[fileIndex]?.push(shift(segment, -offset));\n }\n }\n return spans;\n };\n return collectCrossFileNearMissGroups(\n files,\n spansByFile(exactGroups),\n spansByFile(exactGroups.filter((group) => !group.some((occurrence) => occurrence.nestedInLargerGroup))),\n minTokens,\n minSimilarityPercent\n ).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 occurrence.exactSegments?.map((segment) => shift(segment, -fileData.offset))\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,2 +1,2 @@
1
- import{buildLiteralCountPrefix as e,collectMatchedLines as t,collectSequenceWindowCandidates as n,countRedundantFragments as r,mergeAdjacentGroups as i,resolveDuplicationOptions as a}from"./duplication.js";import{collectCrossFileNearMissGroups as o}from"./crossFileNearMiss.js";import{selectMaximalGroups as s}from"./duplicateSelection.js";function c(e,t){let{minTokens:n,maxGapTokens:r,minSimilarityPercent:i}=a(t),o=e.flatMap(({file:e,candidates:t},n)=>t.map(t=>({...t,regionBucket:n,file:e})));for(let t of h(e,n))o.push(t);let c=s(o,_,(e,t)=>e.regionBucket-t.regionBucket||e.startIndex-t.startIndex),u=v(e,r),d=y([...c.values()],u,r);return b(l(d,p(e,d,u,n,i),n),e,u)}function l(e,t,n){let r=/* @__PURE__ */ new Map;for(let[t,n]of e.entries())for(let{fileIndex:e}of n){let n=r.get(e)??/* @__PURE__ */ new Set;n.add(t),r.set(e,n)}let i=/* @__PURE__ */ new Set,a=/* @__PURE__ */ new Set,o=t.map(t=>{let o=e=>{let r=t.filter(t=>t.segments.some(t=>e.segments.some(e=>u(e,t)))),[i]=r;if(r.length===1&&i)return Math.max(0,i.startTokenIndex-e.startTokenIndex)+Math.max(0,e.endTokenIndex-i.endTokenIndex)<n?i:void 0},s=/* @__PURE__ */ new Map,c=/* @__PURE__ */ new Set;for(let e of t)for(let t of e.spanCountedElsewhere?r.get(e.fileIndex)??[]:[])c.add(t);for(let t of[...c].toSorted((e,t)=>e-t)){let n=e[t]??[],r=n.map(o);if(!(i.has(t)||r.includes(void 0)||new Set(r).size!==r.length)){i.add(t);for(let[e,t]of r.entries()){let r=n[e];if(t&&r){let e=s.get(t)??[];e.push(r),s.set(t,e)}}}}return t.map(e=>{let t=s.get(e);if(!t)return e;let n=d([e,...t]);return a.add(n),n})});for(let t of a)for(let n of r.get(t.fileIndex)??[])if(!i.has(n)&&e[n]?.some(e=>!e.spanCountedElsewhere&&t.segments.every(t=>e.segments.some(e=>e.startTokenIndex<=t.startTokenIndex&&t.endTokenIndex<=e.endTokenIndex)))){t.spanCountedElsewhere=!0;break}return[...e.filter((e,t)=>!i.has(t)),...o]}function u(e,t){return e.startTokenIndex<t.endTokenIndex&&t.startTokenIndex<e.endTokenIndex}function d(e){let t=f(e.flatMap(e=>e.segments));return{...e[0],spanCountedElsewhere:void 0,nestedInLargerGroup:void 0,segments:t,matchedRuns:f(e.flatMap(e=>e.matchedRuns??e.segments)),exactSegments:f(e.flatMap(e=>e.matchedRuns?e.exactSegments??[]:e.segments)),tokenCount:t.reduce((e,t)=>e+t.endTokenIndex-t.startTokenIndex,0),startTokenIndex:Math.min(...e.map(e=>e.startTokenIndex)),endTokenIndex:Math.max(...e.map(e=>e.endTokenIndex)),startIndex:Math.min(...e.map(e=>e.startIndex)),endIndex:Math.max(...e.map(e=>e.endIndex)),startLine:Math.min(...e.map(e=>e.startLine)),endLine:Math.max(...e.map(e=>e.endLine))}}function f(e){let t=[];for(let n of e.toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex)){let e=t.at(-1);e&&n.startTokenIndex<e.endTokenIndex?e.endTokenIndex=Math.max(e.endTokenIndex,n.endTokenIndex):t.push({...n})}return t}function p(e,t,n,r,i){let a=e.map(()=>[]);for(let{fileIndex:e,segments:r}of t.flat()){let t=n[e]??0;for(let n of r)a[e]?.push(m(n,-t))}return o(e,a,r,i).map(t=>t.map(t=>{let r=n[t.fileIndex]??0;return{...t,file:e[t.fileIndex]?.file??``,segments:t.segments.map(e=>m(e,r)),matchedRuns:t.matchedRuns?.map(e=>m(e,r)),startTokenIndex:t.startTokenIndex+r,endTokenIndex:t.endTokenIndex+r}}))}function m({startTokenIndex:e,endTokenIndex:t},n){return{startTokenIndex:e+n,endTokenIndex:t+n}}function h(t,r){let i=[],a=[];for(let[n,{tokens:r,containerStatements:o}]of t.entries())r&&o&&(i.push(n),a.push({tokens:r,literalCountPrefix:e(r),containers:o}));return a.length<2?[]:n(a,r,!0).flatMap(({candidate:e,contextIndex:n})=>{let r=i[n],a=r===void 0?void 0:t[r];return r===void 0||a===void 0?[]:[{...e,regionBucket:r,file:a.file}]})}function g(e){return new Set(e.map(e=>e.file)).size>=2}function _(e){return e.length>=2&&new Set(e.map(e=>e.regionBucket)).size>=2}function v(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 y(e,t,n){let r=e.map(e=>e.map(e=>{let n=e.startTokenIndex+(t[e.regionBucket]??0),r=e.endTokenIndex+(t[e.regionBucket]??0);return{file:e.file,fileIndex:e.regionBucket,spanCountedElsewhere:e.nestedInLargerGroup,nestedInLargerGroup:e.nestedInLargerGroup,segments:[{startTokenIndex:n,endTokenIndex:r}],tokenCount:e.tokenCount,startTokenIndex:n,endTokenIndex:r,startIndex:e.startIndex,endIndex:e.endIndex,startLine:e.startLine,endLine:e.endLine}}).toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex));return i(r,n,g)}function b(e,t,n){let i=[],a=/* @__PURE__ */ new Map,o=new Map(t.map((e,t)=>[e.file,{tokens:e.tokens,codeLineNumbers:e.codeLineNumbers,offset:n[t]??0}])),s=/* @__PURE__ */ new Map,c=0;for(let t of e){c+=r(t);let e=t.map(e=>{let{file:t,startLine:n,endLine:r,tokenCount:i}=e,a=x(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),n=[...new Set(e.map(({file:e})=>e))];for(let e of n)a.set(e,(a.get(e)??0)+1);i.push({files:n,occurrences:e,tokenCount:Math.min(...t.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 x(e,n,r){let i=n.get(e.file);if(!i?.tokens)return;let a=r.get(e.file);a||(a=/* @__PURE__ */ new Set,r.set(e.file,a));let o=t(e.segments.map(e=>m(e,-i.offset)),e.matchedRuns?.map(e=>m(e,-i.offset)),i.tokens,i.codeLineNumbers,e.exactSegments?.map(e=>m(e,-i.offset)));for(let e of o)a.add(e);return[...o].toSorted((e,t)=>e-t)}export{c as measureCrossFileDuplication};
1
+ import{buildLiteralCountPrefix as e,collectMatchedLines as t,collectSequenceWindowCandidates as n,countRedundantFragments as r,mergeAdjacentGroups as i,resolveDuplicationOptions as a}from"./duplication.js";import{collectCrossFileNearMissGroups as o}from"./crossFileNearMiss.js";import{selectMaximalGroups as s}from"./duplicateSelection.js";function c(e,t){let{minTokens:n,maxGapTokens:r,minSimilarityPercent:i}=a(t),o=e.flatMap(({file:e,candidates:t},n)=>t.map(t=>({...t,regionBucket:n,file:e})));for(let t of h(e,n))o.push(t);let c=s(o,_,(e,t)=>e.regionBucket-t.regionBucket||e.startIndex-t.startIndex),u=v(e,r),d=y([...c.values()],u,r);return b(l(d,p(e,d,u,n,i),n),e,u)}function l(e,t,n){let r=/* @__PURE__ */ new Map;for(let[t,n]of e.entries())for(let{fileIndex:e}of n){let n=r.get(e)??/* @__PURE__ */ new Set;n.add(t),r.set(e,n)}let i=/* @__PURE__ */ new Set,a=/* @__PURE__ */ new Set,o=t.map(t=>{let o=e=>{let r=t.filter(t=>t.segments.some(t=>e.segments.some(e=>u(e,t)))),[i]=r;if(r.length===1&&i)return Math.max(0,i.startTokenIndex-e.startTokenIndex)+Math.max(0,e.endTokenIndex-i.endTokenIndex)<n?i:void 0},s=/* @__PURE__ */ new Map,c=/* @__PURE__ */ new Set;for(let e of t)for(let t of e.spanCountedElsewhere?r.get(e.fileIndex)??[]:[])c.add(t);for(let t of[...c].toSorted((e,t)=>e-t)){let n=e[t]??[],r=n.map(o);if(!(i.has(t)||r.includes(void 0)||new Set(r).size!==r.length)){i.add(t);for(let[e,t]of r.entries()){let r=n[e];if(t&&r){let e=s.get(t)??[];e.push(r),s.set(t,e)}}}}return t.map(e=>{let t=s.get(e);if(!t)return e;let n=d([e,...t]);return a.add(n),n})});for(let t of a)for(let n of r.get(t.fileIndex)??[])if(!i.has(n)&&e[n]?.some(e=>!e.spanCountedElsewhere&&t.segments.every(t=>e.segments.some(e=>e.startTokenIndex<=t.startTokenIndex&&t.endTokenIndex<=e.endTokenIndex)))){t.spanCountedElsewhere=!0;break}return[...e.filter((e,t)=>!i.has(t)),...o]}function u(e,t){return e.startTokenIndex<t.endTokenIndex&&t.startTokenIndex<e.endTokenIndex}function d(e){let t=f(e.flatMap(e=>e.segments));return{...e[0],spanCountedElsewhere:void 0,nestedInLargerGroup:void 0,segments:t,matchedRuns:f(e.flatMap(e=>e.matchedRuns??e.segments)),exactSegments:f(e.flatMap(e=>e.matchedRuns?e.exactSegments??[]:e.segments)),tokenCount:t.reduce((e,t)=>e+t.endTokenIndex-t.startTokenIndex,0),startTokenIndex:Math.min(...e.map(e=>e.startTokenIndex)),endTokenIndex:Math.max(...e.map(e=>e.endTokenIndex)),startIndex:Math.min(...e.map(e=>e.startIndex)),endIndex:Math.max(...e.map(e=>e.endIndex)),startLine:Math.min(...e.map(e=>e.startLine)),endLine:Math.max(...e.map(e=>e.endLine))}}function f(e){let t=[];for(let n of e.toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex)){let e=t.at(-1);e&&n.startTokenIndex<e.endTokenIndex?e.endTokenIndex=Math.max(e.endTokenIndex,n.endTokenIndex):t.push({...n})}return t}function p(e,t,n,r,i){let a=t=>{let r=e.map(()=>[]);for(let{fileIndex:e,segments:i}of t.flat()){let t=n[e]??0;for(let n of i)r[e]?.push(m(n,-t))}return r};return o(e,a(t),a(t.filter(e=>!e.some(e=>e.nestedInLargerGroup))),r,i).map(t=>t.map(t=>{let r=n[t.fileIndex]??0;return{...t,file:e[t.fileIndex]?.file??``,segments:t.segments.map(e=>m(e,r)),matchedRuns:t.matchedRuns?.map(e=>m(e,r)),startTokenIndex:t.startTokenIndex+r,endTokenIndex:t.endTokenIndex+r}}))}function m({startTokenIndex:e,endTokenIndex:t},n){return{startTokenIndex:e+n,endTokenIndex:t+n}}function h(t,r){let i=[],a=[];for(let[n,{tokens:r,containerStatements:o}]of t.entries())r&&o&&(i.push(n),a.push({tokens:r,literalCountPrefix:e(r),containers:o}));return a.length<2?[]:n(a,r,!0).flatMap(({candidate:e,contextIndex:n})=>{let r=i[n],a=r===void 0?void 0:t[r];return r===void 0||a===void 0?[]:[{...e,regionBucket:r,file:a.file}]})}function g(e){return new Set(e.map(e=>e.file)).size>=2}function _(e){return e.length>=2&&new Set(e.map(e=>e.regionBucket)).size>=2}function v(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 y(e,t,n){let r=e.map(e=>e.map(e=>{let n=e.startTokenIndex+(t[e.regionBucket]??0),r=e.endTokenIndex+(t[e.regionBucket]??0);return{file:e.file,fileIndex:e.regionBucket,spanCountedElsewhere:e.nestedInLargerGroup,nestedInLargerGroup:e.nestedInLargerGroup,segments:[{startTokenIndex:n,endTokenIndex:r}],tokenCount:e.tokenCount,startTokenIndex:n,endTokenIndex:r,startIndex:e.startIndex,endIndex:e.endIndex,startLine:e.startLine,endLine:e.endLine}}).toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex));return i(r,n,g)}function b(e,t,n){let i=[],a=/* @__PURE__ */ new Map,o=new Map(t.map((e,t)=>[e.file,{tokens:e.tokens,codeLineNumbers:e.codeLineNumbers,offset:n[t]??0}])),s=/* @__PURE__ */ new Map,c=0;for(let t of e){c+=r(t);let e=t.map(e=>{let{file:t,startLine:n,endLine:r,tokenCount:i}=e,a=x(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),n=[...new Set(e.map(({file:e})=>e))];for(let e of n)a.set(e,(a.get(e)??0)+1);i.push({files:n,occurrences:e,tokenCount:Math.min(...t.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 x(e,n,r){let i=n.get(e.file);if(!i?.tokens)return;let a=r.get(e.file);a||(a=/* @__PURE__ */ new Set,r.set(e.file,a));let o=t(e.segments.map(e=>m(e,-i.offset)),e.matchedRuns?.map(e=>m(e,-i.offset)),i.tokens,i.codeLineNumbers,e.exactSegments?.map(e=>m(e,-i.offset)));for(let e of o)a.add(e);return[...o].toSorted((e,t)=>e-t)}export{c as measureCrossFileDuplication};
2
2
  //# sourceMappingURL=crossFileDuplication.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"crossFileDuplication.js","names":[],"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 or which a part of it\n * matched exactly reaches (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 exactGroups = mergeGapAdjacentGroups([...counted.values()], tokenOffsets, maxGapTokens);\n const nearMissGroups = collectNearMissGroups(files, exactGroups, tokenOffsets, minTokens, minSimilarityPercent);\n return summarize(takeOverExactGroups(exactGroups, nearMissGroups, minTokens), files, tokenOffsets);\n}\n\n/**\n * Lets a near-miss group take over the exact groups standing for the same copies, as within a\n * file (collect_near_miss_groups in native/src/duplication.rs): every occurrence of such a group\n * overlaps exactly one copy of the near-miss group (one overlapping none reports content the copies\n * do not share, and one reaching into two would make one copy take over lines of the next), exceeds\n * its span by less than `minTokens` (a copy is listed at the size it matched, not at that of a\n * clone larger by a reportable part, which may run into code no copy matched), and no two overlap\n * the same copy (those repeat within the copy, not between the copies).\n * Each copy then becomes one occurrence with the exact fragments it holds, so the copies are\n * listed once.\n */\nfunction takeOverExactGroups(\n exactGroups: CrossFileOccurrence[][],\n nearMissGroups: CrossFileOccurrence[][],\n minTokens: number\n): CrossFileOccurrence[][] {\n const exactGroupIndexesByFile = new Map<number, Set<number>>();\n for (const [groupIndex, group] of exactGroups.entries()) {\n for (const { fileIndex } of group) {\n const indexes = exactGroupIndexesByFile.get(fileIndex) ?? new Set();\n indexes.add(groupIndex);\n exactGroupIndexesByFile.set(fileIndex, indexes);\n }\n }\n const taken = new Set<number>();\n const coalesced = new Set<CrossFileOccurrence>();\n const merged = nearMissGroups.map((copies) => {\n const copyOf = (occurrence: CrossFileOccurrence): CrossFileOccurrence | undefined => {\n const overlapped = copies.filter((copy) =>\n copy.segments.some((segment) => occurrence.segments.some((matched) => overlap(matched, segment)))\n );\n const [copy] = overlapped;\n if (overlapped.length !== 1 || !copy) {\n return undefined;\n }\n const excess =\n Math.max(0, copy.startTokenIndex - occurrence.startTokenIndex) +\n Math.max(0, occurrence.endTokenIndex - copy.endTokenIndex);\n return excess < minTokens ? copy : undefined;\n };\n const fragmentsByCopy = new Map<CrossFileOccurrence, CrossFileOccurrence[]>();\n const candidateIndexes = new Set<number>();\n for (const copy of copies) {\n for (const groupIndex of copy.spanCountedElsewhere ? (exactGroupIndexesByFile.get(copy.fileIndex) ?? []) : []) {\n candidateIndexes.add(groupIndex);\n }\n }\n for (const groupIndex of [...candidateIndexes].toSorted((left, right) => left - right)) {\n const group = exactGroups[groupIndex] ?? [];\n const owners = group.map(copyOf);\n if (taken.has(groupIndex) || owners.includes(undefined) || new Set(owners).size !== owners.length) {\n continue;\n }\n taken.add(groupIndex);\n for (const [index, owner] of owners.entries()) {\n const fragment = group[index];\n if (owner && fragment) {\n const fragments = fragmentsByCopy.get(owner) ?? [];\n fragments.push(fragment);\n fragmentsByCopy.set(owner, fragments);\n }\n }\n }\n return copies.map((copy): CrossFileOccurrence => {\n const fragments = fragmentsByCopy.get(copy);\n if (!fragments) {\n return copy;\n }\n const whole = coalesceOccurrences([copy, ...fragments]);\n coalesced.add(whole);\n return whole;\n });\n });\n // A copy that took its fragments over counts them itself, unless a group left in place still\n // counts all of it: a larger clone whose matched segments enclose those of the copy, which its\n // fragments were nested in. A smaller run left in place inside the copy counts its own repeats,\n // not the copy.\n for (const copy of coalesced) {\n for (const groupIndex of exactGroupIndexesByFile.get(copy.fileIndex) ?? []) {\n const enclosed =\n !taken.has(groupIndex) &&\n exactGroups[groupIndex]?.some(\n (occurrence) =>\n !occurrence.spanCountedElsewhere &&\n copy.segments.every((segment) =>\n occurrence.segments.some(\n (matched) =>\n matched.startTokenIndex <= segment.startTokenIndex && segment.endTokenIndex <= matched.endTokenIndex\n )\n )\n );\n if (enclosed) {\n copy.spanCountedElsewhere = true;\n break;\n }\n }\n }\n return [...exactGroups.filter((_, groupIndex) => !taken.has(groupIndex)), ...merged];\n}\n\nfunction overlap(left: TokenSegment, right: TokenSegment): boolean {\n return left.startTokenIndex < right.endTokenIndex && right.startTokenIndex < left.endTokenIndex;\n}\n\n/** The parts of one copy as one occurrence: an exact part is matched throughout, a near-miss part where its partners match it. */\nfunction coalesceOccurrences(parts: [CrossFileOccurrence, ...CrossFileOccurrence[]]): CrossFileOccurrence {\n const segments = mergeSegments(parts.flatMap((part) => part.segments));\n return {\n ...parts[0],\n spanCountedElsewhere: undefined,\n nestedInLargerGroup: undefined,\n segments,\n matchedRuns: mergeSegments(parts.flatMap((part) => part.matchedRuns ?? part.segments)),\n exactSegments: mergeSegments(\n parts.flatMap((part) => (part.matchedRuns ? (part.exactSegments ?? []) : part.segments))\n ),\n tokenCount: segments.reduce((sum, segment) => sum + segment.endTokenIndex - segment.startTokenIndex, 0),\n startTokenIndex: Math.min(...parts.map((part) => part.startTokenIndex)),\n endTokenIndex: Math.max(...parts.map((part) => part.endTokenIndex)),\n startIndex: Math.min(...parts.map((part) => part.startIndex)),\n endIndex: Math.max(...parts.map((part) => part.endIndex)),\n startLine: Math.min(...parts.map((part) => part.startLine)),\n endLine: Math.max(...parts.map((part) => part.endLine)),\n };\n}\n\n/** The unions of overlapping segments, in position order. */\nfunction mergeSegments(segments: TokenSegment[]): TokenSegment[] {\n const merged: TokenSegment[] = [];\n for (const segment of segments.toSorted((left, right) => left.startTokenIndex - right.startTokenIndex)) {\n const last = merged.at(-1);\n if (last && segment.startTokenIndex < last.endTokenIndex) {\n last.endTokenIndex = Math.max(last.endTokenIndex, segment.endTokenIndex);\n } else {\n merged.push({ ...segment });\n }\n }\n return merged;\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 // Segment by segment: the gap of a gapped clone is not reported content.\n for (const { fileIndex, segments } of exactGroups.flat()) {\n const offset = tokenOffsets[fileIndex] ?? 0;\n for (const segment of segments) {\n reportedSpansByFile[fileIndex]?.push(shift(segment, -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 occurrence.exactSegments?.map((segment) => shift(segment, -fileData.offset))\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.js","names":[],"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 or which a part of it\n * matched exactly reaches (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 exactGroups = mergeGapAdjacentGroups([...counted.values()], tokenOffsets, maxGapTokens);\n const nearMissGroups = collectNearMissGroups(files, exactGroups, tokenOffsets, minTokens, minSimilarityPercent);\n return summarize(takeOverExactGroups(exactGroups, nearMissGroups, minTokens), files, tokenOffsets);\n}\n\n/**\n * Lets a near-miss group take over the exact groups standing for the same copies, as within a\n * file (collect_near_miss_groups in native/src/duplication.rs): every occurrence of such a group\n * overlaps exactly one copy of the near-miss group (one overlapping none reports content the copies\n * do not share, and one reaching into two would make one copy take over lines of the next), exceeds\n * its span by less than `minTokens` (a copy is listed at the size it matched, not at that of a\n * clone larger by a reportable part, which may run into code no copy matched), and no two overlap\n * the same copy (those repeat within the copy, not between the copies).\n * Each copy then becomes one occurrence with the exact fragments it holds, so the copies are\n * listed once.\n */\nfunction takeOverExactGroups(\n exactGroups: CrossFileOccurrence[][],\n nearMissGroups: CrossFileOccurrence[][],\n minTokens: number\n): CrossFileOccurrence[][] {\n const exactGroupIndexesByFile = new Map<number, Set<number>>();\n for (const [groupIndex, group] of exactGroups.entries()) {\n for (const { fileIndex } of group) {\n const indexes = exactGroupIndexesByFile.get(fileIndex) ?? new Set();\n indexes.add(groupIndex);\n exactGroupIndexesByFile.set(fileIndex, indexes);\n }\n }\n const taken = new Set<number>();\n const coalesced = new Set<CrossFileOccurrence>();\n const merged = nearMissGroups.map((copies) => {\n const copyOf = (occurrence: CrossFileOccurrence): CrossFileOccurrence | undefined => {\n const overlapped = copies.filter((copy) =>\n copy.segments.some((segment) => occurrence.segments.some((matched) => overlap(matched, segment)))\n );\n const [copy] = overlapped;\n if (overlapped.length !== 1 || !copy) {\n return undefined;\n }\n const excess =\n Math.max(0, copy.startTokenIndex - occurrence.startTokenIndex) +\n Math.max(0, occurrence.endTokenIndex - copy.endTokenIndex);\n return excess < minTokens ? copy : undefined;\n };\n const fragmentsByCopy = new Map<CrossFileOccurrence, CrossFileOccurrence[]>();\n const candidateIndexes = new Set<number>();\n for (const copy of copies) {\n for (const groupIndex of copy.spanCountedElsewhere ? (exactGroupIndexesByFile.get(copy.fileIndex) ?? []) : []) {\n candidateIndexes.add(groupIndex);\n }\n }\n for (const groupIndex of [...candidateIndexes].toSorted((left, right) => left - right)) {\n const group = exactGroups[groupIndex] ?? [];\n const owners = group.map(copyOf);\n if (taken.has(groupIndex) || owners.includes(undefined) || new Set(owners).size !== owners.length) {\n continue;\n }\n taken.add(groupIndex);\n for (const [index, owner] of owners.entries()) {\n const fragment = group[index];\n if (owner && fragment) {\n const fragments = fragmentsByCopy.get(owner) ?? [];\n fragments.push(fragment);\n fragmentsByCopy.set(owner, fragments);\n }\n }\n }\n return copies.map((copy): CrossFileOccurrence => {\n const fragments = fragmentsByCopy.get(copy);\n if (!fragments) {\n return copy;\n }\n const whole = coalesceOccurrences([copy, ...fragments]);\n coalesced.add(whole);\n return whole;\n });\n });\n // A copy that took its fragments over counts them itself, unless a group left in place still\n // counts all of it: a larger clone whose matched segments enclose those of the copy, which its\n // fragments were nested in. A smaller run left in place inside the copy counts its own repeats,\n // not the copy.\n for (const copy of coalesced) {\n for (const groupIndex of exactGroupIndexesByFile.get(copy.fileIndex) ?? []) {\n const enclosed =\n !taken.has(groupIndex) &&\n exactGroups[groupIndex]?.some(\n (occurrence) =>\n !occurrence.spanCountedElsewhere &&\n copy.segments.every((segment) =>\n occurrence.segments.some(\n (matched) =>\n matched.startTokenIndex <= segment.startTokenIndex && segment.endTokenIndex <= matched.endTokenIndex\n )\n )\n );\n if (enclosed) {\n copy.spanCountedElsewhere = true;\n break;\n }\n }\n }\n return [...exactGroups.filter((_, groupIndex) => !taken.has(groupIndex)), ...merged];\n}\n\nfunction overlap(left: TokenSegment, right: TokenSegment): boolean {\n return left.startTokenIndex < right.endTokenIndex && right.startTokenIndex < left.endTokenIndex;\n}\n\n/** The parts of one copy as one occurrence: an exact part is matched throughout, a near-miss part where its partners match it. */\nfunction coalesceOccurrences(parts: [CrossFileOccurrence, ...CrossFileOccurrence[]]): CrossFileOccurrence {\n const segments = mergeSegments(parts.flatMap((part) => part.segments));\n return {\n ...parts[0],\n spanCountedElsewhere: undefined,\n nestedInLargerGroup: undefined,\n segments,\n matchedRuns: mergeSegments(parts.flatMap((part) => part.matchedRuns ?? part.segments)),\n exactSegments: mergeSegments(\n parts.flatMap((part) => (part.matchedRuns ? (part.exactSegments ?? []) : part.segments))\n ),\n tokenCount: segments.reduce((sum, segment) => sum + segment.endTokenIndex - segment.startTokenIndex, 0),\n startTokenIndex: Math.min(...parts.map((part) => part.startTokenIndex)),\n endTokenIndex: Math.max(...parts.map((part) => part.endTokenIndex)),\n startIndex: Math.min(...parts.map((part) => part.startIndex)),\n endIndex: Math.max(...parts.map((part) => part.endIndex)),\n startLine: Math.min(...parts.map((part) => part.startLine)),\n endLine: Math.max(...parts.map((part) => part.endLine)),\n };\n}\n\n/** The unions of overlapping segments, in position order. */\nfunction mergeSegments(segments: TokenSegment[]): TokenSegment[] {\n const merged: TokenSegment[] = [];\n for (const segment of segments.toSorted((left, right) => left.startTokenIndex - right.startTokenIndex)) {\n const last = merged.at(-1);\n if (last && segment.startTokenIndex < last.endTokenIndex) {\n last.endTokenIndex = Math.max(last.endTokenIndex, segment.endTokenIndex);\n } else {\n merged.push({ ...segment });\n }\n }\n return merged;\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 // Segment by segment: the gap of a gapped clone is not reported content.\n const spansByFile = (groups: CrossFileOccurrence[][]): TokenSegment[][] => {\n const spans: TokenSegment[][] = files.map(() => []);\n for (const { fileIndex, segments } of groups.flat()) {\n const offset = tokenOffsets[fileIndex] ?? 0;\n for (const segment of segments) {\n spans[fileIndex]?.push(shift(segment, -offset));\n }\n }\n return spans;\n };\n return collectCrossFileNearMissGroups(\n files,\n spansByFile(exactGroups),\n spansByFile(exactGroups.filter((group) => !group.some((occurrence) => occurrence.nestedInLargerGroup))),\n minTokens,\n minSimilarityPercent\n ).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 occurrence.exactSegments?.map((segment) => shift(segment, -fileData.offset))\n );\n for (const line of lines) {\n fileLines.add(line);\n }\n return [...lines].toSorted((left, right) => left - right);\n}\n"],"mappings":"oVA+FA,SAAgB,EACd,EACA,EAC6B,CAC7B,GAAM,CAAE,YAAW,eAAc,wBAAyB,EAA0B,CAAO,EACrF,EAAoC,EAAM,SAAS,CAAE,OAAM,cAAc,IAC7E,EAAW,IAAK,IAAe,CAAE,GAAG,EAAW,aAAc,EAAW,MAAK,EAAE,CACjF,EAGA,IAAK,IAAM,KAAa,EAAwB,EAAO,CAAS,EAC9D,EAAW,KAAK,CAAS,EAE3B,IAAM,EAAU,EACd,EACA,GAEC,EAAM,IAAU,EAAK,aAAe,EAAM,cAAgB,EAAK,WAAa,EAAM,UACrF,EACM,EAAe,EAAoB,EAAO,CAAY,EACtD,EAAc,EAAuB,CAAC,GAAG,EAAQ,OAAO,CAAC,EAAG,EAAc,CAAY,EAE5F,OAAO,EAAU,EAAoB,EADd,EAAsB,EAAO,EAAa,EAAc,EAAW,CAC3B,EAAG,CAAS,EAAG,EAAO,CAAY,CACnG,CAaA,SAAS,EACP,EACA,EACA,EACyB,CACzB,IAAM,kBAA0B,IAAI,IACpC,IAAK,GAAM,CAAC,EAAY,KAAU,EAAY,QAAQ,EACpD,IAAK,GAAM,CAAE,eAAe,EAAO,CACjC,IAAM,EAAU,EAAwB,IAAI,CAAS,mBAAK,IAAI,IAC9D,EAAQ,IAAI,CAAU,EACtB,EAAwB,IAAI,EAAW,CAAO,CAChD,CAEF,IAAM,kBAAQ,IAAI,IACZ,kBAAY,IAAI,IAChB,EAAS,EAAe,IAAK,GAAW,CAC5C,IAAM,EAAU,GAAqE,CACnF,IAAM,EAAa,EAAO,OAAQ,GAChC,EAAK,SAAS,KAAM,GAAY,EAAW,SAAS,KAAM,GAAY,EAAQ,EAAS,CAAO,CAAC,CAAC,CAClG,EACM,CAAC,GAAQ,EACX,KAAW,SAAW,GAAM,EAMhC,OAFE,KAAK,IAAI,EAAG,EAAK,gBAAkB,EAAW,eAAe,EAC7D,KAAK,IAAI,EAAG,EAAW,cAAgB,EAAK,aAAa,EAC3C,EAAY,EAAO,IAAA,EACrC,EACM,kBAAkB,IAAI,IACtB,kBAAmB,IAAI,IAC7B,IAAK,IAAM,KAAQ,EACjB,IAAK,IAAM,KAAc,EAAK,qBAAwB,EAAwB,IAAI,EAAK,SAAS,GAAK,CAAC,EAAK,CAAC,EAC1G,EAAiB,IAAI,CAAU,EAGnC,IAAK,IAAM,IAAc,CAAC,GAAG,CAAgB,CAAC,CAAC,UAAU,EAAM,IAAU,EAAO,CAAK,EAAG,CACtF,IAAM,EAAQ,EAAY,IAAe,CAAC,EACpC,EAAS,EAAM,IAAI,CAAM,EAC3B,OAAM,IAAI,CAAU,GAAK,EAAO,SAAS,IAAA,EAAS,GAAK,IAAI,IAAI,CAAM,CAAC,CAAC,OAAS,EAAO,QAG3F,GAAM,IAAI,CAAU,EACpB,IAAK,GAAM,CAAC,EAAO,KAAU,EAAO,QAAQ,EAAG,CAC7C,IAAM,EAAW,EAAM,GACvB,GAAI,GAAS,EAAU,CACrB,IAAM,EAAY,EAAgB,IAAI,CAAK,GAAK,CAAC,EACjD,EAAU,KAAK,CAAQ,EACvB,EAAgB,IAAI,EAAO,CAAS,CACtC,CACF,CARoB,CAStB,CACA,OAAO,EAAO,IAAK,GAA8B,CAC/C,IAAM,EAAY,EAAgB,IAAI,CAAI,EAC1C,GAAI,CAAC,EACH,OAAO,EAET,IAAM,EAAQ,EAAoB,CAAC,EAAM,GAAG,CAAS,CAAC,EAEtD,OADA,EAAU,IAAI,CAAK,EACZ,CACT,CAAC,CACH,CAAC,EAKD,IAAK,IAAM,KAAQ,EACjB,IAAK,IAAM,KAAc,EAAwB,IAAI,EAAK,SAAS,GAAK,CAAC,EAavE,GAXE,CAAC,EAAM,IAAI,CAAU,GACrB,EAAY,EAAW,EAAE,KACtB,GACC,CAAC,EAAW,sBACZ,EAAK,SAAS,MAAO,GACnB,EAAW,SAAS,KACjB,GACC,EAAQ,iBAAmB,EAAQ,iBAAmB,EAAQ,eAAiB,EAAQ,aAC3F,CACF,CACJ,EACY,CACZ,EAAK,qBAAuB,GAC5B,KACF,CAGJ,MAAO,CAAC,GAAG,EAAY,QAAQ,EAAG,IAAe,CAAC,EAAM,IAAI,CAAU,CAAC,EAAG,GAAG,CAAM,CACrF,CAEA,SAAS,EAAQ,EAAoB,EAA8B,CACjE,OAAO,EAAK,gBAAkB,EAAM,eAAiB,EAAM,gBAAkB,EAAK,aACpF,CAGA,SAAS,EAAoB,EAA6E,CACxG,IAAM,EAAW,EAAc,EAAM,QAAS,GAAS,EAAK,QAAQ,CAAC,EACrE,MAAO,CACL,GAAG,EAAM,GACT,qBAAsB,IAAA,GACtB,oBAAqB,IAAA,GACrB,WACA,YAAa,EAAc,EAAM,QAAS,GAAS,EAAK,aAAe,EAAK,QAAQ,CAAC,EACrF,cAAe,EACb,EAAM,QAAS,GAAU,EAAK,YAAe,EAAK,eAAiB,CAAC,EAAK,EAAK,QAAS,CACzF,EACA,WAAY,EAAS,QAAQ,EAAK,IAAY,EAAM,EAAQ,cAAgB,EAAQ,gBAAiB,CAAC,EACtG,gBAAiB,KAAK,IAAI,GAAG,EAAM,IAAK,GAAS,EAAK,eAAe,CAAC,EACtE,cAAe,KAAK,IAAI,GAAG,EAAM,IAAK,GAAS,EAAK,aAAa,CAAC,EAClE,WAAY,KAAK,IAAI,GAAG,EAAM,IAAK,GAAS,EAAK,UAAU,CAAC,EAC5D,SAAU,KAAK,IAAI,GAAG,EAAM,IAAK,GAAS,EAAK,QAAQ,CAAC,EACxD,UAAW,KAAK,IAAI,GAAG,EAAM,IAAK,GAAS,EAAK,SAAS,CAAC,EAC1D,QAAS,KAAK,IAAI,GAAG,EAAM,IAAK,GAAS,EAAK,OAAO,CAAC,CACxD,CACF,CAGA,SAAS,EAAc,EAA0C,CAC/D,IAAM,EAAyB,CAAC,EAChC,IAAK,IAAM,KAAW,EAAS,UAAU,EAAM,IAAU,EAAK,gBAAkB,EAAM,eAAe,EAAG,CACtG,IAAM,EAAO,EAAO,GAAG,EAAE,EACrB,GAAQ,EAAQ,gBAAkB,EAAK,cACzC,EAAK,cAAgB,KAAK,IAAI,EAAK,cAAe,EAAQ,aAAa,EAEvE,EAAO,KAAK,CAAE,GAAG,CAAQ,CAAC,CAE9B,CACA,OAAO,CACT,CAGA,SAAS,EACP,EACA,EACA,EACA,EACA,EACyB,CAEzB,IAAM,EAAe,GAAsD,CACzE,IAAM,EAA0B,EAAM,QAAU,CAAC,CAAC,EAClD,IAAK,GAAM,CAAE,YAAW,cAAc,EAAO,KAAK,EAAG,CACnD,IAAM,EAAS,EAAa,IAAc,EAC1C,IAAK,IAAM,KAAW,EACpB,EAAM,EAAU,EAAE,KAAK,EAAM,EAAS,CAAC,CAAM,CAAC,CAElD,CACA,OAAO,CACT,EACA,OAAO,EACL,EACA,EAAY,CAAW,EACvB,EAAY,EAAY,OAAQ,GAAU,CAAC,EAAM,KAAM,GAAe,EAAW,mBAAmB,CAAC,CAAC,EACtG,EACA,CACF,CAAC,CAAC,IAAK,GACL,EAAM,IAAK,GAAe,CACxB,IAAM,EAAS,EAAa,EAAW,YAAc,EACrD,MAAO,CACL,GAAG,EACH,KAAM,EAAM,EAAW,UAAU,EAAE,MAAQ,GAC3C,SAAU,EAAW,SAAS,IAAK,GAAY,EAAM,EAAS,CAAM,CAAC,EACrE,YAAa,EAAW,aAAa,IAAK,GAAQ,EAAM,EAAK,CAAM,CAAC,EACpE,gBAAiB,EAAW,gBAAkB,EAC9C,cAAe,EAAW,cAAgB,CAC5C,CACF,CAAC,CACH,CACF,CAEA,SAAS,EAAM,CAAE,kBAAiB,iBAA+B,EAA8B,CAC7F,MAAO,CAAE,gBAAiB,EAAkB,EAAQ,cAAe,EAAgB,CAAO,CAC5F,CAGA,SAAS,EAAwB,EAAyC,EAA0C,CAClH,IAAM,EAA+B,CAAC,EAChC,EAAoC,CAAC,EAC3C,IAAK,GAAM,CAAC,EAAW,CAAE,SAAQ,0BAA0B,EAAM,QAAQ,EACnE,GAAU,IACZ,EAAmB,KAAK,CAAS,EACjC,EAAS,KAAK,CAAE,SAAQ,mBAAoB,EAAwB,CAAM,EAAG,WAAY,CAAoB,CAAC,GAMlH,OAHI,EAAS,OAAS,EACb,CAAC,EAEH,EAAgC,EAAU,EAAW,EAAI,CAAC,CAAC,SAAS,CAAE,YAAW,kBAAmB,CACzG,IAAM,EAAY,EAAmB,GAC/B,EAAO,IAAc,IAAA,GAAY,IAAA,GAAY,EAAM,GACzD,OAAO,IAAc,IAAA,IAAa,IAAS,IAAA,GACvC,CAAC,EACD,CAAC,CAAE,GAAG,EAAW,aAAc,EAAW,KAAM,EAAK,IAAK,CAAC,CACjE,CAAC,CACH,CAGA,SAAS,EAA6B,EAAuC,CAC3E,OAAO,IAAI,IAAI,EAAM,IAAK,GAAe,EAAW,IAAI,CAAC,CAAC,CAAC,MAAQ,CACrE,CAEA,SAAS,EAAmB,EAAuC,CACjE,OAAO,EAAM,QAAU,GAAK,IAAI,IAAI,EAAM,IAAK,GAAc,EAAU,YAAY,CAAC,CAAC,CAAC,MAAQ,CAChG,CAOA,SAAS,EAAoB,EAAyC,EAAgC,CACpG,IAAM,EAAyB,CAAC,EAC5B,EAAS,EACb,IAAK,GAAM,CAAE,SAAQ,gBAAgB,EAAO,CAC1C,EAAa,KAAK,CAAM,EAGxB,IAAI,EAAa,GAAQ,QAAU,EACnC,GAAI,CAAC,EACH,IAAK,IAAM,KAAa,EACtB,EAAa,KAAK,IAAI,EAAY,EAAU,aAAa,EAG7D,GAAU,EAAa,EAAe,CACxC,CACA,OAAO,CACT,CAGA,SAAS,EACP,EACA,EACA,EACyB,CACzB,IAAM,EAAmB,EAAO,IAAK,GACnC,EACG,IAAK,GAAmC,CACvC,IAAM,EAAQ,EAAU,iBAAmB,EAAa,EAAU,eAAiB,GAC7E,EAAM,EAAU,eAAiB,EAAa,EAAU,eAAiB,GAC/E,MAAO,CACL,KAAM,EAAU,KAChB,UAAW,EAAU,aACrB,qBAAsB,EAAU,oBAChC,oBAAqB,EAAU,oBAC/B,SAAU,CAAC,CAAE,gBAAiB,EAAO,cAAe,CAAI,CAAC,EACzD,WAAY,EAAU,WACtB,gBAAiB,EACjB,cAAe,EACf,WAAY,EAAU,WACtB,SAAU,EAAU,SACpB,UAAW,EAAU,UACrB,QAAS,EAAU,OACrB,CACF,CAAC,CAAC,CACD,UAAU,EAAM,IAAU,EAAK,gBAAkB,EAAM,eAAe,CAC3E,EACA,OAAO,EAAoB,EAAkB,EAAc,CAA4B,CACzF,CAEA,SAAS,EACP,EACA,EACA,EAC6B,CAC7B,IAAM,EAA2C,CAAC,EAG5C,kBAAmB,IAAI,IACvB,EAAiB,IAAI,IACzB,EAAM,KAAK,EAAM,IAAU,CACzB,EAAK,KACL,CAAE,OAAQ,EAAK,OAAQ,gBAAiB,EAAK,gBAAiB,OAAQ,EAAa,IAAU,CAAE,CACjG,CAAC,CACH,EACM,kBAAoB,IAAI,IAC1B,EAAsB,EAC1B,IAAK,IAAM,KAAS,EAAQ,CAI1B,GAAuB,EAAwB,CAAK,EACpD,IAAM,EAAc,EACjB,IAAK,GAAe,CACnB,GAAM,CAAE,OAAM,YAAW,UAAS,cAAe,EAC3C,EAAc,EAAuB,EAAY,EAAgB,CAAiB,EACxF,MAAO,CAAE,OAAM,YAAW,UAAS,GAAI,GAAe,CAAE,aAAY,EAAI,YAAW,CACrF,CAAC,CAAC,CACD,UAAU,EAAM,IAAU,EAAK,KAAK,cAAc,EAAM,IAAI,GAAK,EAAK,UAAY,EAAM,SAAS,EAC9F,EAAQ,CAAC,GAAG,IAAI,IAAI,EAAY,KAAK,CAAE,UAAW,CAAI,CAAC,CAAC,EAC9D,IAAK,IAAM,KAAQ,EACjB,EAAiB,IAAI,GAAO,EAAiB,IAAI,CAAI,GAAK,GAAK,CAAC,EAElE,EAAS,KAAK,CAAE,QAAO,cAAa,WAAY,KAAK,IAAI,GAAG,EAAM,KAAK,CAAE,gBAAiB,CAAU,CAAC,CAAE,CAAC,CAC1G,CAOA,OANA,EAAS,MACN,EAAM,IACL,EAAM,WAAa,EAAK,aACvB,EAAK,YAAY,EAAE,EAAE,MAAQ,GAAA,CAAI,cAAc,EAAM,YAAY,EAAE,EAAE,MAAQ,EAAE,IAC/E,EAAK,YAAY,EAAE,EAAE,WAAa,IAAM,EAAM,YAAY,EAAE,EAAE,WAAa,EAChF,EACO,CACL,sBACA,+BAAgC,OAAO,YAAY,CAAgB,EACnE,2BAA4B,OAAO,YACjC,CAAC,GAAG,CAAiB,CAAC,CAAC,KAAK,CAAC,EAAM,KAAW,CAAC,EAAM,CAAC,GAAG,CAAK,CAAC,CAAC,UAAU,EAAM,IAAU,EAAO,CAAK,CAAC,CAAC,CAC1G,EACA,OAAQ,CACV,CACF,CAUA,SAAS,EACP,EACA,EAIA,EACsB,CACtB,IAAM,EAAW,EAAe,IAAI,EAAW,IAAI,EACnD,GAAI,CAAC,GAAU,OACb,OAEF,IAAI,EAAY,EAAkB,IAAI,EAAW,IAAI,EAChD,IACH,kBAAY,IAAI,IAChB,EAAkB,IAAI,EAAW,KAAM,CAAS,GAElD,IAAM,EAAQ,EACZ,EAAW,SAAS,IAAK,GAAY,EAAM,EAAS,CAAC,EAAS,MAAM,CAAC,EACrE,EAAW,aAAa,IAAK,GAAQ,EAAM,EAAK,CAAC,EAAS,MAAM,CAAC,EACjE,EAAS,OACT,EAAS,gBACT,EAAW,eAAe,IAAK,GAAY,EAAM,EAAS,CAAC,EAAS,MAAM,CAAC,CAC7E,EACA,IAAK,IAAM,KAAQ,EACjB,EAAU,IAAI,CAAI,EAEpB,MAAO,CAAC,GAAG,CAAK,CAAC,CAAC,UAAU,EAAM,IAAU,EAAO,CAAK,CAC1D"}