code-gauge 10.0.0 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -34
- package/THIRD-PARTY-NOTICES.txt +8 -8
- package/dist/checkCommand.cjs +2 -2
- package/dist/checkCommand.cjs.map +1 -1
- package/dist/checkCommand.d.ts +2 -5
- package/dist/checkCommand.js +2 -2
- package/dist/checkCommand.js.map +1 -1
- package/dist/crossFileDuplication.cjs +1 -1
- package/dist/crossFileDuplication.cjs.map +1 -1
- package/dist/crossFileDuplication.d.ts +14 -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 +6 -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 +22 -7
- package/dist/duplication.js +1 -1
- package/dist/duplication.js.map +1 -1
- package/dist/nativeMetrics.cjs +1 -1
- package/dist/nativeMetrics.cjs.map +1 -1
- package/dist/nativeMetrics.d.ts +1 -1
- package/dist/nativeMetrics.js +1 -1
- package/dist/nativeMetrics.js.map +1 -1
- package/dist/thresholdCheck.cjs +1 -1
- package/dist/thresholdCheck.cjs.map +1 -1
- package/dist/thresholdCheck.d.ts +10 -8
- package/dist/thresholdCheck.js +1 -1
- package/dist/thresholdCheck.js.map +1 -1
- package/dist/thresholds.cjs +1 -1
- package/dist/thresholds.cjs.map +1 -1
- package/dist/thresholds.d.ts +5 -3
- package/dist/thresholds.js +1 -1
- package/dist/thresholds.js.map +1 -1
- package/dist/types.d.ts +29 -7
- package/dist/wasmBinding.cjs +1 -1
- package/dist/wasmBinding.js +1 -1
- package/native/Cargo.lock +16 -16
- package/native/Cargo.toml +8 -8
- package/native/code-gauge.wasm +0 -0
- package/native/src/complexity.rs +10 -1
- package/native/src/container.rs +158 -7
- package/native/src/dep_degree.rs +13 -14
- package/native/src/duplication.rs +213 -85
- package/native/src/functions.rs +112 -24
- package/native/src/languages.rs +5 -0
- package/native/src/lib.rs +1 -1
- package/native/src/measure.rs +9 -4
- package/native/src/near_miss.rs +303 -52
- package/native/src/types.rs +3 -0
- package/package.json +8 -8
|
@@ -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:
|
|
1
|
+
"use strict";const e=require("./duplication.cjs"),t=require("./crossFileNearMiss.cjs"),n=require("./duplicateSelection.cjs");function r(t,r){let{minTokens:a,maxGapTokens:s,minSimilarityPercent:f}=e.resolveDuplicationOptions(r),p=t.flatMap(({file:e,candidates:t},n)=>t.map(t=>({...t,regionBucket:n,file:e})));for(let e of o(t,a))p.push(e);let m=n.selectMaximalGroups(p,c,(e,t)=>e.regionBucket-t.regionBucket||e.startIndex-t.startIndex),h=l(t,s),g=u([...m.values()],h,s);for(let e of i(t,g,h,a,f))g.push(e);return d(g,t,h)}function i(e,n,r,i,o){let s=e.map(()=>[]);for(let{fileIndex:e,startTokenIndex:t,endTokenIndex:i}of n.flat()){let n=r[e]??0;s[e]?.push({startTokenIndex:t-n,endTokenIndex:i-n})}return t.collectCrossFileNearMissGroups(e,s,i,o).map(t=>t.map(t=>{let n=r[t.fileIndex]??0;return{...t,file:e[t.fileIndex]?.file??``,segments:t.segments.map(e=>a(e,n)),matchedRuns:t.matchedRuns?.map(e=>a(e,n)),startTokenIndex:t.startTokenIndex+n,endTokenIndex:t.endTokenIndex+n}}))}function a({startTokenIndex:e,endTokenIndex:t},n){return{startTokenIndex:e+n,endTokenIndex:t+n}}function o(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 s(e){return new Set(e.map(e=>e.file)).size>=2}function c(e){return e.length>=2&&new Set(e.map(e=>e.regionBucket)).size>=2}function l(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 u(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,s)}function d(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=f(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 f(t,n,r){let i=n.get(t.file);if(!i?.tokens)return;let o=r.get(t.file);o||(o=/* @__PURE__ */ new Set,r.set(t.file,o));let s=e.collectMatchedLines(t.segments.map(e=>a(e,-i.offset)),t.matchedRuns?.map(e=>a(e,-i.offset)),i.tokens,i.codeLineNumbers);for(let e of s)o.add(e);return[...s].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"],"sources":["../src/crossFileDuplication.ts"],"sourcesContent":["import { collectCrossFileNearMissGroups } from './crossFileNearMiss.js';\nimport { selectMaximalGroups, type SelectableRegion } from './duplicateSelection.js';\nimport {\n buildLiteralCountPrefix,\n collectSegmentLines,\n collectSequenceWindowCandidates,\n countRedundantFragments,\n mergeAdjacentGroups,\n resolveDuplicationOptions,\n type CountedOccurrence,\n type CrossFileDuplicateCandidate,\n type CrossFileDuplicationFileData,\n type SequenceWindowContext,\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 startLine: 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 every token of a near-miss\n * block or its matched cores, edited ones included (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 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) => ({\n startTokenIndex: segment.startTokenIndex + offset,\n endTokenIndex: segment.endTokenIndex + offset,\n })),\n startTokenIndex: occurrence.startTokenIndex + offset,\n endTokenIndex: occurrence.endTokenIndex + offset,\n };\n })\n );\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 for (const occurrence of group) {\n collectOccurrenceLines(occurrence, fileDataByName, lineNumbersByFile);\n }\n const occurrences = group\n .map(({ file, startLine, endLine }) => ({ file, startLine, endLine }))\n .toSorted((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n const files = [...new Set(occurrences.map(({ file }) => file))];\n for (const file of files) {\n groupCountByFile.set(file, (groupCountByFile.get(file) ?? 0) + 1);\n }\n 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 segment tokens cover (matched tokens of an exact or gapped\n * occurrence, the whole block or matched cores of a near-miss one) to its file's line set, mapping the\n * project-wide token segments back into the file's own token stream. A file that supplied only\n * candidates (no token stream) is skipped rather than approximated from the bounding line range,\n * which would include gap and comment/blank lines and break the field's 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): void {\n const fileData = fileDataByName.get(occurrence.file);\n if (!fileData?.tokens) {\n return;\n }\n let lines = lineNumbersByFile.get(occurrence.file);\n if (!lines) {\n lines = new Set();\n lineNumbersByFile.set(occurrence.file, lines);\n }\n for (const segment of occurrence.segments) {\n collectSegmentLines(\n {\n startTokenIndex: segment.startTokenIndex - fileData.offset,\n endTokenIndex: segment.endTokenIndex - fileData.offset,\n },\n fileData.tokens,\n fileData.codeLineNumbers,\n lines\n );\n }\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 (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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"}
|
|
@@ -7,7 +7,18 @@ export interface CrossFileDuplicationSourceFile extends Partial<CrossFileDuplica
|
|
|
7
7
|
export interface CrossFileDuplicateOccurrence {
|
|
8
8
|
endLine: number;
|
|
9
9
|
file: string;
|
|
10
|
+
/**
|
|
11
|
+
* The code lines carrying the occurrence's matched tokens (see
|
|
12
|
+
* DuplicateBlockOccurrence.lineNumbers). Absent for a file that supplied only candidates (no
|
|
13
|
+
* `tokens`), whose matched lines are unknowable.
|
|
14
|
+
*/
|
|
15
|
+
lineNumbers?: number[];
|
|
10
16
|
startLine: number;
|
|
17
|
+
/**
|
|
18
|
+
* The tokens of its matched runs, or of a near-miss block or its matched cores with the edited
|
|
19
|
+
* tokens among them; the group's `tokenCount` is the smallest of its occurrences'.
|
|
20
|
+
*/
|
|
21
|
+
tokenCount: number;
|
|
11
22
|
}
|
|
12
23
|
export interface CrossFileDuplicateBlockGroup {
|
|
13
24
|
files: string[];
|
|
@@ -26,8 +37,9 @@ export interface CrossFileDuplicationMetrics {
|
|
|
26
37
|
duplicateBlockGroupCountByFile: Record<string, number>;
|
|
27
38
|
/**
|
|
28
39
|
* Per file, the 1-based code lines covered by the tokens of its cross-file occurrences, sorted
|
|
29
|
-
* ascending: the matched tokens of exact and gapped occurrences, and
|
|
30
|
-
* block or its matched cores
|
|
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 (like within-file
|
|
42
|
+
* near-miss coverage). The unmatched gap of a merged
|
|
31
43
|
* clone and comment/blank lines inside an occurrence's bounding range are excluded (blank rows
|
|
32
44
|
* inside multi-row tokens only when the file supplied codeLineNumbers). A file that supplied
|
|
33
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,
|
|
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 d(e,n))o.push(t);let c=s(o,p,(e,t)=>e.regionBucket-t.regionBucket||e.startIndex-t.startIndex),u=m(e,r),f=h([...c.values()],u,r);for(let t of l(e,f,u,n,i))f.push(t);return g(f,e,u)}function l(e,t,n,r,i){let a=e.map(()=>[]);for(let{fileIndex:e,startTokenIndex:r,endTokenIndex:i}of t.flat()){let t=n[e]??0;a[e]?.push({startTokenIndex:r-t,endTokenIndex:i-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=>u(e,r)),matchedRuns:t.matchedRuns?.map(e=>u(e,r)),startTokenIndex:t.startTokenIndex+r,endTokenIndex:t.endTokenIndex+r}}))}function u({startTokenIndex:e,endTokenIndex:t},n){return{startTokenIndex:e+n,endTokenIndex:t+n}}function d(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 f(e){return new Set(e.map(e=>e.file)).size>=2}function p(e){return e.length>=2&&new Set(e.map(e=>e.regionBucket)).size>=2}function m(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 h(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,f)}function g(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=_(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 _(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=>u(e,-i.offset)),e.matchedRuns?.map(e=>u(e,-i.offset)),i.tokens,i.codeLineNumbers);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 collectSegmentLines,\n collectSequenceWindowCandidates,\n countRedundantFragments,\n mergeAdjacentGroups,\n resolveDuplicationOptions,\n type CountedOccurrence,\n type CrossFileDuplicateCandidate,\n type CrossFileDuplicationFileData,\n type SequenceWindowContext,\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 startLine: 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 every token of a near-miss\n * block or its matched cores, edited ones included (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 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) => ({\n startTokenIndex: segment.startTokenIndex + offset,\n endTokenIndex: segment.endTokenIndex + offset,\n })),\n startTokenIndex: occurrence.startTokenIndex + offset,\n endTokenIndex: occurrence.endTokenIndex + offset,\n };\n })\n );\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 for (const occurrence of group) {\n collectOccurrenceLines(occurrence, fileDataByName, lineNumbersByFile);\n }\n const occurrences = group\n .map(({ file, startLine, endLine }) => ({ file, startLine, endLine }))\n .toSorted((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n const files = [...new Set(occurrences.map(({ file }) => file))];\n for (const file of files) {\n groupCountByFile.set(file, (groupCountByFile.get(file) ?? 0) + 1);\n }\n 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 segment tokens cover (matched tokens of an exact or gapped\n * occurrence, the whole block or matched cores of a near-miss one) to its file's line set, mapping the\n * project-wide token segments back into the file's own token stream. A file that supplied only\n * candidates (no token stream) is skipped rather than approximated from the bounding line range,\n * which would include gap and comment/blank lines and break the field's 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): void {\n const fileData = fileDataByName.get(occurrence.file);\n if (!fileData?.tokens) {\n return;\n }\n let lines = lineNumbersByFile.get(occurrence.file);\n if (!lines) {\n lines = new Set();\n lineNumbersByFile.set(occurrence.file, lines);\n }\n for (const segment of occurrence.segments) {\n collectSegmentLines(\n {\n startTokenIndex: segment.startTokenIndex - fileData.offset,\n endTokenIndex: segment.endTokenIndex - fileData.offset,\n },\n fileData.tokens,\n fileData.codeLineNumbers,\n lines\n );\n }\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 (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":"oVA+FA,SAAgB,EACd,EACA,EAC6B,CAC7B,GAAM,CAAE,YAAW,eAAc,wBAAyB,EAA0B,CAAO,EACrF,EAAoC,EAAM,SAAS,CAAE,OAAM,cAAc,IAC7E,EAAW,IAAK,IAAe,CAAE,GAAG,EAAW,aAAc,EAAW,MAAK,EAAE,CACjF,EAGA,IAAK,IAAM,KAAa,EAAwB,EAAO,CAAS,EAC9D,EAAW,KAAK,CAAS,EAE3B,IAAM,EAAU,EACd,EACA,GAEC,EAAM,IAAU,EAAK,aAAe,EAAM,cAAgB,EAAK,WAAa,EAAM,UACrF,EACM,EAAe,EAAoB,EAAO,CAAY,EACtD,EAAS,EAAuB,CAAC,GAAG,EAAQ,OAAO,CAAC,EAAG,EAAc,CAAY,EACvF,IAAK,IAAM,KAAS,EAAsB,EAAO,EAAQ,EAAc,EAAW,CAAoB,EACpG,EAAO,KAAK,CAAK,EAEnB,OAAO,EAAU,EAAQ,EAAO,CAAY,CAC9C,CAGA,SAAS,EACP,EACA,EACA,EACA,EACA,EACyB,CACzB,IAAM,EAA8E,EAAM,QAAU,CAAC,CAAC,EACtG,IAAK,GAAM,CAAE,YAAW,kBAAiB,mBAAmB,EAAY,KAAK,EAAG,CAC9E,IAAM,EAAS,EAAa,IAAc,EAC1C,EAAoB,EAAU,EAAE,KAAK,CACnC,gBAAiB,EAAkB,EACnC,cAAe,EAAgB,CACjC,CAAC,CACH,CACA,OAAO,EAA+B,EAAO,EAAqB,EAAW,CAAoB,CAAC,CAAC,IAAK,GACtG,EAAM,IAAK,GAAe,CACxB,IAAM,EAAS,EAAa,EAAW,YAAc,EACrD,MAAO,CACL,GAAG,EACH,KAAM,EAAM,EAAW,UAAU,EAAE,MAAQ,GAC3C,SAAU,EAAW,SAAS,IAAK,GAAY,EAAM,EAAS,CAAM,CAAC,EACrE,YAAa,EAAW,aAAa,IAAK,GAAQ,EAAM,EAAK,CAAM,CAAC,EACpE,gBAAiB,EAAW,gBAAkB,EAC9C,cAAe,EAAW,cAAgB,CAC5C,CACF,CAAC,CACH,CACF,CAEA,SAAS,EAAM,CAAE,kBAAiB,iBAA+B,EAA8B,CAC7F,MAAO,CAAE,gBAAiB,EAAkB,EAAQ,cAAe,EAAgB,CAAO,CAC5F,CAGA,SAAS,EAAwB,EAAyC,EAA0C,CAClH,IAAM,EAA+B,CAAC,EAChC,EAAoC,CAAC,EAC3C,IAAK,GAAM,CAAC,EAAW,CAAE,SAAQ,0BAA0B,EAAM,QAAQ,EACnE,GAAU,IACZ,EAAmB,KAAK,CAAS,EACjC,EAAS,KAAK,CAAE,SAAQ,mBAAoB,EAAwB,CAAM,EAAG,WAAY,CAAoB,CAAC,GAMlH,OAHI,EAAS,OAAS,EACb,CAAC,EAEH,EAAgC,EAAU,EAAW,EAAI,CAAC,CAAC,SAAS,CAAE,YAAW,kBAAmB,CACzG,IAAM,EAAY,EAAmB,GAC/B,EAAO,IAAc,IAAA,GAAY,IAAA,GAAY,EAAM,GACzD,OAAO,IAAc,IAAA,IAAa,IAAS,IAAA,GACvC,CAAC,EACD,CAAC,CAAE,GAAG,EAAW,aAAc,EAAW,KAAM,EAAK,IAAK,CAAC,CACjE,CAAC,CACH,CAGA,SAAS,EAA6B,EAAuC,CAC3E,OAAO,IAAI,IAAI,EAAM,IAAK,GAAe,EAAW,IAAI,CAAC,CAAC,CAAC,MAAQ,CACrE,CAEA,SAAS,EAAmB,EAAuC,CACjE,OAAO,EAAM,QAAU,GAAK,IAAI,IAAI,EAAM,IAAK,GAAc,EAAU,YAAY,CAAC,CAAC,CAAC,MAAQ,CAChG,CAOA,SAAS,EAAoB,EAAyC,EAAgC,CACpG,IAAM,EAAyB,CAAC,EAC5B,EAAS,EACb,IAAK,GAAM,CAAE,SAAQ,gBAAgB,EAAO,CAC1C,EAAa,KAAK,CAAM,EAGxB,IAAI,EAAa,GAAQ,QAAU,EACnC,GAAI,CAAC,EACH,IAAK,IAAM,KAAa,EACtB,EAAa,KAAK,IAAI,EAAY,EAAU,aAAa,EAG7D,GAAU,EAAa,EAAe,CACxC,CACA,OAAO,CACT,CAGA,SAAS,EACP,EACA,EACA,EACyB,CACzB,IAAM,EAAmB,EAAO,IAAK,GACnC,EACG,IAAK,GAAmC,CACvC,IAAM,EAAQ,EAAU,iBAAmB,EAAa,EAAU,eAAiB,GAC7E,EAAM,EAAU,eAAiB,EAAa,EAAU,eAAiB,GAC/E,MAAO,CACL,KAAM,EAAU,KAChB,UAAW,EAAU,aACrB,qBAAsB,EAAU,oBAChC,oBAAqB,EAAU,oBAC/B,SAAU,CAAC,CAAE,gBAAiB,EAAO,cAAe,CAAI,CAAC,EACzD,WAAY,EAAU,WACtB,gBAAiB,EACjB,cAAe,EACf,WAAY,EAAU,WACtB,SAAU,EAAU,SACpB,UAAW,EAAU,UACrB,QAAS,EAAU,OACrB,CACF,CAAC,CAAC,CACD,UAAU,EAAM,IAAU,EAAK,gBAAkB,EAAM,eAAe,CAC3E,EACA,OAAO,EAAoB,EAAkB,EAAc,CAA4B,CACzF,CAEA,SAAS,EACP,EACA,EACA,EAC6B,CAC7B,IAAM,EAA2C,CAAC,EAG5C,kBAAmB,IAAI,IACvB,EAAiB,IAAI,IACzB,EAAM,KAAK,EAAM,IAAU,CACzB,EAAK,KACL,CAAE,OAAQ,EAAK,OAAQ,gBAAiB,EAAK,gBAAiB,OAAQ,EAAa,IAAU,CAAE,CACjG,CAAC,CACH,EACM,kBAAoB,IAAI,IAC1B,EAAsB,EAC1B,IAAK,IAAM,KAAS,EAAQ,CAI1B,GAAuB,EAAwB,CAAK,EACpD,IAAM,EAAc,EACjB,IAAK,GAAe,CACnB,GAAM,CAAE,OAAM,YAAW,UAAS,cAAe,EAC3C,EAAc,EAAuB,EAAY,EAAgB,CAAiB,EACxF,MAAO,CAAE,OAAM,YAAW,UAAS,GAAI,GAAe,CAAE,aAAY,EAAI,YAAW,CACrF,CAAC,CAAC,CACD,UAAU,EAAM,IAAU,EAAK,KAAK,cAAc,EAAM,IAAI,GAAK,EAAK,UAAY,EAAM,SAAS,EAC9F,EAAQ,CAAC,GAAG,IAAI,IAAI,EAAY,KAAK,CAAE,UAAW,CAAI,CAAC,CAAC,EAC9D,IAAK,IAAM,KAAQ,EACjB,EAAiB,IAAI,GAAO,EAAiB,IAAI,CAAI,GAAK,GAAK,CAAC,EAElE,EAAS,KAAK,CAAE,QAAO,cAAa,WAAY,KAAK,IAAI,GAAG,EAAM,KAAK,CAAE,gBAAiB,CAAU,CAAC,CAAE,CAAC,CAC1G,CAOA,OANA,EAAS,MACN,EAAM,IACL,EAAM,WAAa,EAAK,aACvB,EAAK,YAAY,EAAE,EAAE,MAAQ,GAAA,CAAI,cAAc,EAAM,YAAY,EAAE,EAAE,MAAQ,EAAE,IAC/E,EAAK,YAAY,EAAE,EAAE,WAAa,IAAM,EAAM,YAAY,EAAE,EAAE,WAAa,EAChF,EACO,CACL,sBACA,+BAAgC,OAAO,YAAY,CAAgB,EACnE,2BAA4B,OAAO,YACjC,CAAC,GAAG,CAAiB,CAAC,CAAC,KAAK,CAAC,EAAM,KAAW,CAAC,EAAM,CAAC,GAAG,CAAK,CAAC,CAAC,UAAU,EAAM,IAAU,EAAO,CAAK,CAAC,CAAC,CAC1G,EACA,OAAQ,CACV,CACF,CAUA,SAAS,EACP,EACA,EAIA,EACsB,CACtB,IAAM,EAAW,EAAe,IAAI,EAAW,IAAI,EACnD,GAAI,CAAC,GAAU,OACb,OAEF,IAAI,EAAY,EAAkB,IAAI,EAAW,IAAI,EAChD,IACH,kBAAY,IAAI,IAChB,EAAkB,IAAI,EAAW,KAAM,CAAS,GAElD,IAAM,EAAQ,EACZ,EAAW,SAAS,IAAK,GAAY,EAAM,EAAS,CAAC,EAAS,MAAM,CAAC,EACrE,EAAW,aAAa,IAAK,GAAQ,EAAM,EAAK,CAAC,EAAS,MAAM,CAAC,EACjE,EAAS,OACT,EAAS,eACX,EACA,IAAK,IAAM,KAAQ,EACjB,EAAU,IAAI,CAAI,EAEpB,MAAO,CAAC,GAAG,CAAK,CAAC,CAAC,UAAU,EAAM,IAAU,EAAO,CAAK,CAC1D"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const e=require("./duplication.cjs");function t(e,t,c,l){if(l>=100)return[];let u=g(e),d=s(u,c,l),f=t.map(i),p=t.map(r),m=u.map(({fileIndex:e,range:t})=>p[e]?.(t)??!1),h=u.map(({fileIndex:e,range:t})=>f[e]?.(t)??!1),_=[];o(u,m,l,(e,t)=>{let n=u[e],r=u[t],i=n&&r&&d(n,r,t);if(i){if(i.kind===`whole`)h[e]&&h[t]||_.push([e,void 0,t,void 0]);else for(let[n,r]of i.cores)_.push([e,n,t,r])}});let v=u.map(()=>!1),y=u.map(()=>[]);for(let[e,t,n,r]of _)for(let[i,a]of[[e,t],[n,r]])a?y[i]?.push(a):v[i]=!0;let b=[],x=[];for(let e of u.keys()){x.push(b.length);let t=v[e]?[]:n(y[e]??[]);t.length===0&&b.push({blockIndex:e,core:void 0});for(let n of t)b.push({blockIndex:e,core:n})}let S=(e,t)=>{let n=x[e]??0;if(!t||v[e])return n;for(let r=n;b[r]?.blockIndex===e;r+=1){let e=b[r]?.core;if(e&&e[0]<=t[0]&&t[1]<=e[1])return r}throw Error(`every local core lies in one of its block's merged cores`)},C=b.map((e,t)=>t),w=e=>{let t=e;for(;C[t]!==t;)t=C[t]??t;for(let n=e;C[n]!==t;){let e=C[n]??t;C[n]=t,n=e}return t},T=b.map(({blockIndex:e,core:t})=>{let n=u[e],[r,i]=t??[n?.range.startTokenIndex??0,n?.range.endTokenIndex??0];return f[n?.fileIndex??0]?.({startTokenIndex:r,endTokenIndex:i})??!1});for(let[e,t,n,r]of _){let i=S(e,t),a=S(n,r);if(T[i]&&T[a])continue;let o=w(i),s=w(a);C[Math.max(o,s)]=Math.min(o,s)}let E=/* @__PURE__ */ new Map;for(let e of b.keys()){let t=w(e),n=E.get(t)??[];n.push(e),E.set(t,n)}let D=[];for(let t of E.values()){if(t.length<2||t.every(e=>T[e]))continue;let n=/* @__PURE__ */ new Map;for(let e of t){let{blockIndex:t=0,core:r}=b[e]??{},i=n.get(t)??{cores:[],anchor:!1};i.cores.push(r),i.anchor||=T[e]??!1,n.set(t,i)}D.push([...n].flatMap(([t,{cores:n,anchor:r}])=>{let i=u[t];return i?[a(i,e,n,r)]:[]}))}return D}function n(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 r(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 i(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 a({fileIndex:e,range:t},n,r,i){let a=r.includes(void 0),o=r.map(e=>e??[t.startTokenIndex,t.endTokenIndex]).toSorted((e,t)=>e[0]-t[0]).map(([e,t])=>({startTokenIndex:e,endTokenIndex:t})),s=o[0]?.startTokenIndex??t.startTokenIndex,c=o.at(-1)?.endTokenIndex??t.endTokenIndex,l=n[e]?.tokens;return{fileIndex:e,spanCountedElsewhere:i||void 0,segments:o,tokenCount:o.reduce((e,t)=>e+t.endTokenIndex-t.startTokenIndex,0),startTokenIndex:s,endTokenIndex:c,startIndex:t.startIndex,endIndex:t.endIndex,startLine:a?t.startLine:(l?.[s]?.startRow??0)+1,endLine:a?t.endLine:(l?.[c-1]?.endRow??0)+1}}function o(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 s(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}},u=new Map(t.map(e=>[e,s(e.contentCounts)])),d=-1,g,_=(t,n,r)=>((d!==n||!g)&&(d=n,g=e.createLcsLengthCounter(t.sequence)),g(r)),b=(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}),l=t.range.startTokenIndex,u=i.range.startTokenIndex,d=[];for(let a of p(f(o))){let[o,f]=a[0]??[0,0],[p,h]=a.at(-1)??[0,0],g=p+5,_=h+5,b=g-o,x=_-f,S=Math.min(b,x),C=r*Math.max(b,x);S>=n&&S*100>=C&&m(a)*100>=50*S&&c(s(v(t.symbols,t.isContent,o,g)),s(v(i.symbols,i.isContent,f,_)))&&e.lcsLength(y(t.symbols.subarray(o,g)),y(i.symbols.subarray(f,_)))*100>=C&&d.push([[l+o,l+g],[u+f,u+_]])}return d.length>0?{kind:`local`,cores:d}:void 0};return(e,t,n)=>{let i=r*Math.max(e.sequence.length,t.sequence.length);return Math.min(e.sequence.length,t.sequence.length)*100>=i&&c(u.get(e),u.get(t))&&(h(e.sortedSequence,t.sortedSequence)*100>=i&&_(t,n,e.sequence)*100>=i||l(e,t,i))?{kind:`whole`}:b(e,t)}}function c(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 l(t,n,r){return t.canonicalSequence!==void 0&&n.canonicalSequence!==void 0&&e.lcsLength(t.canonicalSequence,n.canonicalSequence)*100>=r}function u(e,t,n){if(t.length<2)return;let r=[],i=0;for(let[a,o]of t){let t=a-n;i<t&&r.push(y(e.subarray(i,t))),r.push(y(e.subarray(t,o-n))),i=o-n}i<e.length&&r.push(y(e.subarray(i))),r.sort(d);let a=new Int32Array(e.length),o=0;for(let e of r)a.set(e,o),o+=e.length;return a}function d(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 f(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 p(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 m(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 h(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 g(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=/* @__PURE__ */ new Map;for(let[n,r]of i.entries()){if(r.kind===`id`){let t=c.get(r.text);t===void 0&&(t=c.size,c.set(r.text,t)),e[n]=-(t+1);continue}let i=b(r),a=t.get(i);a===void 0&&(a=t.size,t.set(i,a)),e[n]=a,s[n]=r.isName||r.literalHash!==void 0?1:0}let l=_(a??[]);for(let t of o){let{startTokenIndex:i,endTokenIndex:a}=t,o=e.subarray(i,a),c=s.subarray(i,a),d=y(o),f=x(o),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:c,sequence:d,sortedSequence:d.toSorted(),ngrams:Int32Array.from(p.keys()),uniqueNgrams:Int32Array.from(m,e=>f[e]??0),uniqueNgramOffsets:Int32Array.from(m),contentCounts:v(o,c,0,o.length),canonicalSequence:u(o,l(i,a),i)})}}return n}function _(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 v(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 y(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 b(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 x(e){let t=new Int32Array(Math.max(e.length-5+1,0));for(let n=0;n<t.length;n+=1){let r=5381;for(let t=0;t<5;t+=1){let i=e[n+t]??0;r=Math.imul(r,31)+(i<0?-1:i)|0}t[n]=r}return t}exports.collectCrossFileNearMissGroups=t;
|
|
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;
|
|
2
2
|
//# sourceMappingURL=crossFileNearMiss.cjs.map
|