code-gauge 4.5.0 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -6
- package/dist/crossFileDuplication.cjs +1 -1
- package/dist/crossFileDuplication.cjs.map +1 -1
- package/dist/crossFileDuplication.d.ts +2 -2
- package/dist/crossFileDuplication.js +1 -1
- package/dist/crossFileDuplication.js.map +1 -1
- package/dist/crossFileNearMiss.cjs +1 -1
- package/dist/crossFileNearMiss.cjs.map +1 -1
- package/dist/crossFileNearMiss.d.ts +18 -12
- package/dist/crossFileNearMiss.js +1 -1
- package/dist/crossFileNearMiss.js.map +1 -1
- package/dist/types.d.ts +3 -1
- package/native/src/duplication.rs +332 -234
- package/native/src/lib.rs +1 -0
- package/native/src/near_miss.rs +455 -0
- package/package.json +10 -10
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{createLcsLengthCounter as e}from"./duplication.js";function
|
|
1
|
+
import{createLcsLengthCounter as e,lcsLength as t}from"./duplication.js";function n(e,t,n,l){if(l>=100)return[];let u=_(e),d=c(u,n,l),f=t.map(a),p=t.map(i),m=u.map(({fileIndex:e,range:t})=>p[e]?.(t)??!1),h=u.map(({fileIndex:e,range:t})=>f[e]?.(t)??!1),g=[];s(u,m,l,(e,t)=>{let n=u[e],r=u[t],i=n&&r&&d(n,r,t);if(i){if(i.kind===`whole`)h[e]&&h[t]||g.push([e,void 0,t,void 0]);else for(let[n,r]of i.cores)g.push([e,n,t,r])}});let v=u.map(()=>!1),y=u.map(()=>[]);for(let[e,t,n,r]of g)for(let[i,a]of[[e,t],[n,r]])a?y[i]?.push(a):v[i]=!0;let b=[],x=[];for(let e of u.keys()){x.push(b.length);let t=v[e]?[]:r(y[e]??[]);t.length===0&&b.push({blockIndex:e,core:void 0});for(let n of t)b.push({blockIndex:e,core:n})}let S=(e,t)=>{let n=x[e]??0;if(!t||v[e])return n;for(let r=n;b[r]?.blockIndex===e;r+=1){let e=b[r]?.core;if(e&&e[0]<=t[0]&&t[1]<=e[1])return r}throw Error(`every local core lies in one of its block's merged cores`)},C=b.map((e,t)=>t),w=e=>{let t=e;for(;C[t]!==t;)t=C[t]??t;for(let n=e;C[n]!==t;){let e=C[n]??t;C[n]=t,n=e}return t},T=b.map(({blockIndex:e,core:t})=>{let n=u[e],[r,i]=t??[n?.range.startTokenIndex??0,n?.range.endTokenIndex??0];return f[n?.fileIndex??0]?.({startTokenIndex:r,endTokenIndex:i})??!1});for(let[e,t,n,r]of g){let i=S(e,t),a=S(n,r);if(T[i]&&T[a])continue;let o=w(i),s=w(a);C[Math.max(o,s)]=Math.min(o,s)}let E=new Map;for(let e of b.keys()){let t=w(e),n=E.get(t)??[];n.push(e),E.set(t,n)}let D=[];for(let t of E.values()){if(t.length<2||t.every(e=>T[e]))continue;let n=new Map;for(let e of t){let{blockIndex:t=0,core:r}=b[e]??{},i=n.get(t)??{cores:[],anchor:!1};i.cores.push(r),i.anchor||=T[e]??!1,n.set(t,i)}D.push([...n].flatMap(([t,{cores:n,anchor:r}])=>{let i=u[t];return i?[o(i,e,n,r)]:[]}))}return D}function r(e){let t=[];for(let[n,r]of e.toSorted((e,t)=>e[0]-t[0])){let e=t.at(-1);e&&n<e[1]?e[1]=Math.max(e[1],r):t.push([n,r])}return t}function i(e){let t=[];for(let{startTokenIndex:n,endTokenIndex:r}of e.toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex)){let e=t.at(-1);e&&n<=e[1]?e[1]=Math.max(e[1],r):t.push([n,r])}return e=>{let n=0,r=t.length;for(;n<r;){let i=n+r>>>1;(t[i]?.[0]??0)<=e.startTokenIndex?n=i+1:r=i}return(t[n-1]?.[1]??-1)>=e.endTokenIndex}}function a(e){let t=e.toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex),n=new Int32Array(t.length),r=-1;for(let[e,i]of t.entries())r=Math.max(r,i.endTokenIndex),n[e]=r;return e=>{let r=0,i=t.length;for(;r<i;){let n=r+i>>>1;(t[n]?.startTokenIndex??0)<e.endTokenIndex?r=n+1:i=n}return r>0&&(n[r-1]??-1)>e.startTokenIndex}}function o({fileIndex:e,range:t},n,r,i){let a=r.includes(void 0),o=r.map(e=>e??[t.startTokenIndex,t.endTokenIndex]).toSorted((e,t)=>e[0]-t[0]).map(([e,t])=>({startTokenIndex:e,endTokenIndex:t})),s=o[0]?.startTokenIndex??t.startTokenIndex,c=o.at(-1)?.endTokenIndex??t.endTokenIndex,l=n[e]?.tokens;return{fileIndex:e,spanCountedElsewhere:i||void 0,segments:o,tokenCount:o.reduce((e,t)=>e+t.endTokenIndex-t.startTokenIndex,0),startTokenIndex:s,endTokenIndex:c,startIndex:t.startIndex,endIndex:t.endIndex,startLine:a?t.startLine:(l?.[s]?.startRow??0)+1,endLine:a?t.endLine:(l?.[c-1]?.endRow??0)+1}}function s(e,t,n,r){let i=new Map;for(let t of e)for(let e of t.ngrams)i.set(e,(i.get(e)??0)+1);for(let t of e)t.ngrams=t.ngrams.filter(e=>(i.get(e)??0)<=1e3);let a=Int32Array.from(e,e=>e.fileIndex),o=Uint8Array.from(t,Number),s=Int32Array.from(e,e=>e.sequence.length),c=Int32Array.from(e,e=>e.ngrams.length),l=[...e.keys()].toSorted((e,t)=>(s[e]??0)-(s[t]??0)),u=new Map,d=new Int32Array(e.length),f=[];for(let t of l){let i=a[t],l=o[t]===1,p=Math.min(Math.ceil((s[t]??0)/3),Math.ceil(n*(s[t]??0)/100)),m=e[t]?.ngrams??[];for(let e of m){let n=u.get(e);if(!n){u.set(e,[t]);continue}for(let e=n.length-1;e>=0;--e){let t=n[e]??0;if((s[t]??0)<p)break;a[t]===i||l&&o[t]===1||(d[t]===0&&f.push(t),d[t]=(d[t]??0)+1)}n.push(t)}for(let e of f){let n=d[e]??0;d[e]=0,n*100>=10*Math.min(c[e]??0,m.length)&&r(e,t)}f.length=0}}function c(n,r,i){let a=new Map;for(let e of n)for(let t of e.contentCounts.keys())a.set(t,(a.get(t)??0)+1);let o=e=>Math.min(31-Math.clz32(Math.floor((n.length+1)/e))+1,3),s=new Map;for(let[e,t]of a)s.set(e,o(t));let c=e=>{let t=Int32Array.from(e.keys()).toSorted(),n=Int32Array.from(t,t=>(e.get(t)??0)*(s.get(t)??0)),r=0;for(let e of n)r+=e;return{symbols:t,weightedCounts:n,total:r}},d=new Map(n.map(e=>[e,c(e.contentCounts)])),f=-1,_,v=(t,n,r)=>((f!==n||!_)&&(f=n,_=e(t.sequence)),_(r)),x=(e,n)=>{let a=[];for(let t=0,r=0;t<e.uniqueNgrams.length&&r<n.uniqueNgrams.length;){let i=e.uniqueNgrams[t]??0,o=n.uniqueNgrams[r]??0;i===o&&a.push([e.uniqueNgramOffsets[t]??0,n.uniqueNgramOffsets[r]??0]),i<=o&&(t+=1),o<=i&&(r+=1)}a.sort((e,t)=>e[0]-t[0]);let o=a.filter(([e,t],n)=>{let r=a[n-1],i=a[n+1];return r?.[0]===e-1&&r[1]===t-1||i?.[0]===e+1&&i[1]===t+1}),s=e.range.startTokenIndex,u=n.range.startTokenIndex,d=[];for(let a of m(p(o))){let[o,f]=a[0]??[0,0],[p,m]=a.at(-1)??[0,0],g=p+5,_=m+5,v=g-o,x=_-f,S=Math.min(v,x),C=i*Math.max(v,x);S>=r&&S*100>=C&&h(a)*100>=50*S&&l(c(y(e.symbols,e.isContent,o,g)),c(y(n.symbols,n.isContent,f,_)))&&t(b(e.symbols.subarray(o,g)),b(n.symbols.subarray(f,_)))*100>=C&&d.push([[s+o,s+g],[u+f,u+_]])}return d.length>0?{kind:`local`,cores:d}:void 0};return(e,t,n)=>{let r=i*Math.max(e.sequence.length,t.sequence.length);return Math.min(e.sequence.length,t.sequence.length)*100>=r&&l(d.get(e),d.get(t))&&(g(e.sortedSequence,t.sortedSequence)*100>=r&&v(t,n,e.sequence)*100>=r||u(e,t,r))?{kind:`whole`}:x(e,t)}}function l(e,t){if(!e||!t)return!1;let n=0;for(let r=0,i=0;r<e.symbols.length&&i<t.symbols.length;){let a=e.symbols[r]??0,o=t.symbols[i]??0;a===o&&(n+=Math.min(e.weightedCounts[r]??0,t.weightedCounts[i]??0)),a<=o&&(r+=1),o<=a&&(i+=1)}return n*100>50*Math.max(e.total,t.total)}function u(e,n,r){return e.canonicalSequence!==void 0&&n.canonicalSequence!==void 0&&t(e.canonicalSequence,n.canonicalSequence)*100>=r}function d(e,t,n){if(t.length<2)return;let r=[],i=0;for(let[a,o]of t){let t=a-n;i<t&&r.push(b(e.subarray(i,t))),r.push(b(e.subarray(t,o-n))),i=o-n}i<e.length&&r.push(b(e.subarray(i))),r.sort(f);let a=new Int32Array(e.length),o=0;for(let e of r)a.set(e,o),o+=e.length;return a}function f(e,t){for(let n=0;n<Math.min(e.length,t.length);n+=1){let r=(e[n]??0)-(t[n]??0);if(r!==0)return r}return e.length-t.length}function p(e){let t=[],n=[];for(let[r,[,i]]of e.entries()){let a=0,o=t.length;for(;a<o;){let n=a+o>>>1;(e[t[n]??0]?.[1]??0)<i?a=n+1:o=n}n.push(a>0?t[a-1]??-1:-1),t[a]=r}let r=[];for(let i=t.at(-1)??-1;i>=0;i=n[i]??-1){let t=e[i];t&&r.push(t)}return r.toReversed()}function m(e){let t=[];for(let[n,r]of e.entries()){let i=e[n-1];i!==void 0&&r[0]-(i[0]+5)<=30&&r[1]-(i[1]+5)<=30?t.at(-1)?.push(r):t.push([r])}return t}function h(e){let t=5;for(let n=1;n<e.length;n+=1)t+=Math.min((e[n]?.[0]??0)-(e[n-1]?.[0]??0),5);return t}function g(e,t){let n=0,r=0,i=0;for(;r<e.length&&i<t.length;){let a=e[r]??0,o=t[i]??0;a===o?(n+=1,r+=1,i+=1):a<o?r+=1:i+=1}return n}function _(e){let t=new Map,n=[];for(let[r,{tokens:i,containerStatements:a,nearMissBlocks:o}]of e.entries()){if(!i||!o?.length)continue;let e=new Int32Array(i.length),s=new Uint8Array(i.length),c=new Map;for(let[n,r]of i.entries()){if(r.kind===`id`){let t=c.get(r.text);t===void 0&&(t=c.size,c.set(r.text,t)),e[n]=-(t+1);continue}let i=x(r),a=t.get(i);a===void 0&&(a=t.size,t.set(i,a)),e[n]=a,s[n]=r.isName||r.literalHash!==void 0?1:0}let l=v(a??[]);for(let t of o){let{startTokenIndex:i,endTokenIndex:a}=t,o=e.subarray(i,a),c=s.subarray(i,a),u=b(o),f=S(o),p=new Map;for(let e of f)p.set(e,(p.get(e)??0)+1);let m=f.keys().filter(e=>p.get(f[e]??0)===1).toArray().toSorted((e,t)=>(f[e]??0)-(f[t]??0));n.push({fileIndex:r,range:t,symbols:o,isContent:c,sequence:u,sortedSequence:u.toSorted(),ngrams:Int32Array.from(p.keys()),uniqueNgrams:Int32Array.from(m,e=>f[e]??0),uniqueNgramOffsets:Int32Array.from(m),contentCounts:y(o,c,0,o.length),canonicalSequence:d(o,l(i,a),i)})}}return n}function v(e){let t=e.flat().filter(e=>e.startTokenIndex<e.endTokenIndex).map(e=>[e.startTokenIndex,e.endTokenIndex]).toSorted((e,t)=>e[0]-t[0]||t[1]-e[1]);return(e,n)=>{let r=0,i=t.length;for(;r<i;){let n=r+i>>>1;(t[n]?.[0]??0)<e?r=n+1:i=n}let a=[];for(let i=r;i<t.length;i+=1){let r=t[i];if(!r||r[0]>=n)break;let o=a.at(-1),s=o!==void 0&&r[0]<o[1];r[1]<=n&&(r[0]!==e||r[1]!==n)&&!s&&a.push(r)}return a}}function y(e,t,n,r){let i=new Map;for(let a=n;a<r;a+=1)if(t[a]===1){let t=e[a]??0;i.set(t,(i.get(t)??0)+1)}return i}function b(e){let t=new Map;return e.map(e=>{if(e>=0)return e;let n=t.get(e);return n===void 0&&(n=t.size,t.set(e,n)),-(n+1)})}function x(e){let t=e.textHash^Math.imul(e.literalHash??0,2654435761),n=e.textHash2^Math.imul(e.literalHash2??0,2246822507);return(t>>>0)*2097152+(n>>>11)}function S(e){let t=new Int32Array(Math.max(e.length-5+1,0));for(let n=0;n<t.length;n+=1){let r=5381;for(let t=0;t<5;t+=1){let i=e[n+t]??0;r=Math.imul(r,31)+(i<0?-1:i)|0}t[n]=r}return t}export{n as collectCrossFileNearMissGroups};
|
|
2
2
|
//# sourceMappingURL=crossFileNearMiss.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crossFileNearMiss.js","names":[],"sources":["../src/crossFileNearMiss.ts"],"sourcesContent":["import { createLcsLengthCounter, type CountedOccurrence, type Token, type TokenRange } from './duplication.js';\n\n/**\n * Cross-file near-miss (Type-3) clone detection, following the within-file detector's model (the\n * native collect_near_miss_groups): candidate block pairs are filtered through an n-gram inverted\n * index (NIL, Nakagawa et al. 2021), then verified by token-level longest common subsequence against\n * the larger block (NiCad's per-fragment similarity). Only pairs of blocks in different files are\n * compared: a same-file pair is the within-file detector's concern.\n */\n\nexport interface NearMissSourceFile {\n tokens?: Token[];\n nearMissBlocks?: TokenRange[];\n}\n\n/** A block the exact cross-file pipeline did not report, or one it did (an anchor). */\nexport interface NearMissOccurrence extends CountedOccurrence {\n fileIndex: number;\n}\n\n/** N-gram size of the candidate index (NIL's default). */\nconst ngramSize = 5;\n/** Filtration threshold: shared distinct n-grams over the smaller block's (NIL's default). */\nconst filtrationPercent = 10;\n/**\n * A structural match must also share content: more than this percent of the larger block's\n * content-bearing tokens (names and literal values), so blocks of the same shape that call\n * different APIs on different data are not clones.\n */\nconst minContentSimilarityPercent = 50;\n/**\n * N-grams occurring in more blocks than this are stop n-grams (syntax boilerplate such as a chain\n * of closing braces), left out of the index and of each block's n-gram count. Counting shared\n * n-grams costs the square of an n-gram's block frequency, so without the cap a project's most\n * common n-grams make filtration quadratic in the block count, while they discriminate nothing.\n */\nconst maxNgramBlockFrequency = 1000;\n\ninterface NormalizedBlock {\n fileIndex: number;\n range: TokenRange;\n /** Identifiers as -(first-occurrence index + 1); other tokens as interned symbols (>= 0). */\n sequence: Int32Array;\n /** The sequence sorted, for the token-bag upper bound on the LCS. */\n sortedSequence: Int32Array;\n /** Content-bearing symbols (names and literal values) sorted, for the content gate. */\n sortedContent: Int32Array;\n /** Distinct non-stop n-gram hashes. */\n ngrams: Int32Array;\n}\n\n/**\n * Clusters verified cross-file near-miss pairs into groups. A block overlapping an occurrence of\n * `reportedSpansByFile` (the exact cross-file groups) is an anchor: it links near-miss copies to\n * the content an exact group already reports, and appears in the near-miss group marked\n * `spanCountedElsewhere` so block counting does not count its span twice. Pairs of two anchors are\n * skipped, and a group needs at least one non-anchor block.\n */\nexport function collectCrossFileNearMissGroups(\n files: NearMissSourceFile[],\n reportedSpansByFile: { startTokenIndex: number; endTokenIndex: number }[][],\n minSimilarityPercent: number\n): NearMissOccurrence[][] {\n if (minSimilarityPercent >= 100) {\n return [];\n }\n const blocks = normalizeBlocks(files);\n const overlapsReportedSpan = reportedSpansByFile.map(createOverlapTest);\n const anchored = blocks.map(({ fileIndex, range }) => overlapsReportedSpan[fileIndex]?.(range) ?? false);\n const parent = blocks.map((_, index) => index);\n const find = (index: number): number => {\n let root = index;\n while (parent[root] !== root) {\n root = parent[root] ?? root;\n }\n for (let current = index; parent[current] !== root;) {\n const next = parent[current] ?? root;\n parent[current] = root;\n current = next;\n }\n return root;\n };\n // Every candidate pair of one `right` block is visited consecutively, so one LCS counter (its\n // position masks built once) serves them all.\n let counterBlock = -1;\n let counter: ((sequence: Int32Array) => number) | undefined;\n const lcsLengthWithRight = (right: number, sequence: Int32Array): number => {\n if (counterBlock !== right || !counter) {\n counterBlock = right;\n counter = createLcsLengthCounter(blocks[right]?.sequence ?? new Int32Array());\n }\n return counter(sequence);\n };\n forEachCandidatePair(blocks, anchored, minSimilarityPercent, (left, right) => {\n if (\n isNearMissPair(blocks[left], blocks[right], minSimilarityPercent, (sequence) =>\n lcsLengthWithRight(right, sequence)\n )\n ) {\n const leftRoot = find(left);\n const rightRoot = find(right);\n parent[Math.max(leftRoot, rightRoot)] = Math.min(leftRoot, rightRoot);\n }\n });\n\n const membersByRoot = new Map<number, number[]>();\n for (const index of blocks.keys()) {\n const root = find(index);\n const members = membersByRoot.get(root) ?? [];\n members.push(index);\n membersByRoot.set(root, members);\n }\n const groups: NearMissOccurrence[][] = [];\n for (const members of membersByRoot.values()) {\n // Components form only through cross-file pairs, so two members always span two files.\n if (members.length < 2 || members.every((index) => anchored[index])) {\n continue;\n }\n groups.push(\n members.flatMap((index) => {\n const block = blocks[index];\n return block ? [toOccurrence(block, anchored[index] ?? false)] : [];\n })\n );\n }\n return groups;\n}\n\n/**\n * Whether a range overlaps any of the spans: among the spans starting before the range ends\n * (binary search over sorted starts), the furthest end reaches past the range's start.\n */\nfunction createOverlapTest(\n spans: { startTokenIndex: number; endTokenIndex: number }[]\n): (range: { startTokenIndex: number; endTokenIndex: number }) => boolean {\n const sorted = spans.toSorted((left, right) => left.startTokenIndex - right.startTokenIndex);\n const maxEndPrefix = new Int32Array(sorted.length);\n let maxEnd = -1;\n for (const [index, span] of sorted.entries()) {\n maxEnd = Math.max(maxEnd, span.endTokenIndex);\n maxEndPrefix[index] = maxEnd;\n }\n return (range) => {\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) < range.endTokenIndex) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low > 0 && (maxEndPrefix[low - 1] ?? -1) > range.startTokenIndex;\n };\n}\n\nfunction toOccurrence({ fileIndex, range }: NormalizedBlock, anchor: boolean): NearMissOccurrence {\n return {\n fileIndex,\n spanCountedElsewhere: anchor || undefined,\n segments: [{ startTokenIndex: range.startTokenIndex, endTokenIndex: range.endTokenIndex }],\n tokenCount: range.endTokenIndex - range.startTokenIndex,\n startTokenIndex: range.startTokenIndex,\n endTokenIndex: range.endTokenIndex,\n startIndex: range.startIndex,\n endIndex: range.endIndex,\n startLine: range.startLine,\n endLine: range.endLine,\n };\n}\n\n/**\n * Visits every cross-file block pair sharing at least `filtrationPercent` of the smaller block's\n * non-stop n-grams, except pairs of two anchors and pairs whose length ratio alone rules out the\n * similarity requirement (the LCS cannot exceed the shorter block). Blocks are indexed in ascending\n * length, so each posting list is scanned backwards only while its blocks are long enough; shared\n * counts accumulate in a dense counter, so no pair map is materialized.\n */\nfunction forEachCandidatePair(\n blocks: NormalizedBlock[],\n anchored: boolean[],\n minSimilarityPercent: number,\n visit: (left: number, right: number) => void\n): void {\n const blockFrequency = new Map<number, number>();\n for (const block of blocks) {\n for (const ngram of block.ngrams) {\n blockFrequency.set(ngram, (blockFrequency.get(ngram) ?? 0) + 1);\n }\n }\n for (const block of blocks) {\n block.ngrams = block.ngrams.filter((ngram) => (blockFrequency.get(ngram) ?? 0) <= maxNgramBlockFrequency);\n }\n\n // Typed copies keep the posting loop, which dominates this phase, free of object dereferences.\n const fileIndexes = Int32Array.from(blocks, (block) => block.fileIndex);\n const anchorFlags = Uint8Array.from(anchored, Number);\n const lengths = Int32Array.from(blocks, (block) => block.sequence.length);\n const ngramCounts = Int32Array.from(blocks, (block) => block.ngrams.length);\n const order = [...blocks.keys()].toSorted((left, right) => (lengths[left] ?? 0) - (lengths[right] ?? 0));\n const postings = new Map<number, number[]>();\n const sharedCounts = new Int32Array(blocks.length);\n const touched: number[] = [];\n for (const right of order) {\n const fileIndex = fileIndexes[right];\n const rightAnchored = anchorFlags[right] === 1;\n const minLeftLength = Math.ceil((minSimilarityPercent * (lengths[right] ?? 0)) / 100);\n const ngrams = blocks[right]?.ngrams ?? [];\n for (const ngram of ngrams) {\n const posting = postings.get(ngram);\n if (!posting) {\n postings.set(ngram, [right]);\n continue;\n }\n for (let position = posting.length - 1; position >= 0; position -= 1) {\n const left = posting[position] ?? 0;\n if ((lengths[left] ?? 0) < minLeftLength) {\n break;\n }\n if (fileIndexes[left] === fileIndex || (rightAnchored && anchorFlags[left] === 1)) {\n continue;\n }\n if (sharedCounts[left] === 0) {\n touched.push(left);\n }\n sharedCounts[left] = (sharedCounts[left] ?? 0) + 1;\n }\n posting.push(right);\n }\n for (const left of touched) {\n const shared = sharedCounts[left] ?? 0;\n sharedCounts[left] = 0;\n if (shared * 100 >= filtrationPercent * Math.min(ngramCounts[left] ?? 0, ngrams.length)) {\n visit(left, right);\n }\n }\n touched.length = 0;\n }\n}\n\n/**\n * Verifies a filtered pair, cheapest bounds first. The LCS cannot exceed the shorter block's\n * length nor the token-bag overlap, so either bound falling below the similarity requirement\n * rejects the pair exactly without running the LCS.\n */\nfunction isNearMissPair(\n left: NormalizedBlock | undefined,\n right: NormalizedBlock | undefined,\n minSimilarityPercent: number,\n lcsLengthWithRight: (sequence: Int32Array) => number\n): boolean {\n if (!left || !right) {\n return false;\n }\n const required = minSimilarityPercent * Math.max(left.sequence.length, right.sequence.length);\n if (Math.min(left.sequence.length, right.sequence.length) * 100 < required) {\n return false;\n }\n if (\n sortedOverlap(left.sortedContent, right.sortedContent) * 100 <=\n minContentSimilarityPercent * Math.max(left.sortedContent.length, right.sortedContent.length)\n ) {\n return false;\n }\n if (sortedOverlap(left.sortedSequence, right.sortedSequence) * 100 < required) {\n return false;\n }\n return lcsLengthWithRight(left.sequence) * 100 >= required;\n}\n\n/** Multiset intersection size of two ascending arrays. */\nfunction sortedOverlap(left: Int32Array, right: Int32Array): number {\n let overlap = 0;\n let leftIndex = 0;\n let rightIndex = 0;\n while (leftIndex < left.length && rightIndex < right.length) {\n const leftValue = left[leftIndex] ?? 0;\n const rightValue = right[rightIndex] ?? 0;\n if (leftValue === rightValue) {\n overlap += 1;\n leftIndex += 1;\n rightIndex += 1;\n } else if (leftValue < rightValue) {\n leftIndex += 1;\n } else {\n rightIndex += 1;\n }\n }\n return overlap;\n}\n\n/**\n * Normalizes every block like the within-file detector: identifiers are anonymized by first\n * occurrence within the block, and every other token keeps its text and literal value. Symbols are\n * interned project-wide from the tokens' hash pairs, so equal tokens compare equal across files.\n */\nfunction normalizeBlocks(files: NearMissSourceFile[]): NormalizedBlock[] {\n const symbolByTokenKey = new Map<number, number>();\n const blocks: NormalizedBlock[] = [];\n for (const [fileIndex, { tokens, nearMissBlocks }] of files.entries()) {\n if (!tokens) {\n continue;\n }\n for (const range of nearMissBlocks ?? []) {\n const sequence = new Int32Array(range.endTokenIndex - range.startTokenIndex);\n const content: number[] = [];\n const indexByIdentifier = new Map<string, number>();\n for (let index = range.startTokenIndex; index < range.endTokenIndex; index += 1) {\n const token = tokens[index];\n if (!token) {\n continue;\n }\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 sequence[index - range.startTokenIndex] = -(identifierIndex + 1);\n continue;\n }\n const key = tokenKey(token);\n let symbol = symbolByTokenKey.get(key);\n if (symbol === undefined) {\n symbol = symbolByTokenKey.size;\n symbolByTokenKey.set(key, symbol);\n }\n sequence[index - range.startTokenIndex] = symbol;\n if (token.isName || token.literalHash !== undefined) {\n content.push(symbol);\n }\n }\n blocks.push({\n fileIndex,\n range,\n sequence,\n sortedSequence: sequence.toSorted(),\n sortedContent: Int32Array.from(content).toSorted(),\n ngrams: collectNgrams(sequence),\n });\n }\n }\n return blocks;\n}\n\n/**\n * A 53-bit key from the token's two independent text hashes, each mixed with the matching literal\n * value hash: exact in a JavaScript number, so interning never merges distinct tokens unless 53\n * hash bits collide.\n */\nfunction tokenKey(token: Token): number {\n const primary = token.textHash ^ Math.imul(token.literalHash ?? 0, 0x9E_37_79_B1);\n const secondary = token.textHash2 ^ Math.imul(token.literalHash2 ?? 0, 0x85_EB_CA_6B);\n return (primary >>> 0) * 0x20_00_00 + (secondary >>> 11);\n}\n\nfunction collectNgrams(sequence: Int32Array): Int32Array {\n const ngrams = new Set<number>();\n for (let start = 0; start + ngramSize <= sequence.length; start += 1) {\n let hash = 5381;\n for (let offset = 0; offset < ngramSize; offset += 1) {\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `| 0` wraps the sum to int32 like the native n-gram hash.\n hash = (Math.imul(hash, 31) + (sequence[start + offset] ?? 0)) | 0;\n }\n ngrams.add(hash);\n }\n return Int32Array.from(ngrams);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"crossFileNearMiss.js","names":[],"sources":["../src/crossFileNearMiss.ts"],"sourcesContent":["import {\n createLcsLengthCounter,\n lcsLength,\n type CountedOccurrence,\n type Token,\n type TokenRange,\n} from './duplication.js';\n\n/**\n * Cross-file near-miss (Type-3) clone detection, following the within-file detector's model\n * (native/src/near_miss.rs): candidate block pairs are filtered through an n-gram inverted index\n * (NIL, Nakagawa et al. 2021), then verified by token-level longest common subsequence against the\n * larger block (NiCad's per-fragment similarity), backed by an information-weighted content gate,\n * with a statement-order-insensitive fallback and a local match over the anchored cores of two\n * blocks. Only pairs of blocks in different files are compared: a same-file pair is the\n * within-file detector's concern.\n */\n\nexport interface NearMissSourceFile {\n tokens?: Token[];\n containerStatements?: TokenRange[][];\n nearMissBlocks?: TokenRange[];\n}\n\n/** One copy (a block or its matched cores) in a near-miss group; anchors carry `spanCountedElsewhere` (see collectCrossFileNearMissGroups). */\nexport interface NearMissOccurrence extends CountedOccurrence {\n fileIndex: number;\n}\n\n/** N-gram size of the candidate index and local-match anchors (NIL's default). */\nconst ngramSize = 5;\n/** Filtration threshold: shared distinct n-grams over the smaller block's (NIL's default). */\nconst filtrationPercent = 10;\n/**\n * Pairs whose longer block exceeds this multiple of the shorter are compared only when whole-block\n * similarity still allows their ratio (below a minSimilarityPercent of 34). The candidate scan\n * stops at this floor while walking length-ordered postings, so pairs of very different lengths are\n * neither counted nor verified; `maxNgramBlockFrequency` is what bounds the scan's total cost.\n */\nconst maxLengthRatio = 3;\n/**\n * A structural match must also share content: more than this percent of the larger side's\n * information-weighted content-bearing tokens (names and literal values), so blocks of the same\n * shape that call different APIs on different data are not clones.\n */\nconst minContentSimilarityPercent = 50;\n/**\n * Caps content weights so only names and values spread over more than a quarter of the blocks are\n * discounted: a family of copies shares its content across several blocks, and uncapped rarity\n * weighting would let each copy's few unique edits outweigh everything the family shares.\n */\nconst maxContentWeight = 3;\n/**\n * Anchors must cover at least this percent of the shorter core: sparser chains are coincidental\n * runs of common n-grams in merely similar-looking code, and the cheap bound spares their content\n * and LCS checks. Not the similarity threshold itself, since n-grams repeated within a block\n * (repetitive statements) never anchor.\n */\nconst minAnchorCoveragePercent = 50;\n/** Anchors farther apart than this (in either block) split a local match into separate chains. */\nconst maxAnchorGapTokens = 30;\n/** Statement-order-insensitive comparison needs this many top-level statements per block. */\nconst minReorderStatementCount = 2;\n/**\n * N-grams occurring in more blocks than this are stop n-grams (syntax boilerplate such as a chain\n * of closing braces), left out of the index and of each block's n-gram count. Counting shared\n * n-grams costs the square of an n-gram's block frequency, so without the cap a project's most\n * common n-grams make filtration quadratic in the block count, while they discriminate nothing.\n */\nconst maxNgramBlockFrequency = 1000;\n\ninterface NormalizedBlock {\n fileIndex: number;\n range: TokenRange;\n /** Interned non-identifier symbols (>= 0) and identifiers as -(file-level id + 1). */\n symbols: Int32Array;\n isContent: Uint8Array;\n /** Identifiers anonymized by first occurrence within the block. */\n sequence: Int32Array;\n /** The sequence sorted, for the token-bag upper bound on the LCS. */\n sortedSequence: Int32Array;\n /** Distinct non-stop n-gram hashes. */\n ngrams: Int32Array;\n /**\n * The n-grams occurring exactly once in the block, sorted, with their offsets in the parallel\n * array: two blocks' local-match anchors intersect by merging.\n */\n uniqueNgrams: Int32Array;\n uniqueNgramOffsets: Int32Array;\n contentCounts: Map<number, number>;\n /**\n * The sequence with its top-level statements in canonical order, when it has enough of them for\n * statement-order-insensitive comparison.\n */\n canonicalSequence: Int32Array | undefined;\n}\n\n/** A verified core in each block of a pair, as file token ranges. */\ntype CorePair = [[number, number], [number, number]];\n\n/** How a verified pair matched: whole blocks, or every anchored core pair (one per gap-split chain segment). */\ntype PairMatch = { kind: 'whole' } | { kind: 'local'; cores: CorePair[] };\n\n/**\n * Clusters verified cross-file near-miss pairs into groups. A node (a whole block or a matched\n * core) overlapping an occurrence of `reportedSpansByFile` (the exact cross-file groups) is an\n * anchor: it links near-miss copies to the content an exact group already reports, and appears in\n * the near-miss group marked `spanCountedElsewhere` so block counting does not count its span\n * twice. Pairs of two anchors are skipped (blocks wholly covered by reported spans are not even\n * compared), and a group needs at least one non-anchor node. A block that matched only locally is\n * reported as its matched cores (overlapping cores merged), each clustered with its own partners,\n * so code no verified pair matched never counts as duplicated.\n */\nexport function collectCrossFileNearMissGroups(\n files: NearMissSourceFile[],\n reportedSpansByFile: { startTokenIndex: number; endTokenIndex: number }[][],\n minTokens: number,\n minSimilarityPercent: number\n): NearMissOccurrence[][] {\n if (minSimilarityPercent >= 100) {\n return [];\n }\n const blocks = normalizeBlocks(files);\n const matcher = createMatcher(blocks, minTokens, minSimilarityPercent);\n const overlapsReportedSpan = reportedSpansByFile.map(createOverlapTest);\n const coveredByReportedSpans = reportedSpansByFile.map(createCoverageTest);\n const fullyReported = blocks.map(({ fileIndex, range }) => coveredByReportedSpans[fileIndex]?.(range) ?? false);\n const touchesReported = blocks.map(({ fileIndex, range }) => overlapsReportedSpan[fileIndex]?.(range) ?? false);\n const edges: [number, [number, number] | undefined, number, [number, number] | undefined][] = [];\n forEachCandidatePair(blocks, fullyReported, minSimilarityPercent, (left, right) => {\n const leftBlock = blocks[left];\n const rightBlock = blocks[right];\n const match = leftBlock && rightBlock && matcher(leftBlock, rightBlock, right);\n if (match) {\n if (match.kind === 'whole') {\n // A whole match between two blocks that both overlap reported spans could never join a\n // group, and recording it would collapse the blocks' core nodes.\n if (!(touchesReported[left] && touchesReported[right])) {\n edges.push([left, undefined, right, undefined]);\n }\n } else {\n for (const [leftCore, rightCore] of match.cores) {\n edges.push([left, leftCore, right, rightCore]);\n }\n }\n }\n });\n\n // Clustering runs over (block, core) nodes: a block with a recorded whole match is one node,\n // and otherwise each union of its overlapping local cores is its own node, so disjoint cores\n // matched with different partners fall into separate groups.\n const matchedWhole = blocks.map(() => false);\n const localCores = blocks.map((): [number, number][] => []);\n for (const [left, leftCore, right, rightCore] of edges) {\n for (const [index, core] of [\n [left, leftCore],\n [right, rightCore],\n ] as const) {\n if (core) {\n localCores[index]?.push(core);\n } else {\n matchedWhole[index] = true;\n }\n }\n }\n const nodes: { blockIndex: number; core: [number, number] | undefined }[] = [];\n const firstNodeByBlock: number[] = [];\n for (const blockIndex of blocks.keys()) {\n firstNodeByBlock.push(nodes.length);\n const cores = matchedWhole[blockIndex] ? [] : mergeOverlappingCores(localCores[blockIndex] ?? []);\n if (cores.length === 0) {\n nodes.push({ blockIndex, core: undefined });\n }\n for (const core of cores) {\n nodes.push({ blockIndex, core });\n }\n }\n const nodeOf = (blockIndex: number, core: [number, number] | undefined): number => {\n const first = firstNodeByBlock[blockIndex] ?? 0;\n if (!core || matchedWhole[blockIndex]) {\n return first;\n }\n for (let node = first; nodes[node]?.blockIndex === blockIndex; node += 1) {\n const span = nodes[node]?.core;\n if (span && span[0] <= core[0] && core[1] <= span[1]) {\n return node;\n }\n }\n throw new Error(\"every local core lies in one of its block's merged cores\");\n };\n\n const parent = nodes.map((_, index) => index);\n const find = (index: number): number => {\n let root = index;\n while (parent[root] !== root) {\n root = parent[root] ?? root;\n }\n for (let current = index; parent[current] !== root;) {\n const next = parent[current] ?? root;\n parent[current] = root;\n current = next;\n }\n return root;\n };\n // Anchoring is judged per node: a core is an anchor only when a reported span overlaps the core\n // itself, not merely elsewhere in its block.\n const anchored = nodes.map(({ blockIndex, core }) => {\n const block = blocks[blockIndex];\n const [startTokenIndex, endTokenIndex] = core ?? [\n block?.range.startTokenIndex ?? 0,\n block?.range.endTokenIndex ?? 0,\n ];\n return overlapsReportedSpan[block?.fileIndex ?? 0]?.({ startTokenIndex, endTokenIndex }) ?? false;\n });\n for (const [left, leftCore, right, rightCore] of edges) {\n const leftNode = nodeOf(left, leftCore);\n const rightNode = nodeOf(right, rightCore);\n if (anchored[leftNode] && anchored[rightNode]) {\n continue;\n }\n const leftRoot = find(leftNode);\n const rightRoot = find(rightNode);\n parent[Math.max(leftRoot, rightRoot)] = Math.min(leftRoot, rightRoot);\n }\n\n const membersByRoot = new Map<number, number[]>();\n for (const node of nodes.keys()) {\n const root = find(node);\n const members = membersByRoot.get(root) ?? [];\n members.push(node);\n membersByRoot.set(root, members);\n }\n const groups: NearMissOccurrence[][] = [];\n for (const members of membersByRoot.values()) {\n // Components form only through cross-file pairs, so two members always span two files.\n if (members.length < 2 || members.every((node) => anchored[node])) {\n continue;\n }\n // A group's nodes from one block become ONE occurrence whose segments are its cores, so the\n // fragment-weighted count charges the block as one copy (as for gapped clones), not once per core.\n const coresByBlock = new Map<number, { cores: ([number, number] | undefined)[]; anchor: boolean }>();\n for (const node of members) {\n const { blockIndex = 0, core } = nodes[node] ?? {};\n const entry = coresByBlock.get(blockIndex) ?? { cores: [], anchor: false };\n entry.cores.push(core);\n entry.anchor ||= anchored[node] ?? false;\n coresByBlock.set(blockIndex, entry);\n }\n groups.push(\n [...coresByBlock].flatMap(([blockIndex, { cores, anchor }]) => {\n const block = blocks[blockIndex];\n return block ? [toOccurrence(block, files, cores, anchor)] : [];\n })\n );\n }\n return groups;\n}\n\n/** The unions of overlapping cores, in position order. */\nfunction mergeOverlappingCores(cores: [number, number][]): [number, number][] {\n const merged: [number, number][] = [];\n for (const [start, end] of cores.toSorted((left, right) => left[0] - right[0])) {\n const last = merged.at(-1);\n if (last && start < last[1]) {\n last[1] = Math.max(last[1], end);\n } else {\n merged.push([start, end]);\n }\n }\n return merged;\n}\n\n/** Whether the spans, merged, cover every token of a range. */\nfunction createCoverageTest(\n spans: { startTokenIndex: number; endTokenIndex: number }[]\n): (range: { startTokenIndex: number; endTokenIndex: number }) => boolean {\n // Touching spans merge too: together they cover a range across their boundary.\n const merged: [number, number][] = [];\n for (const { startTokenIndex, endTokenIndex } of spans.toSorted(\n (left, right) => left.startTokenIndex - right.startTokenIndex\n )) {\n const last = merged.at(-1);\n if (last && startTokenIndex <= last[1]) {\n last[1] = Math.max(last[1], endTokenIndex);\n } else {\n merged.push([startTokenIndex, endTokenIndex]);\n }\n }\n return (range) => {\n let low = 0;\n let high = merged.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((merged[middle]?.[0] ?? 0) <= range.startTokenIndex) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return (merged[low - 1]?.[1] ?? -1) >= range.endTokenIndex;\n };\n}\n\n/**\n * Whether a range overlaps any of the spans: among the spans starting before the range ends\n * (binary search over sorted starts), the furthest end reaches past the range's start.\n */\nfunction createOverlapTest(\n spans: { startTokenIndex: number; endTokenIndex: number }[]\n): (range: { startTokenIndex: number; endTokenIndex: number }) => boolean {\n const sorted = spans.toSorted((left, right) => left.startTokenIndex - right.startTokenIndex);\n const maxEndPrefix = new Int32Array(sorted.length);\n let maxEnd = -1;\n for (const [index, span] of sorted.entries()) {\n maxEnd = Math.max(maxEnd, span.endTokenIndex);\n maxEndPrefix[index] = maxEnd;\n }\n return (range) => {\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) < range.endTokenIndex) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low > 0 && (maxEndPrefix[low - 1] ?? -1) > range.startTokenIndex;\n };\n}\n\n/**\n * The block's occurrence with one segment per entry of `cores` (the whole block for `undefined`,\n * a whole match). Source offsets stay the block's: tokens carry none, and near-miss occurrences\n * report lines only.\n */\nfunction toOccurrence(\n { fileIndex, range }: NormalizedBlock,\n files: NearMissSourceFile[],\n cores: ([number, number] | undefined)[],\n anchor: boolean\n): NearMissOccurrence {\n const whole = cores.includes(undefined);\n const segments = cores\n .map((core): [number, number] => core ?? [range.startTokenIndex, range.endTokenIndex])\n .toSorted((left, right) => left[0] - right[0])\n .map(([startTokenIndex, endTokenIndex]) => ({ startTokenIndex, endTokenIndex }));\n const start = segments[0]?.startTokenIndex ?? range.startTokenIndex;\n const end = segments.at(-1)?.endTokenIndex ?? range.endTokenIndex;\n const tokens = files[fileIndex]?.tokens;\n return {\n fileIndex,\n spanCountedElsewhere: anchor || undefined,\n segments,\n tokenCount: segments.reduce((sum, segment) => sum + segment.endTokenIndex - segment.startTokenIndex, 0),\n startTokenIndex: start,\n endTokenIndex: end,\n startIndex: range.startIndex,\n endIndex: range.endIndex,\n startLine: whole ? range.startLine : (tokens?.[start]?.startRow ?? 0) + 1,\n endLine: whole ? range.endLine : (tokens?.[end - 1]?.endRow ?? 0) + 1,\n };\n}\n\n/**\n * Visits every cross-file block pair sharing at least `filtrationPercent` of the smaller block's\n * non-stop n-grams, except pairs of two blocks wholly covered by reported spans and pairs whose\n * length ratio rules out both\n * whole-block similarity and `maxLengthRatio`. Blocks are\n * indexed in ascending length, so each posting list is scanned backwards only while its blocks\n * are long enough; shared counts accumulate in a dense counter, so no pair map is materialized.\n */\nfunction forEachCandidatePair(\n blocks: NormalizedBlock[],\n fullyReported: boolean[],\n minSimilarityPercent: number,\n visit: (left: number, right: number) => void\n): void {\n const blockFrequency = new Map<number, number>();\n for (const block of blocks) {\n for (const ngram of block.ngrams) {\n blockFrequency.set(ngram, (blockFrequency.get(ngram) ?? 0) + 1);\n }\n }\n for (const block of blocks) {\n block.ngrams = block.ngrams.filter((ngram) => (blockFrequency.get(ngram) ?? 0) <= maxNgramBlockFrequency);\n }\n\n // Typed copies keep the posting loop, which dominates this phase, free of object dereferences.\n const fileIndexes = Int32Array.from(blocks, (block) => block.fileIndex);\n const reportedFlags = Uint8Array.from(fullyReported, Number);\n const lengths = Int32Array.from(blocks, (block) => block.sequence.length);\n const ngramCounts = Int32Array.from(blocks, (block) => block.ngrams.length);\n const order = [...blocks.keys()].toSorted((left, right) => (lengths[left] ?? 0) - (lengths[right] ?? 0));\n const postings = new Map<number, number[]>();\n const sharedCounts = new Int32Array(blocks.length);\n const touched: number[] = [];\n for (const right of order) {\n const fileIndex = fileIndexes[right];\n const rightReported = reportedFlags[right] === 1;\n const minLeftLength = Math.min(\n Math.ceil((lengths[right] ?? 0) / maxLengthRatio),\n Math.ceil((minSimilarityPercent * (lengths[right] ?? 0)) / 100)\n );\n const ngrams = blocks[right]?.ngrams ?? [];\n for (const ngram of ngrams) {\n const posting = postings.get(ngram);\n if (!posting) {\n postings.set(ngram, [right]);\n continue;\n }\n for (let position = posting.length - 1; position >= 0; position -= 1) {\n const left = posting[position] ?? 0;\n if ((lengths[left] ?? 0) < minLeftLength) {\n break;\n }\n if (fileIndexes[left] === fileIndex || (rightReported && reportedFlags[left] === 1)) {\n continue;\n }\n if (sharedCounts[left] === 0) {\n touched.push(left);\n }\n sharedCounts[left] = (sharedCounts[left] ?? 0) + 1;\n }\n posting.push(right);\n }\n for (const left of touched) {\n const shared = sharedCounts[left] ?? 0;\n sharedCounts[left] = 0;\n if (shared * 100 >= filtrationPercent * Math.min(ngramCounts[left] ?? 0, ngrams.length)) {\n visit(left, right);\n }\n }\n touched.length = 0;\n }\n}\n\n/**\n * Returns the pair verifier. Content symbols are weighted by integer self-information,\n * 1 + floor(log2((N + 1) / df)) over N blocks capped at `maxContentWeight`, so rare names and\n * values (the logic a copy preserves) outweigh ubiquitous ones, following the\n * information-theoretic weighting of ECScan's essence-clone detection (2025).\n */\nfunction createMatcher(\n blocks: NormalizedBlock[],\n minTokens: number,\n minSimilarityPercent: number\n): (left: NormalizedBlock, right: NormalizedBlock, rightIndex: number) => PairMatch | undefined {\n const documentFrequencies = new Map<number, number>();\n for (const block of blocks) {\n for (const symbol of block.contentCounts.keys()) {\n documentFrequencies.set(symbol, (documentFrequencies.get(symbol) ?? 0) + 1);\n }\n }\n const selfInformation = (documentFrequency: number): number =>\n Math.min(31 - Math.clz32(Math.floor((blocks.length + 1) / documentFrequency)) + 1, maxContentWeight);\n const weights = new Map<number, number>();\n for (const [symbol, frequency] of documentFrequencies) {\n weights.set(symbol, selfInformation(frequency));\n }\n const weigh = (counts: Map<number, number>): WeightedContent => {\n const symbols = Int32Array.from(counts.keys()).toSorted();\n // Span content comes from blocks, so every symbol has a weight.\n const weightedCounts = Int32Array.from(symbols, (symbol) => (counts.get(symbol) ?? 0) * (weights.get(symbol) ?? 0));\n let total = 0;\n for (const count of weightedCounts) {\n total += count;\n }\n return { symbols, weightedCounts, total };\n };\n const blockContents = new Map(blocks.map((block) => [block, weigh(block.contentCounts)]));\n\n // Every candidate pair of one `right` block is visited consecutively, so one LCS counter (its\n // position masks built once) serves them all.\n let counterBlock = -1;\n let counter: ((sequence: Int32Array) => number) | undefined;\n const lcsLengthWithRight = (right: NormalizedBlock, rightIndex: number, sequence: Int32Array): number => {\n if (counterBlock !== rightIndex || !counter) {\n counterBlock = rightIndex;\n counter = createLcsLengthCounter(right.sequence);\n }\n return counter(sequence);\n };\n\n /**\n * Matches the cores two blocks share inside different surroundings (a copy wrapped in added\n * code, or two copies embedded in different code), which whole-block similarity misses\n * (CCAligner's large-gap and LVMapper's large-variance clones). N-grams unique to each block\n * anchor the alignment; their longest chain increasing in both blocks (a run filter keeps only\n * anchors continuing a diagonal, but the chain may shift diagonals at small insertions), split at\n * gaps, delimits the cores, and every core pair that is a near-miss clone in its own right is\n * returned.\n */\n const matchLocally = (left: NormalizedBlock, right: NormalizedBlock): PairMatch | undefined => {\n const anchors: [number, number][] = [];\n for (\n let leftIndex = 0, rightIndex = 0;\n leftIndex < left.uniqueNgrams.length && rightIndex < right.uniqueNgrams.length;\n ) {\n const leftHash = left.uniqueNgrams[leftIndex] ?? 0;\n const rightHash = right.uniqueNgrams[rightIndex] ?? 0;\n if (leftHash === rightHash) {\n anchors.push([left.uniqueNgramOffsets[leftIndex] ?? 0, right.uniqueNgramOffsets[rightIndex] ?? 0]);\n }\n if (leftHash <= rightHash) {\n leftIndex += 1;\n }\n if (rightHash <= leftHash) {\n rightIndex += 1;\n }\n }\n anchors.sort((first, second) => first[0] - second[0]);\n // An isolated 5-gram match is often coincidental (n-grams are identifier-blind); a copied core\n // yields runs of consecutive anchors, so only anchors continuing a diagonal run are chained.\n const runAnchors = anchors.filter(([leftOffset, rightOffset], index) => {\n const previous = anchors[index - 1];\n const next = anchors[index + 1];\n return (\n (previous?.[0] === leftOffset - 1 && previous[1] === rightOffset - 1) ||\n (next?.[0] === leftOffset + 1 && next[1] === rightOffset + 1)\n );\n });\n const leftOffset = left.range.startTokenIndex;\n const rightOffset = right.range.startTokenIndex;\n const cores: CorePair[] = [];\n for (const segment of chainSegments(longestIncreasingChain(runAnchors))) {\n const [leftStart, rightStart] = segment[0] ?? [0, 0];\n const [leftLast, rightLast] = segment.at(-1) ?? [0, 0];\n const leftEnd = leftLast + ngramSize;\n const rightEnd = rightLast + ngramSize;\n const leftLength = leftEnd - leftStart;\n const rightLength = rightEnd - rightStart;\n const shorter = Math.min(leftLength, rightLength);\n const required = minSimilarityPercent * Math.max(leftLength, rightLength);\n if (\n shorter >= minTokens &&\n shorter * 100 >= required &&\n anchoredTokenCount(segment) * 100 >= minAnchorCoveragePercent * shorter &&\n sharesContent(\n weigh(countContent(left.symbols, left.isContent, leftStart, leftEnd)),\n weigh(countContent(right.symbols, right.isContent, rightStart, rightEnd))\n ) &&\n lcsLength(\n anonymize(left.symbols.subarray(leftStart, leftEnd)),\n anonymize(right.symbols.subarray(rightStart, rightEnd))\n ) *\n 100 >=\n required\n ) {\n cores.push([\n [leftOffset + leftStart, leftOffset + leftEnd],\n [rightOffset + rightStart, rightOffset + rightEnd],\n ]);\n }\n }\n return cores.length > 0 ? { kind: 'local', cores } : undefined;\n };\n\n /** Cheapest bounds first: the LCS cannot exceed the shorter block's length nor the bag overlap. */\n return (left, right, rightIndex) => {\n const required = minSimilarityPercent * Math.max(left.sequence.length, right.sequence.length);\n if (\n Math.min(left.sequence.length, right.sequence.length) * 100 >= required &&\n sharesContent(blockContents.get(left), blockContents.get(right)) &&\n ((sortedOverlap(left.sortedSequence, right.sortedSequence) * 100 >= required &&\n lcsLengthWithRight(right, rightIndex, left.sequence) * 100 >= required) ||\n matchesReordered(left, right, required))\n ) {\n return { kind: 'whole' };\n }\n return matchLocally(left, right);\n };\n}\n\n/** Content-bearing symbols, sorted, with their information-weighted counts. */\ninterface WeightedContent {\n symbols: Int32Array;\n weightedCounts: Int32Array;\n total: number;\n}\n\n/**\n * A structural match must be backed by shared content: more than `minContentSimilarityPercent` of\n * the larger side's information-weighted names and literal values. Two sides without content never\n * pass.\n */\nfunction sharesContent(left: WeightedContent | undefined, right: WeightedContent | undefined): boolean {\n if (!left || !right) {\n return false;\n }\n let overlap = 0;\n for (let leftIndex = 0, rightIndex = 0; leftIndex < left.symbols.length && rightIndex < right.symbols.length;) {\n const leftSymbol = left.symbols[leftIndex] ?? 0;\n const rightSymbol = right.symbols[rightIndex] ?? 0;\n if (leftSymbol === rightSymbol) {\n overlap += Math.min(left.weightedCounts[leftIndex] ?? 0, right.weightedCounts[rightIndex] ?? 0);\n }\n if (leftSymbol <= rightSymbol) {\n leftIndex += 1;\n }\n if (rightSymbol <= leftSymbol) {\n rightIndex += 1;\n }\n }\n return overlap * 100 > minContentSimilarityPercent * Math.max(left.total, right.total);\n}\n\n/**\n * Compares the blocks with their top-level statements (each anonymized on its own) in a canonical\n * order, so a copy whose independent statements were swapped still matches.\n */\nfunction matchesReordered(left: NormalizedBlock, right: NormalizedBlock, required: number): boolean {\n return (\n left.canonicalSequence !== undefined &&\n right.canonicalSequence !== undefined &&\n lcsLength(left.canonicalSequence, right.canonicalSequence) * 100 >= required\n );\n}\n\n/**\n * The block's units (its top-level statements, given in file token indexes, and the token runs\n * between them), each anonymized on its own and sorted, concatenated; undefined with too few\n * statements.\n */\nfunction canonicalSequenceOf(\n symbols: Int32Array,\n statements: [number, number][],\n blockStart: number\n): Int32Array | undefined {\n if (statements.length < minReorderStatementCount) {\n return undefined;\n }\n const units: Int32Array[] = [];\n let cursor = 0;\n for (const [statementStart, statementEnd] of statements) {\n const start = statementStart - blockStart;\n if (cursor < start) {\n units.push(anonymize(symbols.subarray(cursor, start)));\n }\n units.push(anonymize(symbols.subarray(start, statementEnd - blockStart)));\n cursor = statementEnd - blockStart;\n }\n if (cursor < symbols.length) {\n units.push(anonymize(symbols.subarray(cursor)));\n }\n units.sort(compareSequences);\n const canonical = new Int32Array(symbols.length);\n let offset = 0;\n for (const unit of units) {\n canonical.set(unit, offset);\n offset += unit.length;\n }\n return canonical;\n}\n\n/** Lexicographic order, matching Rust's Vec<i32> ordering. */\nfunction compareSequences(left: Int32Array, right: Int32Array): number {\n for (let index = 0; index < Math.min(left.length, right.length); index += 1) {\n const difference = (left[index] ?? 0) - (right[index] ?? 0);\n if (difference !== 0) {\n return difference;\n }\n }\n return left.length - right.length;\n}\n\n/**\n * The longest chain of anchors increasing in both blocks (anchors arrive sorted by left offset),\n * via patience sorting over right offsets.\n */\nfunction longestIncreasingChain(anchors: [number, number][]): [number, number][] {\n const tailIndexes: number[] = [];\n const predecessors: number[] = [];\n for (const [index, [, rightOffset]] of anchors.entries()) {\n let low = 0;\n let high = tailIndexes.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((anchors[tailIndexes[middle] ?? 0]?.[1] ?? 0) < rightOffset) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n predecessors.push(low > 0 ? (tailIndexes[low - 1] ?? -1) : -1);\n tailIndexes[low] = index;\n }\n const chain: [number, number][] = [];\n for (let cursor = tailIndexes.at(-1) ?? -1; cursor >= 0; cursor = predecessors[cursor] ?? -1) {\n const anchor = anchors[cursor];\n if (anchor) {\n chain.push(anchor);\n }\n }\n return chain.toReversed();\n}\n\n/** The chain's segments, split where consecutive anchors lie more than `maxAnchorGapTokens` apart in either block. */\nfunction chainSegments(chain: [number, number][]): [number, number][][] {\n const segments: [number, number][][] = [];\n for (const [index, anchor] of chain.entries()) {\n const previous = chain[index - 1];\n const continues =\n previous !== undefined &&\n anchor[0] - (previous[0] + ngramSize) <= maxAnchorGapTokens &&\n anchor[1] - (previous[1] + ngramSize) <= maxAnchorGapTokens;\n if (continues) {\n segments.at(-1)?.push(anchor);\n } else {\n segments.push([anchor]);\n }\n }\n return segments;\n}\n\n/** Left-block tokens the segment's anchors cover (overlapping anchors count once). */\nfunction anchoredTokenCount(segment: [number, number][]): number {\n let count = ngramSize;\n for (let index = 1; index < segment.length; index += 1) {\n count += Math.min((segment[index]?.[0] ?? 0) - (segment[index - 1]?.[0] ?? 0), ngramSize);\n }\n return count;\n}\n\n/** Multiset intersection size of two ascending arrays. */\nfunction sortedOverlap(left: Int32Array, right: Int32Array): number {\n let overlap = 0;\n let leftIndex = 0;\n let rightIndex = 0;\n while (leftIndex < left.length && rightIndex < right.length) {\n const leftValue = left[leftIndex] ?? 0;\n const rightValue = right[rightIndex] ?? 0;\n if (leftValue === rightValue) {\n overlap += 1;\n leftIndex += 1;\n rightIndex += 1;\n } else if (leftValue < rightValue) {\n leftIndex += 1;\n } else {\n rightIndex += 1;\n }\n }\n return overlap;\n}\n\n/**\n * Normalizes every block like the within-file detector. Non-identifier symbols are interned\n * project-wide from the tokens' hash pairs, so equal tokens compare equal across files, while\n * identifiers are interned per file and re-anonymized per compared range.\n */\nfunction normalizeBlocks(files: NearMissSourceFile[]): NormalizedBlock[] {\n const symbolByTokenKey = new Map<number, number>();\n const blocks: NormalizedBlock[] = [];\n for (const [fileIndex, { tokens, containerStatements, nearMissBlocks }] of files.entries()) {\n if (!tokens || !nearMissBlocks?.length) {\n continue;\n }\n const symbols = new Int32Array(tokens.length);\n const isContent = new Uint8Array(tokens.length);\n const idByIdentifier = new Map<string, number>();\n for (const [index, token] of tokens.entries()) {\n if (token.kind === 'id') {\n let id = idByIdentifier.get(token.text);\n if (id === undefined) {\n id = idByIdentifier.size;\n idByIdentifier.set(token.text, id);\n }\n symbols[index] = -(id + 1);\n continue;\n }\n const key = tokenKey(token);\n let symbol = symbolByTokenKey.get(key);\n if (symbol === undefined) {\n symbol = symbolByTokenKey.size;\n symbolByTokenKey.set(key, symbol);\n }\n symbols[index] = symbol;\n isContent[index] = token.isName || token.literalHash !== undefined ? 1 : 0;\n }\n const findStatements = createTopLevelStatementFinder(containerStatements ?? []);\n for (const range of nearMissBlocks) {\n const { startTokenIndex: start, endTokenIndex: end } = range;\n const blockSymbols = symbols.subarray(start, end);\n const blockIsContent = isContent.subarray(start, end);\n const sequence = anonymize(blockSymbols);\n const ngramHashes = collectNgramHashes(blockSymbols);\n const occurrenceCounts = new Map<number, number>();\n for (const hash of ngramHashes) {\n occurrenceCounts.set(hash, (occurrenceCounts.get(hash) ?? 0) + 1);\n }\n const uniqueOffsets = ngramHashes\n .keys()\n .filter((offset) => occurrenceCounts.get(ngramHashes[offset] ?? 0) === 1)\n .toArray()\n .toSorted((first, second) => (ngramHashes[first] ?? 0) - (ngramHashes[second] ?? 0));\n blocks.push({\n fileIndex,\n range,\n symbols: blockSymbols,\n isContent: blockIsContent,\n sequence,\n sortedSequence: sequence.toSorted(),\n ngrams: Int32Array.from(occurrenceCounts.keys()),\n uniqueNgrams: Int32Array.from(uniqueOffsets, (offset) => ngramHashes[offset] ?? 0),\n uniqueNgramOffsets: Int32Array.from(uniqueOffsets),\n contentCounts: countContent(blockSymbols, blockIsContent, 0, blockSymbols.length),\n canonicalSequence: canonicalSequenceOf(blockSymbols, findStatements(start, end), start),\n });\n }\n }\n return blocks;\n}\n\n/**\n * Returns a lookup of the outermost container statements inside a token range, excluding a\n * statement spanning the whole range (the block itself).\n */\nfunction createTopLevelStatementFinder(\n containerStatements: TokenRange[][]\n): (start: number, end: number) => [number, number][] {\n const statements = containerStatements\n .flat()\n .filter((statement) => statement.startTokenIndex < statement.endTokenIndex)\n .map((statement): [number, number] => [statement.startTokenIndex, statement.endTokenIndex])\n .toSorted((left, right) => left[0] - right[0] || right[1] - left[1]);\n return (start, end) => {\n let low = 0;\n let high = statements.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((statements[middle]?.[0] ?? 0) < start) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n const topLevel: [number, number][] = [];\n for (let index = low; index < statements.length; index += 1) {\n const statement = statements[index];\n if (!statement || statement[0] >= end) {\n break;\n }\n const last = topLevel.at(-1);\n const nested = last !== undefined && statement[0] < last[1];\n if (statement[1] <= end && !(statement[0] === start && statement[1] === end) && !nested) {\n topLevel.push(statement);\n }\n }\n return topLevel;\n };\n}\n\nfunction countContent(symbols: Int32Array, isContent: Uint8Array, start: number, end: number): Map<number, number> {\n const counts = new Map<number, number>();\n for (let offset = start; offset < end; offset += 1) {\n if (isContent[offset] === 1) {\n const symbol = symbols[offset] ?? 0;\n counts.set(symbol, (counts.get(symbol) ?? 0) + 1);\n }\n }\n return counts;\n}\n\n/** Identifiers renumbered by first occurrence within `symbols`, so a range compares the same wherever it sits in its file. */\nfunction anonymize(symbols: Int32Array): Int32Array {\n const indexByIdentifier = new Map<number, number>();\n return symbols.map((symbol) => {\n if (symbol >= 0) {\n return symbol;\n }\n let index = indexByIdentifier.get(symbol);\n if (index === undefined) {\n index = indexByIdentifier.size;\n indexByIdentifier.set(symbol, index);\n }\n return -(index + 1);\n });\n}\n\n/**\n * A 53-bit key from the token's two independent text hashes, each mixed with the matching literal\n * value hash: exact in a JavaScript number, so interning never merges distinct tokens unless 53\n * hash bits collide.\n */\nfunction tokenKey(token: Token): number {\n const primary = token.textHash ^ Math.imul(token.literalHash ?? 0, 0x9E_37_79_B1);\n const secondary = token.textHash2 ^ Math.imul(token.literalHash2 ?? 0, 0x85_EB_CA_6B);\n return (primary >>> 0) * 0x20_00_00 + (secondary >>> 11);\n}\n\n/**\n * N-gram hash per start offset, identifier-blind (every identifier hashes as -1) so a block copied\n * into different surroundings (renumbering its identifiers) or with reordered statements still\n * shares its n-grams.\n */\nfunction collectNgramHashes(symbols: Int32Array): Int32Array {\n const hashes = new Int32Array(Math.max(symbols.length - ngramSize + 1, 0));\n for (let start = 0; start < hashes.length; start += 1) {\n let hash = 5381;\n for (let offset = 0; offset < ngramSize; offset += 1) {\n const symbol = symbols[start + offset] ?? 0;\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `| 0` wraps the sum to int32 like the native n-gram hash.\n hash = (Math.imul(hash, 31) + (symbol < 0 ? -1 : symbol)) | 0;\n }\n hashes[start] = hash;\n }\n return hashes;\n}\n"],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -22,7 +22,9 @@ export interface DuplicationOptions {
|
|
|
22
22
|
/**
|
|
23
23
|
* Minimum similarity percent (1-100) for near-miss (Type-3) clone blocks, measured as the
|
|
24
24
|
* token-level longest common subsequence relative to the larger block (NiCad-style per-fragment
|
|
25
|
-
* similarity)
|
|
25
|
+
* similarity), also after putting both blocks' top-level statements in a canonical order (so
|
|
26
|
+
* reordered copies match), or relative to the larger matched core when a copy is embedded in
|
|
27
|
+
* added code. 100 disables near-miss detection and reports exact (Type-1/2) matches plus gapped
|
|
26
28
|
* merges only (default 70). Applies to within-file and cross-file detection.
|
|
27
29
|
*/
|
|
28
30
|
minSimilarityPercent?: number;
|