code-gauge 10.1.0 → 10.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +37 -15
  2. package/THIRD-PARTY-NOTICES.txt +8 -8
  3. package/dist/checkCommand.cjs +1 -1
  4. package/dist/checkCommand.cjs.map +1 -1
  5. package/dist/checkCommand.js +1 -1
  6. package/dist/checkCommand.js.map +1 -1
  7. package/dist/crossFileDuplication.cjs +1 -1
  8. package/dist/crossFileDuplication.cjs.map +1 -1
  9. package/dist/crossFileDuplication.d.ts +3 -2
  10. package/dist/crossFileDuplication.js +1 -1
  11. package/dist/crossFileDuplication.js.map +1 -1
  12. package/dist/crossFileNearMiss.cjs +1 -1
  13. package/dist/crossFileNearMiss.cjs.map +1 -1
  14. package/dist/crossFileNearMiss.d.ts +9 -4
  15. package/dist/crossFileNearMiss.js +1 -1
  16. package/dist/crossFileNearMiss.js.map +1 -1
  17. package/dist/duplication.cjs +1 -1
  18. package/dist/duplication.cjs.map +1 -1
  19. package/dist/duplication.d.ts +28 -7
  20. package/dist/duplication.js +1 -1
  21. package/dist/duplication.js.map +1 -1
  22. package/dist/thresholdCheck.cjs +1 -1
  23. package/dist/thresholdCheck.cjs.map +1 -1
  24. package/dist/thresholdCheck.d.ts +2 -0
  25. package/dist/thresholdCheck.js +1 -1
  26. package/dist/thresholdCheck.js.map +1 -1
  27. package/dist/thresholds.cjs.map +1 -1
  28. package/dist/thresholds.d.ts +2 -3
  29. package/dist/thresholds.js.map +1 -1
  30. package/dist/types.d.ts +20 -8
  31. package/native/Cargo.lock +16 -16
  32. package/native/Cargo.toml +8 -8
  33. package/native/code-gauge.wasm +0 -0
  34. package/native/src/complexity.rs +10 -1
  35. package/native/src/container.rs +158 -7
  36. package/native/src/dep_degree.rs +13 -14
  37. package/native/src/duplication.rs +302 -137
  38. package/native/src/functions.rs +112 -24
  39. package/native/src/languages.rs +5 -0
  40. package/native/src/measure.rs +9 -4
  41. package/native/src/near_miss.rs +303 -52
  42. package/package.json +8 -8
@@ -48,7 +48,7 @@ export interface TokenRange {
48
48
  endLine: number;
49
49
  }
50
50
  /** A contiguous run of matched tokens; gapped (merged) duplicates carry several per occurrence. */
51
- interface TokenSegment {
51
+ export interface TokenSegment {
52
52
  startTokenIndex: number;
53
53
  endTokenIndex: number;
54
54
  }
@@ -63,8 +63,21 @@ interface DuplicateCandidate {
63
63
  endLine: number;
64
64
  }
65
65
  export interface CountedOccurrence {
66
- /** Matched token runs; more than one once gapped groups are merged. */
66
+ /**
67
+ * The token runs the occurrence covers, matched throughout except in a near-miss copy (see
68
+ * `matchedRuns`); more than one once gapped groups are merged.
69
+ */
67
70
  segments: TokenSegment[];
71
+ /**
72
+ * The token runs of a near-miss copy that its partners match, within its segments;
73
+ * absent for an exact or gapped copy, whose segments are matched throughout.
74
+ */
75
+ matchedRuns?: TokenSegment[];
76
+ /**
77
+ * The exact fragments coalesced into a near-miss copy: each line they reach is duplicated,
78
+ * whatever else of the copy shares the line.
79
+ */
80
+ exactSegments?: TokenSegment[];
68
81
  /**
69
82
  * Set on occurrences whose span another reported group already counts: a retained group's
70
83
  * occurrences that a partial gapped merge also paired into a merged group, and cross-file copies
@@ -158,6 +171,11 @@ export declare function lcsLength(a: Int32Array, b: Int32Array): number;
158
171
  * every `b` compared with it.
159
172
  */
160
173
  export declare function createLcsLengthCounter(a: Int32Array): (b: Int32Array) => number;
174
+ /**
175
+ * Which symbols of each sequence a longest common subsequence pairs, by Hirschberg's divide and
176
+ * conquer over the bit-parallel recurrence, which alone yields lengths but no pairing.
177
+ */
178
+ export declare function markLcs(a: Int32Array, b: Int32Array): [Uint8Array, Uint8Array];
161
179
  /**
162
180
  * Merges duplicate groups separated by a small token gap into one gapped (Type-3) clone group: a
163
181
  * copy edited in one spot splits into two exact groups whose occurrences sit side by side in the
@@ -190,9 +208,12 @@ export declare function mergeAdjacentGroups<T extends CountedOccurrence>(groups:
190
208
  * content — so no token span contributes to the count twice.
191
209
  */
192
210
  export declare function countRedundantFragments(group: CountedOccurrence[]): number;
193
- /** Adds the 1-based code lines the segment's matched tokens cover; shared with cross-file coverage. */
194
- export declare function collectSegmentLines(segment: {
195
- startTokenIndex: number;
196
- endTokenIndex: number;
197
- }, tokens: Token[], codeLineNumbers: Set<number> | undefined, duplicatedLines: Set<number>): void;
211
+ /**
212
+ * The 1-based code lines the occurrence's matched tokens cover, given its segments and matched runs
213
+ * as indexes into `tokens`. A line of a near-miss copy counts when its partners match more than half
214
+ * of its tokens: a line the copy adds or rewrites is not duplicated, while a longest common
215
+ * subsequence still pairs a stray token of it (a bracket, a separator) with one of the partner.
216
+ * A line one of `exactSegments` reaches counts in any case.
217
+ */
218
+ export declare function collectMatchedLines(segments: TokenSegment[], matchedRuns: TokenSegment[] | undefined, tokens: Token[], codeLineNumbers: Set<number> | undefined, exactSegments?: TokenSegment[]): Set<number>;
198
219
  export {};
@@ -1,2 +1,2 @@
1
- import{clamp as e}from"@willbooster/shared-lib";const t={minTokens:40,maxGapTokens:30,minSimilarityPercent:70};function n(e){return{minTokens:r(e?.minTokens,t.minTokens),maxGapTokens:r(e?.maxGapTokens,t.maxGapTokens),minSimilarityPercent:r(e?.minSimilarityPercent,t.minSimilarityPercent)}}function r(t,n){return t===void 0||Number.isNaN(t)?n:e(Math.trunc(t),0,4294967295)}function i(e,t){return e*5>=t}function a(e){let t=new Int32Array(e.length+1);for(let[n,r]of e.entries())t[n+1]=(t[n]??0)+(r.literalHash===void 0?0:1);return t}function o(e,t,n){let r=[],i=[],a=[];for(let[t,n]of e.entries())for(let e of n.containers)i.push(t),a.push(e);let o=t=>e[i[t]??0],l=/* @__PURE__ */ new Map,u=a.map((e,n)=>c(o(n)?.tokens??[],e,t));for(let[e,t]of u.entries()){let n=i[e]??0;for(let[r,i]of t.windowKeysByStart.entries())for(let t of i){if(t===void 0)continue;let i=l.get(t);i?(i.count+=1,i.containerIndex!==e&&(i.containerIndex=-1),i.contextIndex!==n&&(i.contextIndex=-1),i.minStart=Math.min(i.minStart,r),i.maxStart=Math.max(i.maxStart,r)):l.set(t,{count:1,containerIndex:e,contextIndex:n,minStart:r,maxStart:r})}}let d=(e,t)=>{if(e===void 0)return!1;let r=l.get(e);return r===void 0||r.count<2?!1:n?r.contextIndex===-1:r.containerIndex===-1||r.maxStart-r.minStart>=t},p=e=>{let t=u[e.containerIndex]?.statementHashes??[],n=t[e.start];for(let r=e.start+1;r<e.start+e.length;r+=1)if(t[r]!==n)return!0;return!1},m=[];for(let[e,t]of u.entries())for(let[n,r]of t.windowKeysByStart.entries())for(let[i,a]of r.entries()){if(!d(a,i)||!p({containerIndex:e,start:n,length:i}))continue;let r=t.windowKeysByStart[n]?.[i+1],o=t.windowKeysByStart[n-1]?.[i+1];d(r,i+1)||d(o,i+1)||m.push({containerIndex:e,start:n,length:i})}let h=new Set(m.map(s)),g=m;for(;g.length>0;){let e=[];for(let t of g){let n=a[t.containerIndex],s=n?.[t.start],c=n?.[t.start+t.length-1],l=o(t.containerIndex);if(!s||!c||!l)continue;let u=`s:${_(l.tokens,l.literalCountPrefix,s.startTokenIndex,c.endTokenIndex)}`;r.push({candidate:f(u,s.startTokenIndex,c.endTokenIndex,s,c),contextIndex:i[t.containerIndex]??0}),e.push(t)}g=[];for(let t of e)for(let e of[t.start,t.start+1]){let n={containerIndex:t.containerIndex,start:e,length:t.length-1},r=u[t.containerIndex]?.windowKeysByStart[e]?.[n.length];!h.has(s(n))&&d(r,n.length)&&p(n)&&(h.add(s(n)),g.push(n))}}return r}function s(e){return`${e.containerIndex}:${e.start}:${e.length}`}function c(e,t,n){let r=t.map(t=>v(e,t.startTokenIndex,t.endTokenIndex)),i=[];for(let e=0;e<t.length;e+=1){let a=[],o=5381,s=0,c=Math.min(t.length,e+100);for(let i=e;i<c;i+=1){let c=t[i],l=r[i];if(!c||l===void 0)break;o=S(o,l),s+=c.endTokenIndex-c.startTokenIndex;let u=i-e+1;a[u]=u>=2&&s>=n?S(o,u):void 0}i.push(a)}return{windowKeysByStart:i,statementHashes:r}}function l(e,t){return u(e)(t)}function u(e){let t=e.length+31>>>5,n=/* @__PURE__ */ new Map;for(let[r,i]of e.entries()){let e=n.get(i);e||(e=new Uint32Array(t),n.set(i,e));let a=r>>>5;e[a]=(e[a]??0)|1<<(r&31)}let r=new Uint32Array(t);return e=>{r.fill(0);for(let i of e){let e=n.get(i),a=1,o=0;for(let n=0;n<t;n+=1){let t=r[n]??0,i=((e?.[n]??0)|t)>>>0,s=(t<<1|a)>>>0;a=t>>>31;let c=i-s-o;o=+(c<0),r[n]=i&~c}}let i=0;for(let e of r)i+=d(e);return i}}function d(e){let t=e-(e>>>1&1431655765);return t=(t&858993459)+(t>>>2&858993459),Math.imul(t+(t>>>4)&252645135,16843009)>>>24&255}function f(e,t,n,r,i){return{fingerprint:e,tokenCount:n-t,startTokenIndex:t,endTokenIndex:n,startIndex:r.startIndex,endIndex:i.endIndex,startLine:r.startLine,endLine:i.endLine}}const p=[],m=[];function h(e){let t=p[e];return t===void 0&&(t=b(`$${e}`),p[e]=t),t}function g(e){let t=m[e];return t===void 0&&(t=x(`$${e}`),m[e]=t),t}function _(e,t,n,r){let[a,o]=y(e,n,r,i((t[r]??0)-(t[n]??0),r-n));return`${a}:${o}:${r-n}`}function v(e,t,n){let[r,i]=y(e,t,n,!1);return r^Math.imul(i,31)}function y(e,t,n,r){let i=/* @__PURE__ */ new Map,a=5381,o=52711;for(let s=t;s<n;s+=1){let t=e[s];if(!t)continue;let n,c;if(t.kind===`id`){let e=i.get(t.text);e===void 0&&(e=i.size,i.set(t.text,e)),n=h(e),c=g(e)}else n=t.textHash,c=t.textHash2;a=Math.imul(a,31)+n|0,o=Math.imul(o,37)^c,r&&t.literalHash!==void 0&&t.literalHash2!==void 0&&(a=Math.imul(a,31)+t.literalHash|0,o=Math.imul(o,37)^t.literalHash2)}return[a,o]}function b(e){let t=5381;for(let n=0;n<e.length;n+=1)t=Math.imul(t,33)^e.charCodeAt(n);return t}function x(e){let t=-2128831035;for(let n=0;n<e.length;n+=1)t=Math.imul(t^e.charCodeAt(n),16777619);return t}function S(e,t){return Math.imul(e,31)+t}function C(e,t,n=()=>!0){if(t<=0||e.length<2)return e;e.sort(E);for(let r=!0;r;){r=!1;let i=w(e,t);for(let a=0;a<e.length&&!r;a+=1)for(let o of i[a]??[]){let i=e[a],s=e[o];if(!i||!s)continue;let c=D(i,s,t,n),l=c??D(s,i,t,n);if(!l)continue;let u=c?l.firstReplaced:l.secondReplaced,d=c?l.secondReplaced:l.firstReplaced;u&&d?(e[a]=l.merged,e.splice(o,1)):d?e[o]=l.merged:u?e[a]=l.merged:e.push(l.merged);for(let e of l.pairedRetained)e.spanCountedElsewhere=!0;e.sort(E),r=!0;break}}return e}function w(e,t){let n=[];for(let[t,r]of e.entries())for(let e of r)n.push({startTokenIndex:e.startTokenIndex,groupIndex:t});n.sort((e,t)=>e.startTokenIndex-t.startTokenIndex);let r=e.map(()=>/* @__PURE__ */ new Set);for(let[i,a]of e.entries())for(let e of a)for(let a=T(n,e.endTokenIndex);a<n.length&&(n[a]?.startTokenIndex??0)<=e.endTokenIndex+t;a+=1){let e=n[a]?.groupIndex??i;e!==i&&r[Math.min(e,i)]?.add(Math.max(e,i))}return r.map(e=>[...e].toSorted((e,t)=>e-t))}function T(e,t){let n=0,r=e.length;for(;n<r;){let i=n+r>>>1;(e[i]?.startTokenIndex??0)<t?n=i+1:r=i}return n}function E(e,t){let n=e[0],r=t[0];return(n?.startTokenIndex??0)-(r?.startTokenIndex??0)||(n?.endTokenIndex??0)-(r?.endTokenIndex??0)}function D(e,t,n,r){let[i,a]=O(e),[o,s]=O(t),c=[],l=0,u=-1;for(let e of o){for(;l<i.length;){let t=i[l];if(t&&t.endTokenIndex+n<e.startTokenIndex)l+=1;else break}let t=i[l];t&&t.endTokenIndex<=e.startTokenIndex&&t.startTokenIndex>=u&&(c.push([t,e]),u=e.endTokenIndex,l+=1)}let d=c.length===a,f=c.length===s;if(c.length<2||!d&&!f)return;let p=d&&!e.some(e=>e.nestedInLargerGroup),m=f&&!t.some(e=>e.nestedInLargerGroup),h=c.map(([e,t])=>({...e,spanCountedElsewhere:void 0,nestedInLargerGroup:void 0,segments:[...e.segments,...t.segments],tokenCount:e.tokenCount+t.tokenCount,endTokenIndex:t.endTokenIndex,endIndex:t.endIndex,endLine:t.endLine}));if(r(h))return{merged:h,firstReplaced:p,secondReplaced:m,pairedRetained:[...p?[]:c.map(([e])=>e),...m?[]:c.map(([,e])=>e)]}}function O(e){let t=[],n=0;for(let r of e)r.nestedInLargerGroup||(n+=1),!r.spanCountedElsewhere&&!r.nestedInLargerGroup&&t.push(r);return[t,n]}function k(e){let t=0,n=0,r=!1;for(let i of e){if(i.spanCountedElsewhere){r=!0;continue}t+=i.segments.length,n=Math.max(n,i.segments.length)}return r?t:t-n}function A(e,t,n,r){for(let i=e.startTokenIndex;i<e.endTokenIndex;i+=1){let e=t[i];for(let t=e?.startRow??0;t<=(e?.endRow??-1);t+=1)(!n||n.has(t+1))&&r.add(t+1)}}export{a as buildLiteralCountPrefix,A as collectSegmentLines,o as collectSequenceWindowCandidates,k as countRedundantFragments,u as createLcsLengthCounter,t as defaultDuplicationOptions,l as lcsLength,C as mergeAdjacentGroups,n as resolveDuplicationOptions};
1
+ import{clamp as e}from"@willbooster/shared-lib";const t={minTokens:40,maxGapTokens:30,minSimilarityPercent:70};function n(e){return{minTokens:r(e?.minTokens,t.minTokens),maxGapTokens:r(e?.maxGapTokens,t.maxGapTokens),minSimilarityPercent:r(e?.minSimilarityPercent,t.minSimilarityPercent)}}function r(t,n){return t===void 0||Number.isNaN(t)?n:e(Math.trunc(t),0,4294967295)}function i(e,t){return e*5>=t}function a(e){let t=new Int32Array(e.length+1);for(let[n,r]of e.entries())t[n+1]=(t[n]??0)+(r.literalHash===void 0?0:1);return t}function o(e,t,n){let r=[],i=[],a=[];for(let[t,n]of e.entries())for(let e of n.containers)i.push(t),a.push(e);let o=t=>e[i[t]??0],l=/* @__PURE__ */ new Map,u=a.map((e,n)=>c(o(n)?.tokens??[],e,t));for(let[e,t]of u.entries()){let n=i[e]??0;for(let[r,i]of t.windowKeysByStart.entries())for(let t of i){if(t===void 0)continue;let i=l.get(t);i?(i.count+=1,i.containerIndex!==e&&(i.containerIndex=-1),i.contextIndex!==n&&(i.contextIndex=-1),i.minStart=Math.min(i.minStart,r),i.maxStart=Math.max(i.maxStart,r)):l.set(t,{count:1,containerIndex:e,contextIndex:n,minStart:r,maxStart:r})}}let d=(e,t)=>{if(e===void 0)return!1;let r=l.get(e);return r===void 0||r.count<2?!1:n?r.contextIndex===-1:r.containerIndex===-1||r.maxStart-r.minStart>=t},f=e=>{let t=u[e.containerIndex]?.statementHashes??[],n=t[e.start];for(let r=e.start+1;r<e.start+e.length;r+=1)if(t[r]!==n)return!0;return!1},p=[];for(let[e,t]of u.entries())for(let[n,r]of t.windowKeysByStart.entries())for(let[i,a]of r.entries()){if(!d(a,i)||!f({containerIndex:e,start:n,length:i}))continue;let r=t.windowKeysByStart[n]?.[i+1],o=t.windowKeysByStart[n-1]?.[i+1];d(r,i+1)||d(o,i+1)||p.push({containerIndex:e,start:n,length:i})}let m=new Set(p.map(s)),h=p;for(;h.length>0;){let e=[];for(let t of h){let n=a[t.containerIndex],s=n?.[t.start],c=n?.[t.start+t.length-1],l=o(t.containerIndex);if(!s||!c||!l)continue;let u=`s:${x(l.tokens,l.literalCountPrefix,s.startTokenIndex,c.endTokenIndex)}`;r.push({candidate:g(u,s.startTokenIndex,c.endTokenIndex,s,c),contextIndex:i[t.containerIndex]??0}),e.push(t)}h=[];for(let t of e)for(let e of[t.start,t.start+1]){let n={containerIndex:t.containerIndex,start:e,length:t.length-1},r=u[t.containerIndex]?.windowKeysByStart[e]?.[n.length];!m.has(s(n))&&d(r,n.length)&&f(n)&&(m.add(s(n)),h.push(n))}}return r}function s(e){return`${e.containerIndex}:${e.start}:${e.length}`}function c(e,t,n){let r=t.map(t=>S(e,t.startTokenIndex,t.endTokenIndex)),i=[];for(let e=0;e<t.length;e+=1){let a=[],o=5381,s=0,c=Math.min(t.length,e+100);for(let i=e;i<c;i+=1){let c=t[i],l=r[i];if(!c||l===void 0)break;o=E(o,l),s+=c.endTokenIndex-c.startTokenIndex;let u=i-e+1;a[u]=u>=2&&s>=n?E(o,u):void 0}i.push(a)}return{windowKeysByStart:i,statementHashes:r}}function l(e,t){return u(e)(t)}function u(e){let t=d(e);return e=>{let n=0;for(let r of t(e))n+=h(r);return n}}function d(e){let t=e.length+31>>>5,n=/* @__PURE__ */ new Map;for(let[r,i]of e.entries()){let e=n.get(i);e||(e=new Uint32Array(t),n.set(i,e));let a=r>>>5;e[a]=(e[a]??0)|1<<(r&31)}let r=new Uint32Array(t);return e=>{r.fill(0);for(let i of e){let e=n.get(i),a=1,o=0;for(let n=0;n<t;n+=1){let t=r[n]??0,i=((e?.[n]??0)|t)>>>0,s=(t<<1|a)>>>0;a=t>>>31;let c=i-s-o;o=+(c<0),r[n]=i&~c}}return r}}function f(e,t){let n=new Uint8Array(e.length),r=new Uint8Array(t.length);return p(e,t,n,r),[n,r]}function p(e,t,n,r){let i=0;for(;i<e.length&&i<t.length&&e[i]===t[i];)i+=1;let a=0;for(;a<e.length-i&&a<t.length-i&&e[e.length-1-a]===t[t.length-1-a];)a+=1;let o=e.length-a,s=t.length-a;n.fill(1,0,i).fill(1,o),r.fill(1,0,i).fill(1,s);let c=e.subarray(i,o),l=t.subarray(i,s),u=n.subarray(i,o),d=r.subarray(i,s);if(c.length===0||l.length===0)return;if(c.length===1){let e=l.indexOf(c[0]??0);e!==-1&&(u[0]=1,d[e]=1);return}let f=c.length>>>1,h=m(c.subarray(0,f),l),g=m(c.subarray(f).toReversed(),l.toReversed()),_=0,v=-1;for(let e=0;e<=l.length;e+=1){let t=(h[e]??0)+(g[l.length-e]??0);t>v&&(v=t,_=e)}p(c.subarray(0,f),l.subarray(0,_),u.subarray(0,f),d.subarray(0,_)),p(c.subarray(f),l.subarray(_),u.subarray(f),d.subarray(_))}function m(e,t){let n=d(t)(e),r=new Int32Array(t.length+1);for(let e=0;e<t.length;e+=1)r[e+1]=(r[e]??0)+((n[e>>>5]??0)>>>(e&31)&1);return r}function h(e){let t=e-(e>>>1&1431655765);return t=(t&858993459)+(t>>>2&858993459),Math.imul(t+(t>>>4)&252645135,16843009)>>>24&255}function g(e,t,n,r,i){return{fingerprint:e,tokenCount:n-t,startTokenIndex:t,endTokenIndex:n,startIndex:r.startIndex,endIndex:i.endIndex,startLine:r.startLine,endLine:i.endLine}}const _=[],v=[];function y(e){let t=_[e];return t===void 0&&(t=w(`$${e}`),_[e]=t),t}function b(e){let t=v[e];return t===void 0&&(t=T(`$${e}`),v[e]=t),t}function x(e,t,n,r){let[a,o]=C(e,n,r,i((t[r]??0)-(t[n]??0),r-n));return`${a}:${o}:${r-n}`}function S(e,t,n){let[r,i]=C(e,t,n,!1);return r^Math.imul(i,31)}function C(e,t,n,r){let i=/* @__PURE__ */ new Map,a=5381,o=52711;for(let s=t;s<n;s+=1){let t=e[s];if(!t)continue;let n,c;if(t.kind===`id`){let e=i.get(t.text);e===void 0&&(e=i.size,i.set(t.text,e)),n=y(e),c=b(e)}else n=t.textHash,c=t.textHash2;a=Math.imul(a,31)+n|0,o=Math.imul(o,37)^c,r&&t.literalHash!==void 0&&t.literalHash2!==void 0&&(a=Math.imul(a,31)+t.literalHash|0,o=Math.imul(o,37)^t.literalHash2)}return[a,o]}function w(e){let t=5381;for(let n=0;n<e.length;n+=1)t=Math.imul(t,33)^e.charCodeAt(n);return t}function T(e){let t=-2128831035;for(let n=0;n<e.length;n+=1)t=Math.imul(t^e.charCodeAt(n),16777619);return t}function E(e,t){return Math.imul(e,31)+t}function D(e,t,n=()=>!0){if(t<=0||e.length<2)return e;e.sort(A);for(let r=!0;r;){r=!1;let i=O(e,t);for(let a=0;a<e.length&&!r;a+=1)for(let o of i[a]??[]){let i=e[a],s=e[o];if(!i||!s)continue;let c=j(i,s,t,n),l=c??j(s,i,t,n);if(!l)continue;let u=c?l.firstReplaced:l.secondReplaced,d=c?l.secondReplaced:l.firstReplaced;u&&d?(e[a]=l.merged,e.splice(o,1)):d?e[o]=l.merged:u?e[a]=l.merged:e.push(l.merged);for(let e of l.pairedRetained)e.spanCountedElsewhere=!0;e.sort(A),r=!0;break}}return e}function O(e,t){let n=[];for(let[t,r]of e.entries())for(let e of r)n.push({startTokenIndex:e.startTokenIndex,groupIndex:t});n.sort((e,t)=>e.startTokenIndex-t.startTokenIndex);let r=e.map(()=>/* @__PURE__ */ new Set);for(let[i,a]of e.entries())for(let e of a)for(let a=k(n,e.endTokenIndex);a<n.length&&(n[a]?.startTokenIndex??0)<=e.endTokenIndex+t;a+=1){let e=n[a]?.groupIndex??i;e!==i&&r[Math.min(e,i)]?.add(Math.max(e,i))}return r.map(e=>[...e].toSorted((e,t)=>e-t))}function k(e,t){let n=0,r=e.length;for(;n<r;){let i=n+r>>>1;(e[i]?.startTokenIndex??0)<t?n=i+1:r=i}return n}function A(e,t){let n=e[0],r=t[0];return(n?.startTokenIndex??0)-(r?.startTokenIndex??0)||(n?.endTokenIndex??0)-(r?.endTokenIndex??0)}function j(e,t,n,r){let[i,a]=M(e),[o,s]=M(t),c=[],l=0,u=-1;for(let e of o){for(;l<i.length;){let t=i[l];if(t&&t.endTokenIndex+n<e.startTokenIndex)l+=1;else break}let t=i[l];t&&t.endTokenIndex<=e.startTokenIndex&&t.startTokenIndex>=u&&(c.push([t,e]),u=e.endTokenIndex,l+=1)}let d=c.length===a,f=c.length===s;if(c.length<2||!d&&!f)return;let p=d&&!e.some(e=>e.nestedInLargerGroup),m=f&&!t.some(e=>e.nestedInLargerGroup),h=c.map(([e,t])=>({...e,spanCountedElsewhere:void 0,nestedInLargerGroup:void 0,matchedRuns:void 0,segments:[...e.segments,...t.segments],tokenCount:e.tokenCount+t.tokenCount,endTokenIndex:t.endTokenIndex,endIndex:t.endIndex,endLine:t.endLine}));if(r(h))return{merged:h,firstReplaced:p,secondReplaced:m,pairedRetained:[...p?[]:c.map(([e])=>e),...m?[]:c.map(([,e])=>e)]}}function M(e){let t=[],n=0;for(let r of e)r.nestedInLargerGroup||(n+=1),!r.spanCountedElsewhere&&!r.nestedInLargerGroup&&t.push(r);return[t,n]}function N(e){let t=0,n=0,r=!1;for(let i of e){if(i.spanCountedElsewhere){r=!0;continue}t+=i.segments.length,n=Math.max(n,i.segments.length)}return r?t:t-n}function P(e,t,n,r,i=[]){let a=/* @__PURE__ */ new Set;for(let e of i)for(let t=e.startTokenIndex;t<e.endTokenIndex;t+=1){let e=n[t];for(let t=e?.startRow??0;t<=(e?.endRow??-1);t+=1)a.add(t+1)}let o=/* @__PURE__ */ new Map,s=/* @__PURE__ */ new Map,c=0;for(let r of e)for(let e=r.startTokenIndex;e<r.endTokenIndex;e+=1){for(;t&&(t[c]?.endTokenIndex??1/0)<=e;)c+=1;let r=!t||(t[c]?.startTokenIndex??1/0)<=e,i=n[e];for(let e=i?.startRow??0;e<=(i?.endRow??-1);e+=1)s.set(e+1,(s.get(e+1)??0)+1),o.set(e+1,(o.get(e+1)??0)+ +!!r)}let l=/* @__PURE__ */ new Set;for(let[e,t]of s)(!r||r.has(e))&&((o.get(e)??0)*2>t||a.has(e))&&l.add(e);return l}export{a as buildLiteralCountPrefix,P as collectMatchedLines,o as collectSequenceWindowCandidates,N as countRedundantFragments,u as createLcsLengthCounter,t as defaultDuplicationOptions,l as lcsLength,f as markLcs,D as mergeAdjacentGroups,n as resolveDuplicationOptions};
2
2
  //# sourceMappingURL=duplication.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"duplication.js","names":[],"sources":["../src/duplication.ts"],"sourcesContent":["import { clamp } from '@willbooster/shared-lib';\nimport type { DuplicationOptions } from './types.js';\n\n/**\n * Project-level duplication machinery operating on normalized token streams. Tokenization itself\n * (parsing, identifier anonymization, literal normalization) happens in the Rust addon, which\n * serializes each file's Token stream and statement structure; the helpers here match statement\n * windows across files, merge gap-adjacent groups, and count duplicated lines over that data.\n */\n\nexport const defaultDuplicationOptions: Required<DuplicationOptions> = {\n minTokens: 40,\n maxGapTokens: 30,\n minSimilarityPercent: 70,\n};\n\n/**\n * Fills defaults for absent settings, applying the same normalization as the native boundary's\n * clampToU32 — NaN (e.g. `Number(unsetEnvVariable)`) counts as absent, and other values truncate\n * and clamp to [0, u32::MAX] — so the TypeScript half of cross-file matching cannot diverge from\n * the natively collected candidates on such input.\n */\nexport function resolveDuplicationOptions(options?: DuplicationOptions): Required<DuplicationOptions> {\n return {\n minTokens: resolveOption(options?.minTokens, defaultDuplicationOptions.minTokens),\n maxGapTokens: resolveOption(options?.maxGapTokens, defaultDuplicationOptions.maxGapTokens),\n minSimilarityPercent: resolveOption(options?.minSimilarityPercent, defaultDuplicationOptions.minSimilarityPercent),\n };\n}\n\nfunction resolveOption(value: number | undefined, fallback: number): number {\n return value === undefined || Number.isNaN(value) ? fallback : clamp(Math.trunc(value), 0, 0xFF_FF_FF_FF);\n}\n\n/** Minimum consecutive statements for a statement-sequence duplicate candidate. */\nconst minSequenceStatementCount = 2;\n/**\n * Caps the window length so statement-sequence enumeration stays linear in the statement count.\n * Heterogeneous clones longer than the cap are reported as capped windows (a deliberate\n * conservative undercount trading completeness for bounded discovery cost).\n */\nconst maxSequenceStatementCount = 100;\n\n/**\n * A region whose normalized tokens are at least 20% literal values is data-like (a lookup table, a\n * constant list, a value-mapping switch), not logic: literal values re-enter its fingerprint so\n * tables that merely share their shape stop counting as copy-paste. Compared in integer math\n * (5 * literals >= total) so the TypeScript and native sides cannot disagree on the boundary.\n */\nfunction isLiteralDense(literalCount: number, tokenCount: number): boolean {\n return literalCount * 5 >= tokenCount;\n}\n\nexport interface Token {\n /** Normalization target: identifiers to anonymize, literal kind tags, or the raw token text. */\n kind: 'id' | 'text';\n text: string;\n /**\n * Two INDEPENDENT hashes of `text` (djb2 and FNV-1a), precomputed so fingerprinting nested\n * regions never re-hashes a token. Feeding the same per-token hash to both fingerprint\n * accumulators would collapse the key to 32 effective bits: one djb2 collision between two\n * token texts would then equate whole regions.\n */\n textHash: number;\n textHash2: number;\n /** Hash pair of a value-carrying literal's value, folded into data-like region fingerprints. */\n literalHash?: number;\n literalHash2?: number;\n /**\n * True for verbatim-kept NAMES (member/callee/type names, named grammar leaves): together with\n * value-carrying literals these are the content-bearing tokens the near-miss content gate\n * counts. Keywords, operators, and punctuation come from unnamed nodes and stay false.\n */\n isName?: boolean;\n /** 0-based source rows the token occupies, so line coverage counts only matched-token lines. */\n startRow: number;\n endRow: number;\n}\n\n/** A token span with its source position; plain data so cross-file matching can retain it. */\nexport interface TokenRange {\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/** A contiguous run of matched tokens; gapped (merged) duplicates carry several per occurrence. */\ninterface TokenSegment {\n startTokenIndex: number;\n endTokenIndex: number;\n}\n\ninterface DuplicateCandidate {\n fingerprint: string;\n tokenCount: number;\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\nexport interface CountedOccurrence {\n /** Matched token runs; more than one once gapped groups are merged. */\n segments: TokenSegment[];\n /**\n * Set on occurrences whose span another reported group already counts: a retained group's\n * occurrences that a partial gapped merge also paired into a merged group, and cross-file copies\n * nested inside a larger group's region. Block counting must not count them again.\n */\n spanCountedElsewhere?: boolean;\n /**\n * Set on cross-file copies nested inside a larger group's region (they also set\n * `spanCountedElsewhere`). They never pair in gapped merging, and they do not keep their group's\n * standalone copies from merging, which they are not copies of. They do keep the merged group\n * from taking their group's place, since only the original group reports the nesting.\n */\n nestedInLargerGroup?: boolean;\n /** Sum of segment token counts (the gap tokens are not matched content). */\n tokenCount: number;\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/** A duplicate region found in one file, exported for cross-file matching by fingerprint. */\nexport interface CrossFileDuplicateCandidate {\n /** Content key: equal fingerprints mean equal normalized token sequences (up to hash collision). */\n fingerprint: string;\n tokenCount: number;\n /** Token positions within the owning file, for cross-file gapped (Type-3) merging. */\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/**\n * One file's contribution to cross-file clone detection: catalogued candidates plus the normalized\n * token stream and statement structure, so the project-level pass can match partial statement runs\n * (windows that a single file cannot know repeat elsewhere) and merge gap-adjacent groups.\n */\nexport interface CrossFileDuplicationFileData {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n /**\n * The mutually disjoint blocks (at least `minTokens` long, not literal-dense) compared for\n * cross-file near-miss (Type-3) clones. Optional for backward compatibility; without it, the\n * file takes part in exact and gapped cross-file matching only.\n */\n nearMissBlocks?: TokenRange[];\n /**\n * 1-based lines that are neither blank nor comment-only, so cross-file line coverage counts only\n * code lines (blank rows inside multi-row tokens such as template literals carry no content).\n * Optional for backward compatibility; without it, every matched-token row counts.\n */\n codeLineNumbers?: Set<number>;\n}\n\n/** literalCountPrefix[i] = value-carrying literal tokens in tokens[0..i), for O(1) density checks. */\nexport function buildLiteralCountPrefix(tokens: Token[]): Int32Array {\n const prefix = new Int32Array(tokens.length + 1);\n for (const [index, token] of tokens.entries()) {\n prefix[index + 1] = (prefix[index] ?? 0) + (token.literalHash === undefined ? 0 : 1);\n }\n return prefix;\n}\n\ninterface WindowOccurrences {\n count: number;\n /** -1 once occurrences span more than one container. */\n containerIndex: number;\n /** -1 once occurrences span more than one context (file). */\n contextIndex: number;\n minStart: number;\n maxStart: number;\n}\n\ninterface SequenceWindow {\n containerIndex: number;\n start: number;\n length: number;\n}\n\n/** One file's token stream and statement containers, as a window-matching context. */\nexport interface SequenceWindowContext {\n tokens: Token[];\n literalCountPrefix: Int32Array;\n containers: TokenRange[][];\n}\n\nexport interface ContextualSequenceCandidate {\n candidate: DuplicateCandidate;\n contextIndex: number;\n}\n\n/**\n * Enumerates runs of consecutive sibling statements over one or more contexts (files). Every\n * container statement participates; only the window length is capped, so enumeration stays linear\n * in the statement count. Windows are grouped by a cheap rolling hash of per-statement\n * fingerprints, and only locally maximal repeated windows — those whose one-statement extensions\n * stop repeating — become candidates with an exact (window-consistent) fingerprint. Without the\n * maximality filter a degenerate file of near-identical statements would fingerprint every\n * sub-window of every repeated region. With `requireMultipleContexts` a window only counts as\n * repeated when its occurrences span at least two contexts (CPD-style cross-file matching): a\n * repeat confined to one file is that file's own concern, and emitting it here would flood the\n * project-level selection with unusable single-file groups.\n */\nexport function collectSequenceWindowCandidates(\n contexts: SequenceWindowContext[],\n minTokens: number,\n requireMultipleContexts: boolean\n): ContextualSequenceCandidate[] {\n const candidates: ContextualSequenceCandidate[] = [];\n const contextIndexByContainer: number[] = [];\n const containers: TokenRange[][] = [];\n for (const [contextIndex, context] of contexts.entries()) {\n for (const statements of context.containers) {\n contextIndexByContainer.push(contextIndex);\n containers.push(statements);\n }\n }\n const contextAt = (containerIndex: number): SequenceWindowContext | undefined =>\n contexts[contextIndexByContainer[containerIndex] ?? 0];\n const occurrencesByWindowKey = new Map<number, WindowOccurrences>();\n const containerWindows = containers.map((statements, containerIndex) =>\n enumerateContainerWindows(contextAt(containerIndex)?.tokens ?? [], statements, minTokens)\n );\n for (const [containerIndex, windows] of containerWindows.entries()) {\n const contextIndex = contextIndexByContainer[containerIndex] ?? 0;\n for (const [start, row] of windows.windowKeysByStart.entries()) {\n for (const windowKey of row) {\n if (windowKey === undefined) {\n continue;\n }\n const occurrences = occurrencesByWindowKey.get(windowKey);\n if (occurrences) {\n occurrences.count += 1;\n if (occurrences.containerIndex !== containerIndex) {\n occurrences.containerIndex = -1;\n }\n if (occurrences.contextIndex !== contextIndex) {\n occurrences.contextIndex = -1;\n }\n occurrences.minStart = Math.min(occurrences.minStart, start);\n occurrences.maxStart = Math.max(occurrences.maxStart, start);\n } else {\n occurrencesByWindowKey.set(windowKey, {\n count: 1,\n containerIndex,\n contextIndex,\n minStart: start,\n maxStart: start,\n });\n }\n }\n }\n }\n\n // A window only \"repeats\" when two of its occurrences can coexist without overlapping: sliding\n // matches inside a homogeneous run (start spread smaller than the window length) can never both\n // be counted and must neither qualify a window nor dominate its sub-windows. Cross-context\n // matching instead requires occurrences in two contexts, which coexist by construction.\n const repeats = (windowKey: number | undefined, length: number): boolean => {\n if (windowKey === undefined) {\n return false;\n }\n const occurrences = occurrencesByWindowKey.get(windowKey);\n if (occurrences === undefined || occurrences.count < 2) {\n return false;\n }\n if (requireMultipleContexts) {\n return occurrences.contextIndex === -1;\n }\n return occurrences.containerIndex === -1 || occurrences.maxStart - occurrences.minStart >= length;\n };\n\n // A window whose statements all share one normalized shape (sixteen `let x = 0;` declarations,\n // a constant table) is a homogeneous preamble, not a copy-paste: requiring two distinct\n // per-statement shapes keeps such runs out of duplicate groups and the duplication ratio.\n const hasDistinctStatements = (window: SequenceWindow): boolean => {\n const hashes = containerWindows[window.containerIndex]?.statementHashes ?? [];\n const firstHash = hashes[window.start];\n for (let index = window.start + 1; index < window.start + window.length; index += 1) {\n if (hashes[index] !== firstHash) {\n return true;\n }\n }\n return false;\n };\n\n const maximalWindows: SequenceWindow[] = [];\n for (const [containerIndex, windows] of containerWindows.entries()) {\n for (const [start, row] of windows.windowKeysByStart.entries()) {\n for (const [length, windowKey] of row.entries()) {\n if (!repeats(windowKey, length) || !hasDistinctStatements({ containerIndex, start, length })) {\n continue;\n }\n // Dominated windows are skipped: the one-statement extension also repeats, so a larger\n // candidate covering this window exists.\n const extendedRight = windows.windowKeysByStart[start]?.[length + 1];\n const extendedLeft = windows.windowKeysByStart[start - 1]?.[length + 1];\n if (repeats(extendedRight, length + 1) || repeats(extendedLeft, length + 1)) {\n continue;\n }\n maximalWindows.push({ containerIndex, start, length });\n }\n }\n }\n\n // The rolling hash anonymizes identifiers per statement, so a window can look repeated coarsely\n // while its exact (window-consistent) fingerprints differ, and a longer window's match can\n // dominate sub-windows that other copies still need (three copies where only two extend one\n // statement further). Every emitted window therefore exposes its repeating, unvisited\n // sub-windows; `visited` bounds the worklist and lengths strictly decrease, so it terminates.\n const visited = new Set(maximalWindows.map(windowId));\n let frontier = maximalWindows;\n while (frontier.length > 0) {\n const emitted: SequenceWindow[] = [];\n for (const window of frontier) {\n const statements = containers[window.containerIndex];\n const first = statements?.[window.start];\n const last = statements?.[window.start + window.length - 1];\n const context = contextAt(window.containerIndex);\n if (!first || !last || !context) {\n continue;\n }\n const fingerprint = `s:${fingerprintKey(context.tokens, context.literalCountPrefix, first.startTokenIndex, last.endTokenIndex)}`;\n candidates.push({\n candidate: toCandidate(fingerprint, first.startTokenIndex, last.endTokenIndex, first, last),\n contextIndex: contextIndexByContainer[window.containerIndex] ?? 0,\n });\n emitted.push(window);\n }\n frontier = [];\n for (const window of emitted) {\n for (const start of [window.start, window.start + 1]) {\n const subWindow = { containerIndex: window.containerIndex, start, length: window.length - 1 };\n const subWindowKey = containerWindows[window.containerIndex]?.windowKeysByStart[start]?.[subWindow.length];\n if (\n visited.has(windowId(subWindow)) ||\n !repeats(subWindowKey, subWindow.length) ||\n !hasDistinctStatements(subWindow)\n ) {\n continue;\n }\n visited.add(windowId(subWindow));\n frontier.push(subWindow);\n }\n }\n }\n return candidates;\n}\n\nfunction windowId(window: SequenceWindow): string {\n return `${window.containerIndex}:${window.start}:${window.length}`;\n}\n\ninterface ContainerWindows {\n /** windowKeysByStart[start][length] is the rolling-hash key of the window, or undefined if it is below the size thresholds. */\n windowKeysByStart: (number | undefined)[][];\n /** Per-statement fingerprint hashes, for the distinct-shape requirement on windows. */\n statementHashes: number[];\n}\n\nfunction enumerateContainerWindows(tokens: Token[], statements: TokenRange[], minTokens: number): ContainerWindows {\n const statementHashes = statements.map((statement) =>\n fingerprintHash(tokens, statement.startTokenIndex, statement.endTokenIndex)\n );\n const windowKeysByStart: (number | undefined)[][] = [];\n for (let start = 0; start < statements.length; start += 1) {\n const row: (number | undefined)[] = [];\n let hash = 5381;\n let tokenCount = 0;\n const maxEnd = Math.min(statements.length, start + maxSequenceStatementCount);\n for (let end = start; end < maxEnd; end += 1) {\n const statement = statements[end];\n const statementHash = statementHashes[end];\n if (!statement || statementHash === undefined) {\n break;\n }\n hash = combineHashes(hash, statementHash);\n tokenCount += statement.endTokenIndex - statement.startTokenIndex;\n const statementCount = end - start + 1;\n row[statementCount] =\n statementCount >= minSequenceStatementCount && tokenCount >= minTokens\n ? combineHashes(hash, statementCount)\n : undefined;\n }\n windowKeysByStart.push(row);\n }\n return { windowKeysByStart, statementHashes };\n}\n\n/**\n * Longest-common-subsequence LENGTH of two symbol sequences via the Allison–Dix bit-parallel\n * recurrence (O(|a|/32 · |b|) words): per symbol of `b`, `x = match | v` and\n * `v = x & ~(x - ((v << 1) | 1))` over multi-word bit vectors; the set bits of `v` count the LCS.\n */\nexport function lcsLength(a: Int32Array, b: Int32Array): number {\n return createLcsLengthCounter(a)(b);\n}\n\n/**\n * lcsLength against a fixed `a`, whose per-symbol position masks are built once and reused for\n * every `b` compared with it.\n */\nexport function createLcsLengthCounter(a: Int32Array): (b: Int32Array) => number {\n const wordCount = (a.length + 31) >>> 5;\n const positionMasks = new Map<number, Uint32Array>();\n for (const [index, symbol] of a.entries()) {\n let mask = positionMasks.get(symbol);\n if (!mask) {\n mask = new Uint32Array(wordCount);\n positionMasks.set(symbol, mask);\n }\n const word = index >>> 5;\n // The Uint32Array store wraps the signed int32 bit pattern to unsigned. The `?? 0` guards in\n // this function are required by noUncheckedIndexedAccess (typed-array reads type as\n // `number | undefined`), not redundancy: every index is in bounds.\n mask[word] = (mask[word] ?? 0) | (1 << (index & 31));\n }\n\n const v = new Uint32Array(wordCount);\n return (b) => {\n v.fill(0);\n for (const symbol of b) {\n const matchMask = positionMasks.get(symbol);\n // `(v << 1) | 1` shifts a carry bit across words; subtraction borrows across words.\n let shiftCarry = 1;\n let borrow = 0;\n for (let word = 0; word < wordCount; word += 1) {\n const previous = v[word] ?? 0;\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `>>> 0` reinterprets the signed int32 bit pattern as unsigned so the borrow subtraction below compares magnitudes; Math.trunc would keep it negative.\n const x = ((matchMask?.[word] ?? 0) | previous) >>> 0;\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- same unsigned reinterpretation as `x`.\n const shifted = ((previous << 1) | shiftCarry) >>> 0;\n shiftCarry = previous >>> 31;\n const difference = x - shifted - borrow;\n borrow = difference < 0 ? 1 : 0;\n // The Uint32Array store wraps the signed int32 bit pattern to unsigned.\n v[word] = x & ~difference;\n }\n }\n\n let length = 0;\n for (const word of v) {\n length += popCount(word);\n }\n return length;\n };\n}\n\nfunction popCount(value: number): number {\n let count = value - ((value >>> 1) & 0x55_55_55_55);\n count = (count & 0x33_33_33_33) + ((count >>> 2) & 0x33_33_33_33);\n return (Math.imul((count + (count >>> 4)) & 0x0F_0F_0F_0F, 0x01_01_01_01) >>> 24) & 0xFF;\n}\n\nfunction toCandidate(\n fingerprint: string,\n startTokenIndex: number,\n endTokenIndex: number,\n first: TokenRange,\n last: TokenRange\n): DuplicateCandidate {\n return {\n fingerprint,\n tokenCount: endTokenIndex - startTokenIndex,\n startTokenIndex,\n endTokenIndex,\n startIndex: first.startIndex,\n endIndex: last.endIndex,\n startLine: first.startLine,\n endLine: last.endLine,\n };\n}\n\n/** Caches of hashText/hashText2 over '$0', '$1', ... so anonymized identifiers hash without allocating. */\nconst anonymizedIndexHashes: number[] = [];\nconst anonymizedIndexHashes2: number[] = [];\n\nfunction anonymizedIndexHash(index: number): number {\n let hash = anonymizedIndexHashes[index];\n if (hash === undefined) {\n hash = hashText(`$${index}`);\n anonymizedIndexHashes[index] = hash;\n }\n return hash;\n}\n\nfunction anonymizedIndexHash2(index: number): number {\n let hash = anonymizedIndexHashes2[index];\n if (hash === undefined) {\n hash = hashText2(`$${index}`);\n anonymizedIndexHashes2[index] = hash;\n }\n return hash;\n}\n\n/**\n * Content key of a token range: two independent 32-bit hashes over the normalized token sequence\n * (identifiers anonymized consistently by first-occurrence order) plus the token count. Regions\n * with equal keys are treated as equal content; a collision would need both 32-bit hashes and the\n * length to coincide, which is negligible for a metrics report. The format and arithmetic match\n * fingerprint_key in native/src/duplication.rs exactly, so window candidates fingerprinted here\n * group together with the per-file candidates the addon catalogues.\n */\nfunction fingerprintKey(\n tokens: Token[],\n literalCountPrefix: Int32Array,\n startTokenIndex: number,\n endTokenIndex: number\n): string {\n const literalCount = (literalCountPrefix[endTokenIndex] ?? 0) - (literalCountPrefix[startTokenIndex] ?? 0);\n const literalDense = isLiteralDense(literalCount, endTokenIndex - startTokenIndex);\n const [primary, secondary] = fingerprintHashPair(tokens, startTokenIndex, endTokenIndex, literalDense);\n return `${primary}:${secondary}:${endTokenIndex - startTokenIndex}`;\n}\n\n/**\n * A single 32-bit summary of a range for the coarse rolling-hash phase. Deliberately\n * density-agnostic: density is a property of the final candidate REGION, and folding literal\n * values into per-statement hashes would make a dense statement inside a logic-heavy window\n * (`const weights = [1, 2, 3];`) block the window from ever being enumerated. The coarse phase\n * over-approximates on shape alone; the exact region fingerprint still applies the density rule.\n */\nfunction fingerprintHash(tokens: Token[], startTokenIndex: number, endTokenIndex: number): number {\n const [primary, secondary] = fingerprintHashPair(tokens, startTokenIndex, endTokenIndex, false);\n // XOR already coerces to int32, matching the native side's i32 arithmetic.\n return primary ^ Math.imul(secondary, 31);\n}\n\nfunction fingerprintHashPair(\n tokens: Token[],\n startTokenIndex: number,\n endTokenIndex: number,\n foldLiteralValues: boolean\n): [number, number] {\n const indexByIdentifier = new Map<string, number>();\n let primary = 5381;\n let secondary = 52_711;\n for (let index = startTokenIndex; index < endTokenIndex; index += 1) {\n const token = tokens[index];\n if (!token) {\n continue;\n }\n // Each accumulator consumes its own independent per-token hash: sharing one would collapse\n // the key to 32 effective bits (a single djb2 collision would equate whole regions).\n let part: number;\n let part2: number;\n if (token.kind === 'id') {\n let identifierIndex = indexByIdentifier.get(token.text);\n if (identifierIndex === undefined) {\n identifierIndex = indexByIdentifier.size;\n indexByIdentifier.set(token.text, identifierIndex);\n }\n part = anonymizedIndexHash(identifierIndex);\n part2 = anonymizedIndexHash2(identifierIndex);\n } else {\n part = token.textHash;\n part2 = token.textHash2;\n }\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `| 0` wraps the sum to int32 (Math.trunc does not), which must match the native side's wrapping i32 arithmetic.\n primary = (Math.imul(primary, 31) + part) | 0;\n secondary = Math.imul(secondary, 37) ^ part2;\n if (foldLiteralValues && token.literalHash !== undefined && token.literalHash2 !== undefined) {\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `| 0` wraps the sum to int32 (Math.trunc does not), which must match the native side's wrapping i32 arithmetic.\n primary = (Math.imul(primary, 31) + token.literalHash) | 0;\n secondary = Math.imul(secondary, 37) ^ token.literalHash2;\n }\n }\n return [primary, secondary];\n}\n\n/** djb2-style hash; XOR keeps the value in signed 32-bit range, which is fine for a grouping key. */\nfunction hashText(text: string): number {\n let hash = 5381;\n for (let index = 0; index < text.length; index += 1) {\n // oxlint-disable-next-line unicorn/prefer-code-point -- djb2 hashes UTF-16 code units; codePointAt would hash surrogate pairs twice (full code point, then the lone low surrogate).\n hash = Math.imul(hash, 33) ^ text.charCodeAt(index);\n }\n return hash;\n}\n\n/** FNV-1a over UTF-16 code units: independent of hashText so the two accumulators never share input. */\nfunction hashText2(text: string): number {\n let hash = -2_128_831_035; // 2166136261 as int32 (the FNV-1a offset basis)\n for (let index = 0; index < text.length; index += 1) {\n // oxlint-disable-next-line unicorn/prefer-code-point -- hashes UTF-16 code units like hashText.\n hash = Math.imul(hash ^ text.charCodeAt(index), 16_777_619);\n }\n return hash;\n}\n\nfunction combineHashes(hash: number, value: number): number {\n return Math.imul(hash, 31) + value;\n}\n\n/**\n * Merges duplicate groups separated by a small token gap into one gapped (Type-3) clone group: a\n * copy edited in one spot splits into two exact groups whose occurrences sit side by side in the\n * same order. Occurrences are paired greedily in source order; a merge happens when the pairing\n * fully pairs at least one group with at least two pairs. Equal-cardinality groups whose\n * occurrences all pair merge into one group as before. When cardinalities differ (a fragment also\n * occurs standalone: prefix ×3, suffix ×2), the fully-paired group is subsumed into the merged\n * gapped group while the other group is RETAINED with ALL its occurrences: dropping the leftover\n * would lose duplicated-line coverage, and reporting it alone would make a single-occurrence group\n * (contradicting duplicateBlockGroupCount's \"appears more than once\" meaning). Line coverage\n * unions ranges, so the overlap between the retained exact group and the merged group is harmless.\n * Merging repeats to a fixpoint so a clone edited in several spots still reassembles; it\n * terminates because a merge either removes the input groups it replaces or marks their paired\n * occurrences as counted elsewhere, and only unmarked occurrences of remaining groups pair, so the\n * number of pairable occurrences strictly decreases with every merge. Gap tokens are not matched\n * content: line coverage and sizes count only the matched segments. Generic so cross-file merging\n * can thread file identity through occurrences.\n */\nexport function mergeAdjacentGroups<T extends CountedOccurrence>(\n groups: T[][],\n maxGapTokens: number,\n isReportableGroup: (group: T[]) => boolean = () => true\n): T[][] {\n if (maxGapTokens <= 0 || groups.length < 2) {\n return groups;\n }\n // Deterministic processing order (mirrored by the native side): by first occurrence position.\n groups.sort(compareGroups);\n for (let restart = true; restart;) {\n restart = false;\n const partnersByGroup = collectGapAdjacentPartners(groups, maxGapTokens);\n for (let leftIndex = 0; leftIndex < groups.length && !restart; leftIndex += 1) {\n for (const rightIndex of partnersByGroup[leftIndex] ?? []) {\n const left = groups[leftIndex];\n const right = groups[rightIndex];\n if (!left || !right) {\n continue;\n }\n const forward = mergeGroups(left, right, maxGapTokens, isReportableGroup);\n const result = forward ?? mergeGroups(right, left, maxGapTokens, isReportableGroup);\n if (!result) {\n continue;\n }\n const leftReplaced = forward ? result.firstReplaced : result.secondReplaced;\n const rightReplaced = forward ? result.secondReplaced : result.firstReplaced;\n if (leftReplaced && rightReplaced) {\n groups[leftIndex] = result.merged;\n groups.splice(rightIndex, 1);\n } else if (rightReplaced) {\n groups[rightIndex] = result.merged;\n } else if (leftReplaced) {\n groups[leftIndex] = result.merged;\n } else {\n // Both groups stay (each is only partly paired, or reports nested copies of its own), so\n // the merged group joins them instead of taking a place.\n groups.push(result.merged);\n }\n // A group that stays keeps ALL its occurrences (line coverage must not shrink, and a\n // reported group must keep >= 2 occurrences), so its paired occurrences now also live\n // inside the merged group's occurrences: mark them so duplicateBlockCount counts each\n // token span once, and so the same pair cannot merge again.\n for (const occurrence of result.pairedRetained) {\n occurrence.spanCountedElsewhere = true;\n }\n groups.sort(compareGroups);\n restart = true;\n break;\n }\n }\n }\n return groups;\n}\n\n/**\n * Per group index, the ascending indexes of later groups that mergeGroups can pair with it in\n * either direction: an occurrence of one starts within `maxGapTokens` after an occurrence of the\n * other ends. Every other pair fails mergeGroups, so skipping it leaves the fixpoint unchanged\n * while each restart stops costing a comparison per pair of groups.\n */\nfunction collectGapAdjacentPartners(groups: CountedOccurrence[][], maxGapTokens: number): number[][] {\n const starts: { startTokenIndex: number; groupIndex: number }[] = [];\n for (const [groupIndex, group] of groups.entries()) {\n for (const occurrence of group) {\n starts.push({ startTokenIndex: occurrence.startTokenIndex, groupIndex });\n }\n }\n starts.sort((left, right) => left.startTokenIndex - right.startTokenIndex);\n const partners = groups.map(() => new Set<number>());\n for (const [groupIndex, group] of groups.entries()) {\n for (const occurrence of group) {\n for (\n let index = lowerBoundByStart(starts, occurrence.endTokenIndex);\n index < starts.length && (starts[index]?.startTokenIndex ?? 0) <= occurrence.endTokenIndex + maxGapTokens;\n index += 1\n ) {\n const other = starts[index]?.groupIndex ?? groupIndex;\n if (other !== groupIndex) {\n partners[Math.min(other, groupIndex)]?.add(Math.max(other, groupIndex));\n }\n }\n }\n }\n return partners.map((set) => [...set].toSorted((left, right) => left - right));\n}\n\nfunction lowerBoundByStart(sorted: { startTokenIndex: number }[], target: number): number {\n let low = 0;\n let high = sorted.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((sorted[middle]?.startTokenIndex ?? 0) < target) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low;\n}\n\nfunction compareGroups(left: CountedOccurrence[], right: CountedOccurrence[]): number {\n const leftFirst = left[0];\n const rightFirst = right[0];\n return (\n (leftFirst?.startTokenIndex ?? 0) - (rightFirst?.startTokenIndex ?? 0) ||\n (leftFirst?.endTokenIndex ?? 0) - (rightFirst?.endTokenIndex ?? 0)\n );\n}\n\ninterface MergeResult<T> {\n merged: T[];\n /**\n * Whether the merged group takes the respective input group's place: its standalone occurrences\n * were all paired and it holds no nested copies that only it can report.\n */\n firstReplaced: boolean;\n secondReplaced: boolean;\n /** The occurrences of groups that stay, which the merged group's spans now also cover. */\n pairedRetained: T[];\n}\n\n/**\n * Pairs `second` occurrences with gap-preceding `first` occurrences, greedily in source order:\n * each trailing occurrence takes the earliest unused leading occurrence within the gap, and a\n * pair's leading must start at or after the previous pair's trailing end so merged spans never\n * overlap. A merge needs at least two pairs (a merged group must still mean \"appears more than\n * once\") and must fully consume at least one group; for equal cardinalities this reduces to the\n * strict all-pairs merge, so pre-partial-merge behavior is unchanged there.\n */\nfunction mergeGroups<T extends CountedOccurrence>(\n first: T[],\n second: T[],\n maxGapTokens: number,\n isReportableGroup: (group: T[]) => boolean\n): MergeResult<T> | undefined {\n // Occurrences a previous partial merge already paired into a merged group must not pair again:\n // their spans already live inside that merged group, so re-pairing them would assemble a second,\n // competing merged group instead of letting the existing merged group extend (and would count\n // the same span twice). Consumption is still judged against the FULL group, so a group holding\n // shared occurrences is never subsumed away; nested copies, which the merged span would not\n // cover anyway, are left out of that judgment so they cannot veto a merge of the standalone\n // copies.\n const [leadings, firstLength] = pairableOccurrences(first);\n const [trailings, secondLength] = pairableOccurrences(second);\n const pairs: [T, T][] = [];\n let leadingIndex = 0;\n let previousTrailingEnd = -1;\n for (const trailing of trailings) {\n // Leadings ending too far before this trailing can never pair a later (even farther) one.\n while (leadingIndex < leadings.length) {\n const leading = leadings[leadingIndex];\n if (leading && leading.endTokenIndex + maxGapTokens < trailing.startTokenIndex) {\n leadingIndex += 1;\n } else {\n break;\n }\n }\n const leading = leadings[leadingIndex];\n if (\n leading &&\n leading.endTokenIndex <= trailing.startTokenIndex &&\n leading.startTokenIndex >= previousTrailingEnd\n ) {\n pairs.push([leading, trailing]);\n previousTrailingEnd = trailing.endTokenIndex;\n leadingIndex += 1;\n }\n }\n const firstFullyPaired = pairs.length === firstLength;\n const secondFullyPaired = pairs.length === secondLength;\n if (pairs.length < 2 || (!firstFullyPaired && !secondFullyPaired)) {\n return undefined;\n }\n // A group holding nested copies stays even when all its standalone copies pair: the merged\n // group's content is larger than what those copies matched, so only the original group can\n // report which files share the matched fragment.\n const firstReplaced = firstFullyPaired && !first.some((occurrence) => occurrence.nestedInLargerGroup);\n const secondReplaced = secondFullyPaired && !second.some((occurrence) => occurrence.nestedInLargerGroup);\n const merged = pairs.map(([leading, trailing]) => ({\n ...leading,\n // A merged occurrence is a fresh span combination; it never inherits shared-span marks.\n spanCountedElsewhere: undefined,\n nestedInLargerGroup: undefined,\n segments: [...leading.segments, ...trailing.segments],\n tokenCount: leading.tokenCount + trailing.tokenCount,\n endTokenIndex: trailing.endTokenIndex,\n endIndex: trailing.endIndex,\n endLine: trailing.endLine,\n }));\n // Only occurrences of one file pair (file offsets exceed the gap), so a merged group spans the\n // files its pairs sit in: with nested copies left out of pairing, that can be fewer files than\n // the input groups covered, and a merged group that is no longer reportable must not form.\n if (!isReportableGroup(merged)) {\n return undefined;\n }\n const pairedRetained = [\n ...(firstReplaced ? [] : pairs.map(([leading]) => leading)),\n ...(secondReplaced ? [] : pairs.map(([, trailing]) => trailing)),\n ];\n return { merged, firstReplaced, secondReplaced, pairedRetained };\n}\n\n/** One pass over a group: its pairable occurrences and its non-nested occurrence count. */\nfunction pairableOccurrences<T extends CountedOccurrence>(group: T[]): [T[], number] {\n const pairable: T[] = [];\n let length = 0;\n for (const occurrence of group) {\n if (!occurrence.nestedInLargerGroup) {\n length += 1;\n }\n // Either flag keeps an occurrence out of pairing: its span is already counted elsewhere, or it\n // is a nested copy of content the merged span would not cover.\n if (!occurrence.spanCountedElsewhere && !occurrence.nestedInLargerGroup) {\n pairable.push(occurrence);\n }\n }\n return [pairable, length];\n}\n\n/**\n * Redundant copies one group adds to duplicateBlockCount. Each redundant occurrence contributes\n * one count per matched fragment, so merging a gapped clone's fragments into one group does not\n * halve duplicateBlockCount: an edited two-fragment pair still counts 2, exactly as its unmerged\n * fragments did. Occurrence shapes can differ within one group (a\n * gap-merged exact pair plus an appended whole-block near-miss copy), so every occurrence's\n * fragments are summed and one representative — the largest — is deducted, keeping the count\n * independent of source order. Occurrences a partial gapped merge also paired into a merged group\n * are skipped: their spans are already counted there, and such a retained group deducts no\n * representative of its own — the merged group's representative already stands for the shared\n * content — so no token span contributes to the count twice.\n */\nexport function countRedundantFragments(group: CountedOccurrence[]): number {\n let fragmentCount = 0;\n let maxFragmentCount = 0;\n let hasSharedOccurrence = false;\n for (const occurrence of group) {\n if (occurrence.spanCountedElsewhere) {\n hasSharedOccurrence = true;\n continue;\n }\n fragmentCount += occurrence.segments.length;\n maxFragmentCount = Math.max(maxFragmentCount, occurrence.segments.length);\n }\n return hasSharedOccurrence ? fragmentCount : fragmentCount - maxFragmentCount;\n}\n\n/** Adds the 1-based code lines the segment's matched tokens cover; shared with cross-file coverage. */\nexport function collectSegmentLines(\n segment: { startTokenIndex: number; endTokenIndex: number },\n tokens: Token[],\n codeLineNumbers: Set<number> | undefined,\n duplicatedLines: Set<number>\n): void {\n for (let index = segment.startTokenIndex; index < segment.endTokenIndex; index += 1) {\n const token = tokens[index];\n for (let row = token?.startRow ?? 0; row <= (token?.endRow ?? -1); row += 1) {\n if (!codeLineNumbers || codeLineNumbers.has(row + 1)) {\n duplicatedLines.add(row + 1);\n }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"duplication.js","names":[],"sources":["../src/duplication.ts"],"sourcesContent":["import { clamp } from '@willbooster/shared-lib';\nimport type { DuplicationOptions } from './types.js';\n\n/**\n * Project-level duplication machinery operating on normalized token streams. Tokenization itself\n * (parsing, identifier anonymization, literal normalization) happens in the Rust addon, which\n * serializes each file's Token stream and statement structure; the helpers here match statement\n * windows across files, merge gap-adjacent groups, and count duplicated lines over that data.\n */\n\nexport const defaultDuplicationOptions: Required<DuplicationOptions> = {\n minTokens: 40,\n maxGapTokens: 30,\n minSimilarityPercent: 70,\n};\n\n/**\n * Fills defaults for absent settings, applying the same normalization as the native boundary's\n * clampToU32 — NaN (e.g. `Number(unsetEnvVariable)`) counts as absent, and other values truncate\n * and clamp to [0, u32::MAX] — so the TypeScript half of cross-file matching cannot diverge from\n * the natively collected candidates on such input.\n */\nexport function resolveDuplicationOptions(options?: DuplicationOptions): Required<DuplicationOptions> {\n return {\n minTokens: resolveOption(options?.minTokens, defaultDuplicationOptions.minTokens),\n maxGapTokens: resolveOption(options?.maxGapTokens, defaultDuplicationOptions.maxGapTokens),\n minSimilarityPercent: resolveOption(options?.minSimilarityPercent, defaultDuplicationOptions.minSimilarityPercent),\n };\n}\n\nfunction resolveOption(value: number | undefined, fallback: number): number {\n return value === undefined || Number.isNaN(value) ? fallback : clamp(Math.trunc(value), 0, 0xFF_FF_FF_FF);\n}\n\n/** Minimum consecutive statements for a statement-sequence duplicate candidate. */\nconst minSequenceStatementCount = 2;\n/**\n * Caps the window length so statement-sequence enumeration stays linear in the statement count.\n * Heterogeneous clones longer than the cap are reported as capped windows (a deliberate\n * conservative undercount trading completeness for bounded discovery cost).\n */\nconst maxSequenceStatementCount = 100;\n\n/**\n * A region whose normalized tokens are at least 20% literal values is data-like (a lookup table, a\n * constant list, a value-mapping switch), not logic: literal values re-enter its fingerprint so\n * tables that merely share their shape stop counting as copy-paste. Compared in integer math\n * (5 * literals >= total) so the TypeScript and native sides cannot disagree on the boundary.\n */\nfunction isLiteralDense(literalCount: number, tokenCount: number): boolean {\n return literalCount * 5 >= tokenCount;\n}\n\nexport interface Token {\n /** Normalization target: identifiers to anonymize, literal kind tags, or the raw token text. */\n kind: 'id' | 'text';\n text: string;\n /**\n * Two INDEPENDENT hashes of `text` (djb2 and FNV-1a), precomputed so fingerprinting nested\n * regions never re-hashes a token. Feeding the same per-token hash to both fingerprint\n * accumulators would collapse the key to 32 effective bits: one djb2 collision between two\n * token texts would then equate whole regions.\n */\n textHash: number;\n textHash2: number;\n /** Hash pair of a value-carrying literal's value, folded into data-like region fingerprints. */\n literalHash?: number;\n literalHash2?: number;\n /**\n * True for verbatim-kept NAMES (member/callee/type names, named grammar leaves): together with\n * value-carrying literals these are the content-bearing tokens the near-miss content gate\n * counts. Keywords, operators, and punctuation come from unnamed nodes and stay false.\n */\n isName?: boolean;\n /** 0-based source rows the token occupies, so line coverage counts only matched-token lines. */\n startRow: number;\n endRow: number;\n}\n\n/** A token span with its source position; plain data so cross-file matching can retain it. */\nexport interface TokenRange {\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/** A contiguous run of matched tokens; gapped (merged) duplicates carry several per occurrence. */\nexport interface TokenSegment {\n startTokenIndex: number;\n endTokenIndex: number;\n}\n\ninterface DuplicateCandidate {\n fingerprint: string;\n tokenCount: number;\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\nexport interface CountedOccurrence {\n /**\n * The token runs the occurrence covers, matched throughout except in a near-miss copy (see\n * `matchedRuns`); more than one once gapped groups are merged.\n */\n segments: TokenSegment[];\n /**\n * The token runs of a near-miss copy that its partners match, within its segments;\n * absent for an exact or gapped copy, whose segments are matched throughout.\n */\n matchedRuns?: TokenSegment[];\n /**\n * The exact fragments coalesced into a near-miss copy: each line they reach is duplicated,\n * whatever else of the copy shares the line.\n */\n exactSegments?: TokenSegment[];\n /**\n * Set on occurrences whose span another reported group already counts: a retained group's\n * occurrences that a partial gapped merge also paired into a merged group, and cross-file copies\n * nested inside a larger group's region. Block counting must not count them again.\n */\n spanCountedElsewhere?: boolean;\n /**\n * Set on cross-file copies nested inside a larger group's region (they also set\n * `spanCountedElsewhere`). They never pair in gapped merging, and they do not keep their group's\n * standalone copies from merging, which they are not copies of. They do keep the merged group\n * from taking their group's place, since only the original group reports the nesting.\n */\n nestedInLargerGroup?: boolean;\n /** Sum of segment token counts (the gap tokens are not matched content). */\n tokenCount: number;\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/** A duplicate region found in one file, exported for cross-file matching by fingerprint. */\nexport interface CrossFileDuplicateCandidate {\n /** Content key: equal fingerprints mean equal normalized token sequences (up to hash collision). */\n fingerprint: string;\n tokenCount: number;\n /** Token positions within the owning file, for cross-file gapped (Type-3) merging. */\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/**\n * One file's contribution to cross-file clone detection: catalogued candidates plus the normalized\n * token stream and statement structure, so the project-level pass can match partial statement runs\n * (windows that a single file cannot know repeat elsewhere) and merge gap-adjacent groups.\n */\nexport interface CrossFileDuplicationFileData {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n /**\n * The mutually disjoint blocks (at least `minTokens` long, not literal-dense) compared for\n * cross-file near-miss (Type-3) clones. Optional for backward compatibility; without it, the\n * file takes part in exact and gapped cross-file matching only.\n */\n nearMissBlocks?: TokenRange[];\n /**\n * 1-based lines that are neither blank nor comment-only, so cross-file line coverage counts only\n * code lines (blank rows inside multi-row tokens such as template literals carry no content).\n * Optional for backward compatibility; without it, every matched-token row counts.\n */\n codeLineNumbers?: Set<number>;\n}\n\n/** literalCountPrefix[i] = value-carrying literal tokens in tokens[0..i), for O(1) density checks. */\nexport function buildLiteralCountPrefix(tokens: Token[]): Int32Array {\n const prefix = new Int32Array(tokens.length + 1);\n for (const [index, token] of tokens.entries()) {\n prefix[index + 1] = (prefix[index] ?? 0) + (token.literalHash === undefined ? 0 : 1);\n }\n return prefix;\n}\n\ninterface WindowOccurrences {\n count: number;\n /** -1 once occurrences span more than one container. */\n containerIndex: number;\n /** -1 once occurrences span more than one context (file). */\n contextIndex: number;\n minStart: number;\n maxStart: number;\n}\n\ninterface SequenceWindow {\n containerIndex: number;\n start: number;\n length: number;\n}\n\n/** One file's token stream and statement containers, as a window-matching context. */\nexport interface SequenceWindowContext {\n tokens: Token[];\n literalCountPrefix: Int32Array;\n containers: TokenRange[][];\n}\n\nexport interface ContextualSequenceCandidate {\n candidate: DuplicateCandidate;\n contextIndex: number;\n}\n\n/**\n * Enumerates runs of consecutive sibling statements over one or more contexts (files). Every\n * container statement participates; only the window length is capped, so enumeration stays linear\n * in the statement count. Windows are grouped by a cheap rolling hash of per-statement\n * fingerprints, and only locally maximal repeated windows — those whose one-statement extensions\n * stop repeating — become candidates with an exact (window-consistent) fingerprint. Without the\n * maximality filter a degenerate file of near-identical statements would fingerprint every\n * sub-window of every repeated region. With `requireMultipleContexts` a window only counts as\n * repeated when its occurrences span at least two contexts (CPD-style cross-file matching): a\n * repeat confined to one file is that file's own concern, and emitting it here would flood the\n * project-level selection with unusable single-file groups.\n */\nexport function collectSequenceWindowCandidates(\n contexts: SequenceWindowContext[],\n minTokens: number,\n requireMultipleContexts: boolean\n): ContextualSequenceCandidate[] {\n const candidates: ContextualSequenceCandidate[] = [];\n const contextIndexByContainer: number[] = [];\n const containers: TokenRange[][] = [];\n for (const [contextIndex, context] of contexts.entries()) {\n for (const statements of context.containers) {\n contextIndexByContainer.push(contextIndex);\n containers.push(statements);\n }\n }\n const contextAt = (containerIndex: number): SequenceWindowContext | undefined =>\n contexts[contextIndexByContainer[containerIndex] ?? 0];\n const occurrencesByWindowKey = new Map<number, WindowOccurrences>();\n const containerWindows = containers.map((statements, containerIndex) =>\n enumerateContainerWindows(contextAt(containerIndex)?.tokens ?? [], statements, minTokens)\n );\n for (const [containerIndex, windows] of containerWindows.entries()) {\n const contextIndex = contextIndexByContainer[containerIndex] ?? 0;\n for (const [start, row] of windows.windowKeysByStart.entries()) {\n for (const windowKey of row) {\n if (windowKey === undefined) {\n continue;\n }\n const occurrences = occurrencesByWindowKey.get(windowKey);\n if (occurrences) {\n occurrences.count += 1;\n if (occurrences.containerIndex !== containerIndex) {\n occurrences.containerIndex = -1;\n }\n if (occurrences.contextIndex !== contextIndex) {\n occurrences.contextIndex = -1;\n }\n occurrences.minStart = Math.min(occurrences.minStart, start);\n occurrences.maxStart = Math.max(occurrences.maxStart, start);\n } else {\n occurrencesByWindowKey.set(windowKey, {\n count: 1,\n containerIndex,\n contextIndex,\n minStart: start,\n maxStart: start,\n });\n }\n }\n }\n }\n\n // A window only \"repeats\" when two of its occurrences can coexist without overlapping: sliding\n // matches inside a homogeneous run (start spread smaller than the window length) can never both\n // be counted and must neither qualify a window nor dominate its sub-windows. Cross-context\n // matching instead requires occurrences in two contexts, which coexist by construction.\n const repeats = (windowKey: number | undefined, length: number): boolean => {\n if (windowKey === undefined) {\n return false;\n }\n const occurrences = occurrencesByWindowKey.get(windowKey);\n if (occurrences === undefined || occurrences.count < 2) {\n return false;\n }\n if (requireMultipleContexts) {\n return occurrences.contextIndex === -1;\n }\n return occurrences.containerIndex === -1 || occurrences.maxStart - occurrences.minStart >= length;\n };\n\n // A window whose statements all share one normalized shape (sixteen `let x = 0;` declarations,\n // a constant table) is a homogeneous preamble, not a copy-paste: requiring two distinct\n // per-statement shapes keeps such runs out of duplicate groups and the duplication ratio.\n const hasDistinctStatements = (window: SequenceWindow): boolean => {\n const hashes = containerWindows[window.containerIndex]?.statementHashes ?? [];\n const firstHash = hashes[window.start];\n for (let index = window.start + 1; index < window.start + window.length; index += 1) {\n if (hashes[index] !== firstHash) {\n return true;\n }\n }\n return false;\n };\n\n const maximalWindows: SequenceWindow[] = [];\n for (const [containerIndex, windows] of containerWindows.entries()) {\n for (const [start, row] of windows.windowKeysByStart.entries()) {\n for (const [length, windowKey] of row.entries()) {\n if (!repeats(windowKey, length) || !hasDistinctStatements({ containerIndex, start, length })) {\n continue;\n }\n // Dominated windows are skipped: the one-statement extension also repeats, so a larger\n // candidate covering this window exists.\n const extendedRight = windows.windowKeysByStart[start]?.[length + 1];\n const extendedLeft = windows.windowKeysByStart[start - 1]?.[length + 1];\n if (repeats(extendedRight, length + 1) || repeats(extendedLeft, length + 1)) {\n continue;\n }\n maximalWindows.push({ containerIndex, start, length });\n }\n }\n }\n\n // The rolling hash anonymizes identifiers per statement, so a window can look repeated coarsely\n // while its exact (window-consistent) fingerprints differ, and a longer window's match can\n // dominate sub-windows that other copies still need (three copies where only two extend one\n // statement further). Every emitted window therefore exposes its repeating, unvisited\n // sub-windows; `visited` bounds the worklist and lengths strictly decrease, so it terminates.\n const visited = new Set(maximalWindows.map(windowId));\n let frontier = maximalWindows;\n while (frontier.length > 0) {\n const emitted: SequenceWindow[] = [];\n for (const window of frontier) {\n const statements = containers[window.containerIndex];\n const first = statements?.[window.start];\n const last = statements?.[window.start + window.length - 1];\n const context = contextAt(window.containerIndex);\n if (!first || !last || !context) {\n continue;\n }\n const fingerprint = `s:${fingerprintKey(context.tokens, context.literalCountPrefix, first.startTokenIndex, last.endTokenIndex)}`;\n candidates.push({\n candidate: toCandidate(fingerprint, first.startTokenIndex, last.endTokenIndex, first, last),\n contextIndex: contextIndexByContainer[window.containerIndex] ?? 0,\n });\n emitted.push(window);\n }\n frontier = [];\n for (const window of emitted) {\n for (const start of [window.start, window.start + 1]) {\n const subWindow = { containerIndex: window.containerIndex, start, length: window.length - 1 };\n const subWindowKey = containerWindows[window.containerIndex]?.windowKeysByStart[start]?.[subWindow.length];\n if (\n visited.has(windowId(subWindow)) ||\n !repeats(subWindowKey, subWindow.length) ||\n !hasDistinctStatements(subWindow)\n ) {\n continue;\n }\n visited.add(windowId(subWindow));\n frontier.push(subWindow);\n }\n }\n }\n return candidates;\n}\n\nfunction windowId(window: SequenceWindow): string {\n return `${window.containerIndex}:${window.start}:${window.length}`;\n}\n\ninterface ContainerWindows {\n /** windowKeysByStart[start][length] is the rolling-hash key of the window, or undefined if it is below the size thresholds. */\n windowKeysByStart: (number | undefined)[][];\n /** Per-statement fingerprint hashes, for the distinct-shape requirement on windows. */\n statementHashes: number[];\n}\n\nfunction enumerateContainerWindows(tokens: Token[], statements: TokenRange[], minTokens: number): ContainerWindows {\n const statementHashes = statements.map((statement) =>\n fingerprintHash(tokens, statement.startTokenIndex, statement.endTokenIndex)\n );\n const windowKeysByStart: (number | undefined)[][] = [];\n for (let start = 0; start < statements.length; start += 1) {\n const row: (number | undefined)[] = [];\n let hash = 5381;\n let tokenCount = 0;\n const maxEnd = Math.min(statements.length, start + maxSequenceStatementCount);\n for (let end = start; end < maxEnd; end += 1) {\n const statement = statements[end];\n const statementHash = statementHashes[end];\n if (!statement || statementHash === undefined) {\n break;\n }\n hash = combineHashes(hash, statementHash);\n tokenCount += statement.endTokenIndex - statement.startTokenIndex;\n const statementCount = end - start + 1;\n row[statementCount] =\n statementCount >= minSequenceStatementCount && tokenCount >= minTokens\n ? combineHashes(hash, statementCount)\n : undefined;\n }\n windowKeysByStart.push(row);\n }\n return { windowKeysByStart, statementHashes };\n}\n\n/**\n * Longest-common-subsequence LENGTH of two symbol sequences via the Allison–Dix bit-parallel\n * recurrence (O(|a|/32 · |b|) words): per symbol of `b`, `x = match | v` and\n * `v = x & ~(x - ((v << 1) | 1))` over multi-word bit vectors; the set bits of `v` count the LCS.\n */\nexport function lcsLength(a: Int32Array, b: Int32Array): number {\n return createLcsLengthCounter(a)(b);\n}\n\n/**\n * lcsLength against a fixed `a`, whose per-symbol position masks are built once and reused for\n * every `b` compared with it.\n */\nexport function createLcsLengthCounter(a: Int32Array): (b: Int32Array) => number {\n const growthBits = createLcsGrowthBits(a);\n return (b) => {\n let length = 0;\n for (const word of growthBits(b)) {\n length += popCount(word);\n }\n return length;\n };\n}\n\n/**\n * Per sequence `b`, one bit per position of `a`, set where the longest common subsequence of `b`\n * with the prefix of `a` ending there is longer than with the prefix before it, so the set bits up\n * to a position count that length. The returned words are overwritten by the next call.\n */\nfunction createLcsGrowthBits(a: Int32Array): (b: Int32Array) => Uint32Array {\n const wordCount = (a.length + 31) >>> 5;\n const positionMasks = new Map<number, Uint32Array>();\n for (const [index, symbol] of a.entries()) {\n let mask = positionMasks.get(symbol);\n if (!mask) {\n mask = new Uint32Array(wordCount);\n positionMasks.set(symbol, mask);\n }\n const word = index >>> 5;\n // The Uint32Array store wraps the signed int32 bit pattern to unsigned. The `?? 0` guards in\n // this function are required by noUncheckedIndexedAccess (typed-array reads type as\n // `number | undefined`), not redundancy: every index is in bounds.\n mask[word] = (mask[word] ?? 0) | (1 << (index & 31));\n }\n\n const v = new Uint32Array(wordCount);\n return (b) => {\n v.fill(0);\n for (const symbol of b) {\n const matchMask = positionMasks.get(symbol);\n // `(v << 1) | 1` shifts a carry bit across words; subtraction borrows across words.\n let shiftCarry = 1;\n let borrow = 0;\n for (let word = 0; word < wordCount; word += 1) {\n const previous = v[word] ?? 0;\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `>>> 0` reinterprets the signed int32 bit pattern as unsigned so the borrow subtraction below compares magnitudes; Math.trunc would keep it negative.\n const x = ((matchMask?.[word] ?? 0) | previous) >>> 0;\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- same unsigned reinterpretation as `x`.\n const shifted = ((previous << 1) | shiftCarry) >>> 0;\n shiftCarry = previous >>> 31;\n const difference = x - shifted - borrow;\n borrow = difference < 0 ? 1 : 0;\n // The Uint32Array store wraps the signed int32 bit pattern to unsigned.\n v[word] = x & ~difference;\n }\n }\n return v;\n };\n}\n\n/**\n * Which symbols of each sequence a longest common subsequence pairs, by Hirschberg's divide and\n * conquer over the bit-parallel recurrence, which alone yields lengths but no pairing.\n */\nexport function markLcs(a: Int32Array, b: Int32Array): [Uint8Array, Uint8Array] {\n const aMatched = new Uint8Array(a.length);\n const bMatched = new Uint8Array(b.length);\n markLcsInto(a, b, aMatched, bMatched);\n return [aMatched, bMatched];\n}\n\nfunction markLcsInto(a: Int32Array, b: Int32Array, aMatched: Uint8Array, bMatched: Uint8Array): void {\n // A common prefix or suffix belongs to some longest common subsequence.\n let prefix = 0;\n while (prefix < a.length && prefix < b.length && a[prefix] === b[prefix]) {\n prefix += 1;\n }\n let suffix = 0;\n while (\n suffix < a.length - prefix &&\n suffix < b.length - prefix &&\n a[a.length - 1 - suffix] === b[b.length - 1 - suffix]\n ) {\n suffix += 1;\n }\n const aEnd = a.length - suffix;\n const bEnd = b.length - suffix;\n aMatched.fill(1, 0, prefix).fill(1, aEnd);\n bMatched.fill(1, 0, prefix).fill(1, bEnd);\n const aRest = a.subarray(prefix, aEnd);\n const bRest = b.subarray(prefix, bEnd);\n const aRestMatched = aMatched.subarray(prefix, aEnd);\n const bRestMatched = bMatched.subarray(prefix, bEnd);\n if (aRest.length === 0 || bRest.length === 0) {\n return;\n }\n if (aRest.length === 1) {\n const position = bRest.indexOf(aRest[0] ?? 0);\n if (position !== -1) {\n aRestMatched[0] = 1;\n bRestMatched[position] = 1;\n }\n return;\n }\n const middle = aRest.length >>> 1;\n const forward = lcsPrefixLengths(aRest.subarray(0, middle), bRest);\n const backward = lcsPrefixLengths(aRest.subarray(middle).toReversed(), bRest.toReversed());\n let split = 0;\n let best = -1;\n for (let candidate = 0; candidate <= bRest.length; candidate += 1) {\n const length = (forward[candidate] ?? 0) + (backward[bRest.length - candidate] ?? 0);\n if (length > best) {\n best = length;\n split = candidate;\n }\n }\n markLcsInto(\n aRest.subarray(0, middle),\n bRest.subarray(0, split),\n aRestMatched.subarray(0, middle),\n bRestMatched.subarray(0, split)\n );\n markLcsInto(\n aRest.subarray(middle),\n bRest.subarray(split),\n aRestMatched.subarray(middle),\n bRestMatched.subarray(split)\n );\n}\n\n/** The longest-common-subsequence length of `a` with every prefix of `b`. */\nfunction lcsPrefixLengths(a: Int32Array, b: Int32Array): Int32Array {\n const bits = createLcsGrowthBits(b)(a);\n const lengths = new Int32Array(b.length + 1);\n for (let index = 0; index < b.length; index += 1) {\n lengths[index + 1] = (lengths[index] ?? 0) + (((bits[index >>> 5] ?? 0) >>> (index & 31)) & 1);\n }\n return lengths;\n}\n\nfunction popCount(value: number): number {\n let count = value - ((value >>> 1) & 0x55_55_55_55);\n count = (count & 0x33_33_33_33) + ((count >>> 2) & 0x33_33_33_33);\n return (Math.imul((count + (count >>> 4)) & 0x0F_0F_0F_0F, 0x01_01_01_01) >>> 24) & 0xFF;\n}\n\nfunction toCandidate(\n fingerprint: string,\n startTokenIndex: number,\n endTokenIndex: number,\n first: TokenRange,\n last: TokenRange\n): DuplicateCandidate {\n return {\n fingerprint,\n tokenCount: endTokenIndex - startTokenIndex,\n startTokenIndex,\n endTokenIndex,\n startIndex: first.startIndex,\n endIndex: last.endIndex,\n startLine: first.startLine,\n endLine: last.endLine,\n };\n}\n\n/** Caches of hashText/hashText2 over '$0', '$1', ... so anonymized identifiers hash without allocating. */\nconst anonymizedIndexHashes: number[] = [];\nconst anonymizedIndexHashes2: number[] = [];\n\nfunction anonymizedIndexHash(index: number): number {\n let hash = anonymizedIndexHashes[index];\n if (hash === undefined) {\n hash = hashText(`$${index}`);\n anonymizedIndexHashes[index] = hash;\n }\n return hash;\n}\n\nfunction anonymizedIndexHash2(index: number): number {\n let hash = anonymizedIndexHashes2[index];\n if (hash === undefined) {\n hash = hashText2(`$${index}`);\n anonymizedIndexHashes2[index] = hash;\n }\n return hash;\n}\n\n/**\n * Content key of a token range: two independent 32-bit hashes over the normalized token sequence\n * (identifiers anonymized consistently by first-occurrence order) plus the token count. Regions\n * with equal keys are treated as equal content; a collision would need both 32-bit hashes and the\n * length to coincide, which is negligible for a metrics report. The format and arithmetic match\n * fingerprint_key in native/src/duplication.rs exactly, so window candidates fingerprinted here\n * group together with the per-file candidates the addon catalogues.\n */\nfunction fingerprintKey(\n tokens: Token[],\n literalCountPrefix: Int32Array,\n startTokenIndex: number,\n endTokenIndex: number\n): string {\n const literalCount = (literalCountPrefix[endTokenIndex] ?? 0) - (literalCountPrefix[startTokenIndex] ?? 0);\n const literalDense = isLiteralDense(literalCount, endTokenIndex - startTokenIndex);\n const [primary, secondary] = fingerprintHashPair(tokens, startTokenIndex, endTokenIndex, literalDense);\n return `${primary}:${secondary}:${endTokenIndex - startTokenIndex}`;\n}\n\n/**\n * A single 32-bit summary of a range for the coarse rolling-hash phase. Deliberately\n * density-agnostic: density is a property of the final candidate REGION, and folding literal\n * values into per-statement hashes would make a dense statement inside a logic-heavy window\n * (`const weights = [1, 2, 3];`) block the window from ever being enumerated. The coarse phase\n * over-approximates on shape alone; the exact region fingerprint still applies the density rule.\n */\nfunction fingerprintHash(tokens: Token[], startTokenIndex: number, endTokenIndex: number): number {\n const [primary, secondary] = fingerprintHashPair(tokens, startTokenIndex, endTokenIndex, false);\n // XOR already coerces to int32, matching the native side's i32 arithmetic.\n return primary ^ Math.imul(secondary, 31);\n}\n\nfunction fingerprintHashPair(\n tokens: Token[],\n startTokenIndex: number,\n endTokenIndex: number,\n foldLiteralValues: boolean\n): [number, number] {\n const indexByIdentifier = new Map<string, number>();\n let primary = 5381;\n let secondary = 52_711;\n for (let index = startTokenIndex; index < endTokenIndex; index += 1) {\n const token = tokens[index];\n if (!token) {\n continue;\n }\n // Each accumulator consumes its own independent per-token hash: sharing one would collapse\n // the key to 32 effective bits (a single djb2 collision would equate whole regions).\n let part: number;\n let part2: number;\n if (token.kind === 'id') {\n let identifierIndex = indexByIdentifier.get(token.text);\n if (identifierIndex === undefined) {\n identifierIndex = indexByIdentifier.size;\n indexByIdentifier.set(token.text, identifierIndex);\n }\n part = anonymizedIndexHash(identifierIndex);\n part2 = anonymizedIndexHash2(identifierIndex);\n } else {\n part = token.textHash;\n part2 = token.textHash2;\n }\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `| 0` wraps the sum to int32 (Math.trunc does not), which must match the native side's wrapping i32 arithmetic.\n primary = (Math.imul(primary, 31) + part) | 0;\n secondary = Math.imul(secondary, 37) ^ part2;\n if (foldLiteralValues && token.literalHash !== undefined && token.literalHash2 !== undefined) {\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `| 0` wraps the sum to int32 (Math.trunc does not), which must match the native side's wrapping i32 arithmetic.\n primary = (Math.imul(primary, 31) + token.literalHash) | 0;\n secondary = Math.imul(secondary, 37) ^ token.literalHash2;\n }\n }\n return [primary, secondary];\n}\n\n/** djb2-style hash; XOR keeps the value in signed 32-bit range, which is fine for a grouping key. */\nfunction hashText(text: string): number {\n let hash = 5381;\n for (let index = 0; index < text.length; index += 1) {\n // oxlint-disable-next-line unicorn/prefer-code-point -- djb2 hashes UTF-16 code units; codePointAt would hash surrogate pairs twice (full code point, then the lone low surrogate).\n hash = Math.imul(hash, 33) ^ text.charCodeAt(index);\n }\n return hash;\n}\n\n/** FNV-1a over UTF-16 code units: independent of hashText so the two accumulators never share input. */\nfunction hashText2(text: string): number {\n let hash = -2_128_831_035; // 2166136261 as int32 (the FNV-1a offset basis)\n for (let index = 0; index < text.length; index += 1) {\n // oxlint-disable-next-line unicorn/prefer-code-point -- hashes UTF-16 code units like hashText.\n hash = Math.imul(hash ^ text.charCodeAt(index), 16_777_619);\n }\n return hash;\n}\n\nfunction combineHashes(hash: number, value: number): number {\n return Math.imul(hash, 31) + value;\n}\n\n/**\n * Merges duplicate groups separated by a small token gap into one gapped (Type-3) clone group: a\n * copy edited in one spot splits into two exact groups whose occurrences sit side by side in the\n * same order. Occurrences are paired greedily in source order; a merge happens when the pairing\n * fully pairs at least one group with at least two pairs. Equal-cardinality groups whose\n * occurrences all pair merge into one group as before. When cardinalities differ (a fragment also\n * occurs standalone: prefix ×3, suffix ×2), the fully-paired group is subsumed into the merged\n * gapped group while the other group is RETAINED with ALL its occurrences: dropping the leftover\n * would lose duplicated-line coverage, and reporting it alone would make a single-occurrence group\n * (contradicting duplicateBlockGroupCount's \"appears more than once\" meaning). Line coverage\n * unions ranges, so the overlap between the retained exact group and the merged group is harmless.\n * Merging repeats to a fixpoint so a clone edited in several spots still reassembles; it\n * terminates because a merge either removes the input groups it replaces or marks their paired\n * occurrences as counted elsewhere, and only unmarked occurrences of remaining groups pair, so the\n * number of pairable occurrences strictly decreases with every merge. Gap tokens are not matched\n * content: line coverage and sizes count only the matched segments. Generic so cross-file merging\n * can thread file identity through occurrences.\n */\nexport function mergeAdjacentGroups<T extends CountedOccurrence>(\n groups: T[][],\n maxGapTokens: number,\n isReportableGroup: (group: T[]) => boolean = () => true\n): T[][] {\n if (maxGapTokens <= 0 || groups.length < 2) {\n return groups;\n }\n // Deterministic processing order (mirrored by the native side): by first occurrence position.\n groups.sort(compareGroups);\n for (let restart = true; restart;) {\n restart = false;\n const partnersByGroup = collectGapAdjacentPartners(groups, maxGapTokens);\n for (let leftIndex = 0; leftIndex < groups.length && !restart; leftIndex += 1) {\n for (const rightIndex of partnersByGroup[leftIndex] ?? []) {\n const left = groups[leftIndex];\n const right = groups[rightIndex];\n if (!left || !right) {\n continue;\n }\n const forward = mergeGroups(left, right, maxGapTokens, isReportableGroup);\n const result = forward ?? mergeGroups(right, left, maxGapTokens, isReportableGroup);\n if (!result) {\n continue;\n }\n const leftReplaced = forward ? result.firstReplaced : result.secondReplaced;\n const rightReplaced = forward ? result.secondReplaced : result.firstReplaced;\n if (leftReplaced && rightReplaced) {\n groups[leftIndex] = result.merged;\n groups.splice(rightIndex, 1);\n } else if (rightReplaced) {\n groups[rightIndex] = result.merged;\n } else if (leftReplaced) {\n groups[leftIndex] = result.merged;\n } else {\n // Both groups stay (each is only partly paired, or reports nested copies of its own), so\n // the merged group joins them instead of taking a place.\n groups.push(result.merged);\n }\n // A group that stays keeps ALL its occurrences (line coverage must not shrink, and a\n // reported group must keep >= 2 occurrences), so its paired occurrences now also live\n // inside the merged group's occurrences: mark them so duplicateBlockCount counts each\n // token span once, and so the same pair cannot merge again.\n for (const occurrence of result.pairedRetained) {\n occurrence.spanCountedElsewhere = true;\n }\n groups.sort(compareGroups);\n restart = true;\n break;\n }\n }\n }\n return groups;\n}\n\n/**\n * Per group index, the ascending indexes of later groups that mergeGroups can pair with it in\n * either direction: an occurrence of one starts within `maxGapTokens` after an occurrence of the\n * other ends. Every other pair fails mergeGroups, so skipping it leaves the fixpoint unchanged\n * while each restart stops costing a comparison per pair of groups.\n */\nfunction collectGapAdjacentPartners(groups: CountedOccurrence[][], maxGapTokens: number): number[][] {\n const starts: { startTokenIndex: number; groupIndex: number }[] = [];\n for (const [groupIndex, group] of groups.entries()) {\n for (const occurrence of group) {\n starts.push({ startTokenIndex: occurrence.startTokenIndex, groupIndex });\n }\n }\n starts.sort((left, right) => left.startTokenIndex - right.startTokenIndex);\n const partners = groups.map(() => new Set<number>());\n for (const [groupIndex, group] of groups.entries()) {\n for (const occurrence of group) {\n for (\n let index = lowerBoundByStart(starts, occurrence.endTokenIndex);\n index < starts.length && (starts[index]?.startTokenIndex ?? 0) <= occurrence.endTokenIndex + maxGapTokens;\n index += 1\n ) {\n const other = starts[index]?.groupIndex ?? groupIndex;\n if (other !== groupIndex) {\n partners[Math.min(other, groupIndex)]?.add(Math.max(other, groupIndex));\n }\n }\n }\n }\n return partners.map((set) => [...set].toSorted((left, right) => left - right));\n}\n\nfunction lowerBoundByStart(sorted: { startTokenIndex: number }[], target: number): number {\n let low = 0;\n let high = sorted.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((sorted[middle]?.startTokenIndex ?? 0) < target) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low;\n}\n\nfunction compareGroups(left: CountedOccurrence[], right: CountedOccurrence[]): number {\n const leftFirst = left[0];\n const rightFirst = right[0];\n return (\n (leftFirst?.startTokenIndex ?? 0) - (rightFirst?.startTokenIndex ?? 0) ||\n (leftFirst?.endTokenIndex ?? 0) - (rightFirst?.endTokenIndex ?? 0)\n );\n}\n\ninterface MergeResult<T> {\n merged: T[];\n /**\n * Whether the merged group takes the respective input group's place: its standalone occurrences\n * were all paired and it holds no nested copies that only it can report.\n */\n firstReplaced: boolean;\n secondReplaced: boolean;\n /** The occurrences of groups that stay, which the merged group's spans now also cover. */\n pairedRetained: T[];\n}\n\n/**\n * Pairs `second` occurrences with gap-preceding `first` occurrences, greedily in source order:\n * each trailing occurrence takes the earliest unused leading occurrence within the gap, and a\n * pair's leading must start at or after the previous pair's trailing end so merged spans never\n * overlap. A merge needs at least two pairs (a merged group must still mean \"appears more than\n * once\") and must fully consume at least one group; for equal cardinalities this reduces to the\n * strict all-pairs merge, so pre-partial-merge behavior is unchanged there.\n */\nfunction mergeGroups<T extends CountedOccurrence>(\n first: T[],\n second: T[],\n maxGapTokens: number,\n isReportableGroup: (group: T[]) => boolean\n): MergeResult<T> | undefined {\n // Occurrences a previous partial merge already paired into a merged group must not pair again:\n // their spans already live inside that merged group, so re-pairing them would assemble a second,\n // competing merged group instead of letting the existing merged group extend (and would count\n // the same span twice). Consumption is still judged against the FULL group, so a group holding\n // shared occurrences is never subsumed away; nested copies, which the merged span would not\n // cover anyway, are left out of that judgment so they cannot veto a merge of the standalone\n // copies.\n const [leadings, firstLength] = pairableOccurrences(first);\n const [trailings, secondLength] = pairableOccurrences(second);\n const pairs: [T, T][] = [];\n let leadingIndex = 0;\n let previousTrailingEnd = -1;\n for (const trailing of trailings) {\n // Leadings ending too far before this trailing can never pair a later (even farther) one.\n while (leadingIndex < leadings.length) {\n const leading = leadings[leadingIndex];\n if (leading && leading.endTokenIndex + maxGapTokens < trailing.startTokenIndex) {\n leadingIndex += 1;\n } else {\n break;\n }\n }\n const leading = leadings[leadingIndex];\n if (\n leading &&\n leading.endTokenIndex <= trailing.startTokenIndex &&\n leading.startTokenIndex >= previousTrailingEnd\n ) {\n pairs.push([leading, trailing]);\n previousTrailingEnd = trailing.endTokenIndex;\n leadingIndex += 1;\n }\n }\n const firstFullyPaired = pairs.length === firstLength;\n const secondFullyPaired = pairs.length === secondLength;\n if (pairs.length < 2 || (!firstFullyPaired && !secondFullyPaired)) {\n return undefined;\n }\n // A group holding nested copies stays even when all its standalone copies pair: the merged\n // group's content is larger than what those copies matched, so only the original group can\n // report which files share the matched fragment.\n const firstReplaced = firstFullyPaired && !first.some((occurrence) => occurrence.nestedInLargerGroup);\n const secondReplaced = secondFullyPaired && !second.some((occurrence) => occurrence.nestedInLargerGroup);\n const merged = pairs.map(([leading, trailing]) => ({\n ...leading,\n // A merged occurrence is a fresh span combination; it never inherits shared-span marks, and\n // its segments are matched throughout.\n spanCountedElsewhere: undefined,\n nestedInLargerGroup: undefined,\n matchedRuns: undefined,\n segments: [...leading.segments, ...trailing.segments],\n tokenCount: leading.tokenCount + trailing.tokenCount,\n endTokenIndex: trailing.endTokenIndex,\n endIndex: trailing.endIndex,\n endLine: trailing.endLine,\n }));\n // Only occurrences of one file pair (file offsets exceed the gap), so a merged group spans the\n // files its pairs sit in: with nested copies left out of pairing, that can be fewer files than\n // the input groups covered, and a merged group that is no longer reportable must not form.\n if (!isReportableGroup(merged)) {\n return undefined;\n }\n const pairedRetained = [\n ...(firstReplaced ? [] : pairs.map(([leading]) => leading)),\n ...(secondReplaced ? [] : pairs.map(([, trailing]) => trailing)),\n ];\n return { merged, firstReplaced, secondReplaced, pairedRetained };\n}\n\n/** One pass over a group: its pairable occurrences and its non-nested occurrence count. */\nfunction pairableOccurrences<T extends CountedOccurrence>(group: T[]): [T[], number] {\n const pairable: T[] = [];\n let length = 0;\n for (const occurrence of group) {\n if (!occurrence.nestedInLargerGroup) {\n length += 1;\n }\n // Either flag keeps an occurrence out of pairing: its span is already counted elsewhere, or it\n // is a nested copy of content the merged span would not cover.\n if (!occurrence.spanCountedElsewhere && !occurrence.nestedInLargerGroup) {\n pairable.push(occurrence);\n }\n }\n return [pairable, length];\n}\n\n/**\n * Redundant copies one group adds to duplicateBlockCount. Each redundant occurrence contributes\n * one count per matched fragment, so merging a gapped clone's fragments into one group does not\n * halve duplicateBlockCount: an edited two-fragment pair still counts 2, exactly as its unmerged\n * fragments did. Occurrence shapes can differ within one group (a\n * gap-merged exact pair plus an appended whole-block near-miss copy), so every occurrence's\n * fragments are summed and one representative — the largest — is deducted, keeping the count\n * independent of source order. Occurrences a partial gapped merge also paired into a merged group\n * are skipped: their spans are already counted there, and such a retained group deducts no\n * representative of its own — the merged group's representative already stands for the shared\n * content — so no token span contributes to the count twice.\n */\nexport function countRedundantFragments(group: CountedOccurrence[]): number {\n let fragmentCount = 0;\n let maxFragmentCount = 0;\n let hasSharedOccurrence = false;\n for (const occurrence of group) {\n if (occurrence.spanCountedElsewhere) {\n hasSharedOccurrence = true;\n continue;\n }\n fragmentCount += occurrence.segments.length;\n maxFragmentCount = Math.max(maxFragmentCount, occurrence.segments.length);\n }\n return hasSharedOccurrence ? fragmentCount : fragmentCount - maxFragmentCount;\n}\n\n/**\n * The 1-based code lines the occurrence's matched tokens cover, given its segments and matched runs\n * as indexes into `tokens`. A line of a near-miss copy counts when its partners match more than half\n * of its tokens: a line the copy adds or rewrites is not duplicated, while a longest common\n * subsequence still pairs a stray token of it (a bracket, a separator) with one of the partner.\n * A line one of `exactSegments` reaches counts in any case.\n */\nexport function collectMatchedLines(\n segments: TokenSegment[],\n matchedRuns: TokenSegment[] | undefined,\n tokens: Token[],\n codeLineNumbers: Set<number> | undefined,\n exactSegments: TokenSegment[] = []\n): Set<number> {\n const exactLines = new Set<number>();\n for (const segment of exactSegments) {\n for (let index = segment.startTokenIndex; index < segment.endTokenIndex; index += 1) {\n const token = tokens[index];\n for (let row = token?.startRow ?? 0; row <= (token?.endRow ?? -1); row += 1) {\n exactLines.add(row + 1);\n }\n }\n }\n const matchedCountByLine = new Map<number, number>();\n const tokenCountByLine = new Map<number, number>();\n let runIndex = 0;\n for (const segment of segments) {\n for (let index = segment.startTokenIndex; index < segment.endTokenIndex; index += 1) {\n while (matchedRuns && (matchedRuns[runIndex]?.endTokenIndex ?? Infinity) <= index) {\n runIndex += 1;\n }\n const matched = !matchedRuns || (matchedRuns[runIndex]?.startTokenIndex ?? Infinity) <= index;\n const token = tokens[index];\n for (let row = token?.startRow ?? 0; row <= (token?.endRow ?? -1); row += 1) {\n tokenCountByLine.set(row + 1, (tokenCountByLine.get(row + 1) ?? 0) + 1);\n matchedCountByLine.set(row + 1, (matchedCountByLine.get(row + 1) ?? 0) + (matched ? 1 : 0));\n }\n }\n }\n const lines = new Set<number>();\n for (const [line, tokenCount] of tokenCountByLine) {\n if (\n (!codeLineNumbers || codeLineNumbers.has(line)) &&\n ((matchedCountByLine.get(line) ?? 0) * 2 > tokenCount || exactLines.has(line))\n ) {\n lines.add(line);\n }\n }\n return lines;\n}\n"],"mappings":"gDAUA,MAAa,EAA0D,CACrE,UAAW,GACX,aAAc,GACd,qBAAsB,EACxB,EAQA,SAAgB,EAA0B,EAA4D,CACpG,MAAO,CACL,UAAW,EAAc,GAAS,UAAW,EAA0B,SAAS,EAChF,aAAc,EAAc,GAAS,aAAc,EAA0B,YAAY,EACzF,qBAAsB,EAAc,GAAS,qBAAsB,EAA0B,oBAAoB,CACnH,CACF,CAEA,SAAS,EAAc,EAA2B,EAA0B,CAC1E,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAAI,EAAW,EAAM,KAAK,MAAM,CAAK,EAAG,EAAG,UAAa,CAC1G,CAiBA,SAAS,EAAe,EAAsB,EAA6B,CACzE,OAAO,EAAe,GAAK,CAC7B,CAoIA,SAAgB,EAAwB,EAA6B,CACnE,IAAM,EAAS,IAAI,WAAW,EAAO,OAAS,CAAC,EAC/C,IAAK,GAAM,CAAC,EAAO,KAAU,EAAO,QAAQ,EAC1C,EAAO,EAAQ,IAAM,EAAO,IAAU,IAAM,EAAM,cAAgB,IAAA,GAAY,EAAI,GAEpF,OAAO,CACT,CA0CA,SAAgB,EACd,EACA,EACA,EAC+B,CAC/B,IAAM,EAA4C,CAAC,EAC7C,EAAoC,CAAC,EACrC,EAA6B,CAAC,EACpC,IAAK,GAAM,CAAC,EAAc,KAAY,EAAS,QAAQ,EACrD,IAAK,IAAM,KAAc,EAAQ,WAC/B,EAAwB,KAAK,CAAY,EACzC,EAAW,KAAK,CAAU,EAG9B,IAAM,EAAa,GACjB,EAAS,EAAwB,IAAmB,GAChD,kBAAyB,IAAI,IAC7B,EAAmB,EAAW,KAAK,EAAY,IACnD,EAA0B,EAAU,CAAc,CAAC,EAAE,QAAU,CAAC,EAAG,EAAY,CAAS,CAC1F,EACA,IAAK,GAAM,CAAC,EAAgB,KAAY,EAAiB,QAAQ,EAAG,CAClE,IAAM,EAAe,EAAwB,IAAmB,EAChE,IAAK,GAAM,CAAC,EAAO,KAAQ,EAAQ,kBAAkB,QAAQ,EAC3D,IAAK,IAAM,KAAa,EAAK,CAC3B,GAAI,IAAc,IAAA,GAChB,SAEF,IAAM,EAAc,EAAuB,IAAI,CAAS,EACpD,GACF,EAAY,OAAS,EACjB,EAAY,iBAAmB,IACjC,EAAY,eAAiB,IAE3B,EAAY,eAAiB,IAC/B,EAAY,aAAe,IAE7B,EAAY,SAAW,KAAK,IAAI,EAAY,SAAU,CAAK,EAC3D,EAAY,SAAW,KAAK,IAAI,EAAY,SAAU,CAAK,GAE3D,EAAuB,IAAI,EAAW,CACpC,MAAO,EACP,iBACA,eACA,SAAU,EACV,SAAU,CACZ,CAAC,CAEL,CAEJ,CAMA,IAAM,GAAW,EAA+B,IAA4B,CAC1E,GAAI,IAAc,IAAA,GAChB,MAAO,GAET,IAAM,EAAc,EAAuB,IAAI,CAAS,EAOxD,OANI,IAAgB,IAAA,IAAa,EAAY,MAAQ,EAC5C,GAEL,EACK,EAAY,eAAiB,GAE/B,EAAY,iBAAmB,IAAM,EAAY,SAAW,EAAY,UAAY,CAC7F,EAKM,EAAyB,GAAoC,CACjE,IAAM,EAAS,EAAiB,EAAO,eAAe,EAAE,iBAAmB,CAAC,EACtE,EAAY,EAAO,EAAO,OAChC,IAAK,IAAI,EAAQ,EAAO,MAAQ,EAAG,EAAQ,EAAO,MAAQ,EAAO,OAAQ,GAAS,EAChF,GAAI,EAAO,KAAW,EACpB,MAAO,GAGX,MAAO,EACT,EAEM,EAAmC,CAAC,EAC1C,IAAK,GAAM,CAAC,EAAgB,KAAY,EAAiB,QAAQ,EAC/D,IAAK,GAAM,CAAC,EAAO,KAAQ,EAAQ,kBAAkB,QAAQ,EAC3D,IAAK,GAAM,CAAC,EAAQ,KAAc,EAAI,QAAQ,EAAG,CAC/C,GAAI,CAAC,EAAQ,EAAW,CAAM,GAAK,CAAC,EAAsB,CAAE,iBAAgB,QAAO,QAAO,CAAC,EACzF,SAIF,IAAM,EAAgB,EAAQ,kBAAkB,EAAM,GAAG,EAAS,GAC5D,EAAe,EAAQ,kBAAkB,EAAQ,EAAE,GAAG,EAAS,GACjE,EAAQ,EAAe,EAAS,CAAC,GAAK,EAAQ,EAAc,EAAS,CAAC,GAG1E,EAAe,KAAK,CAAE,iBAAgB,QAAO,QAAO,CAAC,CACvD,CASJ,IAAM,EAAU,IAAI,IAAI,EAAe,IAAI,CAAQ,CAAC,EAChD,EAAW,EACf,KAAO,EAAS,OAAS,GAAG,CAC1B,IAAM,EAA4B,CAAC,EACnC,IAAK,IAAM,KAAU,EAAU,CAC7B,IAAM,EAAa,EAAW,EAAO,gBAC/B,EAAQ,IAAa,EAAO,OAC5B,EAAO,IAAa,EAAO,MAAQ,EAAO,OAAS,GACnD,EAAU,EAAU,EAAO,cAAc,EAC/C,GAAI,CAAC,GAAS,CAAC,GAAQ,CAAC,EACtB,SAEF,IAAM,EAAc,KAAK,EAAe,EAAQ,OAAQ,EAAQ,mBAAoB,EAAM,gBAAiB,EAAK,aAAa,IAC7H,EAAW,KAAK,CACd,UAAW,EAAY,EAAa,EAAM,gBAAiB,EAAK,cAAe,EAAO,CAAI,EAC1F,aAAc,EAAwB,EAAO,iBAAmB,CAClE,CAAC,EACD,EAAQ,KAAK,CAAM,CACrB,CACA,EAAW,CAAC,EACZ,IAAK,IAAM,KAAU,EACnB,IAAK,IAAM,IAAS,CAAC,EAAO,MAAO,EAAO,MAAQ,CAAC,EAAG,CACpD,IAAM,EAAY,CAAE,eAAgB,EAAO,eAAgB,QAAO,OAAQ,EAAO,OAAS,CAAE,EACtF,EAAe,EAAiB,EAAO,eAAe,EAAE,kBAAkB,EAAM,GAAG,EAAU,QAEjG,GAAQ,IAAI,EAAS,CAAS,CAAC,GAC9B,EAAQ,EAAc,EAAU,MAAM,GACtC,EAAsB,CAAS,IAIlC,EAAQ,IAAI,EAAS,CAAS,CAAC,EAC/B,EAAS,KAAK,CAAS,EACzB,CAEJ,CACA,OAAO,CACT,CAEA,SAAS,EAAS,EAAgC,CAChD,MAAO,GAAG,EAAO,eAAe,GAAG,EAAO,MAAM,GAAG,EAAO,QAC5D,CASA,SAAS,EAA0B,EAAiB,EAA0B,EAAqC,CACjH,IAAM,EAAkB,EAAW,IAAK,GACtC,EAAgB,EAAQ,EAAU,gBAAiB,EAAU,aAAa,CAC5E,EACM,EAA8C,CAAC,EACrD,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAW,OAAQ,GAAS,EAAG,CACzD,IAAM,EAA8B,CAAC,EACjC,EAAO,KACP,EAAa,EACX,EAAS,KAAK,IAAI,EAAW,OAAQ,EAAQ,GAAyB,EAC5E,IAAK,IAAI,EAAM,EAAO,EAAM,EAAQ,GAAO,EAAG,CAC5C,IAAM,EAAY,EAAW,GACvB,EAAgB,EAAgB,GACtC,GAAI,CAAC,GAAa,IAAkB,IAAA,GAClC,MAEF,EAAO,EAAc,EAAM,CAAa,EACxC,GAAc,EAAU,cAAgB,EAAU,gBAClD,IAAM,EAAiB,EAAM,EAAQ,EACrC,EAAI,GACF,GAAkB,GAA6B,GAAc,EACzD,EAAc,EAAM,CAAc,EAClC,IAAA,EACR,CACA,EAAkB,KAAK,CAAG,CAC5B,CACA,MAAO,CAAE,oBAAmB,iBAAgB,CAC9C,CAOA,SAAgB,EAAU,EAAe,EAAuB,CAC9D,OAAO,EAAuB,CAAC,CAAC,CAAC,CAAC,CACpC,CAMA,SAAgB,EAAuB,EAA0C,CAC/E,IAAM,EAAa,EAAoB,CAAC,EACxC,MAAQ,IAAM,CACZ,IAAI,EAAS,EACb,IAAK,IAAM,KAAQ,EAAW,CAAC,EAC7B,GAAU,EAAS,CAAI,EAEzB,OAAO,CACT,CACF,CAOA,SAAS,EAAoB,EAA+C,CAC1E,IAAM,EAAa,EAAE,OAAS,KAAQ,EAChC,kBAAgB,IAAI,IAC1B,IAAK,GAAM,CAAC,EAAO,KAAW,EAAE,QAAQ,EAAG,CACzC,IAAI,EAAO,EAAc,IAAI,CAAM,EAC9B,IACH,EAAO,IAAI,YAAY,CAAS,EAChC,EAAc,IAAI,EAAQ,CAAI,GAEhC,IAAM,EAAO,IAAU,EAIvB,EAAK,IAAS,EAAK,IAAS,GAAM,IAAM,EAAQ,GAClD,CAEA,IAAM,EAAI,IAAI,YAAY,CAAS,EACnC,MAAQ,IAAM,CACZ,EAAE,KAAK,CAAC,EACR,IAAK,IAAM,KAAU,EAAG,CACtB,IAAM,EAAY,EAAc,IAAI,CAAM,EAEtC,EAAa,EACb,EAAS,EACb,IAAK,IAAI,EAAO,EAAG,EAAO,EAAW,GAAQ,EAAG,CAC9C,IAAM,EAAW,EAAE,IAAS,EAEtB,IAAM,IAAY,IAAS,GAAK,KAAc,EAE9C,GAAY,GAAY,EAAK,KAAgB,EACnD,EAAa,IAAa,GAC1B,IAAM,EAAa,EAAI,EAAU,EACjC,EAAS,IAAa,GAEtB,EAAE,GAAQ,EAAI,CAAC,CACjB,CACF,CACA,OAAO,CACT,CACF,CAMA,SAAgB,EAAQ,EAAe,EAAyC,CAC9E,IAAM,EAAW,IAAI,WAAW,EAAE,MAAM,EAClC,EAAW,IAAI,WAAW,EAAE,MAAM,EAExC,OADA,EAAY,EAAG,EAAG,EAAU,CAAQ,EAC7B,CAAC,EAAU,CAAQ,CAC5B,CAEA,SAAS,EAAY,EAAe,EAAe,EAAsB,EAA4B,CAEnG,IAAI,EAAS,EACb,KAAO,EAAS,EAAE,QAAU,EAAS,EAAE,QAAU,EAAE,KAAY,EAAE,IAC/D,GAAU,EAEZ,IAAI,EAAS,EACb,KACE,EAAS,EAAE,OAAS,GACpB,EAAS,EAAE,OAAS,GACpB,EAAE,EAAE,OAAS,EAAI,KAAY,EAAE,EAAE,OAAS,EAAI,IAE9C,GAAU,EAEZ,IAAM,EAAO,EAAE,OAAS,EAClB,EAAO,EAAE,OAAS,EACxB,EAAS,KAAK,EAAG,EAAG,CAAM,CAAC,CAAC,KAAK,EAAG,CAAI,EACxC,EAAS,KAAK,EAAG,EAAG,CAAM,CAAC,CAAC,KAAK,EAAG,CAAI,EACxC,IAAM,EAAQ,EAAE,SAAS,EAAQ,CAAI,EAC/B,EAAQ,EAAE,SAAS,EAAQ,CAAI,EAC/B,EAAe,EAAS,SAAS,EAAQ,CAAI,EAC7C,EAAe,EAAS,SAAS,EAAQ,CAAI,EACnD,GAAI,EAAM,SAAW,GAAK,EAAM,SAAW,EACzC,OAEF,GAAI,EAAM,SAAW,EAAG,CACtB,IAAM,EAAW,EAAM,QAAQ,EAAM,IAAM,CAAC,EACxC,IAAa,KACf,EAAa,GAAK,EAClB,EAAa,GAAY,GAE3B,MACF,CACA,IAAM,EAAS,EAAM,SAAW,EAC1B,EAAU,EAAiB,EAAM,SAAS,EAAG,CAAM,EAAG,CAAK,EAC3D,EAAW,EAAiB,EAAM,SAAS,CAAM,CAAC,CAAC,WAAW,EAAG,EAAM,WAAW,CAAC,EACrF,EAAQ,EACR,EAAO,GACX,IAAK,IAAI,EAAY,EAAG,GAAa,EAAM,OAAQ,GAAa,EAAG,CACjE,IAAM,GAAU,EAAQ,IAAc,IAAM,EAAS,EAAM,OAAS,IAAc,GAC9E,EAAS,IACX,EAAO,EACP,EAAQ,EAEZ,CACA,EACE,EAAM,SAAS,EAAG,CAAM,EACxB,EAAM,SAAS,EAAG,CAAK,EACvB,EAAa,SAAS,EAAG,CAAM,EAC/B,EAAa,SAAS,EAAG,CAAK,CAChC,EACA,EACE,EAAM,SAAS,CAAM,EACrB,EAAM,SAAS,CAAK,EACpB,EAAa,SAAS,CAAM,EAC5B,EAAa,SAAS,CAAK,CAC7B,CACF,CAGA,SAAS,EAAiB,EAAe,EAA2B,CAClE,IAAM,EAAO,EAAoB,CAAC,CAAC,CAAC,CAAC,EAC/B,EAAU,IAAI,WAAW,EAAE,OAAS,CAAC,EAC3C,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAE,OAAQ,GAAS,EAC7C,EAAQ,EAAQ,IAAM,EAAQ,IAAU,KAAQ,EAAK,IAAU,IAAM,MAAQ,EAAQ,IAAO,GAE9F,OAAO,CACT,CAEA,SAAS,EAAS,EAAuB,CACvC,IAAI,EAAQ,GAAU,IAAU,EAAK,YAErC,MADA,IAAS,EAAQ,YAAmB,IAAU,EAAK,WAC3C,KAAK,KAAM,GAAS,IAAU,GAAM,UAAe,QAAa,IAAM,GAAM,GACtF,CAEA,SAAS,EACP,EACA,EACA,EACA,EACA,EACoB,CACpB,MAAO,CACL,cACA,WAAY,EAAgB,EAC5B,kBACA,gBACA,WAAY,EAAM,WAClB,SAAU,EAAK,SACf,UAAW,EAAM,UACjB,QAAS,EAAK,OAChB,CACF,CAGA,MAAM,EAAkC,CAAC,EACnC,EAAmC,CAAC,EAE1C,SAAS,EAAoB,EAAuB,CAClD,IAAI,EAAO,EAAsB,GAKjC,OAJI,IAAS,IAAA,KACX,EAAO,EAAS,IAAI,GAAO,EAC3B,EAAsB,GAAS,GAE1B,CACT,CAEA,SAAS,EAAqB,EAAuB,CACnD,IAAI,EAAO,EAAuB,GAKlC,OAJI,IAAS,IAAA,KACX,EAAO,EAAU,IAAI,GAAO,EAC5B,EAAuB,GAAS,GAE3B,CACT,CAUA,SAAS,EACP,EACA,EACA,EACA,EACQ,CAGR,GAAM,CAAC,EAAS,GAAa,EAAoB,EAAQ,EAAiB,EADrD,GADC,EAAmB,IAAkB,IAAM,EAAmB,IAAoB,GACtD,EAAgB,CACkC,CAAC,EACrG,MAAO,GAAG,EAAQ,GAAG,EAAU,GAAG,EAAgB,GACpD,CASA,SAAS,EAAgB,EAAiB,EAAyB,EAA+B,CAChG,GAAM,CAAC,EAAS,GAAa,EAAoB,EAAQ,EAAiB,EAAe,EAAK,EAE9F,OAAO,EAAU,KAAK,KAAK,EAAW,EAAE,CAC1C,CAEA,SAAS,EACP,EACA,EACA,EACA,EACkB,CAClB,IAAM,kBAAoB,IAAI,IAC1B,EAAU,KACV,EAAY,MAChB,IAAK,IAAI,EAAQ,EAAiB,EAAQ,EAAe,GAAS,EAAG,CACnE,IAAM,EAAQ,EAAO,GACrB,GAAI,CAAC,EACH,SAIF,IAAI,EACA,EACJ,GAAI,EAAM,OAAS,KAAM,CACvB,IAAI,EAAkB,EAAkB,IAAI,EAAM,IAAI,EAClD,IAAoB,IAAA,KACtB,EAAkB,EAAkB,KACpC,EAAkB,IAAI,EAAM,KAAM,CAAe,GAEnD,EAAO,EAAoB,CAAe,EAC1C,EAAQ,EAAqB,CAAe,CAC9C,KACE,GAAO,EAAM,SACb,EAAQ,EAAM,UAGhB,EAAW,KAAK,KAAK,EAAS,EAAE,EAAI,EAAQ,EAC5C,EAAY,KAAK,KAAK,EAAW,EAAE,EAAI,EACnC,GAAqB,EAAM,cAAgB,IAAA,IAAa,EAAM,eAAiB,IAAA,KAEjF,EAAW,KAAK,KAAK,EAAS,EAAE,EAAI,EAAM,YAAe,EACzD,EAAY,KAAK,KAAK,EAAW,EAAE,EAAI,EAAM,aAEjD,CACA,MAAO,CAAC,EAAS,CAAS,CAC5B,CAGA,SAAS,EAAS,EAAsB,CACtC,IAAI,EAAO,KACX,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAK,OAAQ,GAAS,EAEhD,EAAO,KAAK,KAAK,EAAM,EAAE,EAAI,EAAK,WAAW,CAAK,EAEpD,OAAO,CACT,CAGA,SAAS,EAAU,EAAsB,CACvC,IAAI,EAAO,YACX,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAK,OAAQ,GAAS,EAEhD,EAAO,KAAK,KAAK,EAAO,EAAK,WAAW,CAAK,EAAG,QAAU,EAE5D,OAAO,CACT,CAEA,SAAS,EAAc,EAAc,EAAuB,CAC1D,OAAO,KAAK,KAAK,EAAM,EAAE,EAAI,CAC/B,CAoBA,SAAgB,EACd,EACA,EACA,MAAmD,GAC5C,CACP,GAAI,GAAgB,GAAK,EAAO,OAAS,EACvC,OAAO,EAGT,EAAO,KAAK,CAAa,EACzB,IAAK,IAAI,EAAU,GAAM,GAAU,CACjC,EAAU,GACV,IAAM,EAAkB,EAA2B,EAAQ,CAAY,EACvE,IAAK,IAAI,EAAY,EAAG,EAAY,EAAO,QAAU,CAAC,EAAS,GAAa,EAC1E,IAAK,IAAM,KAAc,EAAgB,IAAc,CAAC,EAAG,CACzD,IAAM,EAAO,EAAO,GACd,EAAQ,EAAO,GACrB,GAAI,CAAC,GAAQ,CAAC,EACZ,SAEF,IAAM,EAAU,EAAY,EAAM,EAAO,EAAc,CAAiB,EAClE,EAAS,GAAW,EAAY,EAAO,EAAM,EAAc,CAAiB,EAClF,GAAI,CAAC,EACH,SAEF,IAAM,EAAe,EAAU,EAAO,cAAgB,EAAO,eACvD,EAAgB,EAAU,EAAO,eAAiB,EAAO,cAC3D,GAAgB,GAClB,EAAO,GAAa,EAAO,OAC3B,EAAO,OAAO,EAAY,CAAC,GAClB,EACT,EAAO,GAAc,EAAO,OACnB,EACT,EAAO,GAAa,EAAO,OAI3B,EAAO,KAAK,EAAO,MAAM,EAM3B,IAAK,IAAM,KAAc,EAAO,eAC9B,EAAW,qBAAuB,GAEpC,EAAO,KAAK,CAAa,EACzB,EAAU,GACV,KACF,CAEJ,CACA,OAAO,CACT,CAQA,SAAS,EAA2B,EAA+B,EAAkC,CACnG,IAAM,EAA4D,CAAC,EACnE,IAAK,GAAM,CAAC,EAAY,KAAU,EAAO,QAAQ,EAC/C,IAAK,IAAM,KAAc,EACvB,EAAO,KAAK,CAAE,gBAAiB,EAAW,gBAAiB,YAAW,CAAC,EAG3E,EAAO,MAAM,EAAM,IAAU,EAAK,gBAAkB,EAAM,eAAe,EACzE,IAAM,EAAW,EAAO,wBAAU,IAAI,GAAa,EACnD,IAAK,GAAM,CAAC,EAAY,KAAU,EAAO,QAAQ,EAC/C,IAAK,IAAM,KAAc,EACvB,IACE,IAAI,EAAQ,EAAkB,EAAQ,EAAW,aAAa,EAC9D,EAAQ,EAAO,SAAW,EAAO,EAAM,EAAE,iBAAmB,IAAM,EAAW,cAAgB,EAC7F,GAAS,EACT,CACA,IAAM,EAAQ,EAAO,EAAM,EAAE,YAAc,EACvC,IAAU,GACZ,EAAS,KAAK,IAAI,EAAO,CAAU,EAAE,EAAE,IAAI,KAAK,IAAI,EAAO,CAAU,CAAC,CAE1E,CAGJ,OAAO,EAAS,IAAK,GAAQ,CAAC,GAAG,CAAG,CAAC,CAAC,UAAU,EAAM,IAAU,EAAO,CAAK,CAAC,CAC/E,CAEA,SAAS,EAAkB,EAAuC,EAAwB,CACxF,IAAI,EAAM,EACN,EAAO,EAAO,OAClB,KAAO,EAAM,GAAM,CACjB,IAAM,EAAU,EAAM,IAAU,GAC3B,EAAO,EAAO,EAAE,iBAAmB,GAAK,EAC3C,EAAM,EAAS,EAEf,EAAO,CAEX,CACA,OAAO,CACT,CAEA,SAAS,EAAc,EAA2B,EAAoC,CACpF,IAAM,EAAY,EAAK,GACjB,EAAa,EAAM,GACzB,OACG,GAAW,iBAAmB,IAAM,GAAY,iBAAmB,KACnE,GAAW,eAAiB,IAAM,GAAY,eAAiB,EAEpE,CAsBA,SAAS,EACP,EACA,EACA,EACA,EAC4B,CAQ5B,GAAM,CAAC,EAAU,GAAe,EAAoB,CAAK,EACnD,CAAC,EAAW,GAAgB,EAAoB,CAAM,EACtD,EAAkB,CAAC,EACrB,EAAe,EACf,EAAsB,GAC1B,IAAK,IAAM,KAAY,EAAW,CAEhC,KAAO,EAAe,EAAS,QAAQ,CACrC,IAAM,EAAU,EAAS,GACzB,GAAI,GAAW,EAAQ,cAAgB,EAAe,EAAS,gBAC7D,GAAgB,OAEhB,KAEJ,CACA,IAAM,EAAU,EAAS,GAEvB,GACA,EAAQ,eAAiB,EAAS,iBAClC,EAAQ,iBAAmB,IAE3B,EAAM,KAAK,CAAC,EAAS,CAAQ,CAAC,EAC9B,EAAsB,EAAS,cAC/B,GAAgB,EAEpB,CACA,IAAM,EAAmB,EAAM,SAAW,EACpC,EAAoB,EAAM,SAAW,EAC3C,GAAI,EAAM,OAAS,GAAM,CAAC,GAAoB,CAAC,EAC7C,OAKF,IAAM,EAAgB,GAAoB,CAAC,EAAM,KAAM,GAAe,EAAW,mBAAmB,EAC9F,EAAiB,GAAqB,CAAC,EAAO,KAAM,GAAe,EAAW,mBAAmB,EACjG,EAAS,EAAM,KAAK,CAAC,EAAS,MAAe,CACjD,GAAG,EAGH,qBAAsB,IAAA,GACtB,oBAAqB,IAAA,GACrB,YAAa,IAAA,GACb,SAAU,CAAC,GAAG,EAAQ,SAAU,GAAG,EAAS,QAAQ,EACpD,WAAY,EAAQ,WAAa,EAAS,WAC1C,cAAe,EAAS,cACxB,SAAU,EAAS,SACnB,QAAS,EAAS,OACpB,EAAE,EAIG,KAAkB,CAAM,EAO7B,MAAO,CAAE,SAAQ,gBAAe,iBAAgB,eAAA,CAH9C,GAAI,EAAgB,CAAC,EAAI,EAAM,KAAK,CAAC,KAAa,CAAO,EACzD,GAAI,EAAiB,CAAC,EAAI,EAAM,KAAK,EAAG,KAAc,CAAQ,CAEH,CAAE,CACjE,CAGA,SAAS,EAAiD,EAA2B,CACnF,IAAM,EAAgB,CAAC,EACnB,EAAS,EACb,IAAK,IAAM,KAAc,EAClB,EAAW,sBACd,GAAU,GAIR,CAAC,EAAW,sBAAwB,CAAC,EAAW,qBAClD,EAAS,KAAK,CAAU,EAG5B,MAAO,CAAC,EAAU,CAAM,CAC1B,CAcA,SAAgB,EAAwB,EAAoC,CAC1E,IAAI,EAAgB,EAChB,EAAmB,EACnB,EAAsB,GAC1B,IAAK,IAAM,KAAc,EAAO,CAC9B,GAAI,EAAW,qBAAsB,CACnC,EAAsB,GACtB,QACF,CACA,GAAiB,EAAW,SAAS,OACrC,EAAmB,KAAK,IAAI,EAAkB,EAAW,SAAS,MAAM,CAC1E,CACA,OAAO,EAAsB,EAAgB,EAAgB,CAC/D,CASA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAgC,CAAC,EACpB,CACb,IAAM,kBAAa,IAAI,IACvB,IAAK,IAAM,KAAW,EACpB,IAAK,IAAI,EAAQ,EAAQ,gBAAiB,EAAQ,EAAQ,cAAe,GAAS,EAAG,CACnF,IAAM,EAAQ,EAAO,GACrB,IAAK,IAAI,EAAM,GAAO,UAAY,EAAG,IAAQ,GAAO,QAAU,IAAK,GAAO,EACxE,EAAW,IAAI,EAAM,CAAC,CAE1B,CAEF,IAAM,kBAAqB,IAAI,IACzB,kBAAmB,IAAI,IACzB,EAAW,EACf,IAAK,IAAM,KAAW,EACpB,IAAK,IAAI,EAAQ,EAAQ,gBAAiB,EAAQ,EAAQ,cAAe,GAAS,EAAG,CACnF,KAAO,IAAgB,EAAY,EAAS,EAAE,eAAiB,MAAa,GAC1E,GAAY,EAEd,IAAM,EAAU,CAAC,IAAgB,EAAY,EAAS,EAAE,iBAAmB,MAAa,EAClF,EAAQ,EAAO,GACrB,IAAK,IAAI,EAAM,GAAO,UAAY,EAAG,IAAQ,GAAO,QAAU,IAAK,GAAO,EACxE,EAAiB,IAAI,EAAM,GAAI,EAAiB,IAAI,EAAM,CAAC,GAAK,GAAK,CAAC,EACtE,EAAmB,IAAI,EAAM,GAAI,EAAmB,IAAI,EAAM,CAAC,GAAK,IAAM,IAAgB,CAE9F,CAEF,IAAM,kBAAQ,IAAI,IAClB,IAAK,GAAM,CAAC,EAAM,KAAe,GAE5B,CAAC,GAAmB,EAAgB,IAAI,CAAI,MAC3C,EAAmB,IAAI,CAAI,GAAK,GAAK,EAAI,GAAc,EAAW,IAAI,CAAI,IAE5E,EAAM,IAAI,CAAI,EAGlB,OAAO,CACT"}
@@ -1,2 +1,2 @@
1
- "use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./thresholds.cjs");let n=require("node:assert");n=e.__toESM(n,1);const r=[`file`,`function`,`duplication`],i=new Set(t.functionThresholds.filter(({includesNestedFunctions:e})=>e).map(e=>t.metricNameOf(e)));function a(e,n,r){let i=g(n.crossFile,new Set(e.map(({file:e})=>e))),a=[],l=0;for(let o of e){let{file:e,metrics:d,hunks:m}=o,h=r(d.language),g=m?.length===0?[]:p(t.fileThresholds,d,h);g.length>0&&a.push(f({file:e,startLine:1,endLine:d.lines.total},`file`,g));let v=s(o);l+=v.length;let y=v.map(e=>({fn:e,exceeded:p(t.functionThresholds,e,h)})).filter(({exceeded:e})=>e.length>0);for(let{fn:t,exceeded:n}of y){let r=n.filter(e=>!c(e,t,y));r.length>0&&a.push(u(e,t,r))}a.push(..._(o,i.get(e)??[],h,n))}return{violations:a.toSorted(o),checkedFunctionCount:l}}function o(e,n){return t.levels.indexOf(n.level)-t.levels.indexOf(e.level)||C(e.file,n.file)||e.startLine-n.startLine||r.indexOf(e.kind)-r.indexOf(n.kind)||e.endLine-n.endLine}function s({metrics:e,hunks:t}){let n=e.language===`python`;return t?e.functions.filter(e=>t.some(t=>h(t,e.startLine,e.endLine,n))):e.functions}function c(e,t,n){return i.has(e.metric)?n.some(n=>n.fn!==t&&l(n.fn,t)&&n.exceeded.some(({metric:t})=>t===e.metric)):!1}function l(e,t){let n=e.startLine<t.startLine||e.startLine===t.startLine&&e.startColumn<=t.startColumn,r=e.endLine>t.endLine||e.endLine===t.endLine&&e.endColumn>=t.endColumn;return n&&r}function u(e,t,n){let{startLine:r,endLine:i}=t,a=n.some(({metric:e})=>e===`functionCognitiveComplexity`);return{...f({file:e,startLine:r,endLine:i},`function`,n),name:d(t),...a&&t.cognitiveBlocks.length>0&&{largestBlocks:t.cognitiveBlocks}}}function d({name:e=`<anonymous>`,containerName:t}){return t?`${t}.${e}`:e}function f(e,t,n){return{kind:t,level:n.some(e=>e.level===`error`)?`error`:`warning`,...e,exceeded:n}}function p(e,t,n){return e.flatMap(e=>m(e,t,n,(e,t)=>e>t))}function m(e,n,r,i){let a=e.measure(n),o=t.levels.findLast(t=>i(a,r[t][e.key]));return o?[{metric:t.metricNameOf(e),value:a,level:o,limit:r[o][e.key]}]:[]}function h(e,t,n,r=!1){return e.headCount===0?e.headStart>=t&&(e.headStart<n||r&&e.headStart===n):e.headStart<=n&&e.headStart+e.headCount>t}function g(e,t){let n=/* @__PURE__ */ new Map;for(let r of e?.groups??[])for(let e of r.files)if(t.has(e)){let t=n.get(e)??[];t.push(r),n.set(e,t)}return n}function _({file:e,metrics:n,hunks:r},i,a,{minSimilarityPercent:o}){let s=e=>m(t.duplicationThreshold,{duplicatedLineCount:e},a,(e,t)=>e>=t),c=[...n.duplication.duplicateBlockGroups.map(t=>t.map(t=>({...t,file:e}))),...i.map(e=>e.occurrences.map(e=>v(e)))],l=t=>t.file===e&&s(t.lineNumbers.length).length>0&&(!r||r.some(e=>e.headCount>0&&h(e,t.startLine,t.endLine))),u=/* @__PURE__ */ new Map;for(let e of c)for(let t of e.filter(e=>l(e)))u.set(t,e);return b([...u.keys()]).map(({merged:e,sources:t})=>{let n=n=>b(t.flatMap(e=>{let r=Math.max(...t.filter(t=>x(t,e)).map(e=>e.tokenCount));return(u.get(e)??[]).filter(e=>e.tokenCount*100>=r*n)})).filter(t=>!S(t.merged,e)).toSorted((e,t)=>y(t.sources)-y(e.sources)).map(e=>e.merged),r=n(o);return{...f(e,`duplication`,s(new Set(t.flatMap(e=>e.lineNumbers)).size)),partners:r.length>0?r:n(0)}})}function v(e){let{lineNumbers:t}=e;return n.default.ok(t,`No matched lines for ${e.file}, a scanned file.`),{...e,lineNumbers:t}}function y(e){return e.reduce((e,t)=>e+t.tokenCount,0)}function b(e){let t=[],n=e.toSorted((e,t)=>C(e.file,t.file)||e.startLine-t.startLine);for(let e of n){let n=t.at(-1);if(n&&S(n.merged,e))n.merged.endLine=Math.max(n.merged.endLine,e.endLine),n.sources.push(e);else{let{file:n,startLine:r,endLine:i}=e;t.push({merged:{file:n,startLine:r,endLine:i},sources:[e]})}}return t}function x(e,t){return e.file===t.file&&e.startLine<=t.startLine&&t.endLine<=e.endLine}function S(e,t){return e.file===t.file&&e.startLine<=t.endLine&&t.startLine<=e.endLine}function C(e,t){return e<t?-1:+(e>t)}exports.checkThresholds=a,exports.qualifyFunctionName=d;
1
+ "use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./thresholds.cjs");let n=require("node:assert");n=e.__toESM(n,1);const r=[`file`,`function`,`duplication`],i=new Set(t.functionThresholds.filter(({includesNestedFunctions:e})=>e).map(e=>t.metricNameOf(e)));function a(e,n,r){let i=g(n.crossFile,new Set(e.map(({file:e})=>e))),a=[],l=0;for(let o of e){let{file:e,metrics:d,hunks:m}=o,h=r(d.language),g=m?.length===0?[]:p(t.fileThresholds,d,h);g.length>0&&a.push(f({file:e,startLine:1,endLine:d.lines.total},`file`,g));let v=s(o);l+=v.length;let y=v.map(e=>({fn:e,exceeded:p(t.functionThresholds,e,h)})).filter(({exceeded:e})=>e.length>0);for(let{fn:t,exceeded:n}of y){let r=n.filter(e=>!c(e,t,y));r.length>0&&a.push(u(e,t,r))}a.push(..._(o,i.get(e)??[],h,n))}return{violations:a.toSorted(o),checkedFunctionCount:l}}function o(e,n){return t.levels.indexOf(n.level)-t.levels.indexOf(e.level)||C(e.file,n.file)||e.startLine-n.startLine||r.indexOf(e.kind)-r.indexOf(n.kind)||e.endLine-n.endLine}function s({metrics:e,hunks:t}){let n=e.language===`python`;return t?e.functions.filter(e=>t.some(t=>h(t,e.startLine,e.endLine,n))):e.functions}function c(e,t,n){return i.has(e.metric)?n.some(n=>n.fn!==t&&l(n.fn,t)&&n.exceeded.some(({metric:t})=>t===e.metric)):!1}function l(e,t){let n=e.startLine<t.startLine||e.startLine===t.startLine&&e.startColumn<=t.startColumn,r=e.endLine>t.endLine||e.endLine===t.endLine&&e.endColumn>=t.endColumn;return n&&r}function u(e,t,n){let{startLine:r,endLine:i}=t,a=n.some(({metric:e})=>e===`functionCognitiveComplexity`);return{...f({file:e,startLine:r,endLine:i},`function`,n),name:d(t),...a&&t.cognitiveBlocks.length>0&&{largestBlocks:t.cognitiveBlocks}}}function d({name:e=`<anonymous>`,containerName:t}){return t?`${t}.${e}`:e}function f(e,t,n){return{kind:t,level:n.some(e=>e.level===`error`)?`error`:`warning`,...e,exceeded:n}}function p(e,t,n){return e.flatMap(e=>m(e,t,n,(e,t)=>e>t))}function m(e,n,r,i){let a=e.measure(n),o=t.levels.findLast(t=>i(a,r[t][e.key]));return o?[{metric:t.metricNameOf(e),label:e.label,value:a,level:o,limit:r[o][e.key]}]:[]}function h(e,t,n,r=!1){return e.headCount===0?e.headStart>=t&&(e.headStart<n||r&&e.headStart===n):e.headStart<=n&&e.headStart+e.headCount>t}function g(e,t){let n=/* @__PURE__ */ new Map;for(let r of e?.groups??[])for(let e of r.files)if(t.has(e)){let t=n.get(e)??[];t.push(r),n.set(e,t)}return n}function _({file:e,metrics:n,hunks:r},i,a,{minSimilarityPercent:o}){let s=e=>m(t.duplicationThreshold,{duplicatedLineCount:e},a,(e,t)=>e>=t),c=[...n.duplication.duplicateBlockGroups.map(t=>t.map(t=>({...t,file:e}))),...i.map(e=>e.occurrences.map(e=>v(e)))],l=t=>t.file===e&&s(t.lineNumbers.length).length>0&&(!r||r.some(e=>e.headCount>0&&h(e,t.startLine,t.endLine))),u=new Map(c.flatMap(e=>e.map(t=>[t,e]))),d=(e,t)=>u.get(e)===u.get(t)&&!x(e,t)&&!x(t,e);return b(c.flat().filter(e=>l(e)),d).map(({merged:e,sources:t})=>{let n=e=>b(t.flatMap(n=>{let r=Math.max(...t.filter(e=>x(e,n)).map(e=>e.tokenCount));return(u.get(n)??[]).filter(n=>n.tokenCount*100>=r*e&&!t.some(e=>x(e,n)))}),d).toSorted((e,t)=>y(t.sources)-y(e.sources)).map(e=>e.merged),r=n(o);return{...f(e,`duplication`,s(new Set(t.flatMap(e=>e.lineNumbers)).size)),partners:r.length>0?r:n(0)}})}function v(e){let{lineNumbers:t}=e;return n.default.ok(t,`No matched lines for ${e.file}, a scanned file.`),{...e,lineNumbers:t}}function y(e){return e.reduce((e,t)=>e+t.tokenCount,0)}function b(e,t){let n=[],r=e.toSorted((e,t)=>C(e.file,t.file)||e.startLine-t.startLine);for(let e of r){let r=n.at(-1);if(r&&S(r.merged,e)&&(r.sources.some(t=>x(t,e))||!r.sources.some(n=>t(n,e))))r.merged.endLine=Math.max(r.merged.endLine,e.endLine),r.sources.push(e);else{let{file:t,startLine:r,endLine:i}=e;n.push({merged:{file:t,startLine:r,endLine:i},sources:[e]})}}return n}function x(e,t){return e.file===t.file&&e.startLine<=t.startLine&&t.endLine<=e.endLine}function S(e,t){return e.file===t.file&&e.startLine<=t.endLine&&t.startLine<=e.endLine}function C(e,t){return e<t?-1:+(e>t)}exports.checkThresholds=a,exports.qualifyFunctionName=d;
2
2
  //# sourceMappingURL=thresholdCheck.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"thresholdCheck.cjs","names":["functionThresholds","metricNameOf","fileThresholds","levels","duplicationThreshold"],"sources":["../src/thresholdCheck.ts"],"sourcesContent":["import assert from 'node:assert';\nimport type {\n CrossFileDuplicateBlockGroup,\n CrossFileDuplicateOccurrence,\n CrossFileDuplicationMetrics,\n} from './crossFileDuplication.js';\nimport type { LineHunk } from './git.js';\nimport {\n duplicationThreshold,\n fileThresholds,\n functionThresholds,\n levels,\n metricNameOf,\n type Level,\n type LimitsByLevel,\n type Threshold,\n} from './thresholds.js';\nimport type { CodeMetrics, CognitiveBlock, FunctionMetrics } from './types.js';\n\n/** A measured file to check, under the path cross-file duplication knows it by. */\nexport interface CheckedFile {\n file: string;\n metrics: CodeMetrics;\n /** The change to the file; when set, only what the change touches is checked. */\n hunks?: readonly LineHunk[];\n}\n\n/** The duplication of the measured files, in which duplicated blocks and their copies are found. */\nexport interface CheckedDuplication {\n crossFile?: CrossFileDuplicationMetrics;\n /** The detection's `minSimilarityPercent`, the share of a block a copy of it holds at least. */\n minSimilarityPercent: number;\n}\n\nexport interface ExceededLimit {\n metric: string;\n value: number;\n /** The most severe level whose limit the value violates. */\n level: Level;\n /** That level's limit: the value is allowed up to it; a duplicated block is allowed below it. */\n limit: number;\n}\n\nexport interface BlockLocation {\n file: string;\n startLine: number;\n endLine: number;\n}\n\n/** One copy of a clone group. */\ninterface CloneOccurrence extends BlockLocation {\n /**\n * Its code lines matched in another copy, whose number is the length of a duplicated block. Its\n * line span would also count the lines between the matched parts of one copy, which the copies\n * do not share; a block matched as a whole by a near-miss copy holds all its code lines.\n */\n lineNumbers: readonly number[];\n tokenCount: number;\n}\n\nexport interface Violation extends BlockLocation {\n kind: 'function' | 'file' | 'duplication';\n /** The most severe level among `exceeded`. */\n level: Level;\n /** The function's name, qualified with the class it is a member of, for kind `function`. */\n name?: string;\n exceeded: ExceededLimit[];\n /**\n * The parts adding the most to the cognitive complexity, largest first, for a function that\n * exceeds a limit of it and has such parts: operator sequences and jumps outside any branching\n * construct add to the complexity without being one.\n */\n largestBlocks?: CognitiveBlock[];\n /** The other copies of the block, most matched tokens first, for kind `duplication`. */\n partners?: BlockLocation[];\n}\n\nexport interface CheckResult {\n violations: Violation[];\n checkedFunctionCount: number;\n}\n\nconst violationKinds: readonly Violation['kind'][] = ['file', 'function', 'duplication'];\n\nconst cognitiveComplexityMetric = 'functionCognitiveComplexity';\n\nconst nestedInclusiveMetrics = new Set(\n functionThresholds\n .filter(({ includesNestedFunctions }) => includesNestedFunctions)\n .map((threshold) => metricNameOf(threshold))\n);\n\n/** Every limit the files exceed, errors first, then ordered by path, then line. */\nexport function checkThresholds(\n files: readonly CheckedFile[],\n duplication: CheckedDuplication,\n limitsOf: (language: string) => LimitsByLevel\n): CheckResult {\n const crossFileGroupsByFile = indexGroupsByFile(duplication.crossFile, new Set(files.map(({ file }) => file)));\n const violations: Violation[] = [];\n let checkedFunctionCount = 0;\n for (const checkedFile of files) {\n const { file, metrics, hunks } = checkedFile;\n const limits = limitsOf(metrics.language);\n // A rename or a mode change lists the file as changed without touching any of its lines.\n const fileExceeded = hunks?.length === 0 ? [] : collectExceeded(fileThresholds, metrics, limits);\n if (fileExceeded.length > 0) {\n violations.push(toViolation({ file, startLine: 1, endLine: metrics.lines.total }, 'file', fileExceeded));\n }\n const checkedFunctions = listCheckedFunctions(checkedFile);\n checkedFunctionCount += checkedFunctions.length;\n const exceedingFunctions = checkedFunctions\n .map((fn) => ({ fn, exceeded: collectExceeded(functionThresholds, fn, limits) }))\n .filter(({ exceeded }) => exceeded.length > 0);\n for (const { fn, exceeded } of exceedingFunctions) {\n const ownExceeded = exceeded.filter((limit) => !isReportedByEnclosingFunction(limit, fn, exceedingFunctions));\n if (ownExceeded.length > 0) {\n violations.push(toFunctionViolation(file, fn, ownExceeded));\n }\n }\n violations.push(\n ...collectDuplicationViolations(checkedFile, crossFileGroupsByFile.get(file) ?? [], limits, duplication)\n );\n }\n return { violations: violations.toSorted(compareViolations), checkedFunctionCount };\n}\n\n// The kind is compared before the end line because a file violation's end line is corrected after\n// this ordering (to the last line that exists), and the order must not depend on that correction.\nfunction compareViolations(left: Violation, right: Violation): number {\n return (\n levels.indexOf(right.level) - levels.indexOf(left.level) ||\n compareStrings(left.file, right.file) ||\n left.startLine - right.startLine ||\n violationKinds.indexOf(left.kind) - violationKinds.indexOf(right.kind) ||\n left.endLine - right.endLine\n );\n}\n\n/** The file's functions or, for a changed file, those the change touches. */\nfunction listCheckedFunctions({ metrics, hunks }: CheckedFile): FunctionMetrics[] {\n const endsByIndentation = metrics.language === 'python';\n return hunks\n ? metrics.functions.filter((fn) =>\n hunks.some((hunk) => touchesSpan(hunk, fn.startLine, fn.endLine, endsByIndentation))\n )\n : metrics.functions;\n}\n\n/**\n * Whether a function enclosing `fn` exceeds the same limit with a value that covers `fn`, so\n * reporting it for `fn` too would count the same code twice. The enclosing value is at least that\n * of `fn` and both share the file's limits, so its level is never milder.\n */\nfunction isReportedByEnclosingFunction(\n limit: ExceededLimit,\n fn: FunctionMetrics,\n exceedingFunctions: readonly { fn: FunctionMetrics; exceeded: ExceededLimit[] }[]\n): boolean {\n if (!nestedInclusiveMetrics.has(limit.metric)) {\n return false;\n }\n return exceedingFunctions.some(\n (outer) => outer.fn !== fn && encloses(outer.fn, fn) && outer.exceeded.some(({ metric }) => metric === limit.metric)\n );\n}\n\nfunction encloses(outer: FunctionMetrics, inner: FunctionMetrics): boolean {\n const startsBefore =\n outer.startLine < inner.startLine ||\n (outer.startLine === inner.startLine && outer.startColumn <= inner.startColumn);\n const endsAfter =\n outer.endLine > inner.endLine || (outer.endLine === inner.endLine && outer.endColumn >= inner.endColumn);\n return startsBefore && endsAfter;\n}\n\nfunction toFunctionViolation(file: string, fn: FunctionMetrics, exceeded: ExceededLimit[]): Violation {\n const { startLine, endLine } = fn;\n const exceedsCognitiveComplexity = exceeded.some(({ metric }) => metric === cognitiveComplexityMetric);\n return {\n ...toViolation({ file, startLine, endLine }, 'function', exceeded),\n name: qualifyFunctionName(fn),\n ...(exceedsCognitiveComplexity && fn.cognitiveBlocks.length > 0 && { largestBlocks: fn.cognitiveBlocks }),\n };\n}\n\n/** How reports name a function: with the type or module it is a member of, so that it can be found. */\nexport function qualifyFunctionName({ name = '<anonymous>', containerName }: FunctionMetrics): string {\n return containerName ? `${containerName}.${name}` : name;\n}\n\nfunction toViolation(location: BlockLocation, kind: Violation['kind'], exceeded: ExceededLimit[]): Violation {\n const level = exceeded.some((limit) => limit.level === 'error') ? 'error' : 'warning';\n return { kind, level, ...location, exceeded };\n}\n\nfunction collectExceeded<Subject>(\n thresholds: readonly Threshold<Subject>[],\n subject: Subject,\n limits: LimitsByLevel\n): ExceededLimit[] {\n return thresholds.flatMap((threshold) => findExceeded(threshold, subject, limits, (value, limit) => value > limit));\n}\n\n/** The most severe violated limit of the threshold, if any. */\nfunction findExceeded<Subject>(\n threshold: Threshold<Subject>,\n subject: Subject,\n limits: LimitsByLevel,\n violates: (value: number, limit: number) => boolean\n): ExceededLimit[] {\n const value = threshold.measure(subject);\n const level = levels.findLast((candidate) => violates(value, limits[candidate][threshold.key] as number));\n return level\n ? [{ metric: metricNameOf(threshold), value, level, limit: limits[level][threshold.key] as number }]\n : [];\n}\n\n/**\n * Whether the hunk changes the head lines [startLine, endLine]. A hunk without head lines (a pure\n * deletion) sits between its headStart line and the next one. Where indentation ends a span, its\n * last line is a statement rather than a closing delimiter, so a deletion right after that line may\n * have removed the span's tail; `endsByIndentation` counts it as touching, since the head alone\n * cannot tell it from a deletion after the span.\n */\nfunction touchesSpan(hunk: LineHunk, startLine: number, endLine: number, endsByIndentation = false): boolean {\n return hunk.headCount === 0\n ? hunk.headStart >= startLine && (hunk.headStart < endLine || (endsByIndentation && hunk.headStart === endLine))\n : hunk.headStart <= endLine && hunk.headStart + hunk.headCount > startLine;\n}\n\n/** The cross-file groups with an occurrence in each of the given files. */\nfunction indexGroupsByFile(\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n files: ReadonlySet<string>\n): Map<string, CrossFileDuplicateBlockGroup[]> {\n const groupsByFile = new Map<string, CrossFileDuplicateBlockGroup[]>();\n for (const group of crossFileDuplication?.groups ?? []) {\n for (const file of group.files) {\n if (files.has(file)) {\n const groups = groupsByFile.get(file) ?? [];\n groups.push(group);\n groupsByFile.set(file, groups);\n }\n }\n }\n return groupsByFile;\n}\n\n/**\n * One violation per duplicated region of the file: its clone occurrences, within-file and\n * cross-file, that hold at least the duplicated lines of a level (with `hunks`, only those\n * overlapping added lines), with overlapping occurrences merged into one region.\n */\nfunction collectDuplicationViolations(\n { file, metrics, hunks }: CheckedFile,\n crossFileGroups: readonly CrossFileDuplicateBlockGroup[],\n limits: LimitsByLevel,\n { minSimilarityPercent }: CheckedDuplication\n): Violation[] {\n const findExceededLines = (duplicatedLineCount: number): ExceededLimit[] =>\n findExceeded(duplicationThreshold, { duplicatedLineCount }, limits, (value, limit) => value >= limit);\n const groups: CloneOccurrence[][] = [\n ...metrics.duplication.duplicateBlockGroups.map((group) => group.map((occurrence) => ({ ...occurrence, file }))),\n ...crossFileGroups.map((group) => group.occurrences.map((occurrence) => toCloneOccurrence(occurrence))),\n ];\n const isReported = (block: CloneOccurrence): boolean =>\n block.file === file &&\n findExceededLines(block.lineNumbers.length).length > 0 &&\n (!hunks || hunks.some((hunk) => hunk.headCount > 0 && touchesSpan(hunk, block.startLine, block.endLine)));\n const groupByBlock = new Map<CloneOccurrence, CloneOccurrence[]>();\n for (const group of groups) {\n for (const block of group.filter((occurrence) => isReported(occurrence))) {\n groupByBlock.set(block, group);\n }\n }\n return mergeOverlapping([...groupByBlock.keys()]).map(({ merged, sources }) => {\n const listCopies = (minTokenPercent: number): BlockLocation[] =>\n mergeOverlapping(\n sources.flatMap((source) => {\n // The region merges every clone that overlaps it, so a copy of an occurrence lying\n // within a larger one is measured against the larger one.\n const tokenCount = Math.max(\n ...sources.filter((outer) => contains(outer, source)).map((outer) => outer.tokenCount)\n );\n return (groupByBlock.get(source) ?? []).filter(\n (copy) => copy.tokenCount * 100 >= tokenCount * minTokenPercent\n );\n })\n )\n // The region's own occurrences are among the copies of the groups it belongs to.\n .filter((partner) => !overlaps(partner.merged, merged))\n // The largest say the most about what to share.\n .toSorted((left, right) => sumTokens(right.sources) - sumTokens(left.sources))\n .map((partner) => partner.merged);\n // A group joins a block with every place repeating a part of it: a function another file\n // copies whole is grouped with each file holding a few of its lines. Such a fragment is not a\n // copy of the block: sharing it would not remove the block, and a report listing it reads as\n // one more copy. Sizes are compared in tokens, since the same code may be laid out over any\n // number of lines.\n const copies = listCopies(minSimilarityPercent);\n return {\n ...toViolation(\n merged,\n 'duplication',\n // Occurrences that overlap share lines, which count once.\n findExceededLines(new Set(sources.flatMap((source) => source.lineNumbers)).size)\n ),\n // A group does not always hold the copy a block was matched with as a whole: within a file,\n // a copy that already belongs to an exact group is listed as its part in that group. The\n // fragments then are all that locates the block's copies.\n partners: copies.length > 0 ? copies : listCopies(0),\n };\n });\n}\n\nfunction toCloneOccurrence(occurrence: CrossFileDuplicateOccurrence): CloneOccurrence {\n const { lineNumbers } = occurrence;\n assert.ok(lineNumbers, `No matched lines for ${occurrence.file}, a scanned file.`);\n return { ...occurrence, lineNumbers };\n}\n\nfunction sumTokens(occurrences: readonly CloneOccurrence[]): number {\n return occurrences.reduce((sum, occurrence) => sum + occurrence.tokenCount, 0);\n}\n\n/** The locations with those overlapping in the same file merged, ordered by file, then line. */\nfunction mergeOverlapping(\n locations: readonly CloneOccurrence[]\n): { merged: BlockLocation; sources: CloneOccurrence[] }[] {\n const regions: { merged: BlockLocation; sources: CloneOccurrence[] }[] = [];\n const ordered = locations.toSorted(\n (left, right) => compareStrings(left.file, right.file) || left.startLine - right.startLine\n );\n for (const location of ordered) {\n const last = regions.at(-1);\n if (last && overlaps(last.merged, location)) {\n last.merged.endLine = Math.max(last.merged.endLine, location.endLine);\n last.sources.push(location);\n } else {\n const { file, startLine, endLine } = location;\n regions.push({ merged: { file, startLine, endLine }, sources: [location] });\n }\n }\n return regions;\n}\n\n/** Whether `outer` spans every line of `inner`; a block contains itself. */\nfunction contains(outer: BlockLocation, inner: BlockLocation): boolean {\n return outer.file === inner.file && outer.startLine <= inner.startLine && inner.endLine <= outer.endLine;\n}\n\nfunction overlaps(left: BlockLocation, right: BlockLocation): boolean {\n return left.file === right.file && left.startLine <= right.endLine && right.startLine <= left.endLine;\n}\n\n/** Code-unit order, so reports do not depend on the locale. */\nfunction compareStrings(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"],"mappings":"6IAkFA,MAAM,EAA+C,CAAC,OAAQ,WAAY,aAAa,EAIjF,EAAyB,IAAI,IACjCA,EAAAA,mBACG,QAAQ,CAAE,6BAA8B,CAAuB,CAAC,CAChE,IAAK,GAAcC,EAAAA,aAAa,CAAS,CAAC,CAC/C,EAGA,SAAgB,EACd,EACA,EACA,EACa,CACb,IAAM,EAAwB,EAAkB,EAAY,UAAW,IAAI,IAAI,EAAM,KAAK,CAAE,UAAW,CAAI,CAAC,CAAC,EACvG,EAA0B,CAAC,EAC7B,EAAuB,EAC3B,IAAK,IAAM,KAAe,EAAO,CAC/B,GAAM,CAAE,OAAM,UAAS,SAAU,EAC3B,EAAS,EAAS,EAAQ,QAAQ,EAElC,EAAe,GAAO,SAAW,EAAI,CAAC,EAAI,EAAgBC,EAAAA,eAAgB,EAAS,CAAM,EAC3F,EAAa,OAAS,GACxB,EAAW,KAAK,EAAY,CAAE,OAAM,UAAW,EAAG,QAAS,EAAQ,MAAM,KAAM,EAAG,OAAQ,CAAY,CAAC,EAEzG,IAAM,EAAmB,EAAqB,CAAW,EACzD,GAAwB,EAAiB,OACzC,IAAM,EAAqB,EACxB,IAAK,IAAQ,CAAE,KAAI,SAAU,EAAgBF,EAAAA,mBAAoB,EAAI,CAAM,CAAE,EAAE,CAAC,CAChF,QAAQ,CAAE,cAAe,EAAS,OAAS,CAAC,EAC/C,IAAK,GAAM,CAAE,KAAI,cAAc,EAAoB,CACjD,IAAM,EAAc,EAAS,OAAQ,GAAU,CAAC,EAA8B,EAAO,EAAI,CAAkB,CAAC,EACxG,EAAY,OAAS,GACvB,EAAW,KAAK,EAAoB,EAAM,EAAI,CAAW,CAAC,CAE9D,CACA,EAAW,KACT,GAAG,EAA6B,EAAa,EAAsB,IAAI,CAAI,GAAK,CAAC,EAAG,EAAQ,CAAW,CACzG,CACF,CACA,MAAO,CAAE,WAAY,EAAW,SAAS,CAAiB,EAAG,sBAAqB,CACpF,CAIA,SAAS,EAAkB,EAAiB,EAA0B,CACpE,OACEG,EAAAA,OAAO,QAAQ,EAAM,KAAK,EAAIA,EAAAA,OAAO,QAAQ,EAAK,KAAK,GACvD,EAAe,EAAK,KAAM,EAAM,IAAI,GACpC,EAAK,UAAY,EAAM,WACvB,EAAe,QAAQ,EAAK,IAAI,EAAI,EAAe,QAAQ,EAAM,IAAI,GACrE,EAAK,QAAU,EAAM,OAEzB,CAGA,SAAS,EAAqB,CAAE,UAAS,SAAyC,CAChF,IAAM,EAAoB,EAAQ,WAAa,SAC/C,OAAO,EACH,EAAQ,UAAU,OAAQ,GACxB,EAAM,KAAM,GAAS,EAAY,EAAM,EAAG,UAAW,EAAG,QAAS,CAAiB,CAAC,CACrF,EACA,EAAQ,SACd,CAOA,SAAS,EACP,EACA,EACA,EACS,CAIT,OAHK,EAAuB,IAAI,EAAM,MAAM,EAGrC,EAAmB,KACvB,GAAU,EAAM,KAAO,GAAM,EAAS,EAAM,GAAI,CAAE,GAAK,EAAM,SAAS,MAAM,CAAE,YAAa,IAAW,EAAM,MAAM,CACrH,EAJS,EAKX,CAEA,SAAS,EAAS,EAAwB,EAAiC,CACzE,IAAM,EACJ,EAAM,UAAY,EAAM,WACvB,EAAM,YAAc,EAAM,WAAa,EAAM,aAAe,EAAM,YAC/D,EACJ,EAAM,QAAU,EAAM,SAAY,EAAM,UAAY,EAAM,SAAW,EAAM,WAAa,EAAM,UAChG,OAAO,GAAgB,CACzB,CAEA,SAAS,EAAoB,EAAc,EAAqB,EAAsC,CACpG,GAAM,CAAE,YAAW,WAAY,EACzB,EAA6B,EAAS,MAAM,CAAE,YAAa,IAAW,6BAAyB,EACrG,MAAO,CACL,GAAG,EAAY,CAAE,OAAM,YAAW,SAAQ,EAAG,WAAY,CAAQ,EACjE,KAAM,EAAoB,CAAE,EAC5B,GAAI,GAA8B,EAAG,gBAAgB,OAAS,GAAK,CAAE,cAAe,EAAG,eAAgB,CACzG,CACF,CAGA,SAAgB,EAAoB,CAAE,OAAO,cAAe,iBAA0C,CACpG,OAAO,EAAgB,GAAG,EAAc,GAAG,IAAS,CACtD,CAEA,SAAS,EAAY,EAAyB,EAAyB,EAAsC,CAE3G,MAAO,CAAE,OAAM,MADD,EAAS,KAAM,GAAU,EAAM,QAAU,OAAO,EAAI,QAAU,UACtD,GAAG,EAAU,UAAS,CAC9C,CAEA,SAAS,EACP,EACA,EACA,EACiB,CACjB,OAAO,EAAW,QAAS,GAAc,EAAa,EAAW,EAAS,GAAS,EAAO,IAAU,EAAQ,CAAK,CAAC,CACpH,CAGA,SAAS,EACP,EACA,EACA,EACA,EACiB,CACjB,IAAM,EAAQ,EAAU,QAAQ,CAAO,EACjC,EAAQA,EAAAA,OAAO,SAAU,GAAc,EAAS,EAAO,EAAO,EAAU,CAAC,EAAU,IAAc,CAAC,EACxG,OAAO,EACH,CAAC,CAAE,OAAQF,EAAAA,aAAa,CAAS,EAAG,QAAO,QAAO,MAAO,EAAO,EAAM,CAAC,EAAU,IAAe,CAAC,EACjG,CAAC,CACP,CASA,SAAS,EAAY,EAAgB,EAAmB,EAAiB,EAAoB,GAAgB,CAC3G,OAAO,EAAK,YAAc,EACtB,EAAK,WAAa,IAAc,EAAK,UAAY,GAAY,GAAqB,EAAK,YAAc,GACrG,EAAK,WAAa,GAAW,EAAK,UAAY,EAAK,UAAY,CACrE,CAGA,SAAS,EACP,EACA,EAC6C,CAC7C,IAAM,kBAAe,IAAI,IACzB,IAAK,IAAM,KAAS,GAAsB,QAAU,CAAC,EACnD,IAAK,IAAM,KAAQ,EAAM,MACvB,GAAI,EAAM,IAAI,CAAI,EAAG,CACnB,IAAM,EAAS,EAAa,IAAI,CAAI,GAAK,CAAC,EAC1C,EAAO,KAAK,CAAK,EACjB,EAAa,IAAI,EAAM,CAAM,CAC/B,CAGJ,OAAO,CACT,CAOA,SAAS,EACP,CAAE,OAAM,UAAS,SACjB,EACA,EACA,CAAE,wBACW,CACb,IAAM,EAAqB,GACzB,EAAaG,EAAAA,qBAAsB,CAAE,qBAAoB,EAAG,GAAS,EAAO,IAAU,GAAS,CAAK,EAChG,EAA8B,CAClC,GAAG,EAAQ,YAAY,qBAAqB,IAAK,GAAU,EAAM,IAAK,IAAgB,CAAE,GAAG,EAAY,MAAK,EAAE,CAAC,EAC/G,GAAG,EAAgB,IAAK,GAAU,EAAM,YAAY,IAAK,GAAe,EAAkB,CAAU,CAAC,CAAC,CACxG,EACM,EAAc,GAClB,EAAM,OAAS,GACf,EAAkB,EAAM,YAAY,MAAM,CAAC,CAAC,OAAS,IACpD,CAAC,GAAS,EAAM,KAAM,GAAS,EAAK,UAAY,GAAK,EAAY,EAAM,EAAM,UAAW,EAAM,OAAO,CAAC,GACnG,kBAAe,IAAI,IACzB,IAAK,IAAM,KAAS,EAClB,IAAK,IAAM,KAAS,EAAM,OAAQ,GAAe,EAAW,CAAU,CAAC,EACrE,EAAa,IAAI,EAAO,CAAK,EAGjC,OAAO,EAAiB,CAAC,GAAG,EAAa,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAE,SAAQ,aAAc,CAC7E,IAAM,EAAc,GAClB,EACE,EAAQ,QAAS,GAAW,CAG1B,IAAM,EAAa,KAAK,IACtB,GAAG,EAAQ,OAAQ,GAAU,EAAS,EAAO,CAAM,CAAC,CAAC,CAAC,IAAK,GAAU,EAAM,UAAU,CACvF,EACA,OAAQ,EAAa,IAAI,CAAM,GAAK,CAAC,EAAA,CAAG,OACrC,GAAS,EAAK,WAAa,KAAO,EAAa,CAClD,CACF,CAAC,CACH,CAAC,CAEE,OAAQ,GAAY,CAAC,EAAS,EAAQ,OAAQ,CAAM,CAAC,CAAC,CAEtD,UAAU,EAAM,IAAU,EAAU,EAAM,OAAO,EAAI,EAAU,EAAK,OAAO,CAAC,CAAC,CAC7E,IAAK,GAAY,EAAQ,MAAM,EAM9B,EAAS,EAAW,CAAoB,EAC9C,MAAO,CACL,GAAG,EACD,EACA,cAEA,EAAkB,IAAI,IAAI,EAAQ,QAAS,GAAW,EAAO,WAAW,CAAC,CAAC,CAAC,IAAI,CACjF,EAIA,SAAU,EAAO,OAAS,EAAI,EAAS,EAAW,CAAC,CACrD,CACF,CAAC,CACH,CAEA,SAAS,EAAkB,EAA2D,CACpF,GAAM,CAAE,eAAgB,EAExB,OADA,EAAA,QAAO,GAAG,EAAa,wBAAwB,EAAW,KAAK,kBAAkB,EAC1E,CAAE,GAAG,EAAY,aAAY,CACtC,CAEA,SAAS,EAAU,EAAiD,CAClE,OAAO,EAAY,QAAQ,EAAK,IAAe,EAAM,EAAW,WAAY,CAAC,CAC/E,CAGA,SAAS,EACP,EACyD,CACzD,IAAM,EAAmE,CAAC,EACpE,EAAU,EAAU,UACvB,EAAM,IAAU,EAAe,EAAK,KAAM,EAAM,IAAI,GAAK,EAAK,UAAY,EAAM,SACnF,EACA,IAAK,IAAM,KAAY,EAAS,CAC9B,IAAM,EAAO,EAAQ,GAAG,EAAE,EAC1B,GAAI,GAAQ,EAAS,EAAK,OAAQ,CAAQ,EACxC,EAAK,OAAO,QAAU,KAAK,IAAI,EAAK,OAAO,QAAS,EAAS,OAAO,EACpE,EAAK,QAAQ,KAAK,CAAQ,MACrB,CACL,GAAM,CAAE,OAAM,YAAW,WAAY,EACrC,EAAQ,KAAK,CAAE,OAAQ,CAAE,OAAM,YAAW,SAAQ,EAAG,QAAS,CAAC,CAAQ,CAAE,CAAC,CAC5E,CACF,CACA,OAAO,CACT,CAGA,SAAS,EAAS,EAAsB,EAA+B,CACrE,OAAO,EAAM,OAAS,EAAM,MAAQ,EAAM,WAAa,EAAM,WAAa,EAAM,SAAW,EAAM,OACnG,CAEA,SAAS,EAAS,EAAqB,EAA+B,CACpE,OAAO,EAAK,OAAS,EAAM,MAAQ,EAAK,WAAa,EAAM,SAAW,EAAM,WAAa,EAAK,OAChG,CAGA,SAAS,EAAe,EAAc,EAAuB,CAC3D,OAAO,EAAO,EAAQ,GAAK,IAAO,EACpC"}
1
+ {"version":3,"file":"thresholdCheck.cjs","names":["functionThresholds","metricNameOf","fileThresholds","levels","duplicationThreshold"],"sources":["../src/thresholdCheck.ts"],"sourcesContent":["import assert from 'node:assert';\nimport type {\n CrossFileDuplicateBlockGroup,\n CrossFileDuplicateOccurrence,\n CrossFileDuplicationMetrics,\n} from './crossFileDuplication.js';\nimport type { LineHunk } from './git.js';\nimport {\n duplicationThreshold,\n fileThresholds,\n functionThresholds,\n levels,\n metricNameOf,\n type Level,\n type LimitsByLevel,\n type Threshold,\n} from './thresholds.js';\nimport type { CodeMetrics, CognitiveBlock, FunctionMetrics } from './types.js';\n\n/** A measured file to check, under the path cross-file duplication knows it by. */\nexport interface CheckedFile {\n file: string;\n metrics: CodeMetrics;\n /** The change to the file; when set, only what the change touches is checked. */\n hunks?: readonly LineHunk[];\n}\n\n/** The duplication of the measured files, in which duplicated blocks and their copies are found. */\nexport interface CheckedDuplication {\n crossFile?: CrossFileDuplicationMetrics;\n /** The detection's `minSimilarityPercent`, the share of a block a copy of it holds at least. */\n minSimilarityPercent: number;\n}\n\nexport interface ExceededLimit {\n metric: string;\n /** What reports call the metric (`cognitive complexity`, `duplicated lines`). */\n label: string;\n value: number;\n /** The most severe level whose limit the value violates. */\n level: Level;\n /** That level's limit: the value is allowed up to it; a duplicated block is allowed below it. */\n limit: number;\n}\n\nexport interface BlockLocation {\n file: string;\n startLine: number;\n endLine: number;\n}\n\n/** One copy of a clone group. */\ninterface CloneOccurrence extends BlockLocation {\n /**\n * Its code lines matched in another copy, whose number is the length of a duplicated block. Its\n * line span would also count the lines the copies do not share.\n */\n lineNumbers: readonly number[];\n tokenCount: number;\n}\n\nexport interface Violation extends BlockLocation {\n kind: 'function' | 'file' | 'duplication';\n /** The most severe level among `exceeded`. */\n level: Level;\n /** The function's name, qualified with the class it is a member of, for kind `function`. */\n name?: string;\n exceeded: ExceededLimit[];\n /**\n * The parts adding the most to the cognitive complexity, largest first, for a function that\n * exceeds a limit of it and has such parts: operator sequences and jumps outside any branching\n * construct add to the complexity without being one.\n */\n largestBlocks?: CognitiveBlock[];\n /** The other copies of the block, most matched tokens first, for kind `duplication`. */\n partners?: BlockLocation[];\n}\n\nexport interface CheckResult {\n violations: Violation[];\n checkedFunctionCount: number;\n}\n\nconst violationKinds: readonly Violation['kind'][] = ['file', 'function', 'duplication'];\n\nconst cognitiveComplexityMetric = 'functionCognitiveComplexity';\n\nconst nestedInclusiveMetrics = new Set(\n functionThresholds\n .filter(({ includesNestedFunctions }) => includesNestedFunctions)\n .map((threshold) => metricNameOf(threshold))\n);\n\n/** Every limit the files exceed, errors first, then ordered by path, then line. */\nexport function checkThresholds(\n files: readonly CheckedFile[],\n duplication: CheckedDuplication,\n limitsOf: (language: string) => LimitsByLevel\n): CheckResult {\n const crossFileGroupsByFile = indexGroupsByFile(duplication.crossFile, new Set(files.map(({ file }) => file)));\n const violations: Violation[] = [];\n let checkedFunctionCount = 0;\n for (const checkedFile of files) {\n const { file, metrics, hunks } = checkedFile;\n const limits = limitsOf(metrics.language);\n // A rename or a mode change lists the file as changed without touching any of its lines.\n const fileExceeded = hunks?.length === 0 ? [] : collectExceeded(fileThresholds, metrics, limits);\n if (fileExceeded.length > 0) {\n violations.push(toViolation({ file, startLine: 1, endLine: metrics.lines.total }, 'file', fileExceeded));\n }\n const checkedFunctions = listCheckedFunctions(checkedFile);\n checkedFunctionCount += checkedFunctions.length;\n const exceedingFunctions = checkedFunctions\n .map((fn) => ({ fn, exceeded: collectExceeded(functionThresholds, fn, limits) }))\n .filter(({ exceeded }) => exceeded.length > 0);\n for (const { fn, exceeded } of exceedingFunctions) {\n const ownExceeded = exceeded.filter((limit) => !isReportedByEnclosingFunction(limit, fn, exceedingFunctions));\n if (ownExceeded.length > 0) {\n violations.push(toFunctionViolation(file, fn, ownExceeded));\n }\n }\n violations.push(\n ...collectDuplicationViolations(checkedFile, crossFileGroupsByFile.get(file) ?? [], limits, duplication)\n );\n }\n return { violations: violations.toSorted(compareViolations), checkedFunctionCount };\n}\n\n// The kind is compared before the end line because a file violation's end line is corrected after\n// this ordering (to the last line that exists), and the order must not depend on that correction.\nfunction compareViolations(left: Violation, right: Violation): number {\n return (\n levels.indexOf(right.level) - levels.indexOf(left.level) ||\n compareStrings(left.file, right.file) ||\n left.startLine - right.startLine ||\n violationKinds.indexOf(left.kind) - violationKinds.indexOf(right.kind) ||\n left.endLine - right.endLine\n );\n}\n\n/** The file's functions or, for a changed file, those the change touches. */\nfunction listCheckedFunctions({ metrics, hunks }: CheckedFile): FunctionMetrics[] {\n const endsByIndentation = metrics.language === 'python';\n return hunks\n ? metrics.functions.filter((fn) =>\n hunks.some((hunk) => touchesSpan(hunk, fn.startLine, fn.endLine, endsByIndentation))\n )\n : metrics.functions;\n}\n\n/**\n * Whether a function enclosing `fn` exceeds the same limit with a value that covers `fn`, so\n * reporting it for `fn` too would count the same code twice. The enclosing value is at least that\n * of `fn` and both share the file's limits, so its level is never milder.\n */\nfunction isReportedByEnclosingFunction(\n limit: ExceededLimit,\n fn: FunctionMetrics,\n exceedingFunctions: readonly { fn: FunctionMetrics; exceeded: ExceededLimit[] }[]\n): boolean {\n if (!nestedInclusiveMetrics.has(limit.metric)) {\n return false;\n }\n return exceedingFunctions.some(\n (outer) => outer.fn !== fn && encloses(outer.fn, fn) && outer.exceeded.some(({ metric }) => metric === limit.metric)\n );\n}\n\nfunction encloses(outer: FunctionMetrics, inner: FunctionMetrics): boolean {\n const startsBefore =\n outer.startLine < inner.startLine ||\n (outer.startLine === inner.startLine && outer.startColumn <= inner.startColumn);\n const endsAfter =\n outer.endLine > inner.endLine || (outer.endLine === inner.endLine && outer.endColumn >= inner.endColumn);\n return startsBefore && endsAfter;\n}\n\nfunction toFunctionViolation(file: string, fn: FunctionMetrics, exceeded: ExceededLimit[]): Violation {\n const { startLine, endLine } = fn;\n const exceedsCognitiveComplexity = exceeded.some(({ metric }) => metric === cognitiveComplexityMetric);\n return {\n ...toViolation({ file, startLine, endLine }, 'function', exceeded),\n name: qualifyFunctionName(fn),\n ...(exceedsCognitiveComplexity && fn.cognitiveBlocks.length > 0 && { largestBlocks: fn.cognitiveBlocks }),\n };\n}\n\n/** How reports name a function: with the type or module it is a member of, so that it can be found. */\nexport function qualifyFunctionName({ name = '<anonymous>', containerName }: FunctionMetrics): string {\n return containerName ? `${containerName}.${name}` : name;\n}\n\nfunction toViolation(location: BlockLocation, kind: Violation['kind'], exceeded: ExceededLimit[]): Violation {\n const level = exceeded.some((limit) => limit.level === 'error') ? 'error' : 'warning';\n return { kind, level, ...location, exceeded };\n}\n\nfunction collectExceeded<Subject>(\n thresholds: readonly Threshold<Subject>[],\n subject: Subject,\n limits: LimitsByLevel\n): ExceededLimit[] {\n return thresholds.flatMap((threshold) => findExceeded(threshold, subject, limits, (value, limit) => value > limit));\n}\n\n/** The most severe violated limit of the threshold, if any. */\nfunction findExceeded<Subject>(\n threshold: Threshold<Subject>,\n subject: Subject,\n limits: LimitsByLevel,\n violates: (value: number, limit: number) => boolean\n): ExceededLimit[] {\n const value = threshold.measure(subject);\n const level = levels.findLast((candidate) => violates(value, limits[candidate][threshold.key] as number));\n return level\n ? [\n {\n metric: metricNameOf(threshold),\n label: threshold.label,\n value,\n level,\n limit: limits[level][threshold.key] as number,\n },\n ]\n : [];\n}\n\n/**\n * Whether the hunk changes the head lines [startLine, endLine]. A hunk without head lines (a pure\n * deletion) sits between its headStart line and the next one. Where indentation ends a span, its\n * last line is a statement rather than a closing delimiter, so a deletion right after that line may\n * have removed the span's tail; `endsByIndentation` counts it as touching, since the head alone\n * cannot tell it from a deletion after the span.\n */\nfunction touchesSpan(hunk: LineHunk, startLine: number, endLine: number, endsByIndentation = false): boolean {\n return hunk.headCount === 0\n ? hunk.headStart >= startLine && (hunk.headStart < endLine || (endsByIndentation && hunk.headStart === endLine))\n : hunk.headStart <= endLine && hunk.headStart + hunk.headCount > startLine;\n}\n\n/** The cross-file groups with an occurrence in each of the given files. */\nfunction indexGroupsByFile(\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n files: ReadonlySet<string>\n): Map<string, CrossFileDuplicateBlockGroup[]> {\n const groupsByFile = new Map<string, CrossFileDuplicateBlockGroup[]>();\n for (const group of crossFileDuplication?.groups ?? []) {\n for (const file of group.files) {\n if (files.has(file)) {\n const groups = groupsByFile.get(file) ?? [];\n groups.push(group);\n groupsByFile.set(file, groups);\n }\n }\n }\n return groupsByFile;\n}\n\n/**\n * One violation per duplicated region of the file: its clone occurrences, within-file and\n * cross-file, that hold at least the duplicated lines of a level (with `hunks`, only those\n * overlapping added lines), with overlapping occurrences merged into one region unless they are\n * copies of each other.\n */\nfunction collectDuplicationViolations(\n { file, metrics, hunks }: CheckedFile,\n crossFileGroups: readonly CrossFileDuplicateBlockGroup[],\n limits: LimitsByLevel,\n { minSimilarityPercent }: CheckedDuplication\n): Violation[] {\n const findExceededLines = (duplicatedLineCount: number): ExceededLimit[] =>\n findExceeded(duplicationThreshold, { duplicatedLineCount }, limits, (value, limit) => value >= limit);\n const groups: CloneOccurrence[][] = [\n ...metrics.duplication.duplicateBlockGroups.map((group) => group.map((occurrence) => ({ ...occurrence, file }))),\n ...crossFileGroups.map((group) => group.occurrences.map((occurrence) => toCloneOccurrence(occurrence))),\n ];\n const isReported = (block: CloneOccurrence): boolean =>\n block.file === file &&\n findExceededLines(block.lineNumbers.length).length > 0 &&\n (!hunks || hunks.some((hunk) => hunk.headCount > 0 && touchesSpan(hunk, block.startLine, block.endLine)));\n const groupOf = new Map(groups.flatMap((group) => group.map((occurrence) => [occurrence, group] as const)));\n // Copies of one group that share a line (the branches of an `else if` chain, the entries of a\n // list) stay apart: merged, they would be one block without a copy. One lying within another is\n // a repetition inside that block, not a block beside it.\n const areCopies = (left: CloneOccurrence, right: CloneOccurrence): boolean =>\n groupOf.get(left) === groupOf.get(right) && !contains(left, right) && !contains(right, left);\n const reportedBlocks = groups.flat().filter((occurrence) => isReported(occurrence));\n return mergeOverlapping(reportedBlocks, areCopies).map(({ merged, sources }) => {\n const listCopies = (minTokenPercent: number): BlockLocation[] =>\n mergeOverlapping(\n sources.flatMap((source) => {\n // The region merges every clone that overlaps it, so a copy of an occurrence lying\n // within a larger one is measured against the larger one.\n const tokenCount = Math.max(\n ...sources.filter((outer) => contains(outer, source)).map((outer) => outer.tokenCount)\n );\n return (groupOf.get(source) ?? []).filter(\n (copy) =>\n copy.tokenCount * 100 >= tokenCount * minTokenPercent &&\n // The region's own occurrences are among the copies of the groups it belongs to, and\n // so may be an occurrence within one of them that is not reported itself: a\n // repetition inside the block, not another place.\n !sources.some((outer) => contains(outer, copy))\n );\n }),\n areCopies\n )\n // The largest say the most about what to share.\n .toSorted((left, right) => sumTokens(right.sources) - sumTokens(left.sources))\n .map((partner) => partner.merged);\n // A group joins a block with every place repeating a part of it: a function another file\n // copies whole is grouped with each file holding a few of its lines. Such a fragment is not a\n // copy of the block: sharing it would not remove the block, and a report listing it reads as\n // one more copy. Sizes are compared in tokens, since the same code may be laid out over any\n // number of lines.\n const copies = listCopies(minSimilarityPercent);\n return {\n ...toViolation(\n merged,\n 'duplication',\n // Occurrences that overlap share lines, which count once.\n findExceededLines(new Set(sources.flatMap((source) => source.lineNumbers)).size)\n ),\n // A block whose parts were matched with different places has no copy of its size. The\n // places then are all that locates what it repeats.\n partners: copies.length > 0 ? copies : listCopies(0),\n };\n });\n}\n\nfunction toCloneOccurrence(occurrence: CrossFileDuplicateOccurrence): CloneOccurrence {\n const { lineNumbers } = occurrence;\n assert.ok(lineNumbers, `No matched lines for ${occurrence.file}, a scanned file.`);\n return { ...occurrence, lineNumbers };\n}\n\nfunction sumTokens(occurrences: readonly CloneOccurrence[]): number {\n return occurrences.reduce((sum, occurrence) => sum + occurrence.tokenCount, 0);\n}\n\n/**\n * The locations with those overlapping in the same file merged, except those `areCopies` holds\n * for, ordered by file, then line.\n */\nfunction mergeOverlapping(\n locations: readonly CloneOccurrence[],\n areCopies: (left: CloneOccurrence, right: CloneOccurrence) => boolean\n): { merged: BlockLocation; sources: CloneOccurrence[] }[] {\n const regions: { merged: BlockLocation; sources: CloneOccurrence[] }[] = [];\n const ordered = locations.toSorted(\n (left, right) => compareStrings(left.file, right.file) || left.startLine - right.startLine\n );\n for (const location of ordered) {\n const last = regions.at(-1);\n // A block containing the location holds it whatever it is a copy of.\n const joins =\n last &&\n overlaps(last.merged, location) &&\n (last.sources.some((source) => contains(source, location)) ||\n !last.sources.some((source) => areCopies(source, location)));\n if (joins) {\n last.merged.endLine = Math.max(last.merged.endLine, location.endLine);\n last.sources.push(location);\n } else {\n const { file, startLine, endLine } = location;\n regions.push({ merged: { file, startLine, endLine }, sources: [location] });\n }\n }\n return regions;\n}\n\n/** Whether `outer` spans every line of `inner`; a block contains itself. */\nfunction contains(outer: BlockLocation, inner: BlockLocation): boolean {\n return outer.file === inner.file && outer.startLine <= inner.startLine && inner.endLine <= outer.endLine;\n}\n\nfunction overlaps(left: BlockLocation, right: BlockLocation): boolean {\n return left.file === right.file && left.startLine <= right.endLine && right.startLine <= left.endLine;\n}\n\n/** Code-unit order, so reports do not depend on the locale. */\nfunction compareStrings(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"],"mappings":"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"}
@@ -17,6 +17,8 @@ export interface CheckedDuplication {
17
17
  }
18
18
  export interface ExceededLimit {
19
19
  metric: string;
20
+ /** What reports call the metric (`cognitive complexity`, `duplicated lines`). */
21
+ label: string;
20
22
  value: number;
21
23
  /** The most severe level whose limit the value violates. */
22
24
  level: Level;
@@ -1,2 +1,2 @@
1
- import{duplicationThreshold as e,fileThresholds as t,functionThresholds as n,levels as r,metricNameOf as i}from"./thresholds.js";import a from"node:assert";const o=[`file`,`function`,`duplication`],s=new Set(n.filter(({includesNestedFunctions:e})=>e).map(e=>i(e)));function c(e,r,i){let a=y(r.crossFile,new Set(e.map(({file:e})=>e))),o=[],s=0;for(let c of e){let{file:e,metrics:l,hunks:f}=c,m=i(l.language),_=f?.length===0?[]:g(t,l,m);_.length>0&&o.push(h({file:e,startLine:1,endLine:l.lines.total},`file`,_));let v=u(c);s+=v.length;let y=v.map(e=>({fn:e,exceeded:g(n,e,m)})).filter(({exceeded:e})=>e.length>0);for(let{fn:t,exceeded:n}of y){let r=n.filter(e=>!d(e,t,y));r.length>0&&o.push(p(e,t,r))}o.push(...b(c,a.get(e)??[],m,r))}return{violations:o.toSorted(l),checkedFunctionCount:s}}function l(e,t){return r.indexOf(t.level)-r.indexOf(e.level)||E(e.file,t.file)||e.startLine-t.startLine||o.indexOf(e.kind)-o.indexOf(t.kind)||e.endLine-t.endLine}function u({metrics:e,hunks:t}){let n=e.language===`python`;return t?e.functions.filter(e=>t.some(t=>v(t,e.startLine,e.endLine,n))):e.functions}function d(e,t,n){return s.has(e.metric)?n.some(n=>n.fn!==t&&f(n.fn,t)&&n.exceeded.some(({metric:t})=>t===e.metric)):!1}function f(e,t){let n=e.startLine<t.startLine||e.startLine===t.startLine&&e.startColumn<=t.startColumn,r=e.endLine>t.endLine||e.endLine===t.endLine&&e.endColumn>=t.endColumn;return n&&r}function p(e,t,n){let{startLine:r,endLine:i}=t,a=n.some(({metric:e})=>e===`functionCognitiveComplexity`);return{...h({file:e,startLine:r,endLine:i},`function`,n),name:m(t),...a&&t.cognitiveBlocks.length>0&&{largestBlocks:t.cognitiveBlocks}}}function m({name:e=`<anonymous>`,containerName:t}){return t?`${t}.${e}`:e}function h(e,t,n){return{kind:t,level:n.some(e=>e.level===`error`)?`error`:`warning`,...e,exceeded:n}}function g(e,t,n){return e.flatMap(e=>_(e,t,n,(e,t)=>e>t))}function _(e,t,n,a){let o=e.measure(t),s=r.findLast(t=>a(o,n[t][e.key]));return s?[{metric:i(e),value:o,level:s,limit:n[s][e.key]}]:[]}function v(e,t,n,r=!1){return e.headCount===0?e.headStart>=t&&(e.headStart<n||r&&e.headStart===n):e.headStart<=n&&e.headStart+e.headCount>t}function y(e,t){let n=/* @__PURE__ */ new Map;for(let r of e?.groups??[])for(let e of r.files)if(t.has(e)){let t=n.get(e)??[];t.push(r),n.set(e,t)}return n}function b({file:t,metrics:n,hunks:r},i,a,{minSimilarityPercent:o}){let s=t=>_(e,{duplicatedLineCount:t},a,(e,t)=>e>=t),c=[...n.duplication.duplicateBlockGroups.map(e=>e.map(e=>({...e,file:t}))),...i.map(e=>e.occurrences.map(e=>x(e)))],l=e=>e.file===t&&s(e.lineNumbers.length).length>0&&(!r||r.some(t=>t.headCount>0&&v(t,e.startLine,e.endLine))),u=/* @__PURE__ */ new Map;for(let e of c)for(let t of e.filter(e=>l(e)))u.set(t,e);return C([...u.keys()]).map(({merged:e,sources:t})=>{let n=n=>C(t.flatMap(e=>{let r=Math.max(...t.filter(t=>w(t,e)).map(e=>e.tokenCount));return(u.get(e)??[]).filter(e=>e.tokenCount*100>=r*n)})).filter(t=>!T(t.merged,e)).toSorted((e,t)=>S(t.sources)-S(e.sources)).map(e=>e.merged),r=n(o);return{...h(e,`duplication`,s(new Set(t.flatMap(e=>e.lineNumbers)).size)),partners:r.length>0?r:n(0)}})}function x(e){let{lineNumbers:t}=e;return a.ok(t,`No matched lines for ${e.file}, a scanned file.`),{...e,lineNumbers:t}}function S(e){return e.reduce((e,t)=>e+t.tokenCount,0)}function C(e){let t=[],n=e.toSorted((e,t)=>E(e.file,t.file)||e.startLine-t.startLine);for(let e of n){let n=t.at(-1);if(n&&T(n.merged,e))n.merged.endLine=Math.max(n.merged.endLine,e.endLine),n.sources.push(e);else{let{file:n,startLine:r,endLine:i}=e;t.push({merged:{file:n,startLine:r,endLine:i},sources:[e]})}}return t}function w(e,t){return e.file===t.file&&e.startLine<=t.startLine&&t.endLine<=e.endLine}function T(e,t){return e.file===t.file&&e.startLine<=t.endLine&&t.startLine<=e.endLine}function E(e,t){return e<t?-1:+(e>t)}export{c as checkThresholds,m as qualifyFunctionName};
1
+ import{duplicationThreshold as e,fileThresholds as t,functionThresholds as n,levels as r,metricNameOf as i}from"./thresholds.js";import a from"node:assert";const o=[`file`,`function`,`duplication`],s=new Set(n.filter(({includesNestedFunctions:e})=>e).map(e=>i(e)));function c(e,r,i){let a=y(r.crossFile,new Set(e.map(({file:e})=>e))),o=[],s=0;for(let c of e){let{file:e,metrics:l,hunks:f}=c,m=i(l.language),_=f?.length===0?[]:g(t,l,m);_.length>0&&o.push(h({file:e,startLine:1,endLine:l.lines.total},`file`,_));let v=u(c);s+=v.length;let y=v.map(e=>({fn:e,exceeded:g(n,e,m)})).filter(({exceeded:e})=>e.length>0);for(let{fn:t,exceeded:n}of y){let r=n.filter(e=>!d(e,t,y));r.length>0&&o.push(p(e,t,r))}o.push(...b(c,a.get(e)??[],m,r))}return{violations:o.toSorted(l),checkedFunctionCount:s}}function l(e,t){return r.indexOf(t.level)-r.indexOf(e.level)||E(e.file,t.file)||e.startLine-t.startLine||o.indexOf(e.kind)-o.indexOf(t.kind)||e.endLine-t.endLine}function u({metrics:e,hunks:t}){let n=e.language===`python`;return t?e.functions.filter(e=>t.some(t=>v(t,e.startLine,e.endLine,n))):e.functions}function d(e,t,n){return s.has(e.metric)?n.some(n=>n.fn!==t&&f(n.fn,t)&&n.exceeded.some(({metric:t})=>t===e.metric)):!1}function f(e,t){let n=e.startLine<t.startLine||e.startLine===t.startLine&&e.startColumn<=t.startColumn,r=e.endLine>t.endLine||e.endLine===t.endLine&&e.endColumn>=t.endColumn;return n&&r}function p(e,t,n){let{startLine:r,endLine:i}=t,a=n.some(({metric:e})=>e===`functionCognitiveComplexity`);return{...h({file:e,startLine:r,endLine:i},`function`,n),name:m(t),...a&&t.cognitiveBlocks.length>0&&{largestBlocks:t.cognitiveBlocks}}}function m({name:e=`<anonymous>`,containerName:t}){return t?`${t}.${e}`:e}function h(e,t,n){return{kind:t,level:n.some(e=>e.level===`error`)?`error`:`warning`,...e,exceeded:n}}function g(e,t,n){return e.flatMap(e=>_(e,t,n,(e,t)=>e>t))}function _(e,t,n,a){let o=e.measure(t),s=r.findLast(t=>a(o,n[t][e.key]));return s?[{metric:i(e),label:e.label,value:o,level:s,limit:n[s][e.key]}]:[]}function v(e,t,n,r=!1){return e.headCount===0?e.headStart>=t&&(e.headStart<n||r&&e.headStart===n):e.headStart<=n&&e.headStart+e.headCount>t}function y(e,t){let n=/* @__PURE__ */ new Map;for(let r of e?.groups??[])for(let e of r.files)if(t.has(e)){let t=n.get(e)??[];t.push(r),n.set(e,t)}return n}function b({file:t,metrics:n,hunks:r},i,a,{minSimilarityPercent:o}){let s=t=>_(e,{duplicatedLineCount:t},a,(e,t)=>e>=t),c=[...n.duplication.duplicateBlockGroups.map(e=>e.map(e=>({...e,file:t}))),...i.map(e=>e.occurrences.map(e=>x(e)))],l=e=>e.file===t&&s(e.lineNumbers.length).length>0&&(!r||r.some(t=>t.headCount>0&&v(t,e.startLine,e.endLine))),u=new Map(c.flatMap(e=>e.map(t=>[t,e]))),d=(e,t)=>u.get(e)===u.get(t)&&!w(e,t)&&!w(t,e);return C(c.flat().filter(e=>l(e)),d).map(({merged:e,sources:t})=>{let n=e=>C(t.flatMap(n=>{let r=Math.max(...t.filter(e=>w(e,n)).map(e=>e.tokenCount));return(u.get(n)??[]).filter(n=>n.tokenCount*100>=r*e&&!t.some(e=>w(e,n)))}),d).toSorted((e,t)=>S(t.sources)-S(e.sources)).map(e=>e.merged),r=n(o);return{...h(e,`duplication`,s(new Set(t.flatMap(e=>e.lineNumbers)).size)),partners:r.length>0?r:n(0)}})}function x(e){let{lineNumbers:t}=e;return a.ok(t,`No matched lines for ${e.file}, a scanned file.`),{...e,lineNumbers:t}}function S(e){return e.reduce((e,t)=>e+t.tokenCount,0)}function C(e,t){let n=[],r=e.toSorted((e,t)=>E(e.file,t.file)||e.startLine-t.startLine);for(let e of r){let r=n.at(-1);if(r&&T(r.merged,e)&&(r.sources.some(t=>w(t,e))||!r.sources.some(n=>t(n,e))))r.merged.endLine=Math.max(r.merged.endLine,e.endLine),r.sources.push(e);else{let{file:t,startLine:r,endLine:i}=e;n.push({merged:{file:t,startLine:r,endLine:i},sources:[e]})}}return n}function w(e,t){return e.file===t.file&&e.startLine<=t.startLine&&t.endLine<=e.endLine}function T(e,t){return e.file===t.file&&e.startLine<=t.endLine&&t.startLine<=e.endLine}function E(e,t){return e<t?-1:+(e>t)}export{c as checkThresholds,m as qualifyFunctionName};
2
2
  //# sourceMappingURL=thresholdCheck.js.map