code-gauge 10.2.0 → 10.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/crossFileDuplication.cjs +1 -1
- package/dist/crossFileDuplication.cjs.map +1 -1
- package/dist/crossFileDuplication.d.ts +2 -2
- package/dist/crossFileDuplication.js +1 -1
- package/dist/crossFileDuplication.js.map +1 -1
- package/dist/crossFileNearMiss.cjs +1 -1
- package/dist/crossFileNearMiss.cjs.map +1 -1
- package/dist/crossFileNearMiss.d.ts +7 -4
- package/dist/crossFileNearMiss.js +1 -1
- package/dist/crossFileNearMiss.js.map +1 -1
- package/dist/duplication.cjs +1 -1
- package/dist/duplication.cjs.map +1 -1
- package/dist/duplication.d.ts +7 -1
- package/dist/duplication.js +1 -1
- package/dist/duplication.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/native/code-gauge.wasm +0 -0
- package/native/src/duplication.rs +157 -105
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -137,7 +137,7 @@ that are blank or comment-only do not count, so that a copy has the same length
|
|
|
137
137
|
is commented, and neither do the lines the copies do not share: those between the matched parts of
|
|
138
138
|
an edited copy, and those a block that resembles another as a whole (a near-miss clone, at least
|
|
139
139
|
`minSimilarityPercent` similar) adds or rewrites. A line of such a block counts when its copies
|
|
140
|
-
match more than half of the line's tokens between them. Occurrences of at least the warning or the error
|
|
140
|
+
match more than half of the line's tokens between them, or when a part of the block matched exactly reaches it. Occurrences of at least the warning or the error
|
|
141
141
|
`minDuplicateLines` that overlap in a file are reported as one block covering all of them, at the
|
|
142
142
|
level its duplicated lines reach, each line counted once; copies of each other that merely share a
|
|
143
143
|
line (the branches of an `else if` chain) are reported as blocks of their own.
|
|
@@ -413,7 +413,10 @@ The `duplication` section tunes how clones are detected:
|
|
|
413
413
|
threshold below 34% widens this to whatever the threshold allows): the chain of n-grams unique to
|
|
414
414
|
both blocks (only those continuing a diagonal run) is split at gaps of more than 30 tokens, each
|
|
415
415
|
segment must pass the same threshold on its own, and the verified cores, not the whole blocks,
|
|
416
|
-
are reported.
|
|
416
|
+
are reported. A block that holds a clone already reported is compared too, so an edited copy is
|
|
417
|
+
listed with the identical copies beside it and counts what it shares with them, and two blocks
|
|
418
|
+
alike as wholes are listed as such even when a part of them matches exactly.
|
|
419
|
+
`100` disables near-miss detection. Applies to within-file detection and to cross-file matching alike; across files,
|
|
417
420
|
n-grams shared by more than 1000 blocks (syntax boilerplate) are left out of the filtration index
|
|
418
421
|
so boilerplate cannot make candidate counting quadratic in the block count.
|
|
419
422
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const e=require("./duplication.cjs"),t=require("./crossFileNearMiss.cjs"),n=require("./duplicateSelection.cjs");function r(t,r){let{minTokens:a,maxGapTokens:
|
|
1
|
+
"use strict";const e=require("./duplication.cjs"),t=require("./crossFileNearMiss.cjs"),n=require("./duplicateSelection.cjs");function r(t,r){let{minTokens:a,maxGapTokens:o,minSimilarityPercent:s}=e.resolveDuplicationOptions(r),l=t.flatMap(({file:e,candidates:t},n)=>t.map(t=>({...t,regionBucket:n,file:e})));for(let e of u(t,a))l.push(e);let d=n.selectMaximalGroups(l,f,(e,t)=>e.regionBucket-t.regionBucket||e.startIndex-t.startIndex),g=p(t,o),_=m([...d.values()],g,o);return h(i(_,c(t,_,g,a,s),a),t,g)}function i(e,t,n){let r=/* @__PURE__ */ new Map;for(let[t,n]of e.entries())for(let{fileIndex:e}of n){let n=r.get(e)??/* @__PURE__ */ new Set;n.add(t),r.set(e,n)}let i=/* @__PURE__ */ new Set,s=/* @__PURE__ */ new Set,c=t.map(t=>{let c=e=>{let r=t.filter(t=>t.segments.some(t=>e.segments.some(e=>a(e,t)))),[i]=r;if(r.length===1&&i)return Math.max(0,i.startTokenIndex-e.startTokenIndex)+Math.max(0,e.endTokenIndex-i.endTokenIndex)<n?i:void 0},l=/* @__PURE__ */ new Map,u=/* @__PURE__ */ new Set;for(let e of t)for(let t of e.spanCountedElsewhere?r.get(e.fileIndex)??[]:[])u.add(t);for(let t of[...u].toSorted((e,t)=>e-t)){let n=e[t]??[],r=n.map(c);if(!(i.has(t)||r.includes(void 0)||new Set(r).size!==r.length)){i.add(t);for(let[e,t]of r.entries()){let r=n[e];if(t&&r){let e=l.get(t)??[];e.push(r),l.set(t,e)}}}}return t.map(e=>{let t=l.get(e);if(!t)return e;let n=o([e,...t]);return s.add(n),n})});for(let t of s)for(let n of r.get(t.fileIndex)??[])if(!i.has(n)&&e[n]?.some(e=>!e.spanCountedElsewhere&&t.segments.every(t=>e.segments.some(e=>e.startTokenIndex<=t.startTokenIndex&&t.endTokenIndex<=e.endTokenIndex)))){t.spanCountedElsewhere=!0;break}return[...e.filter((e,t)=>!i.has(t)),...c]}function a(e,t){return e.startTokenIndex<t.endTokenIndex&&t.startTokenIndex<e.endTokenIndex}function o(e){let t=s(e.flatMap(e=>e.segments));return{...e[0],spanCountedElsewhere:void 0,nestedInLargerGroup:void 0,segments:t,matchedRuns:s(e.flatMap(e=>e.matchedRuns??e.segments)),exactSegments:s(e.flatMap(e=>e.matchedRuns?e.exactSegments??[]:e.segments)),tokenCount:t.reduce((e,t)=>e+t.endTokenIndex-t.startTokenIndex,0),startTokenIndex:Math.min(...e.map(e=>e.startTokenIndex)),endTokenIndex:Math.max(...e.map(e=>e.endTokenIndex)),startIndex:Math.min(...e.map(e=>e.startIndex)),endIndex:Math.max(...e.map(e=>e.endIndex)),startLine:Math.min(...e.map(e=>e.startLine)),endLine:Math.max(...e.map(e=>e.endLine))}}function s(e){let t=[];for(let n of e.toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex)){let e=t.at(-1);e&&n.startTokenIndex<e.endTokenIndex?e.endTokenIndex=Math.max(e.endTokenIndex,n.endTokenIndex):t.push({...n})}return t}function c(e,n,r,i,a){let o=e.map(()=>[]);for(let{fileIndex:e,segments:t}of n.flat()){let n=r[e]??0;for(let r of t)o[e]?.push(l(r,-n))}return t.collectCrossFileNearMissGroups(e,o,i,a).map(t=>t.map(t=>{let n=r[t.fileIndex]??0;return{...t,file:e[t.fileIndex]?.file??``,segments:t.segments.map(e=>l(e,n)),matchedRuns:t.matchedRuns?.map(e=>l(e,n)),startTokenIndex:t.startTokenIndex+n,endTokenIndex:t.endTokenIndex+n}}))}function l({startTokenIndex:e,endTokenIndex:t},n){return{startTokenIndex:e+n,endTokenIndex:t+n}}function u(t,n){let r=[],i=[];for(let[n,{tokens:a,containerStatements:o}]of t.entries())a&&o&&(r.push(n),i.push({tokens:a,literalCountPrefix:e.buildLiteralCountPrefix(a),containers:o}));return i.length<2?[]:e.collectSequenceWindowCandidates(i,n,!0).flatMap(({candidate:e,contextIndex:n})=>{let i=r[n],a=i===void 0?void 0:t[i];return i===void 0||a===void 0?[]:[{...e,regionBucket:i,file:a.file}]})}function d(e){return new Set(e.map(e=>e.file)).size>=2}function f(e){return e.length>=2&&new Set(e.map(e=>e.regionBucket)).size>=2}function p(e,t){let n=[],r=0;for(let{tokens:i,candidates:a}of e){n.push(r);let e=i?.length??0;if(!i)for(let t of a)e=Math.max(e,t.endTokenIndex);r+=e+t+1}return n}function m(t,n,r){let i=t.map(e=>e.map(e=>{let t=e.startTokenIndex+(n[e.regionBucket]??0),r=e.endTokenIndex+(n[e.regionBucket]??0);return{file:e.file,fileIndex:e.regionBucket,spanCountedElsewhere:e.nestedInLargerGroup,nestedInLargerGroup:e.nestedInLargerGroup,segments:[{startTokenIndex:t,endTokenIndex:r}],tokenCount:e.tokenCount,startTokenIndex:t,endTokenIndex:r,startIndex:e.startIndex,endIndex:e.endIndex,startLine:e.startLine,endLine:e.endLine}}).toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex));return e.mergeAdjacentGroups(i,r,d)}function h(t,n,r){let i=[],a=/* @__PURE__ */ new Map,o=new Map(n.map((e,t)=>[e.file,{tokens:e.tokens,codeLineNumbers:e.codeLineNumbers,offset:r[t]??0}])),s=/* @__PURE__ */ new Map,c=0;for(let n of t){c+=e.countRedundantFragments(n);let t=n.map(e=>{let{file:t,startLine:n,endLine:r,tokenCount:i}=e,a=g(e,o,s);return{file:t,startLine:n,endLine:r,...a&&{lineNumbers:a},tokenCount:i}}).toSorted((e,t)=>e.file.localeCompare(t.file)||e.startLine-t.startLine),r=[...new Set(t.map(({file:e})=>e))];for(let e of r)a.set(e,(a.get(e)??0)+1);i.push({files:r,occurrences:t,tokenCount:Math.min(...n.map(({tokenCount:e})=>e))})}return i.sort((e,t)=>t.tokenCount-e.tokenCount||(e.occurrences[0]?.file??``).localeCompare(t.occurrences[0]?.file??``)||(e.occurrences[0]?.startLine??0)-(t.occurrences[0]?.startLine??0)),{duplicateBlockCount:c,duplicateBlockGroupCountByFile:Object.fromEntries(a),duplicateLineNumbersByFile:Object.fromEntries([...s].map(([e,t])=>[e,[...t].toSorted((e,t)=>e-t)])),groups:i}}function g(t,n,r){let i=n.get(t.file);if(!i?.tokens)return;let a=r.get(t.file);a||(a=/* @__PURE__ */ new Set,r.set(t.file,a));let o=e.collectMatchedLines(t.segments.map(e=>l(e,-i.offset)),t.matchedRuns?.map(e=>l(e,-i.offset)),i.tokens,i.codeLineNumbers,t.exactSegments?.map(e=>l(e,-i.offset)));for(let e of o)a.add(e);return[...o].toSorted((e,t)=>e-t)}exports.measureCrossFileDuplication=r;
|
|
2
2
|
//# sourceMappingURL=crossFileDuplication.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crossFileDuplication.cjs","names":["resolveDuplicationOptions","selectMaximalGroups","collectCrossFileNearMissGroups","buildLiteralCountPrefix","collectSequenceWindowCandidates","mergeAdjacentGroups","countRedundantFragments","collectMatchedLines"],"sources":["../src/crossFileDuplication.ts"],"sourcesContent":["import { collectCrossFileNearMissGroups } from './crossFileNearMiss.js';\nimport { selectMaximalGroups, type SelectableRegion } from './duplicateSelection.js';\nimport {\n buildLiteralCountPrefix,\n collectMatchedLines,\n collectSequenceWindowCandidates,\n countRedundantFragments,\n mergeAdjacentGroups,\n resolveDuplicationOptions,\n type CountedOccurrence,\n type CrossFileDuplicateCandidate,\n type CrossFileDuplicationFileData,\n type SequenceWindowContext,\n type TokenSegment,\n} from './duplication.js';\nimport type { DuplicationOptions } from './types.js';\n\nexport interface CrossFileDuplicationSourceFile extends Partial<CrossFileDuplicationFileData> {\n file: string;\n candidates: CrossFileDuplicateCandidate[];\n}\n\nexport interface CrossFileDuplicateOccurrence {\n endLine: number;\n file: string;\n /**\n * The code lines carrying the occurrence's matched tokens (see\n * DuplicateBlockOccurrence.lineNumbers). Absent for a file that supplied only candidates (no\n * `tokens`), whose matched lines are unknowable.\n */\n lineNumbers?: number[];\n startLine: number;\n /**\n * The tokens of its matched runs, or of a near-miss block or its matched cores with the edited\n * tokens among them; the group's `tokenCount` is the smallest of its occurrences'.\n */\n tokenCount: number;\n}\n\nexport interface CrossFileDuplicateBlockGroup {\n files: string[];\n occurrences: CrossFileDuplicateOccurrence[];\n /**\n * Token count of the smallest occurrence. For exact and gapped groups it is the matched token\n * count every occurrence shares (gaps are not counted); for near-miss (Type-3) groups it is the\n * token count of the smallest whole block or set of matched cores, edited tokens included.\n */\n tokenCount: number;\n}\n\nexport interface CrossFileDuplicationMetrics {\n /** Number of redundant copies across all groups, counted per matched fragment like within-file. */\n duplicateBlockCount: number;\n /** Groups the file participates in, keyed by the file name passed in. */\n duplicateBlockGroupCountByFile: Record<string, number>;\n /**\n * Per file, the 1-based code lines covered by the tokens of its cross-file occurrences, sorted\n * ascending: the matched tokens of exact and gapped occurrences, and the lines of a near-miss\n * block or its matched cores more than half of whose tokens its partners match (like within-file\n * near-miss coverage). The unmatched gap of a merged\n * clone and comment/blank lines inside an occurrence's bounding range are excluded (blank rows\n * inside multi-row tokens only when the file supplied codeLineNumbers). A file that supplied\n * only candidates (no `tokens`) has no entry — without its token stream the covered lines are\n * unknowable, and an approximate bounding range would break this field's exactness.\n */\n duplicateLineNumbersByFile: Record<string, number[]>;\n groups: CrossFileDuplicateBlockGroup[];\n}\n\ninterface SelectableCandidate extends CrossFileDuplicateCandidate, SelectableRegion {\n regionBucket: number;\n file: string;\n}\n\n/** A cross-file occurrence: a within-file occurrence in the project-wide token index space. */\ninterface CrossFileOccurrence extends CountedOccurrence {\n file: string;\n fileIndex: number;\n}\n\n/**\n * Detects code regions duplicated across files. Per-file candidates (whole block subtrees and full\n * container runs, fingerprinted with the same normalization as within-file duplication) are joined\n * by a project-level window index over per-statement fingerprint sequences (CPD-style), so a\n * copy-pasted partial statement run embedded in different surrounding code is matched even though\n * no single file can know it repeats elsewhere. Candidates are grouped by fingerprint, and only\n * maximal, non-overlapping regions whose group spans at least two files are counted. Groups that\n * shrink to a single file during selection are shed — a within-file repeat is already reported by\n * that file's own duplication metrics. A copy nested inside a larger group's region (two files share\n * a whole function, a third file only a block of it) is reported with its group, so the third\n * file's copy still shows what it duplicates. Groups separated by a small token gap within each file then\n * merge into gapped (Type-3) clone groups under `maxGapTokens`, exactly like within-file merging.\n * Finally, blocks of files that supplied `nearMissBlocks` are compared across files for near-miss\n * (Type-3) clones under `minSimilarityPercent` (see crossFileNearMiss.ts).\n */\nexport function measureCrossFileDuplication(\n files: CrossFileDuplicationSourceFile[],\n options?: DuplicationOptions\n): CrossFileDuplicationMetrics {\n const { minTokens, maxGapTokens, minSimilarityPercent } = resolveDuplicationOptions(options);\n const candidates: SelectableCandidate[] = files.flatMap(({ file, candidates }, fileIndex) =>\n candidates.map((candidate) => ({ ...candidate, regionBucket: fileIndex, file }))\n );\n // Pushed one by one: spreading the project-scale window-candidate array as call arguments\n // overflows V8's argument limit (~124k) and crashes on Node, though Bun/JSC tolerates it.\n for (const candidate of collectWindowCandidates(files, minTokens)) {\n candidates.push(candidate);\n }\n const counted = selectMaximalGroups(\n candidates,\n spansMultipleFiles,\n // File index and position break coverage ties deterministically.\n (left, right) => left.regionBucket - right.regionBucket || left.startIndex - right.startIndex\n );\n const tokenOffsets = computeTokenOffsets(files, maxGapTokens);\n const groups = mergeGapAdjacentGroups([...counted.values()], tokenOffsets, maxGapTokens);\n for (const group of collectNearMissGroups(files, groups, tokenOffsets, minTokens, minSimilarityPercent)) {\n groups.push(group);\n }\n return summarize(groups, files, tokenOffsets);\n}\n\n/** Near-miss groups among the blocks the exact groups leave unreported, in the project token space. */\nfunction collectNearMissGroups(\n files: CrossFileDuplicationSourceFile[],\n exactGroups: CrossFileOccurrence[][],\n tokenOffsets: number[],\n minTokens: number,\n minSimilarityPercent: number\n): CrossFileOccurrence[][] {\n const reportedSpansByFile: { startTokenIndex: number; endTokenIndex: number }[][] = files.map(() => []);\n for (const { fileIndex, startTokenIndex, endTokenIndex } of exactGroups.flat()) {\n const offset = tokenOffsets[fileIndex] ?? 0;\n reportedSpansByFile[fileIndex]?.push({\n startTokenIndex: startTokenIndex - offset,\n endTokenIndex: endTokenIndex - offset,\n });\n }\n return collectCrossFileNearMissGroups(files, reportedSpansByFile, minTokens, minSimilarityPercent).map((group) =>\n group.map((occurrence) => {\n const offset = tokenOffsets[occurrence.fileIndex] ?? 0;\n return {\n ...occurrence,\n file: files[occurrence.fileIndex]?.file ?? '',\n segments: occurrence.segments.map((segment) => shift(segment, offset)),\n matchedRuns: occurrence.matchedRuns?.map((run) => shift(run, offset)),\n startTokenIndex: occurrence.startTokenIndex + offset,\n endTokenIndex: occurrence.endTokenIndex + offset,\n };\n })\n );\n}\n\nfunction shift({ startTokenIndex, endTokenIndex }: TokenSegment, offset: number): TokenSegment {\n return { startTokenIndex: startTokenIndex + offset, endTokenIndex: endTokenIndex + offset };\n}\n\n/** Repeated sub-windows of sibling statements matched across the whole project's files. */\nfunction collectWindowCandidates(files: CrossFileDuplicationSourceFile[], minTokens: number): SelectableCandidate[] {\n const fileIndexByContext: number[] = [];\n const contexts: SequenceWindowContext[] = [];\n for (const [fileIndex, { tokens, containerStatements }] of files.entries()) {\n if (tokens && containerStatements) {\n fileIndexByContext.push(fileIndex);\n contexts.push({ tokens, literalCountPrefix: buildLiteralCountPrefix(tokens), containers: containerStatements });\n }\n }\n if (contexts.length < 2) {\n return [];\n }\n return collectSequenceWindowCandidates(contexts, minTokens, true).flatMap(({ candidate, contextIndex }) => {\n const fileIndex = fileIndexByContext[contextIndex];\n const file = fileIndex === undefined ? undefined : files[fileIndex];\n return fileIndex === undefined || file === undefined\n ? []\n : [{ ...candidate, regionBucket: fileIndex, file: file.file }];\n });\n}\n\n/** A merged group is reported only while it still covers more than one file. */\nfunction spansMultipleFilesAfterMerge(group: CrossFileOccurrence[]): boolean {\n return new Set(group.map((occurrence) => occurrence.file)).size >= 2;\n}\n\nfunction spansMultipleFiles(group: SelectableCandidate[]): boolean {\n return group.length >= 2 && new Set(group.map((candidate) => candidate.regionBucket)).size >= 2;\n}\n\n/**\n * Per-file token offsets that map every file into one project-wide token index space: each file's\n * tokens are offset by more than `maxGapTokens` past the previous file's, so occurrences in\n * different files are never gap-adjacent and merged pairs always stay within one file.\n */\nfunction computeTokenOffsets(files: CrossFileDuplicationSourceFile[], maxGapTokens: number): number[] {\n const tokenOffsets: number[] = [];\n let offset = 0;\n for (const { tokens, candidates } of files) {\n tokenOffsets.push(offset);\n // Accumulated in a loop: spreading a project-scale candidate array as call arguments would\n // overflow V8's argument limit (~124k) and crash on Node.\n let tokenCount = tokens?.length ?? 0;\n if (!tokens) {\n for (const candidate of candidates) {\n tokenCount = Math.max(tokenCount, candidate.endTokenIndex);\n }\n }\n offset += tokenCount + maxGapTokens + 1;\n }\n return tokenOffsets;\n}\n\n/** Reuses the within-file gapped (Type-3) merging in the project-wide token index space. */\nfunction mergeGapAdjacentGroups(\n groups: SelectableCandidate[][],\n tokenOffsets: number[],\n maxGapTokens: number\n): CrossFileOccurrence[][] {\n const occurrenceGroups = groups.map((group) =>\n group\n .map((candidate): CrossFileOccurrence => {\n const start = candidate.startTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n const end = candidate.endTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n return {\n file: candidate.file,\n fileIndex: candidate.regionBucket,\n spanCountedElsewhere: candidate.nestedInLargerGroup,\n nestedInLargerGroup: candidate.nestedInLargerGroup,\n segments: [{ startTokenIndex: start, endTokenIndex: end }],\n tokenCount: candidate.tokenCount,\n startTokenIndex: start,\n endTokenIndex: end,\n startIndex: candidate.startIndex,\n endIndex: candidate.endIndex,\n startLine: candidate.startLine,\n endLine: candidate.endLine,\n };\n })\n .toSorted((left, right) => left.startTokenIndex - right.startTokenIndex)\n );\n return mergeAdjacentGroups(occurrenceGroups, maxGapTokens, spansMultipleFilesAfterMerge);\n}\n\nfunction summarize(\n groups: CrossFileOccurrence[][],\n files: CrossFileDuplicationSourceFile[],\n tokenOffsets: number[]\n): CrossFileDuplicationMetrics {\n const reported: CrossFileDuplicateBlockGroup[] = [];\n // Accumulated in Maps: file names are arbitrary strings, and a plain object would read\n // inherited properties for names like \"constructor\".\n const groupCountByFile = new Map<string, number>();\n const fileDataByName = new Map(\n files.map((file, index) => [\n file.file,\n { tokens: file.tokens, codeLineNumbers: file.codeLineNumbers, offset: tokenOffsets[index] ?? 0 },\n ])\n );\n const lineNumbersByFile = new Map<string, Set<number>>();\n let duplicateBlockCount = 0;\n for (const group of groups) {\n // Mirrors within-file counting: each redundant occurrence contributes one count per matched\n // fragment, gapped merging consolidates the grouping without halving the count, and spans a\n // partial merge shares between a retained group and the merged group count once.\n duplicateBlockCount += countRedundantFragments(group);\n const occurrences = group\n .map((occurrence) => {\n const { file, startLine, endLine, tokenCount } = occurrence;\n const lineNumbers = collectOccurrenceLines(occurrence, fileDataByName, lineNumbersByFile);\n return { file, startLine, endLine, ...(lineNumbers && { lineNumbers }), tokenCount };\n })\n .toSorted((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n const files = [...new Set(occurrences.map(({ file }) => file))];\n for (const file of files) {\n groupCountByFile.set(file, (groupCountByFile.get(file) ?? 0) + 1);\n }\n reported.push({ files, occurrences, tokenCount: Math.min(...group.map(({ tokenCount }) => tokenCount)) });\n }\n reported.sort(\n (left, right) =>\n right.tokenCount - left.tokenCount ||\n (left.occurrences[0]?.file ?? '').localeCompare(right.occurrences[0]?.file ?? '') ||\n (left.occurrences[0]?.startLine ?? 0) - (right.occurrences[0]?.startLine ?? 0)\n );\n return {\n duplicateBlockCount,\n duplicateBlockGroupCountByFile: Object.fromEntries(groupCountByFile),\n duplicateLineNumbersByFile: Object.fromEntries(\n [...lineNumbersByFile].map(([file, lines]) => [file, [...lines].toSorted((left, right) => left - right)])\n ),\n groups: reported,\n };\n}\n\n/**\n * Adds the code lines an occurrence's matched tokens cover (see collectMatchedLines) to its file's\n * line set, mapping the project-wide token segments back into the file's own token stream, and\n * returns them in order.\n * A file that supplied only candidates (no token stream) is skipped rather than approximated from\n * the bounding line range, which would include gap and comment/blank lines and break the field's\n * exactness contract.\n */\nfunction collectOccurrenceLines(\n occurrence: CrossFileOccurrence,\n fileDataByName: Map<\n string,\n { tokens?: CrossFileDuplicationSourceFile['tokens']; codeLineNumbers?: Set<number>; offset: number }\n >,\n lineNumbersByFile: Map<string, Set<number>>\n): number[] | undefined {\n const fileData = fileDataByName.get(occurrence.file);\n if (!fileData?.tokens) {\n return undefined;\n }\n let fileLines = lineNumbersByFile.get(occurrence.file);\n if (!fileLines) {\n fileLines = new Set();\n lineNumbersByFile.set(occurrence.file, fileLines);\n }\n const lines = collectMatchedLines(\n occurrence.segments.map((segment) => shift(segment, -fileData.offset)),\n occurrence.matchedRuns?.map((run) => shift(run, -fileData.offset)),\n fileData.tokens,\n fileData.codeLineNumbers\n );\n for (const line of lines) {\n fileLines.add(line);\n }\n return [...lines].toSorted((left, right) => left - right);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"crossFileDuplication.cjs","names":["resolveDuplicationOptions","selectMaximalGroups","collectCrossFileNearMissGroups","buildLiteralCountPrefix","collectSequenceWindowCandidates","mergeAdjacentGroups","countRedundantFragments","collectMatchedLines"],"sources":["../src/crossFileDuplication.ts"],"sourcesContent":["import { collectCrossFileNearMissGroups } from './crossFileNearMiss.js';\nimport { selectMaximalGroups, type SelectableRegion } from './duplicateSelection.js';\nimport {\n buildLiteralCountPrefix,\n collectMatchedLines,\n collectSequenceWindowCandidates,\n countRedundantFragments,\n mergeAdjacentGroups,\n resolveDuplicationOptions,\n type CountedOccurrence,\n type CrossFileDuplicateCandidate,\n type CrossFileDuplicationFileData,\n type SequenceWindowContext,\n type TokenSegment,\n} from './duplication.js';\nimport type { DuplicationOptions } from './types.js';\n\nexport interface CrossFileDuplicationSourceFile extends Partial<CrossFileDuplicationFileData> {\n file: string;\n candidates: CrossFileDuplicateCandidate[];\n}\n\nexport interface CrossFileDuplicateOccurrence {\n endLine: number;\n file: string;\n /**\n * The code lines carrying the occurrence's matched tokens (see\n * DuplicateBlockOccurrence.lineNumbers). Absent for a file that supplied only candidates (no\n * `tokens`), whose matched lines are unknowable.\n */\n lineNumbers?: number[];\n startLine: number;\n /**\n * The tokens of its matched runs, or of a near-miss block or its matched cores with the edited\n * tokens among them; the group's `tokenCount` is the smallest of its occurrences'.\n */\n tokenCount: number;\n}\n\nexport interface CrossFileDuplicateBlockGroup {\n files: string[];\n occurrences: CrossFileDuplicateOccurrence[];\n /**\n * Token count of the smallest occurrence. For exact and gapped groups it is the matched token\n * count every occurrence shares (gaps are not counted); for near-miss (Type-3) groups it is the\n * token count of the smallest whole block or set of matched cores, edited tokens included.\n */\n tokenCount: number;\n}\n\nexport interface CrossFileDuplicationMetrics {\n /** Number of redundant copies across all groups, counted per matched fragment like within-file. */\n duplicateBlockCount: number;\n /** Groups the file participates in, keyed by the file name passed in. */\n duplicateBlockGroupCountByFile: Record<string, number>;\n /**\n * Per file, the 1-based code lines covered by the tokens of its cross-file occurrences, sorted\n * ascending: the matched tokens of exact and gapped occurrences, and the lines of a near-miss\n * block or its matched cores more than half of whose tokens its partners match or which a part of it\n * matched exactly reaches (like within-file near-miss coverage). The unmatched gap of a merged\n * clone and comment/blank lines inside an occurrence's bounding range are excluded (blank rows\n * inside multi-row tokens only when the file supplied codeLineNumbers). A file that supplied\n * only candidates (no `tokens`) has no entry — without its token stream the covered lines are\n * unknowable, and an approximate bounding range would break this field's exactness.\n */\n duplicateLineNumbersByFile: Record<string, number[]>;\n groups: CrossFileDuplicateBlockGroup[];\n}\n\ninterface SelectableCandidate extends CrossFileDuplicateCandidate, SelectableRegion {\n regionBucket: number;\n file: string;\n}\n\n/** A cross-file occurrence: a within-file occurrence in the project-wide token index space. */\ninterface CrossFileOccurrence extends CountedOccurrence {\n file: string;\n fileIndex: number;\n}\n\n/**\n * Detects code regions duplicated across files. Per-file candidates (whole block subtrees and full\n * container runs, fingerprinted with the same normalization as within-file duplication) are joined\n * by a project-level window index over per-statement fingerprint sequences (CPD-style), so a\n * copy-pasted partial statement run embedded in different surrounding code is matched even though\n * no single file can know it repeats elsewhere. Candidates are grouped by fingerprint, and only\n * maximal, non-overlapping regions whose group spans at least two files are counted. Groups that\n * shrink to a single file during selection are shed — a within-file repeat is already reported by\n * that file's own duplication metrics. A copy nested inside a larger group's region (two files share\n * a whole function, a third file only a block of it) is reported with its group, so the third\n * file's copy still shows what it duplicates. Groups separated by a small token gap within each file then\n * merge into gapped (Type-3) clone groups under `maxGapTokens`, exactly like within-file merging.\n * Finally, blocks of files that supplied `nearMissBlocks` are compared across files for near-miss\n * (Type-3) clones under `minSimilarityPercent` (see crossFileNearMiss.ts).\n */\nexport function measureCrossFileDuplication(\n files: CrossFileDuplicationSourceFile[],\n options?: DuplicationOptions\n): CrossFileDuplicationMetrics {\n const { minTokens, maxGapTokens, minSimilarityPercent } = resolveDuplicationOptions(options);\n const candidates: SelectableCandidate[] = files.flatMap(({ file, candidates }, fileIndex) =>\n candidates.map((candidate) => ({ ...candidate, regionBucket: fileIndex, file }))\n );\n // Pushed one by one: spreading the project-scale window-candidate array as call arguments\n // overflows V8's argument limit (~124k) and crashes on Node, though Bun/JSC tolerates it.\n for (const candidate of collectWindowCandidates(files, minTokens)) {\n candidates.push(candidate);\n }\n const counted = selectMaximalGroups(\n candidates,\n spansMultipleFiles,\n // File index and position break coverage ties deterministically.\n (left, right) => left.regionBucket - right.regionBucket || left.startIndex - right.startIndex\n );\n const tokenOffsets = computeTokenOffsets(files, maxGapTokens);\n const exactGroups = mergeGapAdjacentGroups([...counted.values()], tokenOffsets, maxGapTokens);\n const nearMissGroups = collectNearMissGroups(files, exactGroups, tokenOffsets, minTokens, minSimilarityPercent);\n return summarize(takeOverExactGroups(exactGroups, nearMissGroups, minTokens), files, tokenOffsets);\n}\n\n/**\n * Lets a near-miss group take over the exact groups standing for the same copies, as within a\n * file (collect_near_miss_groups in native/src/duplication.rs): every occurrence of such a group\n * overlaps exactly one copy of the near-miss group (one overlapping none reports content the copies\n * do not share, and one reaching into two would make one copy take over lines of the next), exceeds\n * its span by less than `minTokens` (a copy is listed at the size it matched, not at that of a\n * clone larger by a reportable part, which may run into code no copy matched), and no two overlap\n * the same copy (those repeat within the copy, not between the copies).\n * Each copy then becomes one occurrence with the exact fragments it holds, so the copies are\n * listed once.\n */\nfunction takeOverExactGroups(\n exactGroups: CrossFileOccurrence[][],\n nearMissGroups: CrossFileOccurrence[][],\n minTokens: number\n): CrossFileOccurrence[][] {\n const exactGroupIndexesByFile = new Map<number, Set<number>>();\n for (const [groupIndex, group] of exactGroups.entries()) {\n for (const { fileIndex } of group) {\n const indexes = exactGroupIndexesByFile.get(fileIndex) ?? new Set();\n indexes.add(groupIndex);\n exactGroupIndexesByFile.set(fileIndex, indexes);\n }\n }\n const taken = new Set<number>();\n const coalesced = new Set<CrossFileOccurrence>();\n const merged = nearMissGroups.map((copies) => {\n const copyOf = (occurrence: CrossFileOccurrence): CrossFileOccurrence | undefined => {\n const overlapped = copies.filter((copy) =>\n copy.segments.some((segment) => occurrence.segments.some((matched) => overlap(matched, segment)))\n );\n const [copy] = overlapped;\n if (overlapped.length !== 1 || !copy) {\n return undefined;\n }\n const excess =\n Math.max(0, copy.startTokenIndex - occurrence.startTokenIndex) +\n Math.max(0, occurrence.endTokenIndex - copy.endTokenIndex);\n return excess < minTokens ? copy : undefined;\n };\n const fragmentsByCopy = new Map<CrossFileOccurrence, CrossFileOccurrence[]>();\n const candidateIndexes = new Set<number>();\n for (const copy of copies) {\n for (const groupIndex of copy.spanCountedElsewhere ? (exactGroupIndexesByFile.get(copy.fileIndex) ?? []) : []) {\n candidateIndexes.add(groupIndex);\n }\n }\n for (const groupIndex of [...candidateIndexes].toSorted((left, right) => left - right)) {\n const group = exactGroups[groupIndex] ?? [];\n const owners = group.map(copyOf);\n if (taken.has(groupIndex) || owners.includes(undefined) || new Set(owners).size !== owners.length) {\n continue;\n }\n taken.add(groupIndex);\n for (const [index, owner] of owners.entries()) {\n const fragment = group[index];\n if (owner && fragment) {\n const fragments = fragmentsByCopy.get(owner) ?? [];\n fragments.push(fragment);\n fragmentsByCopy.set(owner, fragments);\n }\n }\n }\n return copies.map((copy): CrossFileOccurrence => {\n const fragments = fragmentsByCopy.get(copy);\n if (!fragments) {\n return copy;\n }\n const whole = coalesceOccurrences([copy, ...fragments]);\n coalesced.add(whole);\n return whole;\n });\n });\n // A copy that took its fragments over counts them itself, unless a group left in place still\n // counts all of it: a larger clone whose matched segments enclose those of the copy, which its\n // fragments were nested in. A smaller run left in place inside the copy counts its own repeats,\n // not the copy.\n for (const copy of coalesced) {\n for (const groupIndex of exactGroupIndexesByFile.get(copy.fileIndex) ?? []) {\n const enclosed =\n !taken.has(groupIndex) &&\n exactGroups[groupIndex]?.some(\n (occurrence) =>\n !occurrence.spanCountedElsewhere &&\n copy.segments.every((segment) =>\n occurrence.segments.some(\n (matched) =>\n matched.startTokenIndex <= segment.startTokenIndex && segment.endTokenIndex <= matched.endTokenIndex\n )\n )\n );\n if (enclosed) {\n copy.spanCountedElsewhere = true;\n break;\n }\n }\n }\n return [...exactGroups.filter((_, groupIndex) => !taken.has(groupIndex)), ...merged];\n}\n\nfunction overlap(left: TokenSegment, right: TokenSegment): boolean {\n return left.startTokenIndex < right.endTokenIndex && right.startTokenIndex < left.endTokenIndex;\n}\n\n/** The parts of one copy as one occurrence: an exact part is matched throughout, a near-miss part where its partners match it. */\nfunction coalesceOccurrences(parts: [CrossFileOccurrence, ...CrossFileOccurrence[]]): CrossFileOccurrence {\n const segments = mergeSegments(parts.flatMap((part) => part.segments));\n return {\n ...parts[0],\n spanCountedElsewhere: undefined,\n nestedInLargerGroup: undefined,\n segments,\n matchedRuns: mergeSegments(parts.flatMap((part) => part.matchedRuns ?? part.segments)),\n exactSegments: mergeSegments(\n parts.flatMap((part) => (part.matchedRuns ? (part.exactSegments ?? []) : part.segments))\n ),\n tokenCount: segments.reduce((sum, segment) => sum + segment.endTokenIndex - segment.startTokenIndex, 0),\n startTokenIndex: Math.min(...parts.map((part) => part.startTokenIndex)),\n endTokenIndex: Math.max(...parts.map((part) => part.endTokenIndex)),\n startIndex: Math.min(...parts.map((part) => part.startIndex)),\n endIndex: Math.max(...parts.map((part) => part.endIndex)),\n startLine: Math.min(...parts.map((part) => part.startLine)),\n endLine: Math.max(...parts.map((part) => part.endLine)),\n };\n}\n\n/** The unions of overlapping segments, in position order. */\nfunction mergeSegments(segments: TokenSegment[]): TokenSegment[] {\n const merged: TokenSegment[] = [];\n for (const segment of segments.toSorted((left, right) => left.startTokenIndex - right.startTokenIndex)) {\n const last = merged.at(-1);\n if (last && segment.startTokenIndex < last.endTokenIndex) {\n last.endTokenIndex = Math.max(last.endTokenIndex, segment.endTokenIndex);\n } else {\n merged.push({ ...segment });\n }\n }\n return merged;\n}\n\n/** Near-miss groups among the blocks the exact groups leave unreported, in the project token space. */\nfunction collectNearMissGroups(\n files: CrossFileDuplicationSourceFile[],\n exactGroups: CrossFileOccurrence[][],\n tokenOffsets: number[],\n minTokens: number,\n minSimilarityPercent: number\n): CrossFileOccurrence[][] {\n const reportedSpansByFile: { startTokenIndex: number; endTokenIndex: number }[][] = files.map(() => []);\n // Segment by segment: the gap of a gapped clone is not reported content.\n for (const { fileIndex, segments } of exactGroups.flat()) {\n const offset = tokenOffsets[fileIndex] ?? 0;\n for (const segment of segments) {\n reportedSpansByFile[fileIndex]?.push(shift(segment, -offset));\n }\n }\n return collectCrossFileNearMissGroups(files, reportedSpansByFile, minTokens, minSimilarityPercent).map((group) =>\n group.map((occurrence) => {\n const offset = tokenOffsets[occurrence.fileIndex] ?? 0;\n return {\n ...occurrence,\n file: files[occurrence.fileIndex]?.file ?? '',\n segments: occurrence.segments.map((segment) => shift(segment, offset)),\n matchedRuns: occurrence.matchedRuns?.map((run) => shift(run, offset)),\n startTokenIndex: occurrence.startTokenIndex + offset,\n endTokenIndex: occurrence.endTokenIndex + offset,\n };\n })\n );\n}\n\nfunction shift({ startTokenIndex, endTokenIndex }: TokenSegment, offset: number): TokenSegment {\n return { startTokenIndex: startTokenIndex + offset, endTokenIndex: endTokenIndex + offset };\n}\n\n/** Repeated sub-windows of sibling statements matched across the whole project's files. */\nfunction collectWindowCandidates(files: CrossFileDuplicationSourceFile[], minTokens: number): SelectableCandidate[] {\n const fileIndexByContext: number[] = [];\n const contexts: SequenceWindowContext[] = [];\n for (const [fileIndex, { tokens, containerStatements }] of files.entries()) {\n if (tokens && containerStatements) {\n fileIndexByContext.push(fileIndex);\n contexts.push({ tokens, literalCountPrefix: buildLiteralCountPrefix(tokens), containers: containerStatements });\n }\n }\n if (contexts.length < 2) {\n return [];\n }\n return collectSequenceWindowCandidates(contexts, minTokens, true).flatMap(({ candidate, contextIndex }) => {\n const fileIndex = fileIndexByContext[contextIndex];\n const file = fileIndex === undefined ? undefined : files[fileIndex];\n return fileIndex === undefined || file === undefined\n ? []\n : [{ ...candidate, regionBucket: fileIndex, file: file.file }];\n });\n}\n\n/** A merged group is reported only while it still covers more than one file. */\nfunction spansMultipleFilesAfterMerge(group: CrossFileOccurrence[]): boolean {\n return new Set(group.map((occurrence) => occurrence.file)).size >= 2;\n}\n\nfunction spansMultipleFiles(group: SelectableCandidate[]): boolean {\n return group.length >= 2 && new Set(group.map((candidate) => candidate.regionBucket)).size >= 2;\n}\n\n/**\n * Per-file token offsets that map every file into one project-wide token index space: each file's\n * tokens are offset by more than `maxGapTokens` past the previous file's, so occurrences in\n * different files are never gap-adjacent and merged pairs always stay within one file.\n */\nfunction computeTokenOffsets(files: CrossFileDuplicationSourceFile[], maxGapTokens: number): number[] {\n const tokenOffsets: number[] = [];\n let offset = 0;\n for (const { tokens, candidates } of files) {\n tokenOffsets.push(offset);\n // Accumulated in a loop: spreading a project-scale candidate array as call arguments would\n // overflow V8's argument limit (~124k) and crash on Node.\n let tokenCount = tokens?.length ?? 0;\n if (!tokens) {\n for (const candidate of candidates) {\n tokenCount = Math.max(tokenCount, candidate.endTokenIndex);\n }\n }\n offset += tokenCount + maxGapTokens + 1;\n }\n return tokenOffsets;\n}\n\n/** Reuses the within-file gapped (Type-3) merging in the project-wide token index space. */\nfunction mergeGapAdjacentGroups(\n groups: SelectableCandidate[][],\n tokenOffsets: number[],\n maxGapTokens: number\n): CrossFileOccurrence[][] {\n const occurrenceGroups = groups.map((group) =>\n group\n .map((candidate): CrossFileOccurrence => {\n const start = candidate.startTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n const end = candidate.endTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n return {\n file: candidate.file,\n fileIndex: candidate.regionBucket,\n spanCountedElsewhere: candidate.nestedInLargerGroup,\n nestedInLargerGroup: candidate.nestedInLargerGroup,\n segments: [{ startTokenIndex: start, endTokenIndex: end }],\n tokenCount: candidate.tokenCount,\n startTokenIndex: start,\n endTokenIndex: end,\n startIndex: candidate.startIndex,\n endIndex: candidate.endIndex,\n startLine: candidate.startLine,\n endLine: candidate.endLine,\n };\n })\n .toSorted((left, right) => left.startTokenIndex - right.startTokenIndex)\n );\n return mergeAdjacentGroups(occurrenceGroups, maxGapTokens, spansMultipleFilesAfterMerge);\n}\n\nfunction summarize(\n groups: CrossFileOccurrence[][],\n files: CrossFileDuplicationSourceFile[],\n tokenOffsets: number[]\n): CrossFileDuplicationMetrics {\n const reported: CrossFileDuplicateBlockGroup[] = [];\n // Accumulated in Maps: file names are arbitrary strings, and a plain object would read\n // inherited properties for names like \"constructor\".\n const groupCountByFile = new Map<string, number>();\n const fileDataByName = new Map(\n files.map((file, index) => [\n file.file,\n { tokens: file.tokens, codeLineNumbers: file.codeLineNumbers, offset: tokenOffsets[index] ?? 0 },\n ])\n );\n const lineNumbersByFile = new Map<string, Set<number>>();\n let duplicateBlockCount = 0;\n for (const group of groups) {\n // Mirrors within-file counting: each redundant occurrence contributes one count per matched\n // fragment, gapped merging consolidates the grouping without halving the count, and spans a\n // partial merge shares between a retained group and the merged group count once.\n duplicateBlockCount += countRedundantFragments(group);\n const occurrences = group\n .map((occurrence) => {\n const { file, startLine, endLine, tokenCount } = occurrence;\n const lineNumbers = collectOccurrenceLines(occurrence, fileDataByName, lineNumbersByFile);\n return { file, startLine, endLine, ...(lineNumbers && { lineNumbers }), tokenCount };\n })\n .toSorted((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n const files = [...new Set(occurrences.map(({ file }) => file))];\n for (const file of files) {\n groupCountByFile.set(file, (groupCountByFile.get(file) ?? 0) + 1);\n }\n reported.push({ files, occurrences, tokenCount: Math.min(...group.map(({ tokenCount }) => tokenCount)) });\n }\n reported.sort(\n (left, right) =>\n right.tokenCount - left.tokenCount ||\n (left.occurrences[0]?.file ?? '').localeCompare(right.occurrences[0]?.file ?? '') ||\n (left.occurrences[0]?.startLine ?? 0) - (right.occurrences[0]?.startLine ?? 0)\n );\n return {\n duplicateBlockCount,\n duplicateBlockGroupCountByFile: Object.fromEntries(groupCountByFile),\n duplicateLineNumbersByFile: Object.fromEntries(\n [...lineNumbersByFile].map(([file, lines]) => [file, [...lines].toSorted((left, right) => left - right)])\n ),\n groups: reported,\n };\n}\n\n/**\n * Adds the code lines an occurrence's matched tokens cover (see collectMatchedLines) to its file's\n * line set, mapping the project-wide token segments back into the file's own token stream, and\n * returns them in order.\n * A file that supplied only candidates (no token stream) is skipped rather than approximated from\n * the bounding line range, which would include gap and comment/blank lines and break the field's\n * exactness contract.\n */\nfunction collectOccurrenceLines(\n occurrence: CrossFileOccurrence,\n fileDataByName: Map<\n string,\n { tokens?: CrossFileDuplicationSourceFile['tokens']; codeLineNumbers?: Set<number>; offset: number }\n >,\n lineNumbersByFile: Map<string, Set<number>>\n): number[] | undefined {\n const fileData = fileDataByName.get(occurrence.file);\n if (!fileData?.tokens) {\n return undefined;\n }\n let fileLines = lineNumbersByFile.get(occurrence.file);\n if (!fileLines) {\n fileLines = new Set();\n lineNumbersByFile.set(occurrence.file, fileLines);\n }\n const lines = collectMatchedLines(\n occurrence.segments.map((segment) => shift(segment, -fileData.offset)),\n occurrence.matchedRuns?.map((run) => shift(run, -fileData.offset)),\n fileData.tokens,\n fileData.codeLineNumbers,\n occurrence.exactSegments?.map((segment) => shift(segment, -fileData.offset))\n );\n for (const line of lines) {\n fileLines.add(line);\n }\n return [...lines].toSorted((left, right) => left - right);\n}\n"],"mappings":"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"}
|
|
@@ -38,8 +38,8 @@ export interface CrossFileDuplicationMetrics {
|
|
|
38
38
|
/**
|
|
39
39
|
* Per file, the 1-based code lines covered by the tokens of its cross-file occurrences, sorted
|
|
40
40
|
* ascending: the matched tokens of exact and gapped occurrences, and the lines of a near-miss
|
|
41
|
-
* block or its matched cores more than half of whose tokens its partners match
|
|
42
|
-
* near-miss coverage). The unmatched gap of a merged
|
|
41
|
+
* block or its matched cores more than half of whose tokens its partners match or which a part of it
|
|
42
|
+
* matched exactly reaches (like within-file near-miss coverage). The unmatched gap of a merged
|
|
43
43
|
* clone and comment/blank lines inside an occurrence's bounding range are excluded (blank rows
|
|
44
44
|
* inside multi-row tokens only when the file supplied codeLineNumbers). A file that supplied
|
|
45
45
|
* only candidates (no `tokens`) has no entry — without its token stream the covered lines are
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{buildLiteralCountPrefix as e,collectMatchedLines as t,collectSequenceWindowCandidates as n,countRedundantFragments as r,mergeAdjacentGroups as i,resolveDuplicationOptions as a}from"./duplication.js";import{collectCrossFileNearMissGroups as o}from"./crossFileNearMiss.js";import{selectMaximalGroups as s}from"./duplicateSelection.js";function c(e,t){let{minTokens:n,maxGapTokens:r,minSimilarityPercent:i}=a(t),o=e.flatMap(({file:e,candidates:t},n)=>t.map(t=>({...t,regionBucket:n,file:e})));for(let t of
|
|
1
|
+
import{buildLiteralCountPrefix as e,collectMatchedLines as t,collectSequenceWindowCandidates as n,countRedundantFragments as r,mergeAdjacentGroups as i,resolveDuplicationOptions as a}from"./duplication.js";import{collectCrossFileNearMissGroups as o}from"./crossFileNearMiss.js";import{selectMaximalGroups as s}from"./duplicateSelection.js";function c(e,t){let{minTokens:n,maxGapTokens:r,minSimilarityPercent:i}=a(t),o=e.flatMap(({file:e,candidates:t},n)=>t.map(t=>({...t,regionBucket:n,file:e})));for(let t of h(e,n))o.push(t);let c=s(o,_,(e,t)=>e.regionBucket-t.regionBucket||e.startIndex-t.startIndex),u=v(e,r),d=y([...c.values()],u,r);return b(l(d,p(e,d,u,n,i),n),e,u)}function l(e,t,n){let r=/* @__PURE__ */ new Map;for(let[t,n]of e.entries())for(let{fileIndex:e}of n){let n=r.get(e)??/* @__PURE__ */ new Set;n.add(t),r.set(e,n)}let i=/* @__PURE__ */ new Set,a=/* @__PURE__ */ new Set,o=t.map(t=>{let o=e=>{let r=t.filter(t=>t.segments.some(t=>e.segments.some(e=>u(e,t)))),[i]=r;if(r.length===1&&i)return Math.max(0,i.startTokenIndex-e.startTokenIndex)+Math.max(0,e.endTokenIndex-i.endTokenIndex)<n?i:void 0},s=/* @__PURE__ */ new Map,c=/* @__PURE__ */ new Set;for(let e of t)for(let t of e.spanCountedElsewhere?r.get(e.fileIndex)??[]:[])c.add(t);for(let t of[...c].toSorted((e,t)=>e-t)){let n=e[t]??[],r=n.map(o);if(!(i.has(t)||r.includes(void 0)||new Set(r).size!==r.length)){i.add(t);for(let[e,t]of r.entries()){let r=n[e];if(t&&r){let e=s.get(t)??[];e.push(r),s.set(t,e)}}}}return t.map(e=>{let t=s.get(e);if(!t)return e;let n=d([e,...t]);return a.add(n),n})});for(let t of a)for(let n of r.get(t.fileIndex)??[])if(!i.has(n)&&e[n]?.some(e=>!e.spanCountedElsewhere&&t.segments.every(t=>e.segments.some(e=>e.startTokenIndex<=t.startTokenIndex&&t.endTokenIndex<=e.endTokenIndex)))){t.spanCountedElsewhere=!0;break}return[...e.filter((e,t)=>!i.has(t)),...o]}function u(e,t){return e.startTokenIndex<t.endTokenIndex&&t.startTokenIndex<e.endTokenIndex}function d(e){let t=f(e.flatMap(e=>e.segments));return{...e[0],spanCountedElsewhere:void 0,nestedInLargerGroup:void 0,segments:t,matchedRuns:f(e.flatMap(e=>e.matchedRuns??e.segments)),exactSegments:f(e.flatMap(e=>e.matchedRuns?e.exactSegments??[]:e.segments)),tokenCount:t.reduce((e,t)=>e+t.endTokenIndex-t.startTokenIndex,0),startTokenIndex:Math.min(...e.map(e=>e.startTokenIndex)),endTokenIndex:Math.max(...e.map(e=>e.endTokenIndex)),startIndex:Math.min(...e.map(e=>e.startIndex)),endIndex:Math.max(...e.map(e=>e.endIndex)),startLine:Math.min(...e.map(e=>e.startLine)),endLine:Math.max(...e.map(e=>e.endLine))}}function f(e){let t=[];for(let n of e.toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex)){let e=t.at(-1);e&&n.startTokenIndex<e.endTokenIndex?e.endTokenIndex=Math.max(e.endTokenIndex,n.endTokenIndex):t.push({...n})}return t}function p(e,t,n,r,i){let a=e.map(()=>[]);for(let{fileIndex:e,segments:r}of t.flat()){let t=n[e]??0;for(let n of r)a[e]?.push(m(n,-t))}return o(e,a,r,i).map(t=>t.map(t=>{let r=n[t.fileIndex]??0;return{...t,file:e[t.fileIndex]?.file??``,segments:t.segments.map(e=>m(e,r)),matchedRuns:t.matchedRuns?.map(e=>m(e,r)),startTokenIndex:t.startTokenIndex+r,endTokenIndex:t.endTokenIndex+r}}))}function m({startTokenIndex:e,endTokenIndex:t},n){return{startTokenIndex:e+n,endTokenIndex:t+n}}function h(t,r){let i=[],a=[];for(let[n,{tokens:r,containerStatements:o}]of t.entries())r&&o&&(i.push(n),a.push({tokens:r,literalCountPrefix:e(r),containers:o}));return a.length<2?[]:n(a,r,!0).flatMap(({candidate:e,contextIndex:n})=>{let r=i[n],a=r===void 0?void 0:t[r];return r===void 0||a===void 0?[]:[{...e,regionBucket:r,file:a.file}]})}function g(e){return new Set(e.map(e=>e.file)).size>=2}function _(e){return e.length>=2&&new Set(e.map(e=>e.regionBucket)).size>=2}function v(e,t){let n=[],r=0;for(let{tokens:i,candidates:a}of e){n.push(r);let e=i?.length??0;if(!i)for(let t of a)e=Math.max(e,t.endTokenIndex);r+=e+t+1}return n}function y(e,t,n){let r=e.map(e=>e.map(e=>{let n=e.startTokenIndex+(t[e.regionBucket]??0),r=e.endTokenIndex+(t[e.regionBucket]??0);return{file:e.file,fileIndex:e.regionBucket,spanCountedElsewhere:e.nestedInLargerGroup,nestedInLargerGroup:e.nestedInLargerGroup,segments:[{startTokenIndex:n,endTokenIndex:r}],tokenCount:e.tokenCount,startTokenIndex:n,endTokenIndex:r,startIndex:e.startIndex,endIndex:e.endIndex,startLine:e.startLine,endLine:e.endLine}}).toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex));return i(r,n,g)}function b(e,t,n){let i=[],a=/* @__PURE__ */ new Map,o=new Map(t.map((e,t)=>[e.file,{tokens:e.tokens,codeLineNumbers:e.codeLineNumbers,offset:n[t]??0}])),s=/* @__PURE__ */ new Map,c=0;for(let t of e){c+=r(t);let e=t.map(e=>{let{file:t,startLine:n,endLine:r,tokenCount:i}=e,a=x(e,o,s);return{file:t,startLine:n,endLine:r,...a&&{lineNumbers:a},tokenCount:i}}).toSorted((e,t)=>e.file.localeCompare(t.file)||e.startLine-t.startLine),n=[...new Set(e.map(({file:e})=>e))];for(let e of n)a.set(e,(a.get(e)??0)+1);i.push({files:n,occurrences:e,tokenCount:Math.min(...t.map(({tokenCount:e})=>e))})}return i.sort((e,t)=>t.tokenCount-e.tokenCount||(e.occurrences[0]?.file??``).localeCompare(t.occurrences[0]?.file??``)||(e.occurrences[0]?.startLine??0)-(t.occurrences[0]?.startLine??0)),{duplicateBlockCount:c,duplicateBlockGroupCountByFile:Object.fromEntries(a),duplicateLineNumbersByFile:Object.fromEntries([...s].map(([e,t])=>[e,[...t].toSorted((e,t)=>e-t)])),groups:i}}function x(e,n,r){let i=n.get(e.file);if(!i?.tokens)return;let a=r.get(e.file);a||(a=/* @__PURE__ */ new Set,r.set(e.file,a));let o=t(e.segments.map(e=>m(e,-i.offset)),e.matchedRuns?.map(e=>m(e,-i.offset)),i.tokens,i.codeLineNumbers,e.exactSegments?.map(e=>m(e,-i.offset)));for(let e of o)a.add(e);return[...o].toSorted((e,t)=>e-t)}export{c as measureCrossFileDuplication};
|
|
2
2
|
//# sourceMappingURL=crossFileDuplication.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crossFileDuplication.js","names":[],"sources":["../src/crossFileDuplication.ts"],"sourcesContent":["import { collectCrossFileNearMissGroups } from './crossFileNearMiss.js';\nimport { selectMaximalGroups, type SelectableRegion } from './duplicateSelection.js';\nimport {\n buildLiteralCountPrefix,\n collectMatchedLines,\n collectSequenceWindowCandidates,\n countRedundantFragments,\n mergeAdjacentGroups,\n resolveDuplicationOptions,\n type CountedOccurrence,\n type CrossFileDuplicateCandidate,\n type CrossFileDuplicationFileData,\n type SequenceWindowContext,\n type TokenSegment,\n} from './duplication.js';\nimport type { DuplicationOptions } from './types.js';\n\nexport interface CrossFileDuplicationSourceFile extends Partial<CrossFileDuplicationFileData> {\n file: string;\n candidates: CrossFileDuplicateCandidate[];\n}\n\nexport interface CrossFileDuplicateOccurrence {\n endLine: number;\n file: string;\n /**\n * The code lines carrying the occurrence's matched tokens (see\n * DuplicateBlockOccurrence.lineNumbers). Absent for a file that supplied only candidates (no\n * `tokens`), whose matched lines are unknowable.\n */\n lineNumbers?: number[];\n startLine: number;\n /**\n * The tokens of its matched runs, or of a near-miss block or its matched cores with the edited\n * tokens among them; the group's `tokenCount` is the smallest of its occurrences'.\n */\n tokenCount: number;\n}\n\nexport interface CrossFileDuplicateBlockGroup {\n files: string[];\n occurrences: CrossFileDuplicateOccurrence[];\n /**\n * Token count of the smallest occurrence. For exact and gapped groups it is the matched token\n * count every occurrence shares (gaps are not counted); for near-miss (Type-3) groups it is the\n * token count of the smallest whole block or set of matched cores, edited tokens included.\n */\n tokenCount: number;\n}\n\nexport interface CrossFileDuplicationMetrics {\n /** Number of redundant copies across all groups, counted per matched fragment like within-file. */\n duplicateBlockCount: number;\n /** Groups the file participates in, keyed by the file name passed in. */\n duplicateBlockGroupCountByFile: Record<string, number>;\n /**\n * Per file, the 1-based code lines covered by the tokens of its cross-file occurrences, sorted\n * ascending: the matched tokens of exact and gapped occurrences, and the lines of a near-miss\n * block or its matched cores more than half of whose tokens its partners match (like within-file\n * near-miss coverage). The unmatched gap of a merged\n * clone and comment/blank lines inside an occurrence's bounding range are excluded (blank rows\n * inside multi-row tokens only when the file supplied codeLineNumbers). A file that supplied\n * only candidates (no `tokens`) has no entry — without its token stream the covered lines are\n * unknowable, and an approximate bounding range would break this field's exactness.\n */\n duplicateLineNumbersByFile: Record<string, number[]>;\n groups: CrossFileDuplicateBlockGroup[];\n}\n\ninterface SelectableCandidate extends CrossFileDuplicateCandidate, SelectableRegion {\n regionBucket: number;\n file: string;\n}\n\n/** A cross-file occurrence: a within-file occurrence in the project-wide token index space. */\ninterface CrossFileOccurrence extends CountedOccurrence {\n file: string;\n fileIndex: number;\n}\n\n/**\n * Detects code regions duplicated across files. Per-file candidates (whole block subtrees and full\n * container runs, fingerprinted with the same normalization as within-file duplication) are joined\n * by a project-level window index over per-statement fingerprint sequences (CPD-style), so a\n * copy-pasted partial statement run embedded in different surrounding code is matched even though\n * no single file can know it repeats elsewhere. Candidates are grouped by fingerprint, and only\n * maximal, non-overlapping regions whose group spans at least two files are counted. Groups that\n * shrink to a single file during selection are shed — a within-file repeat is already reported by\n * that file's own duplication metrics. A copy nested inside a larger group's region (two files share\n * a whole function, a third file only a block of it) is reported with its group, so the third\n * file's copy still shows what it duplicates. Groups separated by a small token gap within each file then\n * merge into gapped (Type-3) clone groups under `maxGapTokens`, exactly like within-file merging.\n * Finally, blocks of files that supplied `nearMissBlocks` are compared across files for near-miss\n * (Type-3) clones under `minSimilarityPercent` (see crossFileNearMiss.ts).\n */\nexport function measureCrossFileDuplication(\n files: CrossFileDuplicationSourceFile[],\n options?: DuplicationOptions\n): CrossFileDuplicationMetrics {\n const { minTokens, maxGapTokens, minSimilarityPercent } = resolveDuplicationOptions(options);\n const candidates: SelectableCandidate[] = files.flatMap(({ file, candidates }, fileIndex) =>\n candidates.map((candidate) => ({ ...candidate, regionBucket: fileIndex, file }))\n );\n // Pushed one by one: spreading the project-scale window-candidate array as call arguments\n // overflows V8's argument limit (~124k) and crashes on Node, though Bun/JSC tolerates it.\n for (const candidate of collectWindowCandidates(files, minTokens)) {\n candidates.push(candidate);\n }\n const counted = selectMaximalGroups(\n candidates,\n spansMultipleFiles,\n // File index and position break coverage ties deterministically.\n (left, right) => left.regionBucket - right.regionBucket || left.startIndex - right.startIndex\n );\n const tokenOffsets = computeTokenOffsets(files, maxGapTokens);\n const groups = mergeGapAdjacentGroups([...counted.values()], tokenOffsets, maxGapTokens);\n for (const group of collectNearMissGroups(files, groups, tokenOffsets, minTokens, minSimilarityPercent)) {\n groups.push(group);\n }\n return summarize(groups, files, tokenOffsets);\n}\n\n/** Near-miss groups among the blocks the exact groups leave unreported, in the project token space. */\nfunction collectNearMissGroups(\n files: CrossFileDuplicationSourceFile[],\n exactGroups: CrossFileOccurrence[][],\n tokenOffsets: number[],\n minTokens: number,\n minSimilarityPercent: number\n): CrossFileOccurrence[][] {\n const reportedSpansByFile: { startTokenIndex: number; endTokenIndex: number }[][] = files.map(() => []);\n for (const { fileIndex, startTokenIndex, endTokenIndex } of exactGroups.flat()) {\n const offset = tokenOffsets[fileIndex] ?? 0;\n reportedSpansByFile[fileIndex]?.push({\n startTokenIndex: startTokenIndex - offset,\n endTokenIndex: endTokenIndex - offset,\n });\n }\n return collectCrossFileNearMissGroups(files, reportedSpansByFile, minTokens, minSimilarityPercent).map((group) =>\n group.map((occurrence) => {\n const offset = tokenOffsets[occurrence.fileIndex] ?? 0;\n return {\n ...occurrence,\n file: files[occurrence.fileIndex]?.file ?? '',\n segments: occurrence.segments.map((segment) => shift(segment, offset)),\n matchedRuns: occurrence.matchedRuns?.map((run) => shift(run, offset)),\n startTokenIndex: occurrence.startTokenIndex + offset,\n endTokenIndex: occurrence.endTokenIndex + offset,\n };\n })\n );\n}\n\nfunction shift({ startTokenIndex, endTokenIndex }: TokenSegment, offset: number): TokenSegment {\n return { startTokenIndex: startTokenIndex + offset, endTokenIndex: endTokenIndex + offset };\n}\n\n/** Repeated sub-windows of sibling statements matched across the whole project's files. */\nfunction collectWindowCandidates(files: CrossFileDuplicationSourceFile[], minTokens: number): SelectableCandidate[] {\n const fileIndexByContext: number[] = [];\n const contexts: SequenceWindowContext[] = [];\n for (const [fileIndex, { tokens, containerStatements }] of files.entries()) {\n if (tokens && containerStatements) {\n fileIndexByContext.push(fileIndex);\n contexts.push({ tokens, literalCountPrefix: buildLiteralCountPrefix(tokens), containers: containerStatements });\n }\n }\n if (contexts.length < 2) {\n return [];\n }\n return collectSequenceWindowCandidates(contexts, minTokens, true).flatMap(({ candidate, contextIndex }) => {\n const fileIndex = fileIndexByContext[contextIndex];\n const file = fileIndex === undefined ? undefined : files[fileIndex];\n return fileIndex === undefined || file === undefined\n ? []\n : [{ ...candidate, regionBucket: fileIndex, file: file.file }];\n });\n}\n\n/** A merged group is reported only while it still covers more than one file. */\nfunction spansMultipleFilesAfterMerge(group: CrossFileOccurrence[]): boolean {\n return new Set(group.map((occurrence) => occurrence.file)).size >= 2;\n}\n\nfunction spansMultipleFiles(group: SelectableCandidate[]): boolean {\n return group.length >= 2 && new Set(group.map((candidate) => candidate.regionBucket)).size >= 2;\n}\n\n/**\n * Per-file token offsets that map every file into one project-wide token index space: each file's\n * tokens are offset by more than `maxGapTokens` past the previous file's, so occurrences in\n * different files are never gap-adjacent and merged pairs always stay within one file.\n */\nfunction computeTokenOffsets(files: CrossFileDuplicationSourceFile[], maxGapTokens: number): number[] {\n const tokenOffsets: number[] = [];\n let offset = 0;\n for (const { tokens, candidates } of files) {\n tokenOffsets.push(offset);\n // Accumulated in a loop: spreading a project-scale candidate array as call arguments would\n // overflow V8's argument limit (~124k) and crash on Node.\n let tokenCount = tokens?.length ?? 0;\n if (!tokens) {\n for (const candidate of candidates) {\n tokenCount = Math.max(tokenCount, candidate.endTokenIndex);\n }\n }\n offset += tokenCount + maxGapTokens + 1;\n }\n return tokenOffsets;\n}\n\n/** Reuses the within-file gapped (Type-3) merging in the project-wide token index space. */\nfunction mergeGapAdjacentGroups(\n groups: SelectableCandidate[][],\n tokenOffsets: number[],\n maxGapTokens: number\n): CrossFileOccurrence[][] {\n const occurrenceGroups = groups.map((group) =>\n group\n .map((candidate): CrossFileOccurrence => {\n const start = candidate.startTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n const end = candidate.endTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n return {\n file: candidate.file,\n fileIndex: candidate.regionBucket,\n spanCountedElsewhere: candidate.nestedInLargerGroup,\n nestedInLargerGroup: candidate.nestedInLargerGroup,\n segments: [{ startTokenIndex: start, endTokenIndex: end }],\n tokenCount: candidate.tokenCount,\n startTokenIndex: start,\n endTokenIndex: end,\n startIndex: candidate.startIndex,\n endIndex: candidate.endIndex,\n startLine: candidate.startLine,\n endLine: candidate.endLine,\n };\n })\n .toSorted((left, right) => left.startTokenIndex - right.startTokenIndex)\n );\n return mergeAdjacentGroups(occurrenceGroups, maxGapTokens, spansMultipleFilesAfterMerge);\n}\n\nfunction summarize(\n groups: CrossFileOccurrence[][],\n files: CrossFileDuplicationSourceFile[],\n tokenOffsets: number[]\n): CrossFileDuplicationMetrics {\n const reported: CrossFileDuplicateBlockGroup[] = [];\n // Accumulated in Maps: file names are arbitrary strings, and a plain object would read\n // inherited properties for names like \"constructor\".\n const groupCountByFile = new Map<string, number>();\n const fileDataByName = new Map(\n files.map((file, index) => [\n file.file,\n { tokens: file.tokens, codeLineNumbers: file.codeLineNumbers, offset: tokenOffsets[index] ?? 0 },\n ])\n );\n const lineNumbersByFile = new Map<string, Set<number>>();\n let duplicateBlockCount = 0;\n for (const group of groups) {\n // Mirrors within-file counting: each redundant occurrence contributes one count per matched\n // fragment, gapped merging consolidates the grouping without halving the count, and spans a\n // partial merge shares between a retained group and the merged group count once.\n duplicateBlockCount += countRedundantFragments(group);\n const occurrences = group\n .map((occurrence) => {\n const { file, startLine, endLine, tokenCount } = occurrence;\n const lineNumbers = collectOccurrenceLines(occurrence, fileDataByName, lineNumbersByFile);\n return { file, startLine, endLine, ...(lineNumbers && { lineNumbers }), tokenCount };\n })\n .toSorted((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n const files = [...new Set(occurrences.map(({ file }) => file))];\n for (const file of files) {\n groupCountByFile.set(file, (groupCountByFile.get(file) ?? 0) + 1);\n }\n reported.push({ files, occurrences, tokenCount: Math.min(...group.map(({ tokenCount }) => tokenCount)) });\n }\n reported.sort(\n (left, right) =>\n right.tokenCount - left.tokenCount ||\n (left.occurrences[0]?.file ?? '').localeCompare(right.occurrences[0]?.file ?? '') ||\n (left.occurrences[0]?.startLine ?? 0) - (right.occurrences[0]?.startLine ?? 0)\n );\n return {\n duplicateBlockCount,\n duplicateBlockGroupCountByFile: Object.fromEntries(groupCountByFile),\n duplicateLineNumbersByFile: Object.fromEntries(\n [...lineNumbersByFile].map(([file, lines]) => [file, [...lines].toSorted((left, right) => left - right)])\n ),\n groups: reported,\n };\n}\n\n/**\n * Adds the code lines an occurrence's matched tokens cover (see collectMatchedLines) to its file's\n * line set, mapping the project-wide token segments back into the file's own token stream, and\n * returns them in order.\n * A file that supplied only candidates (no token stream) is skipped rather than approximated from\n * the bounding line range, which would include gap and comment/blank lines and break the field's\n * exactness contract.\n */\nfunction collectOccurrenceLines(\n occurrence: CrossFileOccurrence,\n fileDataByName: Map<\n string,\n { tokens?: CrossFileDuplicationSourceFile['tokens']; codeLineNumbers?: Set<number>; offset: number }\n >,\n lineNumbersByFile: Map<string, Set<number>>\n): number[] | undefined {\n const fileData = fileDataByName.get(occurrence.file);\n if (!fileData?.tokens) {\n return undefined;\n }\n let fileLines = lineNumbersByFile.get(occurrence.file);\n if (!fileLines) {\n fileLines = new Set();\n lineNumbersByFile.set(occurrence.file, fileLines);\n }\n const lines = collectMatchedLines(\n occurrence.segments.map((segment) => shift(segment, -fileData.offset)),\n occurrence.matchedRuns?.map((run) => shift(run, -fileData.offset)),\n fileData.tokens,\n fileData.codeLineNumbers\n );\n for (const line of lines) {\n fileLines.add(line);\n }\n return [...lines].toSorted((left, right) => left - right);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"crossFileDuplication.js","names":[],"sources":["../src/crossFileDuplication.ts"],"sourcesContent":["import { collectCrossFileNearMissGroups } from './crossFileNearMiss.js';\nimport { selectMaximalGroups, type SelectableRegion } from './duplicateSelection.js';\nimport {\n buildLiteralCountPrefix,\n collectMatchedLines,\n collectSequenceWindowCandidates,\n countRedundantFragments,\n mergeAdjacentGroups,\n resolveDuplicationOptions,\n type CountedOccurrence,\n type CrossFileDuplicateCandidate,\n type CrossFileDuplicationFileData,\n type SequenceWindowContext,\n type TokenSegment,\n} from './duplication.js';\nimport type { DuplicationOptions } from './types.js';\n\nexport interface CrossFileDuplicationSourceFile extends Partial<CrossFileDuplicationFileData> {\n file: string;\n candidates: CrossFileDuplicateCandidate[];\n}\n\nexport interface CrossFileDuplicateOccurrence {\n endLine: number;\n file: string;\n /**\n * The code lines carrying the occurrence's matched tokens (see\n * DuplicateBlockOccurrence.lineNumbers). Absent for a file that supplied only candidates (no\n * `tokens`), whose matched lines are unknowable.\n */\n lineNumbers?: number[];\n startLine: number;\n /**\n * The tokens of its matched runs, or of a near-miss block or its matched cores with the edited\n * tokens among them; the group's `tokenCount` is the smallest of its occurrences'.\n */\n tokenCount: number;\n}\n\nexport interface CrossFileDuplicateBlockGroup {\n files: string[];\n occurrences: CrossFileDuplicateOccurrence[];\n /**\n * Token count of the smallest occurrence. For exact and gapped groups it is the matched token\n * count every occurrence shares (gaps are not counted); for near-miss (Type-3) groups it is the\n * token count of the smallest whole block or set of matched cores, edited tokens included.\n */\n tokenCount: number;\n}\n\nexport interface CrossFileDuplicationMetrics {\n /** Number of redundant copies across all groups, counted per matched fragment like within-file. */\n duplicateBlockCount: number;\n /** Groups the file participates in, keyed by the file name passed in. */\n duplicateBlockGroupCountByFile: Record<string, number>;\n /**\n * Per file, the 1-based code lines covered by the tokens of its cross-file occurrences, sorted\n * ascending: the matched tokens of exact and gapped occurrences, and the lines of a near-miss\n * block or its matched cores more than half of whose tokens its partners match or which a part of it\n * matched exactly reaches (like within-file near-miss coverage). The unmatched gap of a merged\n * clone and comment/blank lines inside an occurrence's bounding range are excluded (blank rows\n * inside multi-row tokens only when the file supplied codeLineNumbers). A file that supplied\n * only candidates (no `tokens`) has no entry — without its token stream the covered lines are\n * unknowable, and an approximate bounding range would break this field's exactness.\n */\n duplicateLineNumbersByFile: Record<string, number[]>;\n groups: CrossFileDuplicateBlockGroup[];\n}\n\ninterface SelectableCandidate extends CrossFileDuplicateCandidate, SelectableRegion {\n regionBucket: number;\n file: string;\n}\n\n/** A cross-file occurrence: a within-file occurrence in the project-wide token index space. */\ninterface CrossFileOccurrence extends CountedOccurrence {\n file: string;\n fileIndex: number;\n}\n\n/**\n * Detects code regions duplicated across files. Per-file candidates (whole block subtrees and full\n * container runs, fingerprinted with the same normalization as within-file duplication) are joined\n * by a project-level window index over per-statement fingerprint sequences (CPD-style), so a\n * copy-pasted partial statement run embedded in different surrounding code is matched even though\n * no single file can know it repeats elsewhere. Candidates are grouped by fingerprint, and only\n * maximal, non-overlapping regions whose group spans at least two files are counted. Groups that\n * shrink to a single file during selection are shed — a within-file repeat is already reported by\n * that file's own duplication metrics. A copy nested inside a larger group's region (two files share\n * a whole function, a third file only a block of it) is reported with its group, so the third\n * file's copy still shows what it duplicates. Groups separated by a small token gap within each file then\n * merge into gapped (Type-3) clone groups under `maxGapTokens`, exactly like within-file merging.\n * Finally, blocks of files that supplied `nearMissBlocks` are compared across files for near-miss\n * (Type-3) clones under `minSimilarityPercent` (see crossFileNearMiss.ts).\n */\nexport function measureCrossFileDuplication(\n files: CrossFileDuplicationSourceFile[],\n options?: DuplicationOptions\n): CrossFileDuplicationMetrics {\n const { minTokens, maxGapTokens, minSimilarityPercent } = resolveDuplicationOptions(options);\n const candidates: SelectableCandidate[] = files.flatMap(({ file, candidates }, fileIndex) =>\n candidates.map((candidate) => ({ ...candidate, regionBucket: fileIndex, file }))\n );\n // Pushed one by one: spreading the project-scale window-candidate array as call arguments\n // overflows V8's argument limit (~124k) and crashes on Node, though Bun/JSC tolerates it.\n for (const candidate of collectWindowCandidates(files, minTokens)) {\n candidates.push(candidate);\n }\n const counted = selectMaximalGroups(\n candidates,\n spansMultipleFiles,\n // File index and position break coverage ties deterministically.\n (left, right) => left.regionBucket - right.regionBucket || left.startIndex - right.startIndex\n );\n const tokenOffsets = computeTokenOffsets(files, maxGapTokens);\n const exactGroups = mergeGapAdjacentGroups([...counted.values()], tokenOffsets, maxGapTokens);\n const nearMissGroups = collectNearMissGroups(files, exactGroups, tokenOffsets, minTokens, minSimilarityPercent);\n return summarize(takeOverExactGroups(exactGroups, nearMissGroups, minTokens), files, tokenOffsets);\n}\n\n/**\n * Lets a near-miss group take over the exact groups standing for the same copies, as within a\n * file (collect_near_miss_groups in native/src/duplication.rs): every occurrence of such a group\n * overlaps exactly one copy of the near-miss group (one overlapping none reports content the copies\n * do not share, and one reaching into two would make one copy take over lines of the next), exceeds\n * its span by less than `minTokens` (a copy is listed at the size it matched, not at that of a\n * clone larger by a reportable part, which may run into code no copy matched), and no two overlap\n * the same copy (those repeat within the copy, not between the copies).\n * Each copy then becomes one occurrence with the exact fragments it holds, so the copies are\n * listed once.\n */\nfunction takeOverExactGroups(\n exactGroups: CrossFileOccurrence[][],\n nearMissGroups: CrossFileOccurrence[][],\n minTokens: number\n): CrossFileOccurrence[][] {\n const exactGroupIndexesByFile = new Map<number, Set<number>>();\n for (const [groupIndex, group] of exactGroups.entries()) {\n for (const { fileIndex } of group) {\n const indexes = exactGroupIndexesByFile.get(fileIndex) ?? new Set();\n indexes.add(groupIndex);\n exactGroupIndexesByFile.set(fileIndex, indexes);\n }\n }\n const taken = new Set<number>();\n const coalesced = new Set<CrossFileOccurrence>();\n const merged = nearMissGroups.map((copies) => {\n const copyOf = (occurrence: CrossFileOccurrence): CrossFileOccurrence | undefined => {\n const overlapped = copies.filter((copy) =>\n copy.segments.some((segment) => occurrence.segments.some((matched) => overlap(matched, segment)))\n );\n const [copy] = overlapped;\n if (overlapped.length !== 1 || !copy) {\n return undefined;\n }\n const excess =\n Math.max(0, copy.startTokenIndex - occurrence.startTokenIndex) +\n Math.max(0, occurrence.endTokenIndex - copy.endTokenIndex);\n return excess < minTokens ? copy : undefined;\n };\n const fragmentsByCopy = new Map<CrossFileOccurrence, CrossFileOccurrence[]>();\n const candidateIndexes = new Set<number>();\n for (const copy of copies) {\n for (const groupIndex of copy.spanCountedElsewhere ? (exactGroupIndexesByFile.get(copy.fileIndex) ?? []) : []) {\n candidateIndexes.add(groupIndex);\n }\n }\n for (const groupIndex of [...candidateIndexes].toSorted((left, right) => left - right)) {\n const group = exactGroups[groupIndex] ?? [];\n const owners = group.map(copyOf);\n if (taken.has(groupIndex) || owners.includes(undefined) || new Set(owners).size !== owners.length) {\n continue;\n }\n taken.add(groupIndex);\n for (const [index, owner] of owners.entries()) {\n const fragment = group[index];\n if (owner && fragment) {\n const fragments = fragmentsByCopy.get(owner) ?? [];\n fragments.push(fragment);\n fragmentsByCopy.set(owner, fragments);\n }\n }\n }\n return copies.map((copy): CrossFileOccurrence => {\n const fragments = fragmentsByCopy.get(copy);\n if (!fragments) {\n return copy;\n }\n const whole = coalesceOccurrences([copy, ...fragments]);\n coalesced.add(whole);\n return whole;\n });\n });\n // A copy that took its fragments over counts them itself, unless a group left in place still\n // counts all of it: a larger clone whose matched segments enclose those of the copy, which its\n // fragments were nested in. A smaller run left in place inside the copy counts its own repeats,\n // not the copy.\n for (const copy of coalesced) {\n for (const groupIndex of exactGroupIndexesByFile.get(copy.fileIndex) ?? []) {\n const enclosed =\n !taken.has(groupIndex) &&\n exactGroups[groupIndex]?.some(\n (occurrence) =>\n !occurrence.spanCountedElsewhere &&\n copy.segments.every((segment) =>\n occurrence.segments.some(\n (matched) =>\n matched.startTokenIndex <= segment.startTokenIndex && segment.endTokenIndex <= matched.endTokenIndex\n )\n )\n );\n if (enclosed) {\n copy.spanCountedElsewhere = true;\n break;\n }\n }\n }\n return [...exactGroups.filter((_, groupIndex) => !taken.has(groupIndex)), ...merged];\n}\n\nfunction overlap(left: TokenSegment, right: TokenSegment): boolean {\n return left.startTokenIndex < right.endTokenIndex && right.startTokenIndex < left.endTokenIndex;\n}\n\n/** The parts of one copy as one occurrence: an exact part is matched throughout, a near-miss part where its partners match it. */\nfunction coalesceOccurrences(parts: [CrossFileOccurrence, ...CrossFileOccurrence[]]): CrossFileOccurrence {\n const segments = mergeSegments(parts.flatMap((part) => part.segments));\n return {\n ...parts[0],\n spanCountedElsewhere: undefined,\n nestedInLargerGroup: undefined,\n segments,\n matchedRuns: mergeSegments(parts.flatMap((part) => part.matchedRuns ?? part.segments)),\n exactSegments: mergeSegments(\n parts.flatMap((part) => (part.matchedRuns ? (part.exactSegments ?? []) : part.segments))\n ),\n tokenCount: segments.reduce((sum, segment) => sum + segment.endTokenIndex - segment.startTokenIndex, 0),\n startTokenIndex: Math.min(...parts.map((part) => part.startTokenIndex)),\n endTokenIndex: Math.max(...parts.map((part) => part.endTokenIndex)),\n startIndex: Math.min(...parts.map((part) => part.startIndex)),\n endIndex: Math.max(...parts.map((part) => part.endIndex)),\n startLine: Math.min(...parts.map((part) => part.startLine)),\n endLine: Math.max(...parts.map((part) => part.endLine)),\n };\n}\n\n/** The unions of overlapping segments, in position order. */\nfunction mergeSegments(segments: TokenSegment[]): TokenSegment[] {\n const merged: TokenSegment[] = [];\n for (const segment of segments.toSorted((left, right) => left.startTokenIndex - right.startTokenIndex)) {\n const last = merged.at(-1);\n if (last && segment.startTokenIndex < last.endTokenIndex) {\n last.endTokenIndex = Math.max(last.endTokenIndex, segment.endTokenIndex);\n } else {\n merged.push({ ...segment });\n }\n }\n return merged;\n}\n\n/** Near-miss groups among the blocks the exact groups leave unreported, in the project token space. */\nfunction collectNearMissGroups(\n files: CrossFileDuplicationSourceFile[],\n exactGroups: CrossFileOccurrence[][],\n tokenOffsets: number[],\n minTokens: number,\n minSimilarityPercent: number\n): CrossFileOccurrence[][] {\n const reportedSpansByFile: { startTokenIndex: number; endTokenIndex: number }[][] = files.map(() => []);\n // Segment by segment: the gap of a gapped clone is not reported content.\n for (const { fileIndex, segments } of exactGroups.flat()) {\n const offset = tokenOffsets[fileIndex] ?? 0;\n for (const segment of segments) {\n reportedSpansByFile[fileIndex]?.push(shift(segment, -offset));\n }\n }\n return collectCrossFileNearMissGroups(files, reportedSpansByFile, minTokens, minSimilarityPercent).map((group) =>\n group.map((occurrence) => {\n const offset = tokenOffsets[occurrence.fileIndex] ?? 0;\n return {\n ...occurrence,\n file: files[occurrence.fileIndex]?.file ?? '',\n segments: occurrence.segments.map((segment) => shift(segment, offset)),\n matchedRuns: occurrence.matchedRuns?.map((run) => shift(run, offset)),\n startTokenIndex: occurrence.startTokenIndex + offset,\n endTokenIndex: occurrence.endTokenIndex + offset,\n };\n })\n );\n}\n\nfunction shift({ startTokenIndex, endTokenIndex }: TokenSegment, offset: number): TokenSegment {\n return { startTokenIndex: startTokenIndex + offset, endTokenIndex: endTokenIndex + offset };\n}\n\n/** Repeated sub-windows of sibling statements matched across the whole project's files. */\nfunction collectWindowCandidates(files: CrossFileDuplicationSourceFile[], minTokens: number): SelectableCandidate[] {\n const fileIndexByContext: number[] = [];\n const contexts: SequenceWindowContext[] = [];\n for (const [fileIndex, { tokens, containerStatements }] of files.entries()) {\n if (tokens && containerStatements) {\n fileIndexByContext.push(fileIndex);\n contexts.push({ tokens, literalCountPrefix: buildLiteralCountPrefix(tokens), containers: containerStatements });\n }\n }\n if (contexts.length < 2) {\n return [];\n }\n return collectSequenceWindowCandidates(contexts, minTokens, true).flatMap(({ candidate, contextIndex }) => {\n const fileIndex = fileIndexByContext[contextIndex];\n const file = fileIndex === undefined ? undefined : files[fileIndex];\n return fileIndex === undefined || file === undefined\n ? []\n : [{ ...candidate, regionBucket: fileIndex, file: file.file }];\n });\n}\n\n/** A merged group is reported only while it still covers more than one file. */\nfunction spansMultipleFilesAfterMerge(group: CrossFileOccurrence[]): boolean {\n return new Set(group.map((occurrence) => occurrence.file)).size >= 2;\n}\n\nfunction spansMultipleFiles(group: SelectableCandidate[]): boolean {\n return group.length >= 2 && new Set(group.map((candidate) => candidate.regionBucket)).size >= 2;\n}\n\n/**\n * Per-file token offsets that map every file into one project-wide token index space: each file's\n * tokens are offset by more than `maxGapTokens` past the previous file's, so occurrences in\n * different files are never gap-adjacent and merged pairs always stay within one file.\n */\nfunction computeTokenOffsets(files: CrossFileDuplicationSourceFile[], maxGapTokens: number): number[] {\n const tokenOffsets: number[] = [];\n let offset = 0;\n for (const { tokens, candidates } of files) {\n tokenOffsets.push(offset);\n // Accumulated in a loop: spreading a project-scale candidate array as call arguments would\n // overflow V8's argument limit (~124k) and crash on Node.\n let tokenCount = tokens?.length ?? 0;\n if (!tokens) {\n for (const candidate of candidates) {\n tokenCount = Math.max(tokenCount, candidate.endTokenIndex);\n }\n }\n offset += tokenCount + maxGapTokens + 1;\n }\n return tokenOffsets;\n}\n\n/** Reuses the within-file gapped (Type-3) merging in the project-wide token index space. */\nfunction mergeGapAdjacentGroups(\n groups: SelectableCandidate[][],\n tokenOffsets: number[],\n maxGapTokens: number\n): CrossFileOccurrence[][] {\n const occurrenceGroups = groups.map((group) =>\n group\n .map((candidate): CrossFileOccurrence => {\n const start = candidate.startTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n const end = candidate.endTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n return {\n file: candidate.file,\n fileIndex: candidate.regionBucket,\n spanCountedElsewhere: candidate.nestedInLargerGroup,\n nestedInLargerGroup: candidate.nestedInLargerGroup,\n segments: [{ startTokenIndex: start, endTokenIndex: end }],\n tokenCount: candidate.tokenCount,\n startTokenIndex: start,\n endTokenIndex: end,\n startIndex: candidate.startIndex,\n endIndex: candidate.endIndex,\n startLine: candidate.startLine,\n endLine: candidate.endLine,\n };\n })\n .toSorted((left, right) => left.startTokenIndex - right.startTokenIndex)\n );\n return mergeAdjacentGroups(occurrenceGroups, maxGapTokens, spansMultipleFilesAfterMerge);\n}\n\nfunction summarize(\n groups: CrossFileOccurrence[][],\n files: CrossFileDuplicationSourceFile[],\n tokenOffsets: number[]\n): CrossFileDuplicationMetrics {\n const reported: CrossFileDuplicateBlockGroup[] = [];\n // Accumulated in Maps: file names are arbitrary strings, and a plain object would read\n // inherited properties for names like \"constructor\".\n const groupCountByFile = new Map<string, number>();\n const fileDataByName = new Map(\n files.map((file, index) => [\n file.file,\n { tokens: file.tokens, codeLineNumbers: file.codeLineNumbers, offset: tokenOffsets[index] ?? 0 },\n ])\n );\n const lineNumbersByFile = new Map<string, Set<number>>();\n let duplicateBlockCount = 0;\n for (const group of groups) {\n // Mirrors within-file counting: each redundant occurrence contributes one count per matched\n // fragment, gapped merging consolidates the grouping without halving the count, and spans a\n // partial merge shares between a retained group and the merged group count once.\n duplicateBlockCount += countRedundantFragments(group);\n const occurrences = group\n .map((occurrence) => {\n const { file, startLine, endLine, tokenCount } = occurrence;\n const lineNumbers = collectOccurrenceLines(occurrence, fileDataByName, lineNumbersByFile);\n return { file, startLine, endLine, ...(lineNumbers && { lineNumbers }), tokenCount };\n })\n .toSorted((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n const files = [...new Set(occurrences.map(({ file }) => file))];\n for (const file of files) {\n groupCountByFile.set(file, (groupCountByFile.get(file) ?? 0) + 1);\n }\n reported.push({ files, occurrences, tokenCount: Math.min(...group.map(({ tokenCount }) => tokenCount)) });\n }\n reported.sort(\n (left, right) =>\n right.tokenCount - left.tokenCount ||\n (left.occurrences[0]?.file ?? '').localeCompare(right.occurrences[0]?.file ?? '') ||\n (left.occurrences[0]?.startLine ?? 0) - (right.occurrences[0]?.startLine ?? 0)\n );\n return {\n duplicateBlockCount,\n duplicateBlockGroupCountByFile: Object.fromEntries(groupCountByFile),\n duplicateLineNumbersByFile: Object.fromEntries(\n [...lineNumbersByFile].map(([file, lines]) => [file, [...lines].toSorted((left, right) => left - right)])\n ),\n groups: reported,\n };\n}\n\n/**\n * Adds the code lines an occurrence's matched tokens cover (see collectMatchedLines) to its file's\n * line set, mapping the project-wide token segments back into the file's own token stream, and\n * returns them in order.\n * A file that supplied only candidates (no token stream) is skipped rather than approximated from\n * the bounding line range, which would include gap and comment/blank lines and break the field's\n * exactness contract.\n */\nfunction collectOccurrenceLines(\n occurrence: CrossFileOccurrence,\n fileDataByName: Map<\n string,\n { tokens?: CrossFileDuplicationSourceFile['tokens']; codeLineNumbers?: Set<number>; offset: number }\n >,\n lineNumbersByFile: Map<string, Set<number>>\n): number[] | undefined {\n const fileData = fileDataByName.get(occurrence.file);\n if (!fileData?.tokens) {\n return undefined;\n }\n let fileLines = lineNumbersByFile.get(occurrence.file);\n if (!fileLines) {\n fileLines = new Set();\n lineNumbersByFile.set(occurrence.file, fileLines);\n }\n const lines = collectMatchedLines(\n occurrence.segments.map((segment) => shift(segment, -fileData.offset)),\n occurrence.matchedRuns?.map((run) => shift(run, -fileData.offset)),\n fileData.tokens,\n fileData.codeLineNumbers,\n occurrence.exactSegments?.map((segment) => shift(segment, -fileData.offset))\n );\n for (const line of lines) {\n fileLines.add(line);\n }\n return [...lines].toSorted((left, right) => left - right);\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const e=require("./duplication.cjs");function t(e,t,i,a){if(a>=100)return[];let o=x(e),s=p(o,i,a),m=t.map(u),h=t.map(l),g=o.map(({fileIndex:e,range:t})=>h[e]?.(t)??!1),_=o.map(({fileIndex:e,range:t})=>m[e]?.(t)??!1),v=({fileIndex:e},[t,n])=>m[e]?.({startTokenIndex:t,endTokenIndex:n})??!1,y=[];f(o,g,a,(e,t)=>{let n=o[e],r=o[t],i=n&&r&&s(n,r,t);if(i){if(!i.cores){_[e]&&_[t]||y.push({left:e,right:t,reordered:i.reordered});return}for(let{pair:a,reordered:o}of i.cores)v(n,a[0])&&v(r,a[1])||y.push({left:e,right:t,cores:a,reordered:o})}});let b=o.map(()=>!1),S=o.map(()=>[]);for(let e of y)for(let[t,r]of n(e))r?S[t]?.push(r):b[t]=!0;let C=[],w=[];for(let e of o.keys()){w.push(C.length);let t=b[e]?[]:c(S[e]??[]);t.length===0&&C.push({blockIndex:e,core:void 0});for(let n of t)C.push({blockIndex:e,core:n})}let T=(e,t)=>{let n=w[e]??0;if(!t||b[e])return n;for(let r=n;C[r]?.blockIndex===e;r+=1){let e=C[r]?.core;if(e&&e[0]<=t[0]&&t[1]<=e[1])return r}throw Error(`every local core lies in one of its block's merged cores`)},E=C.map((e,t)=>t),D=e=>{let t=e;for(;E[t]!==t;)t=E[t]??t;for(let n=e;E[n]!==t;){let e=E[n]??t;E[n]=t,n=e}return t},O=C.map(({blockIndex:e,core:t})=>{let n=o[e],[r,i]=t??[n?.range.startTokenIndex??0,n?.range.endTokenIndex??0];return m[n?.fileIndex??0]?.({startTokenIndex:r,endTokenIndex:i})??!1}),k=y.map(e=>n(e).map(([e,t])=>T(e,t)));for(let[e=0,t=0]of k){let n=D(e),r=D(t);E[Math.max(n,r)]=Math.min(n,r)}let A=r(o,y,k,C.length),j=/* @__PURE__ */ new Map;for(let e of C.keys()){let t=D(e),n=j.get(t)??[];n.push(e),j.set(t,n)}let M=[];for(let t of j.values()){if(t.length<2||t.every(e=>O[e]))continue;let n=/* @__PURE__ */ new Map;for(let e of t){let{blockIndex:t=0,core:r}=C[e]??{},i=n.get(t)??{cores:[],anchor:!1,matchedRuns:[]};i.cores.push(r),i.anchor||=O[e]??!1,i.matchedRuns.push(...A[e]??[]),n.set(t,i)}M.push([...n].flatMap(([t,n])=>{let r=o[t];return r?[d(r,e,n)]:[]}))}return M}function n({left:e,right:t,cores:n}){return[[e,n?.[0]],[t,n?.[1]]]}function r(e,t,n,r){let a=Array.from({length:r},()=>[]);for(let[r,o]of t.entries()){let t=e[o.left],s=e[o.right];if(t&&s){let[e,c]=i(t,s,o);a[n[r]?.[0]??0]?.push(...e),a[n[r]?.[1]??0]?.push(...c)}}return a}function i(t,n,{cores:r,reordered:i}){let c=s(t,r?.[0],i),l=s(n,r?.[1],i);if(g(a(t,r?.[0]),a(n,r?.[1]))<=0){let[t,n]=e.markLcs(c.sequence,l.sequence);return[o(c.positions,t),o(l.positions,n)]}let[u,d]=e.markLcs(l.sequence,c.sequence);return[o(c.positions,d),o(l.positions,u)]}function a(e,t){if(!t)return e.writtenContent;let{sequence:n,positions:r}=s(e,t,!1);return Float64Array.from(n,(t,n)=>t<0?t:e.fileTokenKeys[r[n]??0]??0)}function o(e,t){let n=[];for(let r of e.filter((e,n)=>t[n]===1).toSorted()){let e=n.at(-1);e?.[1]===r?e[1]=r+1:n.push([r,r+1])}return n}function s(e,t,n){let r=e.range.startTokenIndex;return t?{sequence:w(e.symbols.subarray(t[0]-r,t[1]-r)),positions:Int32Array.from({length:t[1]-t[0]},(e,n)=>t[0]+n)}:n&&e.canonical?{sequence:e.canonical.sequence,positions:e.canonical.offsets.map(e=>r+e)}:{sequence:e.sequence,positions:e.sequence.map((e,t)=>r+t)}}function c(e){let t=[];for(let[n,r]of e.toSorted((e,t)=>e[0]-t[0])){let e=t.at(-1);e&&n<e[1]?e[1]=Math.max(e[1],r):t.push([n,r])}return t}function l(e){let t=[];for(let{startTokenIndex:n,endTokenIndex:r}of e.toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex)){let e=t.at(-1);e&&n<=e[1]?e[1]=Math.max(e[1],r):t.push([n,r])}return e=>{let n=0,r=t.length;for(;n<r;){let i=n+r>>>1;(t[i]?.[0]??0)<=e.startTokenIndex?n=i+1:r=i}return(t[n-1]?.[1]??-1)>=e.endTokenIndex}}function u(e){let t=e.toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex),n=new Int32Array(t.length),r=-1;for(let[e,i]of t.entries())r=Math.max(r,i.endTokenIndex),n[e]=r;return e=>{let r=0,i=t.length;for(;r<i;){let n=r+i>>>1;(t[n]?.startTokenIndex??0)<e.endTokenIndex?r=n+1:i=n}return r>0&&(n[r-1]??-1)>e.startTokenIndex}}function d({fileIndex:e,range:t},n,{cores:r,anchor:i,matchedRuns:a}){let o=r.includes(void 0),s=r.map(e=>e??[t.startTokenIndex,t.endTokenIndex]).toSorted((e,t)=>e[0]-t[0]).map(([e,t])=>({startTokenIndex:e,endTokenIndex:t})),l=s[0]?.startTokenIndex??t.startTokenIndex,u=s.at(-1)?.endTokenIndex??t.endTokenIndex,d=n[e]?.tokens;return{fileIndex:e,spanCountedElsewhere:i||void 0,segments:s,matchedRuns:c(a).map(([e,t])=>({startTokenIndex:e,endTokenIndex:t})),tokenCount:s.reduce((e,t)=>e+t.endTokenIndex-t.startTokenIndex,0),startTokenIndex:l,endTokenIndex:u,startIndex:t.startIndex,endIndex:t.endIndex,startLine:o?t.startLine:(d?.[l]?.startRow??0)+1,endLine:o?t.endLine:(d?.[u-1]?.endRow??0)+1}}function f(e,t,n,r){let i=/* @__PURE__ */ new Map;for(let t of e)for(let e of t.ngrams)i.set(e,(i.get(e)??0)+1);for(let t of e)t.ngrams=t.ngrams.filter(e=>(i.get(e)??0)<=1e3);let a=Int32Array.from(e,e=>e.fileIndex),o=Uint8Array.from(t,Number),s=Int32Array.from(e,e=>e.sequence.length),c=Int32Array.from(e,e=>e.ngrams.length),l=[...e.keys()].toSorted((e,t)=>(s[e]??0)-(s[t]??0)),u=/* @__PURE__ */ new Map,d=new Int32Array(e.length),f=[];for(let t of l){let i=a[t],l=o[t]===1,p=Math.min(Math.ceil((s[t]??0)/3),Math.ceil(n*(s[t]??0)/100)),m=e[t]?.ngrams??[];for(let e of m){let n=u.get(e);if(!n){u.set(e,[t]);continue}for(let e=n.length-1;e>=0;--e){let t=n[e]??0;if((s[t]??0)<p)break;a[t]===i||l&&o[t]===1||(d[t]===0&&f.push(t),d[t]=(d[t]??0)+1)}n.push(t)}for(let e of f){let n=d[e]??0;d[e]=0,n*100>=10*Math.min(c[e]??0,m.length)&&r(e,t)}f.length=0}}function p(t,n,r){let i=/* @__PURE__ */ new Map;for(let e of t)for(let t of e.contentCounts.keys())i.set(t,(i.get(t)??0)+1);let a=e=>Math.min(31-Math.clz32(Math.floor((t.length+1)/e))+1,3),o=/* @__PURE__ */ new Map;for(let[e,t]of i)o.set(e,a(t));let s=e=>{let t=Int32Array.from(e.keys()).toSorted(),n=Int32Array.from(t,t=>(e.get(t)??0)*(o.get(t)??0)),r=0;for(let e of n)r+=e;return{symbols:t,weightedCounts:n,total:r}},c=new Map(t.map(e=>[e,s(e.contentCounts)])),l=-1,u,d=(t,n,r)=>((l!==n||!u)&&(l=n,u=e.createLcsLengthCounter(t.sequence)),u(r)),f=(t,i)=>{let a=[];for(let e=0,n=0;e<t.uniqueNgrams.length&&n<i.uniqueNgrams.length;){let r=t.uniqueNgrams[e]??0,o=i.uniqueNgrams[n]??0;r===o&&a.push([t.uniqueNgramOffsets[e]??0,i.uniqueNgramOffsets[n]??0]),r<=o&&(e+=1),o<=r&&(n+=1)}a.sort((e,t)=>e[0]-t[0]);let o=a.filter(([e,t],n)=>{let r=a[n-1],i=a[n+1];return r?.[0]===e-1&&r[1]===t-1||i?.[0]===e+1&&i[1]===t+1}),c=t.range.startTokenIndex,l=i.range.startTokenIndex,u=[];for(let a of v(_(o))){let[o,d]=a[0]??[0,0],[f,p]=a.at(-1)??[0,0],h=f+5,g=p+5,_=h-o,v=g-d,b=Math.min(_,v),x=r*Math.max(_,v);b<n||b*100<x||y(a)*100<50*b||!m(s(C(t.symbols,t.isContent,o,h)),s(C(i.symbols,i.isContent,d,g)))||e.lcsLength(w(t.symbols.subarray(o,h)),w(i.symbols.subarray(d,g)))*100>=x&&u.push({pair:[[c+o,c+h],[l+d,l+g]],reordered:!1})}return u.length>0?{cores:u}:void 0};return(t,n,i)=>{let a=r*Math.max(t.sequence.length,n.sequence.length);if(Math.min(t.sequence.length,n.sequence.length)*100>=a&&m(c.get(t),c.get(n))){if(b(t.sortedSequence,n.sortedSequence)*100>=a&&d(n,i,t.sequence)*100>=a)return{reordered:!1};if(t.canonical&&n.canonical&&e.lcsLength(t.canonical.sequence,n.canonical.sequence)*100>=a)return{reordered:!0}}if(g(n.writtenContent,t.writtenContent)<0){let e=f(n,t);return e?.cores&&{cores:e.cores.map(({pair:[e,t],...n})=>({...n,pair:[t,e]}))}}return f(t,n)}}function m(e,t){if(!e||!t)return!1;let n=0;for(let r=0,i=0;r<e.symbols.length&&i<t.symbols.length;){let a=e.symbols[r]??0,o=t.symbols[i]??0;a===o&&(n+=Math.min(e.weightedCounts[r]??0,t.weightedCounts[i]??0)),a<=o&&(r+=1),o<=a&&(i+=1)}return n*100>50*Math.max(e.total,t.total)}function h(e,t,n,r){if(n.length<2)return;let i=[],a=(n,r)=>{let a=w(e.subarray(n,r)),o=Float64Array.from(a,(e,r)=>e<0?e:t[n+r]??0);i.push({unit:a,content:o,start:n})},o=0;for(let[e,t]of n){let n=e-r;o<n&&a(o,n),a(n,t-r),o=t-r}o<e.length&&a(o,e.length),i.sort((e,t)=>g(e.content,t.content)||e.start-t.start);let s=new Int32Array(e.length),c=new Int32Array(e.length),l=0;for(let{unit:e,start:t}of i){s.set(e,l);for(let n=0;n<e.length;n+=1)c[l+n]=t+n;l+=e.length}return{sequence:s,offsets:c}}function g(e,t){for(let n=0;n<Math.min(e.length,t.length);n+=1){let r=(e[n]??0)-(t[n]??0);if(r!==0)return r}return e.length-t.length}function _(e){let t=[],n=[];for(let[r,[,i]]of e.entries()){let a=0,o=t.length;for(;a<o;){let n=a+o>>>1;(e[t[n]??0]?.[1]??0)<i?a=n+1:o=n}n.push(a>0?t[a-1]??-1:-1),t[a]=r}let r=[];for(let i=t.at(-1)??-1;i>=0;i=n[i]??-1){let t=e[i];t&&r.push(t)}return r.toReversed()}function v(e){let t=[];for(let[n,r]of e.entries()){let i=e[n-1];i!==void 0&&r[0]-(i[0]+5)<=30&&r[1]-(i[1]+5)<=30?t.at(-1)?.push(r):t.push([r])}return t}function y(e){let t=5;for(let n=1;n<e.length;n+=1)t+=Math.min((e[n]?.[0]??0)-(e[n-1]?.[0]??0),5);return t}function b(e,t){let n=0,r=0,i=0;for(;r<e.length&&i<t.length;){let a=e[r]??0,o=t[i]??0;a===o?(n+=1,r+=1,i+=1):a<o?r+=1:i+=1}return n}function x(e){let t=/* @__PURE__ */ new Map,n=[];for(let[r,{tokens:i,containerStatements:a,nearMissBlocks:o}]of e.entries()){if(!i||!o?.length)continue;let e=new Int32Array(i.length),s=new Uint8Array(i.length),c=new Float64Array(i.length),l=/* @__PURE__ */ new Map;for(let[n,r]of i.entries()){if(r.kind===`id`){let t=l.get(r.text);t===void 0&&(t=l.size,l.set(r.text,t)),e[n]=-(t+1);continue}let i=T(r);c[n]=i;let a=t.get(i);a===void 0&&(a=t.size,t.set(i,a)),e[n]=a,s[n]=r.isName||r.literalHash!==void 0?1:0}let u=S(a??[]);for(let t of o){let{startTokenIndex:i,endTokenIndex:a}=t,o=e.subarray(i,a),l=s.subarray(i,a),d=w(o),f=E(o,c.subarray(i,a)),p=/* @__PURE__ */ new Map;for(let e of f)p.set(e,(p.get(e)??0)+1);let m=f.keys().filter(e=>p.get(f[e]??0)===1).toArray().toSorted((e,t)=>(f[e]??0)-(f[t]??0));n.push({fileIndex:r,range:t,symbols:o,isContent:l,fileTokenKeys:c,sequence:d,writtenContent:Float64Array.from(d,(e,t)=>e<0?e:c[i+t]??0),sortedSequence:d.toSorted(),ngrams:Int32Array.from(p.keys()),uniqueNgrams:Int32Array.from(m,e=>f[e]??0),uniqueNgramOffsets:Int32Array.from(m),contentCounts:C(o,l,0,o.length),canonical:h(o,c.subarray(i,a),u(i,a),i)})}}return n}function S(e){let t=e.flat().filter(e=>e.startTokenIndex<e.endTokenIndex).map(e=>[e.startTokenIndex,e.endTokenIndex]).toSorted((e,t)=>e[0]-t[0]||t[1]-e[1]);return(e,n)=>{let r=0,i=t.length;for(;r<i;){let n=r+i>>>1;(t[n]?.[0]??0)<e?r=n+1:i=n}let a=[];for(let i=r;i<t.length;i+=1){let r=t[i];if(!r||r[0]>=n)break;let o=a.at(-1),s=o!==void 0&&r[0]<o[1];r[1]<=n&&(r[0]!==e||r[1]!==n)&&!s&&a.push(r)}return a}}function C(e,t,n,r){let i=/* @__PURE__ */ new Map;for(let a=n;a<r;a+=1)if(t[a]===1){let t=e[a]??0;i.set(t,(i.get(t)??0)+1)}return i}function w(e){let t=/* @__PURE__ */ new Map;return e.map(e=>{if(e>=0)return e;let n=t.get(e);return n===void 0&&(n=t.size,t.set(e,n)),-(n+1)})}function T(e){let t=e.textHash^Math.imul(e.literalHash??0,2654435761),n=e.textHash2^Math.imul(e.literalHash2??0,2246822507);return(t>>>0)*2097152+(n>>>11)}function E(e,t){let n=new Int32Array(Math.max(e.length-5+1,0));for(let r=0;r<n.length;r+=1){let i=5381;for(let n=0;n<5;n+=1){let a=t[r+n]??0,o=(e[r+n]??0)<0?-1:a^Math.floor(a/4294967296);i=Math.imul(i,31)+o|0}n[r]=i}return n}exports.collectCrossFileNearMissGroups=t;
|
|
1
|
+
"use strict";const e=require("./duplication.cjs");function t(e,t,a,o){if(o>=100)return[];let s=b(e),p=f(s,a,o),m=t.map(l),h=(e,[t,n])=>m[e]?.(t,n)??0,g=(e,t)=>t.reduce((t,n)=>t+n[1]-n[0]-h(e,n),0),_=s.map(e=>{let t=n(e);return h(e.fileIndex,t)===t[1]-t[0]}),v=[];d(s,_,o,(e,t)=>{let r=s[e],a=s[t],o=r&&a&&p(r,a,t);if(o)for(let{pair:s,reordered:c}of o.cores??[{pair:void 0,reordered:o.reordered}]){let[o,l]=s??[n(r),n(a)],u=(e,t)=>g(r.fileIndex,e)+g(a.fileIndex,t)>0;if(!u([o],[l]))continue;let d=i(r,a,{cores:s,reordered:c});u(...d)&&v.push({left:e,right:t,cores:s,matchedRuns:d})}});let y=s.map(()=>!1),x=s.map(()=>[]);for(let e of v)for(let[t,n]of r(e))n?x[t]?.push(n):y[t]=!0;let S=[],C=[];for(let e of s.keys()){C.push(S.length);let t=y[e]?[]:c(x[e]??[]);t.length===0&&S.push({blockIndex:e,core:void 0});for(let n of t)S.push({blockIndex:e,core:n})}let w=(e,t)=>{let n=C[e]??0;if(!t||y[e])return n;for(let r=n;S[r]?.blockIndex===e;r+=1){let e=S[r]?.core;if(e&&e[0]<=t[0]&&t[1]<=e[1])return r}throw Error(`every local core lies in one of its block's merged cores`)},T=S.map((e,t)=>t),E=e=>{let t=e;for(;T[t]!==t;)t=T[t]??t;for(let n=e;T[n]!==t;){let e=T[n]??t;T[n]=t,n=e}return t},D=S.map(({blockIndex:e,core:t})=>{let n=s[e],[r,i]=t??[n?.range.startTokenIndex??0,n?.range.endTokenIndex??0];return{length:i-r,reported:h(n?.fileIndex??0,[r,i])}}),O=D.map(({reported:e})=>e>0),k=D.map(e=>e.reported===e.length),A=v.map(e=>r(e).map(([e,t])=>w(e,t)));for(let[e=0,t=0]of A){let n=E(e),r=E(t);T[Math.max(n,r)]=Math.min(n,r)}let j=S.map(()=>[]);for(let[e,{matchedRuns:t}]of v.entries()){let[n=0,r=0]=A[e]??[];j[n]?.push(...t[0]),j[r]?.push(...t[1])}let M=/* @__PURE__ */ new Map;for(let e of S.keys()){let t=E(e),n=M.get(t)??[];n.push(e),M.set(t,n)}let N=[];for(let t of M.values()){if(t.length<2||t.every(e=>k[e]))continue;let n=/* @__PURE__ */ new Map;for(let e of t){let{blockIndex:t=0,core:r}=S[e]??{},i=n.get(t)??{cores:[],anchor:!1,matchedRuns:[]};i.cores.push(r),i.anchor||=O[e]??!1,i.matchedRuns.push(...j[e]??[]),n.set(t,i)}N.push([...n].flatMap(([t,n])=>{let r=s[t];return r?[u(r,e,n)]:[]}))}return N}function n({range:e}){return[e.startTokenIndex,e.endTokenIndex]}function r({left:e,right:t,cores:n}){return[[e,n?.[0]],[t,n?.[1]]]}function i(t,n,{cores:r,reordered:i}){let c=s(t,r?.[0],i),l=s(n,r?.[1],i);if(h(a(t,r?.[0]),a(n,r?.[1]))<=0){let[t,n]=e.markLcs(c.sequence,l.sequence);return[o(c.positions,t),o(l.positions,n)]}let[u,d]=e.markLcs(l.sequence,c.sequence);return[o(c.positions,d),o(l.positions,u)]}function a(e,t){if(!t)return e.writtenContent;let{sequence:n,positions:r}=s(e,t,!1);return Float64Array.from(n,(t,n)=>t<0?t:e.fileTokenKeys[r[n]??0]??0)}function o(e,t){let n=[];for(let r of e.filter((e,n)=>t[n]===1).toSorted()){let e=n.at(-1);e?.[1]===r?e[1]=r+1:n.push([r,r+1])}return n}function s(e,t,n){let r=e.range.startTokenIndex;return t?{sequence:C(e.symbols.subarray(t[0]-r,t[1]-r)),positions:Int32Array.from({length:t[1]-t[0]},(e,n)=>t[0]+n)}:n&&e.canonical?{sequence:e.canonical.sequence,positions:e.canonical.offsets.map(e=>r+e)}:{sequence:e.sequence,positions:e.sequence.map((e,t)=>r+t)}}function c(e){let t=[];for(let[n,r]of e.toSorted((e,t)=>e[0]-t[0])){let e=t.at(-1);e&&n<e[1]?e[1]=Math.max(e[1],r):t.push([n,r])}return t}function l(e){let t=c(e.map(e=>[e.startTokenIndex,e.endTokenIndex])),n=0,r=[0];for(let[e,i]of t)n+=i-e,r.push(n);let i=e=>{let n=0,i=t.length;for(;n<i;){let r=n+i>>>1;(t[r]?.[0]??0)<e?n=r+1:i=r}return(r[n]??0)-Math.max(0,(t[n-1]?.[1]??0)-e)};return(e,t)=>i(t)-i(e)}function u({fileIndex:e,range:t},n,{cores:r,anchor:i,matchedRuns:a}){let o=r.includes(void 0),s=r.map(e=>e??[t.startTokenIndex,t.endTokenIndex]).toSorted((e,t)=>e[0]-t[0]).map(([e,t])=>({startTokenIndex:e,endTokenIndex:t})),l=s[0]?.startTokenIndex??t.startTokenIndex,u=s.at(-1)?.endTokenIndex??t.endTokenIndex,d=n[e]?.tokens;return{fileIndex:e,spanCountedElsewhere:i||void 0,segments:s,matchedRuns:c(a).map(([e,t])=>({startTokenIndex:e,endTokenIndex:t})),tokenCount:s.reduce((e,t)=>e+t.endTokenIndex-t.startTokenIndex,0),startTokenIndex:l,endTokenIndex:u,startIndex:t.startIndex,endIndex:t.endIndex,startLine:o?t.startLine:(d?.[l]?.startRow??0)+1,endLine:o?t.endLine:(d?.[u-1]?.endRow??0)+1}}function d(e,t,n,r){let i=/* @__PURE__ */ new Map;for(let t of e)for(let e of t.ngrams)i.set(e,(i.get(e)??0)+1);for(let t of e)t.ngrams=t.ngrams.filter(e=>(i.get(e)??0)<=1e3);let a=Int32Array.from(e,e=>e.fileIndex),o=Uint8Array.from(t,Number),s=Int32Array.from(e,e=>e.sequence.length),c=Int32Array.from(e,e=>e.ngrams.length),l=[...e.keys()].toSorted((e,t)=>(s[e]??0)-(s[t]??0)),u=/* @__PURE__ */ new Map,d=new Int32Array(e.length),f=[];for(let t of l){let i=a[t],l=o[t]===1,p=Math.min(Math.ceil((s[t]??0)/3),Math.ceil(n*(s[t]??0)/100)),m=e[t]?.ngrams??[];for(let e of m){let n=u.get(e);if(!n){u.set(e,[t]);continue}for(let e=n.length-1;e>=0;--e){let t=n[e]??0;if((s[t]??0)<p)break;a[t]===i||l&&o[t]===1||(d[t]===0&&f.push(t),d[t]=(d[t]??0)+1)}n.push(t)}for(let e of f){let n=d[e]??0;d[e]=0,n*100>=10*Math.min(c[e]??0,m.length)&&r(e,t)}f.length=0}}function f(t,n,r){let i=/* @__PURE__ */ new Map;for(let e of t)for(let t of e.contentCounts.keys())i.set(t,(i.get(t)??0)+1);let a=e=>Math.min(31-Math.clz32(Math.floor((t.length+1)/e))+1,3),o=/* @__PURE__ */ new Map;for(let[e,t]of i)o.set(e,a(t));let s=e=>{let t=Int32Array.from(e.keys()).toSorted(),n=Int32Array.from(t,t=>(e.get(t)??0)*(o.get(t)??0)),r=0;for(let e of n)r+=e;return{symbols:t,weightedCounts:n,total:r}},c=new Map(t.map(e=>[e,s(e.contentCounts)])),l=-1,u,d=(t,n,r)=>((l!==n||!u)&&(l=n,u=e.createLcsLengthCounter(t.sequence)),u(r)),f=(t,i)=>{let a=[];for(let e=0,n=0;e<t.uniqueNgrams.length&&n<i.uniqueNgrams.length;){let r=t.uniqueNgrams[e]??0,o=i.uniqueNgrams[n]??0;r===o&&a.push([t.uniqueNgramOffsets[e]??0,i.uniqueNgramOffsets[n]??0]),r<=o&&(e+=1),o<=r&&(n+=1)}a.sort((e,t)=>e[0]-t[0]);let o=a.filter(([e,t],n)=>{let r=a[n-1],i=a[n+1];return r?.[0]===e-1&&r[1]===t-1||i?.[0]===e+1&&i[1]===t+1}),c=t.range.startTokenIndex,l=i.range.startTokenIndex,u=[];for(let a of _(g(o))){let[o,d]=a[0]??[0,0],[f,m]=a.at(-1)??[0,0],h=f+5,g=m+5,_=h-o,y=g-d,b=Math.min(_,y),x=r*Math.max(_,y);b<n||b*100<x||v(a)*100<50*b||!p(s(S(t.symbols,t.isContent,o,h)),s(S(i.symbols,i.isContent,d,g)))||e.lcsLength(C(t.symbols.subarray(o,h)),C(i.symbols.subarray(d,g)))*100>=x&&u.push({pair:[[c+o,c+h],[l+d,l+g]],reordered:!1})}return u.length>0?{cores:u}:void 0};return(t,n,i)=>{let a=r*Math.max(t.sequence.length,n.sequence.length);if(Math.min(t.sequence.length,n.sequence.length)*100>=a&&p(c.get(t),c.get(n))){if(y(t.sortedSequence,n.sortedSequence)*100>=a&&d(n,i,t.sequence)*100>=a)return{reordered:!1};if(t.canonical&&n.canonical&&e.lcsLength(t.canonical.sequence,n.canonical.sequence)*100>=a)return{reordered:!0}}if(h(n.writtenContent,t.writtenContent)<0){let e=f(n,t);return e?.cores&&{cores:e.cores.map(({pair:[e,t],...n})=>({...n,pair:[t,e]}))}}return f(t,n)}}function p(e,t){if(!e||!t)return!1;let n=0;for(let r=0,i=0;r<e.symbols.length&&i<t.symbols.length;){let a=e.symbols[r]??0,o=t.symbols[i]??0;a===o&&(n+=Math.min(e.weightedCounts[r]??0,t.weightedCounts[i]??0)),a<=o&&(r+=1),o<=a&&(i+=1)}return n*100>50*Math.max(e.total,t.total)}function m(e,t,n,r){if(n.length<2)return;let i=[],a=(n,r)=>{let a=C(e.subarray(n,r)),o=Float64Array.from(a,(e,r)=>e<0?e:t[n+r]??0);i.push({unit:a,content:o,start:n})},o=0;for(let[e,t]of n){let n=e-r;o<n&&a(o,n),a(n,t-r),o=t-r}o<e.length&&a(o,e.length),i.sort((e,t)=>h(e.content,t.content)||e.start-t.start);let s=new Int32Array(e.length),c=new Int32Array(e.length),l=0;for(let{unit:e,start:t}of i){s.set(e,l);for(let n=0;n<e.length;n+=1)c[l+n]=t+n;l+=e.length}return{sequence:s,offsets:c}}function h(e,t){for(let n=0;n<Math.min(e.length,t.length);n+=1){let r=(e[n]??0)-(t[n]??0);if(r!==0)return r}return e.length-t.length}function g(e){let t=[],n=[];for(let[r,[,i]]of e.entries()){let a=0,o=t.length;for(;a<o;){let n=a+o>>>1;(e[t[n]??0]?.[1]??0)<i?a=n+1:o=n}n.push(a>0?t[a-1]??-1:-1),t[a]=r}let r=[];for(let i=t.at(-1)??-1;i>=0;i=n[i]??-1){let t=e[i];t&&r.push(t)}return r.toReversed()}function _(e){let t=[];for(let[n,r]of e.entries()){let i=e[n-1];i!==void 0&&r[0]-(i[0]+5)<=30&&r[1]-(i[1]+5)<=30?t.at(-1)?.push(r):t.push([r])}return t}function v(e){let t=5;for(let n=1;n<e.length;n+=1)t+=Math.min((e[n]?.[0]??0)-(e[n-1]?.[0]??0),5);return t}function y(e,t){let n=0,r=0,i=0;for(;r<e.length&&i<t.length;){let a=e[r]??0,o=t[i]??0;a===o?(n+=1,r+=1,i+=1):a<o?r+=1:i+=1}return n}function b(e){let t=/* @__PURE__ */ new Map,n=[];for(let[r,{tokens:i,containerStatements:a,nearMissBlocks:o}]of e.entries()){if(!i||!o?.length)continue;let e=new Int32Array(i.length),s=new Uint8Array(i.length),c=new Float64Array(i.length),l=/* @__PURE__ */ new Map;for(let[n,r]of i.entries()){if(r.kind===`id`){let t=l.get(r.text);t===void 0&&(t=l.size,l.set(r.text,t)),e[n]=-(t+1);continue}let i=w(r);c[n]=i;let a=t.get(i);a===void 0&&(a=t.size,t.set(i,a)),e[n]=a,s[n]=r.isName||r.literalHash!==void 0?1:0}let u=x(a??[]);for(let t of o){let{startTokenIndex:i,endTokenIndex:a}=t,o=e.subarray(i,a),l=s.subarray(i,a),d=C(o),f=T(o,c.subarray(i,a)),p=/* @__PURE__ */ new Map;for(let e of f)p.set(e,(p.get(e)??0)+1);let h=f.keys().filter(e=>p.get(f[e]??0)===1).toArray().toSorted((e,t)=>(f[e]??0)-(f[t]??0));n.push({fileIndex:r,range:t,symbols:o,isContent:l,fileTokenKeys:c,sequence:d,writtenContent:Float64Array.from(d,(e,t)=>e<0?e:c[i+t]??0),sortedSequence:d.toSorted(),ngrams:Int32Array.from(p.keys()),uniqueNgrams:Int32Array.from(h,e=>f[e]??0),uniqueNgramOffsets:Int32Array.from(h),contentCounts:S(o,l,0,o.length),canonical:m(o,c.subarray(i,a),u(i,a),i)})}}return n}function x(e){let t=e.flat().filter(e=>e.startTokenIndex<e.endTokenIndex).map(e=>[e.startTokenIndex,e.endTokenIndex]).toSorted((e,t)=>e[0]-t[0]||t[1]-e[1]);return(e,n)=>{let r=0,i=t.length;for(;r<i;){let n=r+i>>>1;(t[n]?.[0]??0)<e?r=n+1:i=n}let a=[];for(let i=r;i<t.length;i+=1){let r=t[i];if(!r||r[0]>=n)break;let o=a.at(-1),s=o!==void 0&&r[0]<o[1];r[1]<=n&&(r[0]!==e||r[1]!==n)&&!s&&a.push(r)}return a}}function S(e,t,n,r){let i=/* @__PURE__ */ new Map;for(let a=n;a<r;a+=1)if(t[a]===1){let t=e[a]??0;i.set(t,(i.get(t)??0)+1)}return i}function C(e){let t=/* @__PURE__ */ new Map;return e.map(e=>{if(e>=0)return e;let n=t.get(e);return n===void 0&&(n=t.size,t.set(e,n)),-(n+1)})}function w(e){let t=e.textHash^Math.imul(e.literalHash??0,2654435761),n=e.textHash2^Math.imul(e.literalHash2??0,2246822507);return(t>>>0)*2097152+(n>>>11)}function T(e,t){let n=new Int32Array(Math.max(e.length-5+1,0));for(let r=0;r<n.length;r+=1){let i=5381;for(let n=0;n<5;n+=1){let a=t[r+n]??0,o=(e[r+n]??0)<0?-1:a^Math.floor(a/4294967296);i=Math.imul(i,31)+o|0}n[r]=i}return n}exports.collectCrossFileNearMissGroups=t;
|
|
2
2
|
//# sourceMappingURL=crossFileNearMiss.cjs.map
|