code-gauge 1.11.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +38 -21
  2. package/dist/_virtual/_rolldown/runtime.cjs +1 -1
  3. package/dist/cli.cjs +3 -3
  4. package/dist/cli.cjs.map +1 -1
  5. package/dist/cli.js +3 -3
  6. package/dist/cli.js.map +1 -1
  7. package/dist/cliConfig.cjs +1 -1
  8. package/dist/cliConfig.cjs.map +1 -1
  9. package/dist/cliConfig.d.ts +9 -0
  10. package/dist/cliConfig.js +1 -1
  11. package/dist/cliConfig.js.map +1 -1
  12. package/dist/crossFileDuplication.cjs +2 -0
  13. package/dist/crossFileDuplication.cjs.map +1 -0
  14. package/dist/crossFileDuplication.d.ts +31 -0
  15. package/dist/crossFileDuplication.js +2 -0
  16. package/dist/crossFileDuplication.js.map +1 -0
  17. package/dist/duplicateSelection.cjs +2 -0
  18. package/dist/duplicateSelection.cjs.map +1 -0
  19. package/dist/duplicateSelection.d.ts +25 -0
  20. package/dist/duplicateSelection.js +2 -0
  21. package/dist/duplicateSelection.js.map +1 -0
  22. package/dist/duplication.cjs +1 -1
  23. package/dist/duplication.cjs.map +1 -1
  24. package/dist/duplication.d.ts +25 -5
  25. package/dist/duplication.js +1 -1
  26. package/dist/duplication.js.map +1 -1
  27. package/dist/index.cjs +1 -1
  28. package/dist/index.d.ts +5 -2
  29. package/dist/index.js +1 -1
  30. package/dist/languages.cjs +1 -1
  31. package/dist/languages.cjs.map +1 -1
  32. package/dist/languages.js +1 -1
  33. package/dist/languages.js.map +1 -1
  34. package/dist/metrics.cjs +2 -2
  35. package/dist/metrics.cjs.map +1 -1
  36. package/dist/metrics.d.ts +8 -0
  37. package/dist/metrics.js +2 -2
  38. package/dist/metrics.js.map +1 -1
  39. package/dist/nativeMetrics.cjs.map +1 -1
  40. package/dist/nativeMetrics.js.map +1 -1
  41. package/dist/ncss.cjs +2 -0
  42. package/dist/ncss.cjs.map +1 -0
  43. package/dist/ncss.d.ts +11 -0
  44. package/dist/ncss.js +2 -0
  45. package/dist/ncss.js.map +1 -0
  46. package/dist/types.d.ts +40 -1
  47. package/dist/typescriptProject.cjs.map +1 -1
  48. package/package.json +3 -3
@@ -0,0 +1,31 @@
1
+ import type { CrossFileDuplicateCandidate } from './duplication.js';
2
+ export interface CrossFileDuplicationSourceFile {
3
+ file: string;
4
+ candidates: CrossFileDuplicateCandidate[];
5
+ }
6
+ export interface CrossFileDuplicateOccurrence {
7
+ endLine: number;
8
+ file: string;
9
+ startLine: number;
10
+ }
11
+ export interface CrossFileDuplicateBlockGroup {
12
+ files: string[];
13
+ occurrences: CrossFileDuplicateOccurrence[];
14
+ /** Normalized token count of one occurrence (all occurrences share it). */
15
+ tokenCount: number;
16
+ }
17
+ export interface CrossFileDuplicationMetrics {
18
+ /** Number of redundant copies across all groups, i.e. sum of (occurrenceCount - 1). */
19
+ duplicateBlockCount: number;
20
+ /** Groups the file participates in, keyed by the file name passed in. */
21
+ duplicateBlockGroupCountByFile: Record<string, number>;
22
+ groups: CrossFileDuplicateBlockGroup[];
23
+ }
24
+ /**
25
+ * Detects code regions duplicated across files: per-file candidates (whole block subtrees and
26
+ * full container runs, fingerprinted with the same normalization as within-file duplication) are
27
+ * grouped by fingerprint, and only maximal, non-overlapping regions whose group spans at least two
28
+ * files are counted. Groups that shrink to a single file during selection are shed — a
29
+ * within-file repeat is already reported by that file's own duplication metrics.
30
+ */
31
+ export declare function measureCrossFileDuplication(files: CrossFileDuplicationSourceFile[]): CrossFileDuplicationMetrics;
@@ -0,0 +1,2 @@
1
+ import{selectMaximalGroups as e}from"./duplicateSelection.js";function t(t){let i=t.flatMap(({file:e,candidates:t},n)=>t.map(t=>({...t,regionBucket:n,file:e})));return r(e(i,n,(e,t)=>e.regionBucket-t.regionBucket||e.startIndex-t.startIndex))}function n(e){return e.length>=2&&new Set(e.map(e=>e.regionBucket)).size>=2}function r(e){let t=[],n=new Map,r=0;for(let i of e.values()){r+=i.length-1;let e=i.map(({file:e,startLine:t,endLine:n})=>({file:e,startLine:t,endLine:n})).toSorted((e,t)=>e.file.localeCompare(t.file)||e.startLine-t.startLine),a=[...new Set(e.map(({file:e})=>e))];for(let e of a)n.set(e,(n.get(e)??0)+1);t.push({files:a,occurrences:e,tokenCount:i[0]?.tokenCount??0})}return t.sort((e,t)=>t.tokenCount-e.tokenCount||(e.occurrences[0]?.file??``).localeCompare(t.occurrences[0]?.file??``)||(e.occurrences[0]?.startLine??0)-(t.occurrences[0]?.startLine??0)),{duplicateBlockCount:r,duplicateBlockGroupCountByFile:Object.fromEntries(n),groups:t}}export{t as measureCrossFileDuplication};
2
+ //# sourceMappingURL=crossFileDuplication.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crossFileDuplication.js","names":[],"sources":["../src/crossFileDuplication.ts"],"sourcesContent":["import { selectMaximalGroups } from './duplicateSelection.js';\nimport type { CrossFileDuplicateCandidate } from './duplication.js';\n\nexport interface CrossFileDuplicationSourceFile {\n file: string;\n candidates: CrossFileDuplicateCandidate[];\n}\n\nexport interface CrossFileDuplicateOccurrence {\n endLine: number;\n file: string;\n startLine: number;\n}\n\nexport interface CrossFileDuplicateBlockGroup {\n files: string[];\n occurrences: CrossFileDuplicateOccurrence[];\n /** Normalized token count of one occurrence (all occurrences share it). */\n tokenCount: number;\n}\n\nexport interface CrossFileDuplicationMetrics {\n /** Number of redundant copies across all groups, i.e. sum of (occurrenceCount - 1). */\n duplicateBlockCount: number;\n /** Groups the file participates in, keyed by the file name passed in. */\n duplicateBlockGroupCountByFile: Record<string, number>;\n groups: CrossFileDuplicateBlockGroup[];\n}\n\ninterface SelectableCandidate extends CrossFileDuplicateCandidate {\n regionBucket: number;\n file: string;\n}\n\n/**\n * Detects code regions duplicated across files: per-file candidates (whole block subtrees and\n * full container runs, fingerprinted with the same normalization as within-file duplication) are\n * grouped by fingerprint, and only maximal, non-overlapping regions whose group spans at least two\n * files are counted. Groups that shrink to a single file during selection are shed — a\n * within-file repeat is already reported by that file's own duplication metrics.\n */\nexport function measureCrossFileDuplication(files: CrossFileDuplicationSourceFile[]): CrossFileDuplicationMetrics {\n const candidates: SelectableCandidate[] = files.flatMap(({ file, candidates }, fileIndex) =>\n candidates.map((candidate) => ({ ...candidate, regionBucket: fileIndex, file }))\n );\n const counted = selectMaximalGroups(\n candidates,\n spansMultipleFiles,\n // File index and position break coverage ties deterministically.\n (left, right) => left.regionBucket - right.regionBucket || left.startIndex - right.startIndex\n );\n return summarize(counted);\n}\n\nfunction spansMultipleFiles(group: SelectableCandidate[]): boolean {\n return group.length >= 2 && new Set(group.map((candidate) => candidate.regionBucket)).size >= 2;\n}\n\nfunction summarize(counted: Map<string, SelectableCandidate[]>): CrossFileDuplicationMetrics {\n const groups: CrossFileDuplicateBlockGroup[] = [];\n // Accumulated in a Map: file names are arbitrary strings, and a plain object would read\n // inherited properties for names like \"constructor\".\n const groupCountByFile = new Map<string, number>();\n let duplicateBlockCount = 0;\n for (const group of counted.values()) {\n duplicateBlockCount += group.length - 1;\n const occurrences = group\n .map(({ file, startLine, endLine }) => ({ file, startLine, endLine }))\n .toSorted((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n const files = [...new Set(occurrences.map(({ file }) => file))];\n for (const file of files) {\n groupCountByFile.set(file, (groupCountByFile.get(file) ?? 0) + 1);\n }\n groups.push({ files, occurrences, tokenCount: group[0]?.tokenCount ?? 0 });\n }\n groups.sort(\n (left, right) =>\n right.tokenCount - left.tokenCount ||\n (left.occurrences[0]?.file ?? '').localeCompare(right.occurrences[0]?.file ?? '') ||\n (left.occurrences[0]?.startLine ?? 0) - (right.occurrences[0]?.startLine ?? 0)\n );\n return { duplicateBlockCount, duplicateBlockGroupCountByFile: Object.fromEntries(groupCountByFile), groups };\n}\n"],"mappings":"8DAyCA,SAAgB,EAA4B,EAAsE,CAChH,IAAM,EAAoC,EAAM,SAAS,CAAE,OAAM,cAAc,IAC7E,EAAW,IAAK,IAAe,CAAE,GAAG,EAAW,aAAc,EAAW,MAAK,EAAE,CACjF,EAOA,OAAO,EANS,EACd,EACA,GAEC,EAAM,IAAU,EAAK,aAAe,EAAM,cAAgB,EAAK,WAAa,EAAM,UAE9D,CAAC,CAC1B,CAEA,SAAS,EAAmB,EAAuC,CACjE,OAAO,EAAM,QAAU,GAAK,IAAI,IAAI,EAAM,IAAK,GAAc,EAAU,YAAY,CAAC,CAAC,CAAC,MAAQ,CAChG,CAEA,SAAS,EAAU,EAA0E,CAC3F,IAAM,EAAyC,CAAC,EAG1C,EAAmB,IAAI,IACzB,EAAsB,EAC1B,IAAK,IAAM,KAAS,EAAQ,OAAO,EAAG,CACpC,GAAuB,EAAM,OAAS,EACtC,IAAM,EAAc,EACjB,KAAK,CAAE,OAAM,YAAW,cAAe,CAAE,OAAM,YAAW,SAAQ,EAAE,CAAC,CACrE,UAAU,EAAM,IAAU,EAAK,KAAK,cAAc,EAAM,IAAI,GAAK,EAAK,UAAY,EAAM,SAAS,EAC9F,EAAQ,CAAC,GAAG,IAAI,IAAI,EAAY,KAAK,CAAE,UAAW,CAAI,CAAC,CAAC,EAC9D,IAAK,IAAM,KAAQ,EACjB,EAAiB,IAAI,GAAO,EAAiB,IAAI,CAAI,GAAK,GAAK,CAAC,EAElE,EAAO,KAAK,CAAE,QAAO,cAAa,WAAY,EAAM,EAAE,EAAE,YAAc,CAAE,CAAC,CAC3E,CAOA,OANA,EAAO,MACJ,EAAM,IACL,EAAM,WAAa,EAAK,aACvB,EAAK,YAAY,EAAE,EAAE,MAAQ,GAAA,CAAI,cAAc,EAAM,YAAY,EAAE,EAAE,MAAQ,EAAE,IAC/E,EAAK,YAAY,EAAE,EAAE,WAAa,IAAM,EAAM,YAAY,EAAE,EAAE,WAAa,EAChF,EACO,CAAE,sBAAqB,+BAAgC,OAAO,YAAY,CAAgB,EAAG,QAAO,CAC7G"}
@@ -0,0 +1,2 @@
1
+ "use strict";function e(e,n,r){let i=new Map;for(let t of e){let e=i.get(t.fingerprint)??[];e.push(t),i.set(t.fingerprint,e)}let a=[...i.values()].map(t).filter(n),o=new Map(a.map(e=>[e[0]?.fingerprint??``,e.length])),s=e=>e.tokenCount*(o.get(e.fingerprint)??1),c=a.flat();c.sort((e,t)=>s(t)-s(e)||(r?r(e,t):0));for(let e=0;;e+=1){let t=new Map,r=new Map;for(let e of c){let n=t.get(e.regionBucket??0)??[];if(n.some(t=>t.startIndex<e.endIndex&&e.startIndex<t.endIndex))continue;n.push(e),t.set(e.regionBucket??0,n);let i=r.get(e.fingerprint)??[];i.push(e),r.set(e.fingerprint,i)}let i,a=-1;for(let[e,t]of r){let r=t[0]?.tokenCount??0;!n(t)&&r>a&&(i=e,a=r)}if(i===void 0)return r;if(e>=20){for(let[e,t]of r)n(t)||r.delete(e);return r}c=c.filter(e=>e.fingerprint!==i)}}function t(e){let t=new Map;for(let n of e){let e=`${n.regionBucket??0}:${n.startIndex}:${n.endIndex}`,r=t.get(e);(!r||n.tokenCount>r.tokenCount)&&t.set(e,n)}return[...t.values()]}exports.dedupeByRegion=t,exports.selectMaximalGroups=e;
2
+ //# sourceMappingURL=duplicateSelection.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duplicateSelection.cjs","names":[],"sources":["../src/duplicateSelection.ts"],"sourcesContent":["/**\n * Maximal, non-overlapping duplicate-group selection shared by the within-file and cross-file\n * detectors. Candidates are grouped by fingerprint, ranked by total coverage, kept greedily\n * without overlapping a kept region, and groups that fall below the survivor requirement are shed\n * one at a time (largest first) so their regions stop blocking smaller groups.\n */\n\nexport interface SelectableRegion {\n fingerprint: string;\n tokenCount: number;\n startIndex: number;\n endIndex: number;\n /**\n * Regions can only overlap within the same bucket. The within-file detector uses one bucket;\n * the cross-file detector buckets by file index.\n */\n regionBucket?: number;\n}\n\n/** Caps how often the maximal-region selection reruns after shedding failed duplicate groups. */\nconst maxSelectionRerunCount = 20;\n\n/**\n * @param isSurvivingGroup whether a selected group counts (e.g. at least two occurrences, or\n * occurrences spanning at least two files); failing groups are shed and re-selected without.\n * @param compareTies optional deterministic tie-break applied after the coverage ranking.\n */\nexport function selectMaximalGroups<T extends SelectableRegion>(\n candidates: T[],\n isSurvivingGroup: (group: T[]) => boolean,\n compareTies?: (left: T, right: T) => number\n): Map<string, T[]> {\n const byFingerprint = new Map<string, T[]>();\n for (const candidate of candidates) {\n const group = byFingerprint.get(candidate.fingerprint) ?? [];\n group.push(candidate);\n byFingerprint.set(candidate.fingerprint, group);\n }\n\n const groups = [...byFingerprint.values()].map(dedupeByRegion).filter(isSurvivingGroup);\n // Greedy order ranks by total coverage (region size × copies): a 3×3-statement group must beat\n // a 2×4-statement group overlapping two of its copies, or the third copy is silently dropped\n // and the reported duplication shrinks as more copies are added.\n const groupSizeByFingerprint = new Map(groups.map((group) => [group[0]?.fingerprint ?? '', group.length]));\n const coverage = (candidate: T): number =>\n candidate.tokenCount * (groupSizeByFingerprint.get(candidate.fingerprint) ?? 1);\n let duplicates = groups.flat();\n duplicates.sort((left, right) => coverage(right) - coverage(left) || (compareTies ? compareTies(left, right) : 0));\n\n // Greedy selection can keep a candidate whose group ends up below the survivor requirement;\n // such an uncounted region must not block smaller groups, so the largest failed group is\n // removed and the selection reruns. One group at a time: freeing a failed group's regions can\n // rescue another. The rerun cap bounds degenerate inputs; past it the remaining failed groups\n // are dropped, trading a sliver of recall on such files for bounded runtime.\n for (let rerun = 0; ; rerun += 1) {\n const keptRegionsByBucket = new Map<number, { startIndex: number; endIndex: number }[]>();\n const counted = new Map<string, T[]>();\n for (const candidate of duplicates) {\n const keptRegions = keptRegionsByBucket.get(candidate.regionBucket ?? 0) ?? [];\n if (\n keptRegions.some((region) => region.startIndex < candidate.endIndex && candidate.startIndex < region.endIndex)\n ) {\n continue;\n }\n keptRegions.push(candidate);\n keptRegionsByBucket.set(candidate.regionBucket ?? 0, keptRegions);\n const group = counted.get(candidate.fingerprint) ?? [];\n group.push(candidate);\n counted.set(candidate.fingerprint, group);\n }\n\n let failedFingerprint: string | undefined;\n let failedTokenCount = -1;\n for (const [fingerprint, group] of counted) {\n const tokenCount = group[0]?.tokenCount ?? 0;\n if (!isSurvivingGroup(group) && tokenCount > failedTokenCount) {\n failedFingerprint = fingerprint;\n failedTokenCount = tokenCount;\n }\n }\n // No failed fingerprint means every counted group met the survivor requirement.\n if (failedFingerprint === undefined) {\n return counted;\n }\n if (rerun >= maxSelectionRerunCount) {\n for (const [fingerprint, group] of counted) {\n if (!isSurvivingGroup(group)) {\n counted.delete(fingerprint);\n }\n }\n return counted;\n }\n\n duplicates = duplicates.filter((candidate) => candidate.fingerprint !== failedFingerprint);\n }\n}\n\n/** Drops candidates covering the same source region (a block and the statement run spanning it). */\nexport function dedupeByRegion<T extends SelectableRegion>(group: T[]): T[] {\n const byRegion = new Map<string, T>();\n for (const candidate of group) {\n const key = `${candidate.regionBucket ?? 0}:${candidate.startIndex}:${candidate.endIndex}`;\n const existing = byRegion.get(key);\n if (!existing || candidate.tokenCount > existing.tokenCount) {\n byRegion.set(key, candidate);\n }\n }\n return [...byRegion.values()];\n}\n"],"mappings":"aA2BA,SAAgB,EACd,EACA,EACA,EACkB,CAClB,IAAM,EAAgB,IAAI,IAC1B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAM,EAAQ,EAAc,IAAI,EAAU,WAAW,GAAK,CAAC,EAC3D,EAAM,KAAK,CAAS,EACpB,EAAc,IAAI,EAAU,YAAa,CAAK,CAChD,CAEA,IAAM,EAAS,CAAC,GAAG,EAAc,OAAO,CAAC,CAAC,CAAC,IAAI,CAAc,CAAC,CAAC,OAAO,CAAgB,EAIhF,EAAyB,IAAI,IAAI,EAAO,IAAK,GAAU,CAAC,EAAM,EAAE,EAAE,aAAe,GAAI,EAAM,MAAM,CAAC,CAAC,EACnG,EAAY,GAChB,EAAU,YAAc,EAAuB,IAAI,EAAU,WAAW,GAAK,GAC3E,EAAa,EAAO,KAAK,EAC7B,EAAW,MAAM,EAAM,IAAU,EAAS,CAAK,EAAI,EAAS,CAAI,IAAM,EAAc,EAAY,EAAM,CAAK,EAAI,EAAE,EAOjH,IAAK,IAAI,EAAQ,GAAK,GAAS,EAAG,CAChC,IAAM,EAAsB,IAAI,IAC1B,EAAU,IAAI,IACpB,IAAK,IAAM,KAAa,EAAY,CAClC,IAAM,EAAc,EAAoB,IAAI,EAAU,cAAgB,CAAC,GAAK,CAAC,EAC7E,GACE,EAAY,KAAM,GAAW,EAAO,WAAa,EAAU,UAAY,EAAU,WAAa,EAAO,QAAQ,EAE7G,SAEF,EAAY,KAAK,CAAS,EAC1B,EAAoB,IAAI,EAAU,cAAgB,EAAG,CAAW,EAChE,IAAM,EAAQ,EAAQ,IAAI,EAAU,WAAW,GAAK,CAAC,EACrD,EAAM,KAAK,CAAS,EACpB,EAAQ,IAAI,EAAU,YAAa,CAAK,CAC1C,CAEA,IAAI,EACA,EAAmB,GACvB,IAAK,GAAM,CAAC,EAAa,KAAU,EAAS,CAC1C,IAAM,EAAa,EAAM,EAAE,EAAE,YAAc,EACvC,CAAC,EAAiB,CAAK,GAAK,EAAa,IAC3C,EAAoB,EACpB,EAAmB,EAEvB,CAEA,GAAI,IAAsB,IAAA,GACxB,OAAO,EAET,GAAI,GAAS,GAAwB,CACnC,IAAK,GAAM,CAAC,EAAa,KAAU,EAC5B,EAAiB,CAAK,GACzB,EAAQ,OAAO,CAAW,EAG9B,OAAO,CACT,CAEA,EAAa,EAAW,OAAQ,GAAc,EAAU,cAAgB,CAAiB,CAC3F,CACF,CAGA,SAAgB,EAA2C,EAAiB,CAC1E,IAAM,EAAW,IAAI,IACrB,IAAK,IAAM,KAAa,EAAO,CAC7B,IAAM,EAAM,GAAG,EAAU,cAAgB,EAAE,GAAG,EAAU,WAAW,GAAG,EAAU,WAC1E,EAAW,EAAS,IAAI,CAAG,GAC7B,CAAC,GAAY,EAAU,WAAa,EAAS,aAC/C,EAAS,IAAI,EAAK,CAAS,CAE/B,CACA,MAAO,CAAC,GAAG,EAAS,OAAO,CAAC,CAC9B"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Maximal, non-overlapping duplicate-group selection shared by the within-file and cross-file
3
+ * detectors. Candidates are grouped by fingerprint, ranked by total coverage, kept greedily
4
+ * without overlapping a kept region, and groups that fall below the survivor requirement are shed
5
+ * one at a time (largest first) so their regions stop blocking smaller groups.
6
+ */
7
+ export interface SelectableRegion {
8
+ fingerprint: string;
9
+ tokenCount: number;
10
+ startIndex: number;
11
+ endIndex: number;
12
+ /**
13
+ * Regions can only overlap within the same bucket. The within-file detector uses one bucket;
14
+ * the cross-file detector buckets by file index.
15
+ */
16
+ regionBucket?: number;
17
+ }
18
+ /**
19
+ * @param isSurvivingGroup whether a selected group counts (e.g. at least two occurrences, or
20
+ * occurrences spanning at least two files); failing groups are shed and re-selected without.
21
+ * @param compareTies optional deterministic tie-break applied after the coverage ranking.
22
+ */
23
+ export declare function selectMaximalGroups<T extends SelectableRegion>(candidates: T[], isSurvivingGroup: (group: T[]) => boolean, compareTies?: (left: T, right: T) => number): Map<string, T[]>;
24
+ /** Drops candidates covering the same source region (a block and the statement run spanning it). */
25
+ export declare function dedupeByRegion<T extends SelectableRegion>(group: T[]): T[];
@@ -0,0 +1,2 @@
1
+ function e(e,n,r){let i=new Map;for(let t of e){let e=i.get(t.fingerprint)??[];e.push(t),i.set(t.fingerprint,e)}let a=[...i.values()].map(t).filter(n),o=new Map(a.map(e=>[e[0]?.fingerprint??``,e.length])),s=e=>e.tokenCount*(o.get(e.fingerprint)??1),c=a.flat();c.sort((e,t)=>s(t)-s(e)||(r?r(e,t):0));for(let e=0;;e+=1){let t=new Map,r=new Map;for(let e of c){let n=t.get(e.regionBucket??0)??[];if(n.some(t=>t.startIndex<e.endIndex&&e.startIndex<t.endIndex))continue;n.push(e),t.set(e.regionBucket??0,n);let i=r.get(e.fingerprint)??[];i.push(e),r.set(e.fingerprint,i)}let i,a=-1;for(let[e,t]of r){let r=t[0]?.tokenCount??0;!n(t)&&r>a&&(i=e,a=r)}if(i===void 0)return r;if(e>=20){for(let[e,t]of r)n(t)||r.delete(e);return r}c=c.filter(e=>e.fingerprint!==i)}}function t(e){let t=new Map;for(let n of e){let e=`${n.regionBucket??0}:${n.startIndex}:${n.endIndex}`,r=t.get(e);(!r||n.tokenCount>r.tokenCount)&&t.set(e,n)}return[...t.values()]}export{t as dedupeByRegion,e as selectMaximalGroups};
2
+ //# sourceMappingURL=duplicateSelection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duplicateSelection.js","names":[],"sources":["../src/duplicateSelection.ts"],"sourcesContent":["/**\n * Maximal, non-overlapping duplicate-group selection shared by the within-file and cross-file\n * detectors. Candidates are grouped by fingerprint, ranked by total coverage, kept greedily\n * without overlapping a kept region, and groups that fall below the survivor requirement are shed\n * one at a time (largest first) so their regions stop blocking smaller groups.\n */\n\nexport interface SelectableRegion {\n fingerprint: string;\n tokenCount: number;\n startIndex: number;\n endIndex: number;\n /**\n * Regions can only overlap within the same bucket. The within-file detector uses one bucket;\n * the cross-file detector buckets by file index.\n */\n regionBucket?: number;\n}\n\n/** Caps how often the maximal-region selection reruns after shedding failed duplicate groups. */\nconst maxSelectionRerunCount = 20;\n\n/**\n * @param isSurvivingGroup whether a selected group counts (e.g. at least two occurrences, or\n * occurrences spanning at least two files); failing groups are shed and re-selected without.\n * @param compareTies optional deterministic tie-break applied after the coverage ranking.\n */\nexport function selectMaximalGroups<T extends SelectableRegion>(\n candidates: T[],\n isSurvivingGroup: (group: T[]) => boolean,\n compareTies?: (left: T, right: T) => number\n): Map<string, T[]> {\n const byFingerprint = new Map<string, T[]>();\n for (const candidate of candidates) {\n const group = byFingerprint.get(candidate.fingerprint) ?? [];\n group.push(candidate);\n byFingerprint.set(candidate.fingerprint, group);\n }\n\n const groups = [...byFingerprint.values()].map(dedupeByRegion).filter(isSurvivingGroup);\n // Greedy order ranks by total coverage (region size × copies): a 3×3-statement group must beat\n // a 2×4-statement group overlapping two of its copies, or the third copy is silently dropped\n // and the reported duplication shrinks as more copies are added.\n const groupSizeByFingerprint = new Map(groups.map((group) => [group[0]?.fingerprint ?? '', group.length]));\n const coverage = (candidate: T): number =>\n candidate.tokenCount * (groupSizeByFingerprint.get(candidate.fingerprint) ?? 1);\n let duplicates = groups.flat();\n duplicates.sort((left, right) => coverage(right) - coverage(left) || (compareTies ? compareTies(left, right) : 0));\n\n // Greedy selection can keep a candidate whose group ends up below the survivor requirement;\n // such an uncounted region must not block smaller groups, so the largest failed group is\n // removed and the selection reruns. One group at a time: freeing a failed group's regions can\n // rescue another. The rerun cap bounds degenerate inputs; past it the remaining failed groups\n // are dropped, trading a sliver of recall on such files for bounded runtime.\n for (let rerun = 0; ; rerun += 1) {\n const keptRegionsByBucket = new Map<number, { startIndex: number; endIndex: number }[]>();\n const counted = new Map<string, T[]>();\n for (const candidate of duplicates) {\n const keptRegions = keptRegionsByBucket.get(candidate.regionBucket ?? 0) ?? [];\n if (\n keptRegions.some((region) => region.startIndex < candidate.endIndex && candidate.startIndex < region.endIndex)\n ) {\n continue;\n }\n keptRegions.push(candidate);\n keptRegionsByBucket.set(candidate.regionBucket ?? 0, keptRegions);\n const group = counted.get(candidate.fingerprint) ?? [];\n group.push(candidate);\n counted.set(candidate.fingerprint, group);\n }\n\n let failedFingerprint: string | undefined;\n let failedTokenCount = -1;\n for (const [fingerprint, group] of counted) {\n const tokenCount = group[0]?.tokenCount ?? 0;\n if (!isSurvivingGroup(group) && tokenCount > failedTokenCount) {\n failedFingerprint = fingerprint;\n failedTokenCount = tokenCount;\n }\n }\n // No failed fingerprint means every counted group met the survivor requirement.\n if (failedFingerprint === undefined) {\n return counted;\n }\n if (rerun >= maxSelectionRerunCount) {\n for (const [fingerprint, group] of counted) {\n if (!isSurvivingGroup(group)) {\n counted.delete(fingerprint);\n }\n }\n return counted;\n }\n\n duplicates = duplicates.filter((candidate) => candidate.fingerprint !== failedFingerprint);\n }\n}\n\n/** Drops candidates covering the same source region (a block and the statement run spanning it). */\nexport function dedupeByRegion<T extends SelectableRegion>(group: T[]): T[] {\n const byRegion = new Map<string, T>();\n for (const candidate of group) {\n const key = `${candidate.regionBucket ?? 0}:${candidate.startIndex}:${candidate.endIndex}`;\n const existing = byRegion.get(key);\n if (!existing || candidate.tokenCount > existing.tokenCount) {\n byRegion.set(key, candidate);\n }\n }\n return [...byRegion.values()];\n}\n"],"mappings":"AA2BA,SAAgB,EACd,EACA,EACA,EACkB,CAClB,IAAM,EAAgB,IAAI,IAC1B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAM,EAAQ,EAAc,IAAI,EAAU,WAAW,GAAK,CAAC,EAC3D,EAAM,KAAK,CAAS,EACpB,EAAc,IAAI,EAAU,YAAa,CAAK,CAChD,CAEA,IAAM,EAAS,CAAC,GAAG,EAAc,OAAO,CAAC,CAAC,CAAC,IAAI,CAAc,CAAC,CAAC,OAAO,CAAgB,EAIhF,EAAyB,IAAI,IAAI,EAAO,IAAK,GAAU,CAAC,EAAM,EAAE,EAAE,aAAe,GAAI,EAAM,MAAM,CAAC,CAAC,EACnG,EAAY,GAChB,EAAU,YAAc,EAAuB,IAAI,EAAU,WAAW,GAAK,GAC3E,EAAa,EAAO,KAAK,EAC7B,EAAW,MAAM,EAAM,IAAU,EAAS,CAAK,EAAI,EAAS,CAAI,IAAM,EAAc,EAAY,EAAM,CAAK,EAAI,EAAE,EAOjH,IAAK,IAAI,EAAQ,GAAK,GAAS,EAAG,CAChC,IAAM,EAAsB,IAAI,IAC1B,EAAU,IAAI,IACpB,IAAK,IAAM,KAAa,EAAY,CAClC,IAAM,EAAc,EAAoB,IAAI,EAAU,cAAgB,CAAC,GAAK,CAAC,EAC7E,GACE,EAAY,KAAM,GAAW,EAAO,WAAa,EAAU,UAAY,EAAU,WAAa,EAAO,QAAQ,EAE7G,SAEF,EAAY,KAAK,CAAS,EAC1B,EAAoB,IAAI,EAAU,cAAgB,EAAG,CAAW,EAChE,IAAM,EAAQ,EAAQ,IAAI,EAAU,WAAW,GAAK,CAAC,EACrD,EAAM,KAAK,CAAS,EACpB,EAAQ,IAAI,EAAU,YAAa,CAAK,CAC1C,CAEA,IAAI,EACA,EAAmB,GACvB,IAAK,GAAM,CAAC,EAAa,KAAU,EAAS,CAC1C,IAAM,EAAa,EAAM,EAAE,EAAE,YAAc,EACvC,CAAC,EAAiB,CAAK,GAAK,EAAa,IAC3C,EAAoB,EACpB,EAAmB,EAEvB,CAEA,GAAI,IAAsB,IAAA,GACxB,OAAO,EAET,GAAI,GAAS,GAAwB,CACnC,IAAK,GAAM,CAAC,EAAa,KAAU,EAC5B,EAAiB,CAAK,GACzB,EAAQ,OAAO,CAAW,EAG9B,OAAO,CACT,CAEA,EAAa,EAAW,OAAQ,GAAc,EAAU,cAAgB,CAAiB,CAC3F,CACF,CAGA,SAAgB,EAA2C,EAAiB,CAC1E,IAAM,EAAW,IAAI,IACrB,IAAK,IAAM,KAAa,EAAO,CAC7B,IAAM,EAAM,GAAG,EAAU,cAAgB,EAAE,GAAG,EAAU,WAAW,GAAG,EAAU,WAC1E,EAAW,EAAS,IAAI,CAAG,GAC7B,CAAC,GAAY,EAAU,WAAa,EAAS,aAC/C,EAAS,IAAI,EAAK,CAAS,CAE/B,CACA,MAAO,CAAC,GAAG,EAAS,OAAO,CAAC,CAC9B"}
@@ -1,2 +1,2 @@
1
- "use strict";const e=new Set(`statement_block.block.compound_statement.body_statement.constructor_body.do_block.if_statement.for_statement.for_in_statement.enhanced_for_statement.for_range_loop.while_statement.do_statement.try_statement.try_with_resources_statement.with_statement.switch_statement.switch_expression.switch_case.switch_block_statement_group.switch_rule.case_clause.case_statement.match_statement.match_arm.except_clause.catch_clause.finally_clause.elif_clause.ensure.expression_statement.return_statement.return_expression.if_expression.for_expression.while_expression.loop_expression.match_expression.jsx_element.jsx_self_closing_element.if.unless.case.case_match.while.until.for.begin.when`.split(`.`)),t=new Set([`program`,`source_file`,`translation_unit`,`module`,`statement_block`,`block`,`compound_statement`,`body_statement`,`constructor_body`,`class_body`,`block_body`,`do_block`,`do`,`ensure`,`then`,`else`,`case_statement`,`switch_block_statement_group`,`switch_rule`,`expression_case`,`type_case`,`communication_case`,`default_case`]),n=new Set([`identifier`,`constant`,`instance_variable`,`class_variable`,`global_variable`]),r=new Set([`shorthand_property_identifier`,`shorthand_property_identifier_pattern`]),i=new Map([[`number`,`#num`],[`number_literal`,`#num`],[`integer`,`#num`],[`float`,`#num`],[`integer_literal`,`#num`],[`float_literal`,`#num`],[`int_literal`,`#num`],[`rune_literal`,`#char`],[`imaginary_literal`,`#num`],[`decimal_integer_literal`,`#num`],[`hex_integer_literal`,`#num`],[`octal_integer_literal`,`#num`],[`binary_integer_literal`,`#num`],[`decimal_floating_point_literal`,`#num`],[`hex_floating_point_literal`,`#num`],[`string_fragment`,`#str`],[`multiline_string_fragment`,`#str`],[`string_content`,`#str`],[`raw_string_content`,`#str`],[`heredoc_content`,`#str`],[`heredoc_beginning`,`#heredoc`],[`heredoc_end`,`#heredoc`],[`string`,`#str`],[`template_string`,`#str`],[`string_literal`,`#str`],[`interpreted_string_literal`,`#str`],[`raw_string_literal`,`#str`],[`raw_string`,`#str`],[`escape_sequence`,`#str`],[`char_literal`,`#char`],[`character_literal`,`#char`],[`character`,`#char`],[`regex_pattern`,`#regex`]]),a=new Set([`comment`,`line_comment`,`block_comment`]),o=new Set([`string_fragment`,`multiline_string_fragment`,`string_content`,`raw_string_content`,`escape_sequence`,`heredoc_content`,`string_start`,`string_end`]),s=new Map([[`call_expression`,`function`],[`method_invocation`,`name`],[`call`,`method`],[`attribute`,`attribute`],[`macro_invocation`,`macro`],[`field_access`,`field`],[`new_expression`,`constructor`],[`keyword_argument`,`name`],[`element_value_pair`,`key`],[`generic_function`,`function`],[`template_function`,`name`]]);function c(e,t){let n=[],r=[],i=[];return l(e,n,r,i),w(x([...p(n,r),...m(n,i)]),t,n)}function l(n,r,i,o){function s(n){let c=r.length,l=n.childCount===0?void 0:u(n);if(n.childCount===0)d(n,r);else if(l!==void 0)r.push({kind:`text`,text:l,startRow:n.startPosition.row,endRow:n.endPosition.row});else if(!a.has(n.type)){let e=[],r=n.isNamed&&t.has(n.type);for(let t of n.children){let n=s(t);r&&t.isNamed&&!a.has(t.type)&&e.push(n)}r&&e.length>=2&&o.push(e)}let f={startTokenIndex:c,endTokenIndex:r.length,node:n};return n.isNamed&&e.has(n.type)&&i.push(f),f}s(n)}function u(e){let t=e.isNamed?i.get(e.type):void 0;if(t!==void 0)return e.namedChildren.every(e=>o.has(e.type))?t:void 0}function d(e,t){if(a.has(e.type))return;let o=e.startPosition.row,s=e.endPosition.row;if(e.isNamed&&r.has(e.type)){t.push({kind:`text`,text:e.text,startRow:o,endRow:s},{kind:`text`,text:`:`,startRow:o,endRow:s},{kind:`id`,text:e.text,startRow:o,endRow:s});return}if(e.isNamed&&n.has(e.type)&&!f(e)){t.push({kind:`id`,text:e.text,startRow:o,endRow:s});return}let c=e.isNamed?i.get(e.type):void 0;t.push({kind:`text`,text:c??e.text,startRow:o,endRow:s})}function f(e){let t=e.parent;if(!t)return!1;if(t.type===`method_reference`||t.type===`call`&&t.childForFieldName(`function`)?.id===e.id||e.type===`constant`&&t.type===`call`&&t.childForFieldName(`receiver`)?.id===e.id||t.type===`method_invocation`&&t.childForFieldName(`object`)?.id===e.id&&/^\p{Lu}/u.test(e.text))return!0;if((t.type===`scoped_identifier`||t.type===`qualified_identifier`)&&(t.childForFieldName(`name`)?.id===e.id||t.childForFieldName(`path`)?.id===e.id)){let e=t;for(;e.parent&&(e.parent.type===`scoped_identifier`||e.parent.type===`qualified_identifier`||e.parent.type===`generic_function`||e.parent.type===`template_function`);)e=e.parent;if(e.parent?.type===`call_expression`&&e.parent.childForFieldName(`function`)?.id===e.id)return!0}if(t.type===`literal_element`&&t.parent?.type===`keyed_element`&&t.parent.namedChild(0)?.id===t.id)return!0;let n=s.get(t.type);return n!==void 0&&t.childForFieldName(n)?.id===e.id}function p(e,t){let n=[];for(let r of t)r.endTokenIndex-r.startTokenIndex<40||n.push(_(`b:${v(e,r.startTokenIndex,r.endTokenIndex)}`,r.startTokenIndex,r.endTokenIndex,r.node,r.node));return n}function m(e,t){let n=[],r=new Map,i=t.map(t=>g(e,t));for(let[e,t]of i.entries())for(let[n,i]of t.windowKeysByStart.entries())for(let t of i){if(t===void 0)continue;let i=r.get(t);i?(i.count+=1,i.containerIndex!==e&&(i.containerIndex=-1),i.minStart=Math.min(i.minStart,n),i.maxStart=Math.max(i.maxStart,n)):r.set(t,{count:1,containerIndex:e,minStart:n,maxStart:n})}let a=(e,t)=>{if(e===void 0)return!1;let n=r.get(e);return n!==void 0&&n.count>=2&&(n.containerIndex===-1||n.maxStart-n.minStart>=t)},o=e=>{let t=i[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},s=[];for(let[e,t]of i.entries())for(let[n,r]of t.windowKeysByStart.entries())for(let[i,c]of r.entries()){if(!a(c,i)||!o({containerIndex:e,start:n,length:i}))continue;let r=t.windowKeysByStart[n]?.[i+1],l=t.windowKeysByStart[n-1]?.[i+1];a(r,i+1)||a(l,i+1)||s.push({containerIndex:e,start:n,length:i})}let c=new Set(s.map(h)),l=s;for(;l.length>0;){let r=[];for(let i of l){let a=t[i.containerIndex],o=a?.[i.start],s=a?.[i.start+i.length-1];if(!o||!s)continue;let c=`s:${v(e,o.startTokenIndex,s.endTokenIndex)}`;n.push(_(c,o.startTokenIndex,s.endTokenIndex,o.node,s.node)),r.push(i)}l=[];for(let e of r)for(let t of[e.start,e.start+1]){let n={containerIndex:e.containerIndex,start:t,length:e.length-1},r=i[e.containerIndex]?.windowKeysByStart[t]?.[n.length];c.has(h(n))||!a(r,n.length)||!o(n)||(c.add(h(n)),l.push(n))}}return n}function h(e){return`${e.containerIndex}:${e.start}:${e.length}`}function g(e,t){let n=t.map(t=>y(v(e,t.startTokenIndex,t.endTokenIndex))),r=[];for(let e=0;e<t.length;e+=1){let i=[],a=5381,o=0,s=Math.min(t.length,e+100);for(let r=e;r<s;r+=1){let s=t[r],c=n[r];if(!s||c===void 0)break;a=b(a,c),o+=s.endTokenIndex-s.startTokenIndex;let l=r-e+1;i[l]=l>=2&&o>=40?b(a,l):void 0}r.push(i)}return{windowKeysByStart:r,statementHashes:n}}function _(e,t,n,r,i){return{fingerprint:e,tokenCount:n-t,startTokenIndex:t,endTokenIndex:n,startIndex:r.startIndex,endIndex:i.endIndex,startLine:r.startPosition.row+1,endLine:i.endPosition.row+1}}function v(e,t,n){let r=new Map,i=[];for(let a=t;a<n;a+=1){let t=e[a];if(t)if(t.kind===`id`){let e=r.get(t.text);e===void 0&&(e=r.size,r.set(t.text,e)),i.push(`$${e}`)}else i.push(t.text)}return i.join(` `)}function y(e){let t=5381;for(let n=0;n<e.length;n+=1)t=Math.imul(t,33)^e.charCodeAt(n);return t}function b(e,t){return Math.imul(e,31)+t}function x(e){let t=new Map;for(let n of e){let e=t.get(n.fingerprint)??[];e.push(n),t.set(n.fingerprint,e)}let n=[...t.values()].map(S).filter(e=>e.length>=2),r=new Map(n.map(e=>[e[0]?.fingerprint??``,e.length])),i=e=>e.tokenCount*(r.get(e.fingerprint)??1),a=n.flat();a.sort((e,t)=>i(t)-i(e));for(let e=0;;e+=1){let t=[],n=new Map;for(let e of a){if(t.some(t=>C(t,e)))continue;t.push(e);let r=n.get(e.fingerprint)??[];r.push(e),n.set(e.fingerprint,r)}let r,i=-1;for(let[e,t]of n){let n=t[0]?.tokenCount??0;t.length<2&&n>i&&(r=e,i=n)}if(r===void 0)return n;if(e>=20){for(let[e,t]of n)t.length<2&&n.delete(e);return n}a=a.filter(e=>e.fingerprint!==r)}}function S(e){let t=new Map;for(let n of e){let e=`${n.startIndex}:${n.endIndex}`,r=t.get(e);(!r||n.tokenCount>r.tokenCount)&&t.set(e,n)}return[...t.values()]}function C(e,t){return e.startIndex<t.endIndex&&t.startIndex<e.endIndex}function w(e,t,n){let r=0,i=0,a=[],o=new Set;for(let s of e.values()){r+=s.length-1;for(let e of s){i=Math.max(i,e.tokenCount);for(let r=e.startTokenIndex;r<e.endTokenIndex;r+=1){let e=n[r];for(let n=e?.startRow??0;n<=(e?.endRow??-1);n+=1)t.has(n+1)&&o.add(n+1)}}a.push(s.map(({startLine:e,endLine:t})=>({startLine:e,endLine:t})).toSorted((e,t)=>e.startLine-t.startLine))}return a.sort((e,t)=>(e[0]?.startLine??0)-(t[0]?.startLine??0)),{duplicateBlockCount:r,duplicateBlockGroupCount:e.size,duplicateBlockGroups:a,duplicateLineCount:o.size,duplicationRatio:t.size===0?0:o.size/t.size,maxDuplicateBlockSize:i}}exports.measureDuplication=c;
1
+ "use strict";const e=require("./duplicateSelection.cjs"),t=new Set(`statement_block.block.compound_statement.body_statement.constructor_body.do_block.if_statement.for_statement.for_in_statement.enhanced_for_statement.for_range_loop.while_statement.do_statement.try_statement.try_with_resources_statement.with_statement.switch_statement.switch_expression.switch_case.switch_block_statement_group.switch_rule.case_clause.case_statement.match_statement.match_arm.except_clause.catch_clause.finally_clause.elif_clause.ensure.expression_statement.return_statement.return_expression.if_expression.for_expression.while_expression.loop_expression.match_expression.jsx_element.jsx_self_closing_element.if.unless.case.case_match.while.until.for.begin.when`.split(`.`)),n=new Set([`program`,`source_file`,`translation_unit`,`module`,`statement_block`,`block`,`compound_statement`,`body_statement`,`constructor_body`,`class_body`,`block_body`,`do_block`,`do`,`ensure`,`then`,`else`,`case_statement`,`switch_block_statement_group`,`switch_rule`,`expression_case`,`type_case`,`communication_case`,`default_case`]),r=new Set([`identifier`,`constant`,`instance_variable`,`class_variable`,`global_variable`]),i=new Set([`shorthand_property_identifier`,`shorthand_property_identifier_pattern`]),a=new Map([[`number`,`#num`],[`number_literal`,`#num`],[`integer`,`#num`],[`float`,`#num`],[`integer_literal`,`#num`],[`float_literal`,`#num`],[`int_literal`,`#num`],[`rune_literal`,`#char`],[`imaginary_literal`,`#num`],[`decimal_integer_literal`,`#num`],[`hex_integer_literal`,`#num`],[`octal_integer_literal`,`#num`],[`binary_integer_literal`,`#num`],[`decimal_floating_point_literal`,`#num`],[`hex_floating_point_literal`,`#num`],[`string_fragment`,`#str`],[`multiline_string_fragment`,`#str`],[`string_content`,`#str`],[`raw_string_content`,`#str`],[`heredoc_content`,`#str`],[`heredoc_beginning`,`#heredoc`],[`heredoc_end`,`#heredoc`],[`string`,`#str`],[`template_string`,`#str`],[`string_literal`,`#str`],[`interpreted_string_literal`,`#str`],[`raw_string_literal`,`#str`],[`raw_string`,`#str`],[`escape_sequence`,`#str`],[`char_literal`,`#char`],[`character_literal`,`#char`],[`character`,`#char`],[`regex_pattern`,`#regex`]]),o=new Set([`#num`,`#str`,`#char`,`#regex`]),s=new Set([`comment`,`line_comment`,`block_comment`]),c=new Set([`string_fragment`,`multiline_string_fragment`,`string_content`,`raw_string_content`,`escape_sequence`,`heredoc_content`]),l=new Set([`string_fragment`,`multiline_string_fragment`,`string_content`,`raw_string_content`,`escape_sequence`,`heredoc_content`,`string_start`,`string_end`]),u=new Map([[`call_expression`,`function`],[`method_invocation`,`name`],[`call`,`method`],[`attribute`,`attribute`],[`macro_invocation`,`macro`],[`field_access`,`field`],[`new_expression`,`constructor`],[`keyword_argument`,`name`],[`element_value_pair`,`key`],[`generic_function`,`function`],[`template_function`,`name`]]),d={minTokens:40,maxGapTokens:30};function f(e,t){return e*5>=t}function p(t,n,r){let i=r?.minTokens??d.minTokens,a=r?.maxGapTokens??d.maxGapTokens,o=[],s=[],c=[];h(t,o,s,c);let l=S(o),u=[...w(o,l,s,i),...T(o,l,c,i)];return U(B(z(e.selectMaximalGroups(u,e=>e.length>=2)),a),n,o)}function m(t,n){let r=n?.minTokens??d.minTokens,i=[],a=[],o=[];h(t,i,a,o);let s=S(i),c=w(i,s,a,r);for(let e of o){let t=e[0],n=e.at(-1);!t||!n||n.endTokenIndex-t.startTokenIndex<r||c.push(O(`s:${N(i,s,t.startTokenIndex,n.endTokenIndex)}`,t.startTokenIndex,n.endTokenIndex,t.node,n.node))}return e.dedupeByRegion(c).map(({fingerprint:e,tokenCount:t,startIndex:n,endIndex:r,startLine:i,endLine:a})=>({fingerprint:e,tokenCount:t,startIndex:n,endIndex:r,startLine:i,endLine:a}))}function h(e,r,i,a){function o(e){let c=r.length,l=e.childCount===0?void 0:g(e);if(e.childCount===0)_(e,r);else if(l!==void 0)r.push(v(l,y(e,l),e.startPosition.row,e.endPosition.row));else if(!s.has(e.type)){let t=[],r=e.isNamed&&n.has(e.type);for(let n of e.children){let e=o(n);r&&n.isNamed&&!s.has(n.type)&&t.push(e)}r&&t.length>0&&a.push(t)}let u={startTokenIndex:c,endTokenIndex:r.length,node:e};return e.isNamed&&t.has(e.type)&&i.push(u),u}o(e)}function g(e){let t=e.isNamed?a.get(e.type):void 0;if(t!==void 0)return e.namedChildren.every(e=>l.has(e.type))?t:void 0}function _(e,t){if(s.has(e.type))return;let n=e.startPosition.row,o=e.endPosition.row;if(e.isNamed&&i.has(e.type)){t.push(v(e.text,void 0,n,o),v(`:`,void 0,n,o),{kind:`id`,text:e.text,textHash:0,textHash2:0,startRow:n,endRow:o});return}if(e.isNamed&&r.has(e.type)&&!C(e)){t.push({kind:`id`,text:e.text,textHash:0,textHash2:0,startRow:n,endRow:o});return}let c=e.isNamed?a.get(e.type):void 0;c===void 0?t.push(v(e.text,void 0,n,o)):t.push(v(c,y(e,c),n,o))}function v(e,t,n,r){let i={kind:`text`,text:e,textHash:I(e),textHash2:L(e),startRow:n,endRow:r};return t!==void 0&&o.has(e)&&(i.literalHash=I(t),i.literalHash2=L(t)),i}function y(e,t){if(t!==`#str`&&t!==`#char`||c.has(e.type))return e.text;let n=e.namedChildren.filter(e=>c.has(e.type));return n.length>0?n.map(e=>e.text).join(``):x(e.text)}const b=new Set([`"`,`'`,"`"]);function x(e){let t=e[0];return e.length>=2&&t!==void 0&&b.has(t)&&e.endsWith(t)?e.slice(1,-1):e}function S(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 C(e){let t=e.parent;if(!t)return!1;if(t.type===`method_reference`||t.type===`call`&&t.childForFieldName(`function`)?.id===e.id||e.type===`constant`&&t.type===`call`&&t.childForFieldName(`receiver`)?.id===e.id||t.type===`method_invocation`&&t.childForFieldName(`object`)?.id===e.id&&/^\p{Lu}/u.test(e.text))return!0;if((t.type===`scoped_identifier`||t.type===`qualified_identifier`)&&(t.childForFieldName(`name`)?.id===e.id||t.childForFieldName(`path`)?.id===e.id)){let e=t;for(;e.parent&&(e.parent.type===`scoped_identifier`||e.parent.type===`qualified_identifier`||e.parent.type===`generic_function`||e.parent.type===`template_function`);)e=e.parent;if(e.parent?.type===`call_expression`&&e.parent.childForFieldName(`function`)?.id===e.id)return!0}if(t.type===`literal_element`&&t.parent?.type===`keyed_element`&&t.parent.namedChild(0)?.id===t.id)return!0;let n=u.get(t.type);return n!==void 0&&t.childForFieldName(n)?.id===e.id}function w(e,t,n,r){let i=[];for(let a of n)a.endTokenIndex-a.startTokenIndex<r||i.push(O(`b:${N(e,t,a.startTokenIndex,a.endTokenIndex)}`,a.startTokenIndex,a.endTokenIndex,a.node,a.node));return i}function T(e,t,n,r){let i=[],a=new Map,o=n.map(t=>D(e,t,r));for(let[e,t]of o.entries())for(let[n,r]of t.windowKeysByStart.entries())for(let t of r){if(t===void 0)continue;let r=a.get(t);r?(r.count+=1,r.containerIndex!==e&&(r.containerIndex=-1),r.minStart=Math.min(r.minStart,n),r.maxStart=Math.max(r.maxStart,n)):a.set(t,{count:1,containerIndex:e,minStart:n,maxStart:n})}let s=(e,t)=>{if(e===void 0)return!1;let n=a.get(e);return n!==void 0&&n.count>=2&&(n.containerIndex===-1||n.maxStart-n.minStart>=t)},c=e=>{let t=o[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},l=[];for(let[e,t]of o.entries())for(let[n,r]of t.windowKeysByStart.entries())for(let[i,a]of r.entries()){if(!s(a,i)||!c({containerIndex:e,start:n,length:i}))continue;let r=t.windowKeysByStart[n]?.[i+1],o=t.windowKeysByStart[n-1]?.[i+1];s(r,i+1)||s(o,i+1)||l.push({containerIndex:e,start:n,length:i})}let u=new Set(l.map(E)),d=l;for(;d.length>0;){let r=[];for(let a of d){let o=n[a.containerIndex],s=o?.[a.start],c=o?.[a.start+a.length-1];if(!s||!c)continue;let l=`s:${N(e,t,s.startTokenIndex,c.endTokenIndex)}`;i.push(O(l,s.startTokenIndex,c.endTokenIndex,s.node,c.node)),r.push(a)}d=[];for(let e of r)for(let t of[e.start,e.start+1]){let n={containerIndex:e.containerIndex,start:t,length:e.length-1},r=o[e.containerIndex]?.windowKeysByStart[t]?.[n.length];u.has(E(n))||!s(r,n.length)||!c(n)||(u.add(E(n)),d.push(n))}}return i}function E(e){return`${e.containerIndex}:${e.start}:${e.length}`}function D(e,t,n){let r=t.map(t=>P(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=R(o,l),s+=c.endTokenIndex-c.startTokenIndex;let u=i-e+1;a[u]=u>=2&&s>=n?R(o,u):void 0}i.push(a)}return{windowKeysByStart:i,statementHashes:r}}function O(e,t,n,r,i){return{fingerprint:e,tokenCount:n-t,startTokenIndex:t,endTokenIndex:n,startIndex:r.startIndex,endIndex:i.endIndex,startLine:r.startPosition.row+1,endLine:i.endPosition.row+1}}const k=[],A=[];function j(e){let t=k[e];return t===void 0&&(t=I(`$${e}`),k[e]=t),t}function M(e){let t=A[e];return t===void 0&&(t=L(`$${e}`),A[e]=t),t}function N(e,t,n,r){let[i,a]=F(e,n,r,f((t[r]??0)-(t[n]??0),r-n));return`${i}:${a}:${r-n}`}function P(e,t,n){let[r,i]=F(e,t,n,!1);return r^Math.imul(i,31)}function F(e,t,n,r){let i=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=j(e),c=M(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 I(e){let t=5381;for(let n=0;n<e.length;n+=1)t=Math.imul(t,33)^e.charCodeAt(n);return t}function L(e){let t=-2128831035;for(let n=0;n<e.length;n+=1)t=Math.imul(t^e.charCodeAt(n),16777619);return t}function R(e,t){return Math.imul(e,31)+t}function z(e){let t=[];for(let n of e.values()){let e=n.map(e=>({segments:[{startTokenIndex:e.startTokenIndex,endTokenIndex:e.endTokenIndex}],tokenCount:e.tokenCount,startTokenIndex:e.startTokenIndex,endTokenIndex:e.endTokenIndex,startIndex:e.startIndex,endIndex:e.endIndex,startLine:e.startLine,endLine:e.endLine}));e.sort((e,t)=>e.startTokenIndex-t.startTokenIndex||e.endTokenIndex-t.endTokenIndex),t.push(e)}return t}function B(e,t){if(t<=0||e.length<2)return e;e.sort(V);for(let n=!0;n;){n=!1;for(let r=0;r<e.length&&!n;r+=1)for(let i=r+1;i<e.length;i+=1){let a=e[r],o=e[i];if(!a||!o)continue;let s=H(a,o,t)??H(o,a,t);if(s){e[r]=s,e.splice(i,1),e.sort(V),n=!0;break}}}return e}function V(e,t){let n=e[0],r=t[0];return(n?.startTokenIndex??0)-(r?.startTokenIndex??0)||(n?.endTokenIndex??0)-(r?.endTokenIndex??0)}function H(e,t,n){if(e.length===t.length){for(let[r,i]of e.entries()){let a=t[r];if(!a)return;let o=a.startTokenIndex-i.endTokenIndex;if(o<0||o>n)return;let s=e[r+1];if(s&&a.endTokenIndex>s.startTokenIndex)return}return e.map((e,n)=>{let r=t[n];return r?{segments:[...e.segments,...r.segments],tokenCount:e.tokenCount+r.tokenCount,startTokenIndex:e.startTokenIndex,endTokenIndex:r.endTokenIndex,startIndex:e.startIndex,endIndex:r.endIndex,startLine:e.startLine,endLine:r.endLine}:e})}}function U(e,t,n){let r=0,i=0,a=[],o=new Set;for(let s of e){r+=(s.length-1)*(s[0]?.segments.length??1);for(let e of s){i=Math.max(i,e.tokenCount);for(let r of e.segments)for(let e=r.startTokenIndex;e<r.endTokenIndex;e+=1){let r=n[e];for(let e=r?.startRow??0;e<=(r?.endRow??-1);e+=1)t.has(e+1)&&o.add(e+1)}}a.push(s.map(({startLine:e,endLine:t})=>({startLine:e,endLine:t})).toSorted((e,t)=>e.startLine-t.startLine))}return a.sort((e,t)=>(e[0]?.startLine??0)-(t[0]?.startLine??0)),{duplicateBlockCount:r,duplicateBlockGroupCount:e.length,duplicateBlockGroups:a,duplicateLineCount:o.size,duplicationRatio:t.size===0?0:o.size/t.size,maxDuplicateBlockSize:i}}exports.collectCrossFileDuplicateCandidates=m,exports.defaultDuplicationOptions=d,exports.measureDuplication=p;
2
2
  //# sourceMappingURL=duplication.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"duplication.cjs","names":[],"sources":["../src/duplication.ts"],"sourcesContent":["import type Parser from 'tree-sitter';\nimport type { DuplicationMetrics } from './types.js';\n\n/**\n * Block-like nodes considered as whole-subtree duplicate candidates. Detection itself is\n * token-based, so this set only decides which subtrees are compared; Ruby's keyword-like node\n * types (`if`, `case`, ...) are safe here because only named nodes become candidates.\n */\nconst duplicateBlockTypes = new Set([\n 'statement_block',\n 'block',\n 'compound_statement',\n 'body_statement',\n 'constructor_body',\n 'do_block',\n 'if_statement',\n 'for_statement',\n 'for_in_statement',\n 'enhanced_for_statement',\n 'for_range_loop',\n 'while_statement',\n 'do_statement',\n 'try_statement',\n 'try_with_resources_statement',\n 'with_statement',\n 'switch_statement',\n 'switch_expression',\n 'switch_case',\n 'switch_block_statement_group',\n 'switch_rule',\n 'case_clause',\n 'case_statement',\n 'match_statement',\n 'match_arm',\n 'except_clause',\n 'catch_clause',\n 'finally_clause',\n 'elif_clause',\n 'ensure',\n 'expression_statement',\n 'return_statement',\n 'return_expression',\n 'if_expression',\n 'for_expression',\n 'while_expression',\n 'loop_expression',\n 'match_expression',\n 'jsx_element',\n 'jsx_self_closing_element',\n // Ruby\n 'if',\n 'unless',\n 'case',\n 'case_match',\n 'while',\n 'until',\n 'for',\n 'begin',\n 'when',\n]);\n\n/** Nodes whose direct named children form statement sequences scanned for copy-pasted runs. */\nconst statementContainerTypes = new Set([\n 'program',\n 'source_file',\n 'translation_unit',\n 'module',\n 'statement_block',\n 'block',\n 'compound_statement',\n 'body_statement',\n 'constructor_body',\n 'class_body',\n 'block_body',\n 'do_block',\n // Ruby loop bodies are a named `do` node, and `ensure` holds statements directly.\n 'do',\n 'ensure',\n 'then',\n 'else',\n // Case-like nodes hold their statements directly, without an inner block.\n 'case_statement',\n 'switch_block_statement_group',\n 'switch_rule',\n 'expression_case',\n 'type_case',\n 'communication_case',\n 'default_case',\n]);\n\n/**\n * Identifier leaves anonymized by occurrence order so consistently renamed copies still match.\n * Member/type names (`property_identifier`, `field_identifier`, `type_identifier`, ...) are kept\n * verbatim instead: calling a different API is a semantic difference, not a rename.\n */\nconst anonymizedIdentifierTypes = new Set([\n 'identifier',\n 'constant',\n 'instance_variable',\n 'class_variable',\n 'global_variable',\n]);\n\n/**\n * JS shorthand properties (`{ alpha }`) both emit the property name (semantic output shape) and\n * reference the binding, so they tokenize as the desugared `name: binding` — one verbatim text\n * token plus one anonymized id token — matching how the explicit form is tokenized.\n */\nconst shorthandPropertyTypes = new Set(['shorthand_property_identifier', 'shorthand_property_identifier_pattern']);\n\n/** Literal leaves normalized to a kind tag so copies differing only in literal values still match. */\nconst literalKindByType = new Map([\n ['number', '#num'],\n ['number_literal', '#num'],\n ['integer', '#num'],\n ['float', '#num'],\n ['integer_literal', '#num'],\n ['float_literal', '#num'],\n ['int_literal', '#num'],\n ['rune_literal', '#char'],\n ['imaginary_literal', '#num'],\n ['decimal_integer_literal', '#num'],\n ['hex_integer_literal', '#num'],\n ['octal_integer_literal', '#num'],\n ['binary_integer_literal', '#num'],\n ['decimal_floating_point_literal', '#num'],\n ['hex_floating_point_literal', '#num'],\n ['string_fragment', '#str'],\n ['multiline_string_fragment', '#str'],\n ['string_content', '#str'],\n ['raw_string_content', '#str'],\n ['heredoc_content', '#str'],\n // Heredoc marker names (`<<~SQL` vs `<<~QUERY`) have no string-value significance.\n ['heredoc_beginning', '#heredoc'],\n ['heredoc_end', '#heredoc'],\n // Strings are leaves in some grammars (Go/Rust) and fragment containers in others.\n ['string', '#str'],\n ['template_string', '#str'],\n ['string_literal', '#str'],\n ['interpreted_string_literal', '#str'],\n ['raw_string_literal', '#str'],\n ['raw_string', '#str'],\n ['escape_sequence', '#str'],\n ['char_literal', '#char'],\n ['character_literal', '#char'],\n ['character', '#char'],\n ['regex_pattern', '#regex'],\n]);\n\nconst commentTypes = new Set(['comment', 'line_comment', 'block_comment']);\n\n/** Children of a string node that carry only literal content; anything else is interpolation. */\nconst stringFragmentTypes = new Set([\n 'string_fragment',\n 'multiline_string_fragment',\n 'string_content',\n 'raw_string_content',\n 'escape_sequence',\n 'heredoc_content',\n // Python string delimiters are named children; they never carry interpolation.\n 'string_start',\n 'string_end',\n]);\n\n/**\n * Where a grammar names callees/members with a plain `identifier` (Java `method_invocation.name`,\n * Ruby `call.method`, Python `attribute.attribute`, plain calls elsewhere), the leaf in that field\n * must stay verbatim like `property_identifier` does: calling a different API is a semantic\n * difference, not a rename.\n */\nconst semanticNameFieldByParentType = new Map([\n ['call_expression', 'function'],\n ['method_invocation', 'name'],\n ['call', 'method'],\n ['attribute', 'attribute'],\n ['macro_invocation', 'macro'],\n // Java names accessed fields with a plain identifier in the `field` field.\n ['field_access', 'field'],\n // JS/TS `new Foo(...)` names the constructed API in the `constructor` field.\n ['new_expression', 'constructor'],\n // Python `f(timeout=...)` and Java `@Anno(key=...)` name parameters of the callee's API.\n ['keyword_argument', 'name'],\n ['element_value_pair', 'key'],\n // Rust turbofish and C++ template callees (`compute::<u32>(...)`); type arguments stay\n // anonymized via their own node types.\n ['generic_function', 'function'],\n ['template_function', 'name'],\n]);\n\n/** Minimum normalized token count for a region to be considered for duplication, to skip trivial repeats. */\nconst minDuplicateTokenCount = 40;\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/** Caps how often the maximal-region selection reruns after shedding failed duplicate groups. */\nconst maxSelectionRerunCount = 20;\n\ninterface Token {\n /** Normalization target: identifiers to anonymize, literal kind tags, or the raw token text. */\n kind: 'id' | 'text';\n text: string;\n /** 0-based source rows the token occupies, so line coverage counts only matched-token lines. */\n startRow: number;\n endRow: number;\n}\n\ninterface TokenRange {\n startTokenIndex: number;\n endTokenIndex: number;\n node: Parser.SyntaxNode;\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\n/**\n * Detects copy-pasted regions within a file. Regions are compared by their normalized token\n * sequence: identifiers are anonymized consistently by first-occurrence order (`a.f(a, b)` matches\n * `x.f(x, y)` but not `x.f(y, z)`), literals are normalized by kind, and member/type names and all\n * keywords/operators are kept verbatim. Candidates are whole block-like subtrees plus runs of\n * consecutive sibling statements, so a copy pasted into the middle of a longer block is still\n * found. Only maximal, non-overlapping regions are counted.\n */\nexport function measureDuplication(root: Parser.SyntaxNode, codeLineNumbers: Set<number>): DuplicationMetrics {\n const tokens: Token[] = [];\n const blockRanges: TokenRange[] = [];\n const containerStatementRanges: TokenRange[][] = [];\n collectTokens(root, tokens, blockRanges, containerStatementRanges);\n\n const candidates = [\n ...collectBlockCandidates(tokens, blockRanges),\n ...collectSequenceCandidates(tokens, containerStatementRanges),\n ];\n const counted = selectMaximalDuplicates(candidates);\n return summarizeDuplicates(counted, codeLineNumbers, tokens);\n}\n\nfunction collectTokens(\n root: Parser.SyntaxNode,\n tokens: Token[],\n blockRanges: TokenRange[],\n containerStatementRanges: TokenRange[][]\n): void {\n function visit(node: Parser.SyntaxNode): TokenRange {\n const startTokenIndex = tokens.length;\n const atomicKind = node.childCount === 0 ? undefined : atomicLiteralKind(node);\n if (node.childCount === 0) {\n appendLeafToken(node, tokens);\n } else if (atomicKind !== undefined) {\n // Interpolation-free strings collapse to their kind tag so copies differing only in quote\n // style or content still match; delimiter tokens would otherwise break the equivalence.\n tokens.push({ kind: 'text', text: atomicKind, startRow: node.startPosition.row, endRow: node.endPosition.row });\n } else if (!commentTypes.has(node.type)) {\n const statementRanges: TokenRange[] = [];\n const isContainer = node.isNamed && statementContainerTypes.has(node.type);\n for (const child of node.children) {\n const childRange = visit(child);\n if (isContainer && child.isNamed && !commentTypes.has(child.type)) {\n statementRanges.push(childRange);\n }\n }\n if (isContainer && statementRanges.length >= minSequenceStatementCount) {\n containerStatementRanges.push(statementRanges);\n }\n }\n\n const range = { startTokenIndex, endTokenIndex: tokens.length, node };\n if (node.isNamed && duplicateBlockTypes.has(node.type)) {\n blockRanges.push(range);\n }\n return range;\n }\n\n visit(root);\n}\n\n/** The kind tag of a string-like node with no interpolation, or undefined to descend normally. */\nfunction atomicLiteralKind(node: Parser.SyntaxNode): string | undefined {\n const kind = node.isNamed ? literalKindByType.get(node.type) : undefined;\n if (kind === undefined) {\n return undefined;\n }\n return node.namedChildren.every((child) => stringFragmentTypes.has(child.type)) ? kind : undefined;\n}\n\nfunction appendLeafToken(node: Parser.SyntaxNode, tokens: Token[]): void {\n if (commentTypes.has(node.type)) {\n return;\n }\n\n const startRow = node.startPosition.row;\n const endRow = node.endPosition.row;\n if (node.isNamed && shorthandPropertyTypes.has(node.type)) {\n tokens.push(\n { kind: 'text', text: node.text, startRow, endRow },\n { kind: 'text', text: ':', startRow, endRow },\n { kind: 'id', text: node.text, startRow, endRow }\n );\n return;\n }\n\n if (node.isNamed && anonymizedIdentifierTypes.has(node.type) && !isSemanticNameLeaf(node)) {\n tokens.push({ kind: 'id', text: node.text, startRow, endRow });\n return;\n }\n\n // Anything else keeps its text: keywords, operators, punctuation, and semantic names such as\n // `property_identifier`/`type_identifier`, which must distinguish otherwise-identical structures.\n const literalKind = node.isNamed ? literalKindByType.get(node.type) : undefined;\n tokens.push({ kind: 'text', text: literalKind ?? node.text, startRow, endRow });\n}\n\nfunction isSemanticNameLeaf(node: Parser.SyntaxNode): boolean {\n const parent = node.parent;\n if (!parent) {\n return false;\n }\n\n // Java method references (`Foo::bar`) name their identifiers without grammar fields; both the\n // type/object and the referenced method are semantic.\n if (parent.type === 'method_reference') {\n return true;\n }\n\n // `call` names its callee `method` in Ruby but `function` in Python; accept both fields.\n if (parent.type === 'call' && parent.childForFieldName('function')?.id === node.id) {\n return true;\n }\n\n // A Ruby constant receiving a call (`Alpha.new(...)`) names the invoked API; constants used as\n // plain values stay anonymized so renamed clones referencing constants still match.\n if (node.type === 'constant' && parent.type === 'call' && parent.childForFieldName('receiver')?.id === node.id) {\n return true;\n }\n\n // Java static receivers (`Alpha.run(...)`) name the invoked type. The tokenizer has no symbol\n // table, so PascalCase — Java's universal type-naming convention — is the discriminator;\n // camelCase instance receivers stay anonymized for rename tolerance.\n if (\n parent.type === 'method_invocation' &&\n parent.childForFieldName('object')?.id === node.id &&\n /^\\p{Lu}/u.test(node.text)\n ) {\n return true;\n }\n\n // Qualified callees (Rust `crate::alpha::make(...)`, C++ `detail::make(...)`) and generic\n // callees (`compute::<u32>(...)`, `compute<int>(...)`) wrap their identifiers arbitrarily deep;\n // every path/name segment is semantic there — but only in call position, so renamed clones that\n // merely reference scoped constants or `use` paths still match.\n if (\n (parent.type === 'scoped_identifier' || parent.type === 'qualified_identifier') &&\n (parent.childForFieldName('name')?.id === node.id || parent.childForFieldName('path')?.id === node.id)\n ) {\n let outer = parent;\n while (\n outer.parent &&\n (outer.parent.type === 'scoped_identifier' ||\n outer.parent.type === 'qualified_identifier' ||\n outer.parent.type === 'generic_function' ||\n outer.parent.type === 'template_function')\n ) {\n outer = outer.parent;\n }\n if (outer.parent?.type === 'call_expression' && outer.parent.childForFieldName('function')?.id === outer.id) {\n return true;\n }\n }\n\n // Go struct-literal keys (`Config{Timeout: ...}`) have no `key` field in the grammar: the key\n // is the keyed_element's first named child, a literal_element wrapping the identifier.\n if (\n parent.type === 'literal_element' &&\n parent.parent?.type === 'keyed_element' &&\n parent.parent.namedChild(0)?.id === parent.id\n ) {\n return true;\n }\n\n const field = semanticNameFieldByParentType.get(parent.type);\n return field !== undefined && parent.childForFieldName(field)?.id === node.id;\n}\n\nfunction collectBlockCandidates(tokens: Token[], blockRanges: TokenRange[]): DuplicateCandidate[] {\n const candidates: DuplicateCandidate[] = [];\n for (const range of blockRanges) {\n const tokenCount = range.endTokenIndex - range.startTokenIndex;\n if (tokenCount < minDuplicateTokenCount) {\n continue;\n }\n candidates.push(\n toCandidate(\n `b:${fingerprintTokens(tokens, range.startTokenIndex, range.endTokenIndex)}`,\n range.startTokenIndex,\n range.endTokenIndex,\n range.node,\n range.node\n )\n );\n }\n return candidates;\n}\n\ninterface WindowOccurrences {\n count: number;\n /** -1 once occurrences span more than one container. */\n containerIndex: number;\n minStart: number;\n maxStart: number;\n}\n\ninterface SequenceWindow {\n containerIndex: number;\n start: number;\n length: number;\n}\n\n/**\n * Enumerates runs of consecutive sibling statements. Every container statement participates; only\n * the window length is capped, so enumeration stays linear in the statement count. Windows are\n * grouped by a cheap rolling hash of per-statement fingerprints, and only locally maximal repeated\n * windows — those whose one-statement extensions stop repeating — become candidates with an exact\n * (window-consistent) fingerprint. Without the maximality filter a degenerate file of\n * near-identical statements would fingerprint every sub-window of every repeated region.\n */\nfunction collectSequenceCandidates(tokens: Token[], containers: TokenRange[][]): DuplicateCandidate[] {\n const candidates: DuplicateCandidate[] = [];\n const occurrencesByWindowKey = new Map<number, WindowOccurrences>();\n const containerWindows = containers.map((statements) => enumerateContainerWindows(tokens, statements));\n for (const [containerIndex, windows] of containerWindows.entries()) {\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 occurrences.minStart = Math.min(occurrences.minStart, start);\n occurrences.maxStart = Math.max(occurrences.maxStart, start);\n } else {\n occurrencesByWindowKey.set(windowKey, { count: 1, containerIndex, minStart: start, maxStart: start });\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.\n const repeats = (windowKey: number | undefined, length: number): boolean => {\n if (windowKey === undefined) {\n return false;\n }\n const occurrences = occurrencesByWindowKey.get(windowKey);\n return (\n occurrences !== undefined &&\n occurrences.count >= 2 &&\n (occurrences.containerIndex === -1 || occurrences.maxStart - occurrences.minStart >= length)\n );\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 if (!first || !last) {\n continue;\n }\n const fingerprint = `s:${fingerprintTokens(tokens, first.startTokenIndex, last.endTokenIndex)}`;\n candidates.push(toCandidate(fingerprint, first.startTokenIndex, last.endTokenIndex, first.node, last.node));\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[]): ContainerWindows {\n const statementHashes = statements.map((statement) =>\n hashText(fingerprintTokens(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 >= minDuplicateTokenCount\n ? combineHashes(hash, statementCount)\n : undefined;\n }\n windowKeysByStart.push(row);\n }\n return { windowKeysByStart, statementHashes };\n}\n\nfunction toCandidate(\n fingerprint: string,\n startTokenIndex: number,\n endTokenIndex: number,\n firstNode: Parser.SyntaxNode,\n lastNode: Parser.SyntaxNode\n): DuplicateCandidate {\n return {\n fingerprint,\n tokenCount: endTokenIndex - startTokenIndex,\n startTokenIndex,\n endTokenIndex,\n startIndex: firstNode.startIndex,\n endIndex: lastNode.endIndex,\n startLine: firstNode.startPosition.row + 1,\n endLine: lastNode.endPosition.row + 1,\n };\n}\n\n/** Serializes a token range with identifiers anonymized consistently by first-occurrence order. */\nfunction fingerprintTokens(tokens: Token[], startTokenIndex: number, endTokenIndex: number): string {\n const indexByIdentifier = new Map<string, number>();\n const parts: string[] = [];\n for (let index = startTokenIndex; index < 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 parts.push(`$${identifierIndex}`);\n } else {\n parts.push(token.text);\n }\n }\n return parts.join(' ');\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\nfunction combineHashes(hash: number, value: number): number {\n return Math.imul(hash, 31) + value;\n}\n\n/**\n * Keeps only maximal, non-overlapping duplicates: candidates are grouped by fingerprint, larger\n * regions win over regions overlapping them, and groups reduced below two survivors are dropped.\n */\nfunction selectMaximalDuplicates(candidates: DuplicateCandidate[]): Map<string, DuplicateCandidate[]> {\n const byFingerprint = new Map<string, DuplicateCandidate[]>();\n for (const candidate of candidates) {\n const group = byFingerprint.get(candidate.fingerprint) ?? [];\n group.push(candidate);\n byFingerprint.set(candidate.fingerprint, group);\n }\n\n const groups = [...byFingerprint.values()].map(dedupeByRegion).filter((group) => group.length >= 2);\n // Greedy order ranks by total coverage (region size × copies): a 3×3-statement group must beat\n // a 2×4-statement group overlapping two of its copies, or the third copy is silently dropped\n // and the reported duplication shrinks as more copies are added.\n const groupSizeByFingerprint = new Map(groups.map((group) => [group[0]?.fingerprint ?? '', group.length]));\n const coverage = (candidate: DuplicateCandidate): number =>\n candidate.tokenCount * (groupSizeByFingerprint.get(candidate.fingerprint) ?? 1);\n let duplicates = groups.flat();\n duplicates.sort((left, right) => coverage(right) - coverage(left));\n\n // Greedy selection can keep a candidate whose group ends up below two survivors; such an\n // uncounted region must not block smaller groups, so the largest failed group is removed and the\n // selection reruns. One group at a time: freeing a failed group's regions can rescue another.\n // The rerun cap bounds degenerate inputs; past it the remaining failed groups are dropped,\n // trading a sliver of recall on such files for bounded runtime.\n for (let rerun = 0; ; rerun += 1) {\n const keptRegions: { startIndex: number; endIndex: number }[] = [];\n const counted = new Map<string, DuplicateCandidate[]>();\n for (const candidate of duplicates) {\n if (keptRegions.some((region) => overlaps(region, candidate))) {\n continue;\n }\n keptRegions.push(candidate);\n const group = counted.get(candidate.fingerprint) ?? [];\n group.push(candidate);\n counted.set(candidate.fingerprint, group);\n }\n\n let failedFingerprint: string | undefined;\n let failedTokenCount = -1;\n for (const [fingerprint, group] of counted) {\n const tokenCount = group[0]?.tokenCount ?? 0;\n if (group.length < 2 && tokenCount > failedTokenCount) {\n failedFingerprint = fingerprint;\n failedTokenCount = tokenCount;\n }\n }\n // No failed fingerprint means every counted group kept at least two survivors.\n if (failedFingerprint === undefined) {\n return counted;\n }\n if (rerun >= maxSelectionRerunCount) {\n for (const [fingerprint, group] of counted) {\n if (group.length < 2) {\n counted.delete(fingerprint);\n }\n }\n return counted;\n }\n\n duplicates = duplicates.filter((candidate) => candidate.fingerprint !== failedFingerprint);\n }\n}\n\n/** Drops candidates covering the same source region (a block and the statement run spanning it). */\nfunction dedupeByRegion(group: DuplicateCandidate[]): DuplicateCandidate[] {\n const byRegion = new Map<string, DuplicateCandidate>();\n for (const candidate of group) {\n const key = `${candidate.startIndex}:${candidate.endIndex}`;\n const existing = byRegion.get(key);\n if (!existing || candidate.tokenCount > existing.tokenCount) {\n byRegion.set(key, candidate);\n }\n }\n return [...byRegion.values()];\n}\n\nfunction overlaps(\n left: { startIndex: number; endIndex: number },\n right: { startIndex: number; endIndex: number }\n): boolean {\n return left.startIndex < right.endIndex && right.startIndex < left.endIndex;\n}\n\nfunction summarizeDuplicates(\n counted: Map<string, DuplicateCandidate[]>,\n codeLineNumbers: Set<number>,\n tokens: Token[]\n): DuplicationMetrics {\n let duplicateBlockCount = 0;\n let maxDuplicateBlockSize = 0;\n const duplicateBlockGroups: { startLine: number; endLine: number }[][] = [];\n const duplicatedLines = new Set<number>();\n for (const group of counted.values()) {\n duplicateBlockCount += group.length - 1;\n for (const candidate of group) {\n maxDuplicateBlockSize = Math.max(maxDuplicateBlockSize, candidate.tokenCount);\n // Only CODE lines carrying matched tokens count: comments and blank gaps inside a\n // candidate's bounding range — and blank rows inside a multi-row token (heredocs, template\n // literals) — are not duplicated content and would push the ratio past 1.\n for (let index = candidate.startTokenIndex; index < candidate.endTokenIndex; index += 1) {\n const token = tokens[index];\n for (let row = token?.startRow ?? 0; row <= (token?.endRow ?? -1); row += 1) {\n if (codeLineNumbers.has(row + 1)) {\n duplicatedLines.add(row + 1);\n }\n }\n }\n }\n duplicateBlockGroups.push(\n group\n .map(({ startLine, endLine }) => ({ startLine, endLine }))\n .toSorted((left, right) => left.startLine - right.startLine)\n );\n }\n duplicateBlockGroups.sort((left, right) => (left[0]?.startLine ?? 0) - (right[0]?.startLine ?? 0));\n\n return {\n duplicateBlockCount,\n duplicateBlockGroupCount: counted.size,\n duplicateBlockGroups,\n duplicateLineCount: duplicatedLines.size,\n duplicationRatio: codeLineNumbers.size === 0 ? 0 : duplicatedLines.size / codeLineNumbers.size,\n maxDuplicateBlockSize,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"duplication.cjs","names":["selectMaximalGroups","dedupeByRegion"],"sources":["../src/duplication.ts"],"sourcesContent":["import type Parser from 'tree-sitter';\nimport { dedupeByRegion, selectMaximalGroups } from './duplicateSelection.js';\nimport type { DuplicationMetrics, DuplicationOptions } from './types.js';\n\n/**\n * Block-like nodes considered as whole-subtree duplicate candidates. Detection itself is\n * token-based, so this set only decides which subtrees are compared; Ruby's keyword-like node\n * types (`if`, `case`, ...) are safe here because only named nodes become candidates.\n */\nconst duplicateBlockTypes = new Set([\n 'statement_block',\n 'block',\n 'compound_statement',\n 'body_statement',\n 'constructor_body',\n 'do_block',\n 'if_statement',\n 'for_statement',\n 'for_in_statement',\n 'enhanced_for_statement',\n 'for_range_loop',\n 'while_statement',\n 'do_statement',\n 'try_statement',\n 'try_with_resources_statement',\n 'with_statement',\n 'switch_statement',\n 'switch_expression',\n 'switch_case',\n 'switch_block_statement_group',\n 'switch_rule',\n 'case_clause',\n 'case_statement',\n 'match_statement',\n 'match_arm',\n 'except_clause',\n 'catch_clause',\n 'finally_clause',\n 'elif_clause',\n 'ensure',\n 'expression_statement',\n 'return_statement',\n 'return_expression',\n 'if_expression',\n 'for_expression',\n 'while_expression',\n 'loop_expression',\n 'match_expression',\n 'jsx_element',\n 'jsx_self_closing_element',\n // Ruby\n 'if',\n 'unless',\n 'case',\n 'case_match',\n 'while',\n 'until',\n 'for',\n 'begin',\n 'when',\n]);\n\n/** Nodes whose direct named children form statement sequences scanned for copy-pasted runs. */\nconst statementContainerTypes = new Set([\n 'program',\n 'source_file',\n 'translation_unit',\n 'module',\n 'statement_block',\n 'block',\n 'compound_statement',\n 'body_statement',\n 'constructor_body',\n 'class_body',\n 'block_body',\n 'do_block',\n // Ruby loop bodies are a named `do` node, and `ensure` holds statements directly.\n 'do',\n 'ensure',\n 'then',\n 'else',\n // Case-like nodes hold their statements directly, without an inner block.\n 'case_statement',\n 'switch_block_statement_group',\n 'switch_rule',\n 'expression_case',\n 'type_case',\n 'communication_case',\n 'default_case',\n]);\n\n/**\n * Identifier leaves anonymized by occurrence order so consistently renamed copies still match.\n * Member/type names (`property_identifier`, `field_identifier`, `type_identifier`, ...) are kept\n * verbatim instead: calling a different API is a semantic difference, not a rename.\n */\nconst anonymizedIdentifierTypes = new Set([\n 'identifier',\n 'constant',\n 'instance_variable',\n 'class_variable',\n 'global_variable',\n]);\n\n/**\n * JS shorthand properties (`{ alpha }`) both emit the property name (semantic output shape) and\n * reference the binding, so they tokenize as the desugared `name: binding` — one verbatim text\n * token plus one anonymized id token — matching how the explicit form is tokenized.\n */\nconst shorthandPropertyTypes = new Set(['shorthand_property_identifier', 'shorthand_property_identifier_pattern']);\n\n/** Literal leaves normalized to a kind tag so copies differing only in literal values still match. */\nconst literalKindByType = new Map([\n ['number', '#num'],\n ['number_literal', '#num'],\n ['integer', '#num'],\n ['float', '#num'],\n ['integer_literal', '#num'],\n ['float_literal', '#num'],\n ['int_literal', '#num'],\n ['rune_literal', '#char'],\n ['imaginary_literal', '#num'],\n ['decimal_integer_literal', '#num'],\n ['hex_integer_literal', '#num'],\n ['octal_integer_literal', '#num'],\n ['binary_integer_literal', '#num'],\n ['decimal_floating_point_literal', '#num'],\n ['hex_floating_point_literal', '#num'],\n ['string_fragment', '#str'],\n ['multiline_string_fragment', '#str'],\n ['string_content', '#str'],\n ['raw_string_content', '#str'],\n ['heredoc_content', '#str'],\n // Heredoc marker names (`<<~SQL` vs `<<~QUERY`) have no string-value significance.\n ['heredoc_beginning', '#heredoc'],\n ['heredoc_end', '#heredoc'],\n // Strings are leaves in some grammars (Go/Rust) and fragment containers in others.\n ['string', '#str'],\n ['template_string', '#str'],\n ['string_literal', '#str'],\n ['interpreted_string_literal', '#str'],\n ['raw_string_literal', '#str'],\n ['raw_string', '#str'],\n ['escape_sequence', '#str'],\n ['char_literal', '#char'],\n ['character_literal', '#char'],\n ['character', '#char'],\n ['regex_pattern', '#regex'],\n]);\n\n/**\n * Kind tags whose raw source text re-enters the fingerprint in literal-dense (data-like) regions.\n * `#heredoc` is excluded: heredoc marker names are naming choices, not data values.\n */\nconst valueCarryingLiteralKinds = new Set(['#num', '#str', '#char', '#regex']);\n\nconst commentTypes = new Set(['comment', 'line_comment', 'block_comment']);\n\n/**\n * String children that carry actual content, i.e. stringFragmentTypes minus the delimiter nodes\n * (Python's `string_start`/`string_end`), for delimiter-independent literal values.\n */\nconst stringContentFragmentTypes = new Set([\n 'string_fragment',\n 'multiline_string_fragment',\n 'string_content',\n 'raw_string_content',\n 'escape_sequence',\n 'heredoc_content',\n]);\n\n/** Children of a string node that carry only literal content; anything else is interpolation. */\nconst stringFragmentTypes = new Set([\n 'string_fragment',\n 'multiline_string_fragment',\n 'string_content',\n 'raw_string_content',\n 'escape_sequence',\n 'heredoc_content',\n // Python string delimiters are named children; they never carry interpolation.\n 'string_start',\n 'string_end',\n]);\n\n/**\n * Where a grammar names callees/members with a plain `identifier` (Java `method_invocation.name`,\n * Ruby `call.method`, Python `attribute.attribute`, plain calls elsewhere), the leaf in that field\n * must stay verbatim like `property_identifier` does: calling a different API is a semantic\n * difference, not a rename.\n */\nconst semanticNameFieldByParentType = new Map([\n ['call_expression', 'function'],\n ['method_invocation', 'name'],\n ['call', 'method'],\n ['attribute', 'attribute'],\n ['macro_invocation', 'macro'],\n // Java names accessed fields with a plain identifier in the `field` field.\n ['field_access', 'field'],\n // JS/TS `new Foo(...)` names the constructed API in the `constructor` field.\n ['new_expression', 'constructor'],\n // Python `f(timeout=...)` and Java `@Anno(key=...)` name parameters of the callee's API.\n ['keyword_argument', 'name'],\n ['element_value_pair', 'key'],\n // Rust turbofish and C++ template callees (`compute::<u32>(...)`); type arguments stay\n // anonymized via their own node types.\n ['generic_function', 'function'],\n ['template_function', 'name'],\n]);\n\nexport const defaultDuplicationOptions: Required<DuplicationOptions> = {\n minTokens: 40,\n maxGapTokens: 30,\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. Logic-heavy code sits well\n * below the bound (5-10% literals) while object/array tables sit above it (25-50%); punctuation\n * and member names dilute tables, which is why the bound is far below half. Compared in integer\n * math (5 * literals >= total) so the TypeScript and native backends cannot disagree on the\n * boundary.\n */\nfunction isLiteralDense(literalCount: number, tokenCount: number): boolean {\n return literalCount * 5 >= tokenCount;\n}\n\ninterface 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 /** 0-based source rows the token occupies, so line coverage counts only matched-token lines. */\n startRow: number;\n endRow: number;\n}\n\ninterface TokenRange {\n startTokenIndex: number;\n endTokenIndex: number;\n node: Parser.SyntaxNode;\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\ninterface CountedOccurrence {\n /** Matched token runs; more than one once gapped groups are merged. */\n segments: TokenSegment[];\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 startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/**\n * Detects copy-pasted regions within a file. Regions are compared by their normalized token\n * sequence: identifiers are anonymized consistently by first-occurrence order (`a.f(a, b)` matches\n * `x.f(x, y)` but not `x.f(y, z)`), literals are normalized by kind, and member/type names and all\n * keywords/operators are kept verbatim. Literal-dense (data-like) regions additionally require\n * equal literal values. Candidates are whole block-like subtrees plus runs of consecutive sibling\n * statements, so a copy pasted into the middle of a longer block is still found. Only maximal,\n * non-overlapping regions are counted, and adjacent groups separated by a small token gap merge\n * into one gapped (Type-3) clone group.\n */\nexport function measureDuplication(\n root: Parser.SyntaxNode,\n codeLineNumbers: Set<number>,\n options?: DuplicationOptions\n): DuplicationMetrics {\n const minTokens = options?.minTokens ?? defaultDuplicationOptions.minTokens;\n const maxGapTokens = options?.maxGapTokens ?? defaultDuplicationOptions.maxGapTokens;\n const tokens: Token[] = [];\n const blockRanges: TokenRange[] = [];\n const containerStatementRanges: TokenRange[][] = [];\n collectTokens(root, tokens, blockRanges, containerStatementRanges);\n const literalCountPrefix = buildLiteralCountPrefix(tokens);\n\n const candidates = [\n ...collectBlockCandidates(tokens, literalCountPrefix, blockRanges, minTokens),\n ...collectSequenceCandidates(tokens, literalCountPrefix, containerStatementRanges, minTokens),\n ];\n const counted = selectMaximalGroups(candidates, (group) => group.length >= 2);\n const groups = mergeAdjacentGroups(toCountedGroups(counted), maxGapTokens);\n return summarizeDuplicates(groups, codeLineNumbers, tokens);\n}\n\n/**\n * Collects this file's duplicate-candidate fingerprints for cross-file clone detection: whole\n * block-like subtrees plus each statement container's full run (so wholly copied files and class\n * bodies match even when no inner block clears the threshold on its own). Nested and overlapping\n * candidates are all returned; the project-level selection keeps only maximal ones.\n */\nexport function collectCrossFileDuplicateCandidates(\n root: Parser.SyntaxNode,\n options?: DuplicationOptions\n): CrossFileDuplicateCandidate[] {\n const minTokens = options?.minTokens ?? defaultDuplicationOptions.minTokens;\n const tokens: Token[] = [];\n const blockRanges: TokenRange[] = [];\n const containerStatementRanges: TokenRange[][] = [];\n collectTokens(root, tokens, blockRanges, containerStatementRanges);\n const literalCountPrefix = buildLiteralCountPrefix(tokens);\n\n const candidates = collectBlockCandidates(tokens, literalCountPrefix, blockRanges, minTokens);\n for (const statements of containerStatementRanges) {\n const first = statements[0];\n const last = statements.at(-1);\n if (!first || !last) {\n continue;\n }\n const tokenCount = last.endTokenIndex - first.startTokenIndex;\n if (tokenCount < minTokens) {\n continue;\n }\n candidates.push(\n toCandidate(\n `s:${fingerprintKey(tokens, literalCountPrefix, first.startTokenIndex, last.endTokenIndex)}`,\n first.startTokenIndex,\n last.endTokenIndex,\n first.node,\n last.node\n )\n );\n }\n return dedupeByRegion(candidates).map(({ fingerprint, tokenCount, startIndex, endIndex, startLine, endLine }) => ({\n fingerprint,\n tokenCount,\n startIndex,\n endIndex,\n startLine,\n endLine,\n }));\n}\n\nfunction collectTokens(\n root: Parser.SyntaxNode,\n tokens: Token[],\n blockRanges: TokenRange[],\n containerStatementRanges: TokenRange[][]\n): void {\n function visit(node: Parser.SyntaxNode): TokenRange {\n const startTokenIndex = tokens.length;\n const atomicKind = node.childCount === 0 ? undefined : atomicLiteralKind(node);\n if (node.childCount === 0) {\n appendLeafToken(node, tokens);\n } else if (atomicKind !== undefined) {\n // Interpolation-free strings collapse to their kind tag so copies differing only in quote\n // style or content still match; delimiter tokens would otherwise break the equivalence.\n tokens.push(\n makeTextToken(atomicKind, literalValueText(node, atomicKind), node.startPosition.row, node.endPosition.row)\n );\n } else if (!commentTypes.has(node.type)) {\n const statementRanges: TokenRange[] = [];\n const isContainer = node.isNamed && statementContainerTypes.has(node.type);\n for (const child of node.children) {\n const childRange = visit(child);\n if (isContainer && child.isNamed && !commentTypes.has(child.type)) {\n statementRanges.push(childRange);\n }\n }\n // Single-statement containers are recorded too: within-file window enumeration needs two\n // statements and simply yields nothing for them, but cross-file matching must still see a\n // file whose only top-level statement is not a catalogued block type (a lone exported table).\n if (isContainer && statementRanges.length > 0) {\n containerStatementRanges.push(statementRanges);\n }\n }\n\n const range = { startTokenIndex, endTokenIndex: tokens.length, node };\n if (node.isNamed && duplicateBlockTypes.has(node.type)) {\n blockRanges.push(range);\n }\n return range;\n }\n\n visit(root);\n}\n\n/** The kind tag of a string-like node with no interpolation, or undefined to descend normally. */\nfunction atomicLiteralKind(node: Parser.SyntaxNode): string | undefined {\n const kind = node.isNamed ? literalKindByType.get(node.type) : undefined;\n if (kind === undefined) {\n return undefined;\n }\n return node.namedChildren.every((child) => stringFragmentTypes.has(child.type)) ? kind : undefined;\n}\n\nfunction appendLeafToken(node: Parser.SyntaxNode, tokens: Token[]): void {\n if (commentTypes.has(node.type)) {\n return;\n }\n\n const startRow = node.startPosition.row;\n const endRow = node.endPosition.row;\n if (node.isNamed && shorthandPropertyTypes.has(node.type)) {\n tokens.push(\n makeTextToken(node.text, undefined, startRow, endRow),\n makeTextToken(':', undefined, startRow, endRow),\n { kind: 'id', text: node.text, textHash: 0, textHash2: 0, startRow, endRow }\n );\n return;\n }\n\n if (node.isNamed && anonymizedIdentifierTypes.has(node.type) && !isSemanticNameLeaf(node)) {\n tokens.push({ kind: 'id', text: node.text, textHash: 0, textHash2: 0, startRow, endRow });\n return;\n }\n\n // Anything else keeps its text: keywords, operators, punctuation, and semantic names such as\n // `property_identifier`/`type_identifier`, which must distinguish otherwise-identical structures.\n const literalKind = node.isNamed ? literalKindByType.get(node.type) : undefined;\n if (literalKind === undefined) {\n tokens.push(makeTextToken(node.text, undefined, startRow, endRow));\n } else {\n tokens.push(makeTextToken(literalKind, literalValueText(node, literalKind), startRow, endRow));\n }\n}\n\nfunction makeTextToken(text: string, literalValueText: string | undefined, startRow: number, endRow: number): Token {\n const token: Token = { kind: 'text', text, textHash: hashText(text), textHash2: hashText2(text), startRow, endRow };\n if (literalValueText !== undefined && valueCarryingLiteralKinds.has(text)) {\n token.literalHash = hashText(literalValueText);\n token.literalHash2 = hashText2(literalValueText);\n }\n return token;\n}\n\n/**\n * The value of a literal as folded into literal-dense fingerprints. Strings hash their CONTENT,\n * not their source spelling: formatters rewrite quote style on paste (`'one'` vs `\"one\"`), so\n * delimiters must not make two copied tables differ. Content comes from the fragment children when\n * the grammar provides them (which also drops Python's `string_start`/`string_end` delimiter\n * nodes), else from the text with one matching pair of surrounding quotes stripped. Numbers keep\n * their raw text: formatters preserve numeric spelling, and canonicalizing values (`0x10` vs `16`)\n * identically in JavaScript and Rust would be far riskier than the rare mismatch it would unify.\n */\nfunction literalValueText(node: Parser.SyntaxNode, kind: string): string {\n if (kind !== '#str' && kind !== '#char') {\n return node.text;\n }\n // Fragment leaves (string_fragment, escape_sequence, heredoc_content, ...) already carry bare\n // content; a quote appearing there is content, not a delimiter.\n if (stringContentFragmentTypes.has(node.type)) {\n return node.text;\n }\n const fragments = node.namedChildren.filter((child) => stringContentFragmentTypes.has(child.type));\n if (fragments.length > 0) {\n return fragments.map((child) => child.text).join('');\n }\n return stripMatchingQuotes(node.text);\n}\n\nconst quoteCharacters = new Set(['\"', \"'\", '`']);\n\nfunction stripMatchingQuotes(text: string): string {\n const first = text[0];\n return text.length >= 2 && first !== undefined && quoteCharacters.has(first) && text.endsWith(first)\n ? text.slice(1, -1)\n : text;\n}\n\n/** literalCountPrefix[i] = value-carrying literal tokens in tokens[0..i), for O(1) density checks. */\nfunction 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\nfunction isSemanticNameLeaf(node: Parser.SyntaxNode): boolean {\n const parent = node.parent;\n if (!parent) {\n return false;\n }\n\n // Java method references (`Foo::bar`) name their identifiers without grammar fields; both the\n // type/object and the referenced method are semantic.\n if (parent.type === 'method_reference') {\n return true;\n }\n\n // `call` names its callee `method` in Ruby but `function` in Python; accept both fields.\n if (parent.type === 'call' && parent.childForFieldName('function')?.id === node.id) {\n return true;\n }\n\n // A Ruby constant receiving a call (`Alpha.new(...)`) names the invoked API; constants used as\n // plain values stay anonymized so renamed clones referencing constants still match.\n if (node.type === 'constant' && parent.type === 'call' && parent.childForFieldName('receiver')?.id === node.id) {\n return true;\n }\n\n // Java static receivers (`Alpha.run(...)`) name the invoked type. The tokenizer has no symbol\n // table, so PascalCase — Java's universal type-naming convention — is the discriminator;\n // camelCase instance receivers stay anonymized for rename tolerance.\n if (\n parent.type === 'method_invocation' &&\n parent.childForFieldName('object')?.id === node.id &&\n /^\\p{Lu}/u.test(node.text)\n ) {\n return true;\n }\n\n // Qualified callees (Rust `crate::alpha::make(...)`, C++ `detail::make(...)`) and generic\n // callees (`compute::<u32>(...)`, `compute<int>(...)`) wrap their identifiers arbitrarily deep;\n // every path/name segment is semantic there — but only in call position, so renamed clones that\n // merely reference scoped constants or `use` paths still match.\n if (\n (parent.type === 'scoped_identifier' || parent.type === 'qualified_identifier') &&\n (parent.childForFieldName('name')?.id === node.id || parent.childForFieldName('path')?.id === node.id)\n ) {\n let outer = parent;\n while (\n outer.parent &&\n (outer.parent.type === 'scoped_identifier' ||\n outer.parent.type === 'qualified_identifier' ||\n outer.parent.type === 'generic_function' ||\n outer.parent.type === 'template_function')\n ) {\n outer = outer.parent;\n }\n if (outer.parent?.type === 'call_expression' && outer.parent.childForFieldName('function')?.id === outer.id) {\n return true;\n }\n }\n\n // Go struct-literal keys (`Config{Timeout: ...}`) have no `key` field in the grammar: the key\n // is the keyed_element's first named child, a literal_element wrapping the identifier.\n if (\n parent.type === 'literal_element' &&\n parent.parent?.type === 'keyed_element' &&\n parent.parent.namedChild(0)?.id === parent.id\n ) {\n return true;\n }\n\n const field = semanticNameFieldByParentType.get(parent.type);\n return field !== undefined && parent.childForFieldName(field)?.id === node.id;\n}\n\nfunction collectBlockCandidates(\n tokens: Token[],\n literalCountPrefix: Int32Array,\n blockRanges: TokenRange[],\n minTokens: number\n): DuplicateCandidate[] {\n const candidates: DuplicateCandidate[] = [];\n for (const range of blockRanges) {\n const tokenCount = range.endTokenIndex - range.startTokenIndex;\n if (tokenCount < minTokens) {\n continue;\n }\n candidates.push(\n toCandidate(\n `b:${fingerprintKey(tokens, literalCountPrefix, range.startTokenIndex, range.endTokenIndex)}`,\n range.startTokenIndex,\n range.endTokenIndex,\n range.node,\n range.node\n )\n );\n }\n return candidates;\n}\n\ninterface WindowOccurrences {\n count: number;\n /** -1 once occurrences span more than one container. */\n containerIndex: number;\n minStart: number;\n maxStart: number;\n}\n\ninterface SequenceWindow {\n containerIndex: number;\n start: number;\n length: number;\n}\n\n/**\n * Enumerates runs of consecutive sibling statements. Every container statement participates; only\n * the window length is capped, so enumeration stays linear in the statement count. Windows are\n * grouped by a cheap rolling hash of per-statement fingerprints, and only locally maximal repeated\n * windows — those whose one-statement extensions stop repeating — become candidates with an exact\n * (window-consistent) fingerprint. Without the maximality filter a degenerate file of\n * near-identical statements would fingerprint every sub-window of every repeated region.\n */\nfunction collectSequenceCandidates(\n tokens: Token[],\n literalCountPrefix: Int32Array,\n containers: TokenRange[][],\n minTokens: number\n): DuplicateCandidate[] {\n const candidates: DuplicateCandidate[] = [];\n const occurrencesByWindowKey = new Map<number, WindowOccurrences>();\n const containerWindows = containers.map((statements) => enumerateContainerWindows(tokens, statements, minTokens));\n for (const [containerIndex, windows] of containerWindows.entries()) {\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 occurrences.minStart = Math.min(occurrences.minStart, start);\n occurrences.maxStart = Math.max(occurrences.maxStart, start);\n } else {\n occurrencesByWindowKey.set(windowKey, { count: 1, containerIndex, minStart: start, maxStart: start });\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.\n const repeats = (windowKey: number | undefined, length: number): boolean => {\n if (windowKey === undefined) {\n return false;\n }\n const occurrences = occurrencesByWindowKey.get(windowKey);\n return (\n occurrences !== undefined &&\n occurrences.count >= 2 &&\n (occurrences.containerIndex === -1 || occurrences.maxStart - occurrences.minStart >= length)\n );\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 if (!first || !last) {\n continue;\n }\n const fingerprint = `s:${fingerprintKey(tokens, literalCountPrefix, first.startTokenIndex, last.endTokenIndex)}`;\n candidates.push(toCandidate(fingerprint, first.startTokenIndex, last.endTokenIndex, first.node, last.node));\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\nfunction toCandidate(\n fingerprint: string,\n startTokenIndex: number,\n endTokenIndex: number,\n firstNode: Parser.SyntaxNode,\n lastNode: Parser.SyntaxNode\n): DuplicateCandidate {\n return {\n fingerprint,\n tokenCount: endTokenIndex - startTokenIndex,\n startTokenIndex,\n endTokenIndex,\n startIndex: firstNode.startIndex,\n endIndex: lastNode.endIndex,\n startLine: firstNode.startPosition.row + 1,\n endLine: lastNode.endPosition.row + 1,\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. Hashing per-token instead of\n * serializing the whole range to a string keeps fingerprinting allocation-free for nested regions.\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 backend'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 backend'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 backend'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\nfunction toCountedGroups(counted: Map<string, DuplicateCandidate[]>): CountedOccurrence[][] {\n const groups: CountedOccurrence[][] = [];\n for (const group of counted.values()) {\n const occurrences = group.map((candidate) => ({\n segments: [{ startTokenIndex: candidate.startTokenIndex, endTokenIndex: candidate.endTokenIndex }],\n tokenCount: candidate.tokenCount,\n startTokenIndex: candidate.startTokenIndex,\n endTokenIndex: candidate.endTokenIndex,\n startIndex: candidate.startIndex,\n endIndex: candidate.endIndex,\n startLine: candidate.startLine,\n endLine: candidate.endLine,\n }));\n occurrences.sort(\n (left, right) => left.startTokenIndex - right.startTokenIndex || left.endTokenIndex - right.endTokenIndex\n );\n groups.push(occurrences);\n }\n return groups;\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. Two groups merge when they have the same number of occurrences and, pairing\n * occurrences in source order, every pair is gap-adjacent without crossing into the next pair.\n * Merging repeats to a fixpoint so a clone edited in several spots still reassembles. Gap tokens\n * are not matched content: line coverage and sizes count only the matched segments.\n */\nfunction mergeAdjacentGroups(groups: CountedOccurrence[][], maxGapTokens: number): CountedOccurrence[][] {\n if (maxGapTokens <= 0 || groups.length < 2) {\n return groups;\n }\n // Deterministic processing order (mirrored by the native backend): by first occurrence position.\n groups.sort(compareGroups);\n for (let restart = true; restart;) {\n restart = false;\n for (let leftIndex = 0; leftIndex < groups.length && !restart; leftIndex += 1) {\n for (let rightIndex = leftIndex + 1; rightIndex < groups.length; rightIndex += 1) {\n const left = groups[leftIndex];\n const right = groups[rightIndex];\n if (!left || !right) {\n continue;\n }\n const merged = mergeGroups(left, right, maxGapTokens) ?? mergeGroups(right, left, maxGapTokens);\n if (merged) {\n groups[leftIndex] = merged;\n groups.splice(rightIndex, 1);\n groups.sort(compareGroups);\n restart = true;\n break;\n }\n }\n }\n }\n return groups;\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\n/** The merged group when every `second` occurrence gap-follows its `first` counterpart, else undefined. */\nfunction mergeGroups(\n first: CountedOccurrence[],\n second: CountedOccurrence[],\n maxGapTokens: number\n): CountedOccurrence[] | undefined {\n if (first.length !== second.length) {\n return undefined;\n }\n for (const [index, leading] of first.entries()) {\n const trailing = second[index];\n if (!trailing) {\n return undefined;\n }\n const gap = trailing.startTokenIndex - leading.endTokenIndex;\n if (gap < 0 || gap > maxGapTokens) {\n return undefined;\n }\n // The merged span must stay clear of the next pair, or spans would overlap.\n const next = first[index + 1];\n if (next && trailing.endTokenIndex > next.startTokenIndex) {\n return undefined;\n }\n }\n return first.map((leading, index) => {\n const trailing = second[index];\n if (!trailing) {\n return leading;\n }\n return {\n segments: [...leading.segments, ...trailing.segments],\n tokenCount: leading.tokenCount + trailing.tokenCount,\n startTokenIndex: leading.startTokenIndex,\n endTokenIndex: trailing.endTokenIndex,\n startIndex: leading.startIndex,\n endIndex: trailing.endIndex,\n startLine: leading.startLine,\n endLine: trailing.endLine,\n };\n });\n}\n\nfunction summarizeDuplicates(\n groups: CountedOccurrence[][],\n codeLineNumbers: Set<number>,\n tokens: Token[]\n): DuplicationMetrics {\n let duplicateBlockCount = 0;\n let maxDuplicateBlockSize = 0;\n const duplicateBlockGroups: { startLine: number; endLine: number }[][] = [];\n const duplicatedLines = new Set<number>();\n for (const group of groups) {\n // Each redundant occurrence contributes one count per matched fragment, so merging a gapped\n // clone's fragments into one group does not halve the count a `duplicateBlock` threshold sees:\n // an edited two-fragment pair still counts 2, exactly as its unmerged fragments did.\n duplicateBlockCount += (group.length - 1) * (group[0]?.segments.length ?? 1);\n for (const occurrence of group) {\n maxDuplicateBlockSize = Math.max(maxDuplicateBlockSize, occurrence.tokenCount);\n // Only CODE lines carrying matched tokens count: comments and blank gaps inside an\n // occurrence's bounding range — the unmatched gap of a merged clone, and blank rows inside a\n // multi-row token (heredocs, template literals) — are not duplicated content and would push\n // the ratio past 1.\n for (const segment of occurrence.segments) {\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.has(row + 1)) {\n duplicatedLines.add(row + 1);\n }\n }\n }\n }\n }\n duplicateBlockGroups.push(\n group\n .map(({ startLine, endLine }) => ({ startLine, endLine }))\n .toSorted((left, right) => left.startLine - right.startLine)\n );\n }\n duplicateBlockGroups.sort((left, right) => (left[0]?.startLine ?? 0) - (right[0]?.startLine ?? 0));\n\n return {\n duplicateBlockCount,\n duplicateBlockGroupCount: groups.length,\n duplicateBlockGroups,\n duplicateLineCount: duplicatedLines.size,\n duplicationRatio: codeLineNumbers.size === 0 ? 0 : duplicatedLines.size / codeLineNumbers.size,\n maxDuplicateBlockSize,\n };\n}\n"],"mappings":"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"}
@@ -1,11 +1,31 @@
1
1
  import type Parser from 'tree-sitter';
2
- import type { DuplicationMetrics } from './types.js';
2
+ import type { DuplicationMetrics, DuplicationOptions } from './types.js';
3
+ export declare const defaultDuplicationOptions: Required<DuplicationOptions>;
4
+ /** A duplicate region found in one file, exported for cross-file matching by fingerprint. */
5
+ export interface CrossFileDuplicateCandidate {
6
+ /** Content key: equal fingerprints mean equal normalized token sequences (up to hash collision). */
7
+ fingerprint: string;
8
+ tokenCount: number;
9
+ startIndex: number;
10
+ endIndex: number;
11
+ startLine: number;
12
+ endLine: number;
13
+ }
3
14
  /**
4
15
  * Detects copy-pasted regions within a file. Regions are compared by their normalized token
5
16
  * sequence: identifiers are anonymized consistently by first-occurrence order (`a.f(a, b)` matches
6
17
  * `x.f(x, y)` but not `x.f(y, z)`), literals are normalized by kind, and member/type names and all
7
- * keywords/operators are kept verbatim. Candidates are whole block-like subtrees plus runs of
8
- * consecutive sibling statements, so a copy pasted into the middle of a longer block is still
9
- * found. Only maximal, non-overlapping regions are counted.
18
+ * keywords/operators are kept verbatim. Literal-dense (data-like) regions additionally require
19
+ * equal literal values. Candidates are whole block-like subtrees plus runs of consecutive sibling
20
+ * statements, so a copy pasted into the middle of a longer block is still found. Only maximal,
21
+ * non-overlapping regions are counted, and adjacent groups separated by a small token gap merge
22
+ * into one gapped (Type-3) clone group.
10
23
  */
11
- export declare function measureDuplication(root: Parser.SyntaxNode, codeLineNumbers: Set<number>): DuplicationMetrics;
24
+ export declare function measureDuplication(root: Parser.SyntaxNode, codeLineNumbers: Set<number>, options?: DuplicationOptions): DuplicationMetrics;
25
+ /**
26
+ * Collects this file's duplicate-candidate fingerprints for cross-file clone detection: whole
27
+ * block-like subtrees plus each statement container's full run (so wholly copied files and class
28
+ * bodies match even when no inner block clears the threshold on its own). Nested and overlapping
29
+ * candidates are all returned; the project-level selection keeps only maximal ones.
30
+ */
31
+ export declare function collectCrossFileDuplicateCandidates(root: Parser.SyntaxNode, options?: DuplicationOptions): CrossFileDuplicateCandidate[];
@@ -1,2 +1,2 @@
1
- const e=new Set(`statement_block.block.compound_statement.body_statement.constructor_body.do_block.if_statement.for_statement.for_in_statement.enhanced_for_statement.for_range_loop.while_statement.do_statement.try_statement.try_with_resources_statement.with_statement.switch_statement.switch_expression.switch_case.switch_block_statement_group.switch_rule.case_clause.case_statement.match_statement.match_arm.except_clause.catch_clause.finally_clause.elif_clause.ensure.expression_statement.return_statement.return_expression.if_expression.for_expression.while_expression.loop_expression.match_expression.jsx_element.jsx_self_closing_element.if.unless.case.case_match.while.until.for.begin.when`.split(`.`)),t=new Set([`program`,`source_file`,`translation_unit`,`module`,`statement_block`,`block`,`compound_statement`,`body_statement`,`constructor_body`,`class_body`,`block_body`,`do_block`,`do`,`ensure`,`then`,`else`,`case_statement`,`switch_block_statement_group`,`switch_rule`,`expression_case`,`type_case`,`communication_case`,`default_case`]),n=new Set([`identifier`,`constant`,`instance_variable`,`class_variable`,`global_variable`]),r=new Set([`shorthand_property_identifier`,`shorthand_property_identifier_pattern`]),i=new Map([[`number`,`#num`],[`number_literal`,`#num`],[`integer`,`#num`],[`float`,`#num`],[`integer_literal`,`#num`],[`float_literal`,`#num`],[`int_literal`,`#num`],[`rune_literal`,`#char`],[`imaginary_literal`,`#num`],[`decimal_integer_literal`,`#num`],[`hex_integer_literal`,`#num`],[`octal_integer_literal`,`#num`],[`binary_integer_literal`,`#num`],[`decimal_floating_point_literal`,`#num`],[`hex_floating_point_literal`,`#num`],[`string_fragment`,`#str`],[`multiline_string_fragment`,`#str`],[`string_content`,`#str`],[`raw_string_content`,`#str`],[`heredoc_content`,`#str`],[`heredoc_beginning`,`#heredoc`],[`heredoc_end`,`#heredoc`],[`string`,`#str`],[`template_string`,`#str`],[`string_literal`,`#str`],[`interpreted_string_literal`,`#str`],[`raw_string_literal`,`#str`],[`raw_string`,`#str`],[`escape_sequence`,`#str`],[`char_literal`,`#char`],[`character_literal`,`#char`],[`character`,`#char`],[`regex_pattern`,`#regex`]]),a=new Set([`comment`,`line_comment`,`block_comment`]),o=new Set([`string_fragment`,`multiline_string_fragment`,`string_content`,`raw_string_content`,`escape_sequence`,`heredoc_content`,`string_start`,`string_end`]),s=new Map([[`call_expression`,`function`],[`method_invocation`,`name`],[`call`,`method`],[`attribute`,`attribute`],[`macro_invocation`,`macro`],[`field_access`,`field`],[`new_expression`,`constructor`],[`keyword_argument`,`name`],[`element_value_pair`,`key`],[`generic_function`,`function`],[`template_function`,`name`]]);function c(e,t){let n=[],r=[],i=[];return l(e,n,r,i),w(x([...p(n,r),...m(n,i)]),t,n)}function l(n,r,i,o){function s(n){let c=r.length,l=n.childCount===0?void 0:u(n);if(n.childCount===0)d(n,r);else if(l!==void 0)r.push({kind:`text`,text:l,startRow:n.startPosition.row,endRow:n.endPosition.row});else if(!a.has(n.type)){let e=[],r=n.isNamed&&t.has(n.type);for(let t of n.children){let n=s(t);r&&t.isNamed&&!a.has(t.type)&&e.push(n)}r&&e.length>=2&&o.push(e)}let f={startTokenIndex:c,endTokenIndex:r.length,node:n};return n.isNamed&&e.has(n.type)&&i.push(f),f}s(n)}function u(e){let t=e.isNamed?i.get(e.type):void 0;if(t!==void 0)return e.namedChildren.every(e=>o.has(e.type))?t:void 0}function d(e,t){if(a.has(e.type))return;let o=e.startPosition.row,s=e.endPosition.row;if(e.isNamed&&r.has(e.type)){t.push({kind:`text`,text:e.text,startRow:o,endRow:s},{kind:`text`,text:`:`,startRow:o,endRow:s},{kind:`id`,text:e.text,startRow:o,endRow:s});return}if(e.isNamed&&n.has(e.type)&&!f(e)){t.push({kind:`id`,text:e.text,startRow:o,endRow:s});return}let c=e.isNamed?i.get(e.type):void 0;t.push({kind:`text`,text:c??e.text,startRow:o,endRow:s})}function f(e){let t=e.parent;if(!t)return!1;if(t.type===`method_reference`||t.type===`call`&&t.childForFieldName(`function`)?.id===e.id||e.type===`constant`&&t.type===`call`&&t.childForFieldName(`receiver`)?.id===e.id||t.type===`method_invocation`&&t.childForFieldName(`object`)?.id===e.id&&/^\p{Lu}/u.test(e.text))return!0;if((t.type===`scoped_identifier`||t.type===`qualified_identifier`)&&(t.childForFieldName(`name`)?.id===e.id||t.childForFieldName(`path`)?.id===e.id)){let e=t;for(;e.parent&&(e.parent.type===`scoped_identifier`||e.parent.type===`qualified_identifier`||e.parent.type===`generic_function`||e.parent.type===`template_function`);)e=e.parent;if(e.parent?.type===`call_expression`&&e.parent.childForFieldName(`function`)?.id===e.id)return!0}if(t.type===`literal_element`&&t.parent?.type===`keyed_element`&&t.parent.namedChild(0)?.id===t.id)return!0;let n=s.get(t.type);return n!==void 0&&t.childForFieldName(n)?.id===e.id}function p(e,t){let n=[];for(let r of t)r.endTokenIndex-r.startTokenIndex<40||n.push(_(`b:${v(e,r.startTokenIndex,r.endTokenIndex)}`,r.startTokenIndex,r.endTokenIndex,r.node,r.node));return n}function m(e,t){let n=[],r=new Map,i=t.map(t=>g(e,t));for(let[e,t]of i.entries())for(let[n,i]of t.windowKeysByStart.entries())for(let t of i){if(t===void 0)continue;let i=r.get(t);i?(i.count+=1,i.containerIndex!==e&&(i.containerIndex=-1),i.minStart=Math.min(i.minStart,n),i.maxStart=Math.max(i.maxStart,n)):r.set(t,{count:1,containerIndex:e,minStart:n,maxStart:n})}let a=(e,t)=>{if(e===void 0)return!1;let n=r.get(e);return n!==void 0&&n.count>=2&&(n.containerIndex===-1||n.maxStart-n.minStart>=t)},o=e=>{let t=i[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},s=[];for(let[e,t]of i.entries())for(let[n,r]of t.windowKeysByStart.entries())for(let[i,c]of r.entries()){if(!a(c,i)||!o({containerIndex:e,start:n,length:i}))continue;let r=t.windowKeysByStart[n]?.[i+1],l=t.windowKeysByStart[n-1]?.[i+1];a(r,i+1)||a(l,i+1)||s.push({containerIndex:e,start:n,length:i})}let c=new Set(s.map(h)),l=s;for(;l.length>0;){let r=[];for(let i of l){let a=t[i.containerIndex],o=a?.[i.start],s=a?.[i.start+i.length-1];if(!o||!s)continue;let c=`s:${v(e,o.startTokenIndex,s.endTokenIndex)}`;n.push(_(c,o.startTokenIndex,s.endTokenIndex,o.node,s.node)),r.push(i)}l=[];for(let e of r)for(let t of[e.start,e.start+1]){let n={containerIndex:e.containerIndex,start:t,length:e.length-1},r=i[e.containerIndex]?.windowKeysByStart[t]?.[n.length];c.has(h(n))||!a(r,n.length)||!o(n)||(c.add(h(n)),l.push(n))}}return n}function h(e){return`${e.containerIndex}:${e.start}:${e.length}`}function g(e,t){let n=t.map(t=>y(v(e,t.startTokenIndex,t.endTokenIndex))),r=[];for(let e=0;e<t.length;e+=1){let i=[],a=5381,o=0,s=Math.min(t.length,e+100);for(let r=e;r<s;r+=1){let s=t[r],c=n[r];if(!s||c===void 0)break;a=b(a,c),o+=s.endTokenIndex-s.startTokenIndex;let l=r-e+1;i[l]=l>=2&&o>=40?b(a,l):void 0}r.push(i)}return{windowKeysByStart:r,statementHashes:n}}function _(e,t,n,r,i){return{fingerprint:e,tokenCount:n-t,startTokenIndex:t,endTokenIndex:n,startIndex:r.startIndex,endIndex:i.endIndex,startLine:r.startPosition.row+1,endLine:i.endPosition.row+1}}function v(e,t,n){let r=new Map,i=[];for(let a=t;a<n;a+=1){let t=e[a];if(t)if(t.kind===`id`){let e=r.get(t.text);e===void 0&&(e=r.size,r.set(t.text,e)),i.push(`$${e}`)}else i.push(t.text)}return i.join(` `)}function y(e){let t=5381;for(let n=0;n<e.length;n+=1)t=Math.imul(t,33)^e.charCodeAt(n);return t}function b(e,t){return Math.imul(e,31)+t}function x(e){let t=new Map;for(let n of e){let e=t.get(n.fingerprint)??[];e.push(n),t.set(n.fingerprint,e)}let n=[...t.values()].map(S).filter(e=>e.length>=2),r=new Map(n.map(e=>[e[0]?.fingerprint??``,e.length])),i=e=>e.tokenCount*(r.get(e.fingerprint)??1),a=n.flat();a.sort((e,t)=>i(t)-i(e));for(let e=0;;e+=1){let t=[],n=new Map;for(let e of a){if(t.some(t=>C(t,e)))continue;t.push(e);let r=n.get(e.fingerprint)??[];r.push(e),n.set(e.fingerprint,r)}let r,i=-1;for(let[e,t]of n){let n=t[0]?.tokenCount??0;t.length<2&&n>i&&(r=e,i=n)}if(r===void 0)return n;if(e>=20){for(let[e,t]of n)t.length<2&&n.delete(e);return n}a=a.filter(e=>e.fingerprint!==r)}}function S(e){let t=new Map;for(let n of e){let e=`${n.startIndex}:${n.endIndex}`,r=t.get(e);(!r||n.tokenCount>r.tokenCount)&&t.set(e,n)}return[...t.values()]}function C(e,t){return e.startIndex<t.endIndex&&t.startIndex<e.endIndex}function w(e,t,n){let r=0,i=0,a=[],o=new Set;for(let s of e.values()){r+=s.length-1;for(let e of s){i=Math.max(i,e.tokenCount);for(let r=e.startTokenIndex;r<e.endTokenIndex;r+=1){let e=n[r];for(let n=e?.startRow??0;n<=(e?.endRow??-1);n+=1)t.has(n+1)&&o.add(n+1)}}a.push(s.map(({startLine:e,endLine:t})=>({startLine:e,endLine:t})).toSorted((e,t)=>e.startLine-t.startLine))}return a.sort((e,t)=>(e[0]?.startLine??0)-(t[0]?.startLine??0)),{duplicateBlockCount:r,duplicateBlockGroupCount:e.size,duplicateBlockGroups:a,duplicateLineCount:o.size,duplicationRatio:t.size===0?0:o.size/t.size,maxDuplicateBlockSize:i}}export{c as measureDuplication};
1
+ import{dedupeByRegion as e,selectMaximalGroups as t}from"./duplicateSelection.js";const n=new Set(`statement_block.block.compound_statement.body_statement.constructor_body.do_block.if_statement.for_statement.for_in_statement.enhanced_for_statement.for_range_loop.while_statement.do_statement.try_statement.try_with_resources_statement.with_statement.switch_statement.switch_expression.switch_case.switch_block_statement_group.switch_rule.case_clause.case_statement.match_statement.match_arm.except_clause.catch_clause.finally_clause.elif_clause.ensure.expression_statement.return_statement.return_expression.if_expression.for_expression.while_expression.loop_expression.match_expression.jsx_element.jsx_self_closing_element.if.unless.case.case_match.while.until.for.begin.when`.split(`.`)),r=new Set([`program`,`source_file`,`translation_unit`,`module`,`statement_block`,`block`,`compound_statement`,`body_statement`,`constructor_body`,`class_body`,`block_body`,`do_block`,`do`,`ensure`,`then`,`else`,`case_statement`,`switch_block_statement_group`,`switch_rule`,`expression_case`,`type_case`,`communication_case`,`default_case`]),i=new Set([`identifier`,`constant`,`instance_variable`,`class_variable`,`global_variable`]),a=new Set([`shorthand_property_identifier`,`shorthand_property_identifier_pattern`]),o=new Map([[`number`,`#num`],[`number_literal`,`#num`],[`integer`,`#num`],[`float`,`#num`],[`integer_literal`,`#num`],[`float_literal`,`#num`],[`int_literal`,`#num`],[`rune_literal`,`#char`],[`imaginary_literal`,`#num`],[`decimal_integer_literal`,`#num`],[`hex_integer_literal`,`#num`],[`octal_integer_literal`,`#num`],[`binary_integer_literal`,`#num`],[`decimal_floating_point_literal`,`#num`],[`hex_floating_point_literal`,`#num`],[`string_fragment`,`#str`],[`multiline_string_fragment`,`#str`],[`string_content`,`#str`],[`raw_string_content`,`#str`],[`heredoc_content`,`#str`],[`heredoc_beginning`,`#heredoc`],[`heredoc_end`,`#heredoc`],[`string`,`#str`],[`template_string`,`#str`],[`string_literal`,`#str`],[`interpreted_string_literal`,`#str`],[`raw_string_literal`,`#str`],[`raw_string`,`#str`],[`escape_sequence`,`#str`],[`char_literal`,`#char`],[`character_literal`,`#char`],[`character`,`#char`],[`regex_pattern`,`#regex`]]),s=new Set([`#num`,`#str`,`#char`,`#regex`]),c=new Set([`comment`,`line_comment`,`block_comment`]),l=new Set([`string_fragment`,`multiline_string_fragment`,`string_content`,`raw_string_content`,`escape_sequence`,`heredoc_content`]),u=new Set([`string_fragment`,`multiline_string_fragment`,`string_content`,`raw_string_content`,`escape_sequence`,`heredoc_content`,`string_start`,`string_end`]),d=new Map([[`call_expression`,`function`],[`method_invocation`,`name`],[`call`,`method`],[`attribute`,`attribute`],[`macro_invocation`,`macro`],[`field_access`,`field`],[`new_expression`,`constructor`],[`keyword_argument`,`name`],[`element_value_pair`,`key`],[`generic_function`,`function`],[`template_function`,`name`]]),f={minTokens:40,maxGapTokens:30};function p(e,t){return e*5>=t}function m(e,n,r){let i=r?.minTokens??f.minTokens,a=r?.maxGapTokens??f.maxGapTokens,o=[],s=[],c=[];g(e,o,s,c);let l=C(o),u=[...T(o,l,s,i),...E(o,l,c,i)];return W(V(B(t(u,e=>e.length>=2)),a),n,o)}function h(t,n){let r=n?.minTokens??f.minTokens,i=[],a=[],o=[];g(t,i,a,o);let s=C(i),c=T(i,s,a,r);for(let e of o){let t=e[0],n=e.at(-1);!t||!n||n.endTokenIndex-t.startTokenIndex<r||c.push(k(`s:${P(i,s,t.startTokenIndex,n.endTokenIndex)}`,t.startTokenIndex,n.endTokenIndex,t.node,n.node))}return e(c).map(({fingerprint:e,tokenCount:t,startIndex:n,endIndex:r,startLine:i,endLine:a})=>({fingerprint:e,tokenCount:t,startIndex:n,endIndex:r,startLine:i,endLine:a}))}function g(e,t,i,a){function o(e){let s=t.length,l=e.childCount===0?void 0:_(e);if(e.childCount===0)v(e,t);else if(l!==void 0)t.push(y(l,b(e,l),e.startPosition.row,e.endPosition.row));else if(!c.has(e.type)){let t=[],n=e.isNamed&&r.has(e.type);for(let r of e.children){let e=o(r);n&&r.isNamed&&!c.has(r.type)&&t.push(e)}n&&t.length>0&&a.push(t)}let u={startTokenIndex:s,endTokenIndex:t.length,node:e};return e.isNamed&&n.has(e.type)&&i.push(u),u}o(e)}function _(e){let t=e.isNamed?o.get(e.type):void 0;if(t!==void 0)return e.namedChildren.every(e=>u.has(e.type))?t:void 0}function v(e,t){if(c.has(e.type))return;let n=e.startPosition.row,r=e.endPosition.row;if(e.isNamed&&a.has(e.type)){t.push(y(e.text,void 0,n,r),y(`:`,void 0,n,r),{kind:`id`,text:e.text,textHash:0,textHash2:0,startRow:n,endRow:r});return}if(e.isNamed&&i.has(e.type)&&!w(e)){t.push({kind:`id`,text:e.text,textHash:0,textHash2:0,startRow:n,endRow:r});return}let s=e.isNamed?o.get(e.type):void 0;s===void 0?t.push(y(e.text,void 0,n,r)):t.push(y(s,b(e,s),n,r))}function y(e,t,n,r){let i={kind:`text`,text:e,textHash:L(e),textHash2:R(e),startRow:n,endRow:r};return t!==void 0&&s.has(e)&&(i.literalHash=L(t),i.literalHash2=R(t)),i}function b(e,t){if(t!==`#str`&&t!==`#char`||l.has(e.type))return e.text;let n=e.namedChildren.filter(e=>l.has(e.type));return n.length>0?n.map(e=>e.text).join(``):S(e.text)}const x=new Set([`"`,`'`,"`"]);function S(e){let t=e[0];return e.length>=2&&t!==void 0&&x.has(t)&&e.endsWith(t)?e.slice(1,-1):e}function C(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 w(e){let t=e.parent;if(!t)return!1;if(t.type===`method_reference`||t.type===`call`&&t.childForFieldName(`function`)?.id===e.id||e.type===`constant`&&t.type===`call`&&t.childForFieldName(`receiver`)?.id===e.id||t.type===`method_invocation`&&t.childForFieldName(`object`)?.id===e.id&&/^\p{Lu}/u.test(e.text))return!0;if((t.type===`scoped_identifier`||t.type===`qualified_identifier`)&&(t.childForFieldName(`name`)?.id===e.id||t.childForFieldName(`path`)?.id===e.id)){let e=t;for(;e.parent&&(e.parent.type===`scoped_identifier`||e.parent.type===`qualified_identifier`||e.parent.type===`generic_function`||e.parent.type===`template_function`);)e=e.parent;if(e.parent?.type===`call_expression`&&e.parent.childForFieldName(`function`)?.id===e.id)return!0}if(t.type===`literal_element`&&t.parent?.type===`keyed_element`&&t.parent.namedChild(0)?.id===t.id)return!0;let n=d.get(t.type);return n!==void 0&&t.childForFieldName(n)?.id===e.id}function T(e,t,n,r){let i=[];for(let a of n)a.endTokenIndex-a.startTokenIndex<r||i.push(k(`b:${P(e,t,a.startTokenIndex,a.endTokenIndex)}`,a.startTokenIndex,a.endTokenIndex,a.node,a.node));return i}function E(e,t,n,r){let i=[],a=new Map,o=n.map(t=>O(e,t,r));for(let[e,t]of o.entries())for(let[n,r]of t.windowKeysByStart.entries())for(let t of r){if(t===void 0)continue;let r=a.get(t);r?(r.count+=1,r.containerIndex!==e&&(r.containerIndex=-1),r.minStart=Math.min(r.minStart,n),r.maxStart=Math.max(r.maxStart,n)):a.set(t,{count:1,containerIndex:e,minStart:n,maxStart:n})}let s=(e,t)=>{if(e===void 0)return!1;let n=a.get(e);return n!==void 0&&n.count>=2&&(n.containerIndex===-1||n.maxStart-n.minStart>=t)},c=e=>{let t=o[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},l=[];for(let[e,t]of o.entries())for(let[n,r]of t.windowKeysByStart.entries())for(let[i,a]of r.entries()){if(!s(a,i)||!c({containerIndex:e,start:n,length:i}))continue;let r=t.windowKeysByStart[n]?.[i+1],o=t.windowKeysByStart[n-1]?.[i+1];s(r,i+1)||s(o,i+1)||l.push({containerIndex:e,start:n,length:i})}let u=new Set(l.map(D)),d=l;for(;d.length>0;){let r=[];for(let a of d){let o=n[a.containerIndex],s=o?.[a.start],c=o?.[a.start+a.length-1];if(!s||!c)continue;let l=`s:${P(e,t,s.startTokenIndex,c.endTokenIndex)}`;i.push(k(l,s.startTokenIndex,c.endTokenIndex,s.node,c.node)),r.push(a)}d=[];for(let e of r)for(let t of[e.start,e.start+1]){let n={containerIndex:e.containerIndex,start:t,length:e.length-1},r=o[e.containerIndex]?.windowKeysByStart[t]?.[n.length];u.has(D(n))||!s(r,n.length)||!c(n)||(u.add(D(n)),d.push(n))}}return i}function D(e){return`${e.containerIndex}:${e.start}:${e.length}`}function O(e,t,n){let r=t.map(t=>F(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=z(o,l),s+=c.endTokenIndex-c.startTokenIndex;let u=i-e+1;a[u]=u>=2&&s>=n?z(o,u):void 0}i.push(a)}return{windowKeysByStart:i,statementHashes:r}}function k(e,t,n,r,i){return{fingerprint:e,tokenCount:n-t,startTokenIndex:t,endTokenIndex:n,startIndex:r.startIndex,endIndex:i.endIndex,startLine:r.startPosition.row+1,endLine:i.endPosition.row+1}}const A=[],j=[];function M(e){let t=A[e];return t===void 0&&(t=L(`$${e}`),A[e]=t),t}function N(e){let t=j[e];return t===void 0&&(t=R(`$${e}`),j[e]=t),t}function P(e,t,n,r){let[i,a]=I(e,n,r,p((t[r]??0)-(t[n]??0),r-n));return`${i}:${a}:${r-n}`}function F(e,t,n){let[r,i]=I(e,t,n,!1);return r^Math.imul(i,31)}function I(e,t,n,r){let i=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=M(e),c=N(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 L(e){let t=5381;for(let n=0;n<e.length;n+=1)t=Math.imul(t,33)^e.charCodeAt(n);return t}function R(e){let t=-2128831035;for(let n=0;n<e.length;n+=1)t=Math.imul(t^e.charCodeAt(n),16777619);return t}function z(e,t){return Math.imul(e,31)+t}function B(e){let t=[];for(let n of e.values()){let e=n.map(e=>({segments:[{startTokenIndex:e.startTokenIndex,endTokenIndex:e.endTokenIndex}],tokenCount:e.tokenCount,startTokenIndex:e.startTokenIndex,endTokenIndex:e.endTokenIndex,startIndex:e.startIndex,endIndex:e.endIndex,startLine:e.startLine,endLine:e.endLine}));e.sort((e,t)=>e.startTokenIndex-t.startTokenIndex||e.endTokenIndex-t.endTokenIndex),t.push(e)}return t}function V(e,t){if(t<=0||e.length<2)return e;e.sort(H);for(let n=!0;n;){n=!1;for(let r=0;r<e.length&&!n;r+=1)for(let i=r+1;i<e.length;i+=1){let a=e[r],o=e[i];if(!a||!o)continue;let s=U(a,o,t)??U(o,a,t);if(s){e[r]=s,e.splice(i,1),e.sort(H),n=!0;break}}}return e}function H(e,t){let n=e[0],r=t[0];return(n?.startTokenIndex??0)-(r?.startTokenIndex??0)||(n?.endTokenIndex??0)-(r?.endTokenIndex??0)}function U(e,t,n){if(e.length===t.length){for(let[r,i]of e.entries()){let a=t[r];if(!a)return;let o=a.startTokenIndex-i.endTokenIndex;if(o<0||o>n)return;let s=e[r+1];if(s&&a.endTokenIndex>s.startTokenIndex)return}return e.map((e,n)=>{let r=t[n];return r?{segments:[...e.segments,...r.segments],tokenCount:e.tokenCount+r.tokenCount,startTokenIndex:e.startTokenIndex,endTokenIndex:r.endTokenIndex,startIndex:e.startIndex,endIndex:r.endIndex,startLine:e.startLine,endLine:r.endLine}:e})}}function W(e,t,n){let r=0,i=0,a=[],o=new Set;for(let s of e){r+=(s.length-1)*(s[0]?.segments.length??1);for(let e of s){i=Math.max(i,e.tokenCount);for(let r of e.segments)for(let e=r.startTokenIndex;e<r.endTokenIndex;e+=1){let r=n[e];for(let e=r?.startRow??0;e<=(r?.endRow??-1);e+=1)t.has(e+1)&&o.add(e+1)}}a.push(s.map(({startLine:e,endLine:t})=>({startLine:e,endLine:t})).toSorted((e,t)=>e.startLine-t.startLine))}return a.sort((e,t)=>(e[0]?.startLine??0)-(t[0]?.startLine??0)),{duplicateBlockCount:r,duplicateBlockGroupCount:e.length,duplicateBlockGroups:a,duplicateLineCount:o.size,duplicationRatio:t.size===0?0:o.size/t.size,maxDuplicateBlockSize:i}}export{h as collectCrossFileDuplicateCandidates,f as defaultDuplicationOptions,m as measureDuplication};
2
2
  //# sourceMappingURL=duplication.js.map