code-gauge 10.0.0 → 10.1.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.
@@ -1,2 +1,2 @@
1
- import{buildLiteralCountPrefix as e,collectSegmentLines 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 u(e,n))o.push(t);let c=s(o,f,(e,t)=>e.regionBucket-t.regionBucket||e.startIndex-t.startIndex),d=p(e,r),g=m([...c.values()],d,r);for(let t of l(e,g,d,n,i))g.push(t);return h(g,e,d)}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=>({startTokenIndex:e.startTokenIndex+r,endTokenIndex:e.endTokenIndex+r})),startTokenIndex:t.startTokenIndex+r,endTokenIndex:t.endTokenIndex+r}}))}function u(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 d(e){return new Set(e.map(e=>e.file)).size>=2}function f(e){return e.length>=2&&new Set(e.map(e=>e.regionBucket)).size>=2}function p(e,t){let n=[],r=0;for(let{tokens:i,candidates:a}of e){n.push(r);let e=i?.length??0;if(!i)for(let t of a)e=Math.max(e,t.endTokenIndex);r+=e+t+1}return n}function m(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,d)}function h(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);for(let e of t)g(e,o,s);let e=t.map(({file:e,startLine:t,endLine:n})=>({file:e,startLine:t,endLine:n})).toSorted((e,t)=>e.file.localeCompare(t.file)||e.startLine-t.startLine),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 g(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));for(let n of e.segments)t({startTokenIndex:n.startTokenIndex-i.offset,endTokenIndex:n.endTokenIndex-i.offset},i.tokens,i.codeLineNumbers,a)}export{c as measureCrossFileDuplication};
1
+ import{buildLiteralCountPrefix as e,collectSegmentLines 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 u(e,n))o.push(t);let c=s(o,f,(e,t)=>e.regionBucket-t.regionBucket||e.startIndex-t.startIndex),d=p(e,r),g=m([...c.values()],d,r);for(let t of l(e,g,d,n,i))g.push(t);return h(g,e,d)}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=>({startTokenIndex:e.startTokenIndex+r,endTokenIndex:e.endTokenIndex+r})),startTokenIndex:t.startTokenIndex+r,endTokenIndex:t.endTokenIndex+r}}))}function u(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 d(e){return new Set(e.map(e=>e.file)).size>=2}function f(e){return e.length>=2&&new Set(e.map(e=>e.regionBucket)).size>=2}function p(e,t){let n=[],r=0;for(let{tokens:i,candidates:a}of e){n.push(r);let e=i?.length??0;if(!i)for(let t of a)e=Math.max(e,t.endTokenIndex);r+=e+t+1}return n}function m(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,d)}function h(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=g(e,o,s);return{file:t,startLine:n,endLine:r,...a&&{lineNumbers:a},tokenCount:i}}).toSorted((e,t)=>e.file.localeCompare(t.file)||e.startLine-t.startLine),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 g(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=/* @__PURE__ */ new Set;for(let n of e.segments)t({startTokenIndex:n.startTokenIndex-i.offset,endTokenIndex:n.endTokenIndex-i.offset},i.tokens,i.codeLineNumbers,o);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 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 /**\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 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 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 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, and 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 = new Set<number>();\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 for (const line of lines) {\n fileLines.add(line);\n }\n return [...lines].toSorted((left, right) => left - right);\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
1
  "use strict";let e=require("@willbooster/shared-lib");function t(e,t,n,i,a=!1){return JSON.parse(f().measureCodeNative(...r(e,t,n,i,a)))}function n(e,t,n,i,a=!1){return f().measureCodeNativeAsync(...r(e,t,n,i,a)).then(e=>JSON.parse(e))}function r(e,t,n,r,i){return[o(e),t,n,s(r?.minTokens),s(r?.maxGapTokens),s(r?.minSimilarityPercent),i]}function i(e,t,n){return JSON.parse(f().collectCrossFileDataNative(o(e),t,s(n)))}function a(e,t){return JSON.parse(f().collectFunctionTokenSequencesNative(o(e),t)).map(e=>Int32Array.from(e))}function o(e){return e.isWellFormed()?e:e.toWellFormed()}function s(t){return t===void 0||Number.isNaN(t)?void 0:(0,e.clamp)(Math.trunc(t),0,4294967295)}var c=class extends Error{};let l,u;function d(e){l=e}function f(){if(l)return l;if(u)throw u;let t=process.getBuiltinModule(`node:module`).createRequire(require("url").pathToFileURL(__filename).href),n=[`code-gauge-${p()}`,`../native/code-gauge.node`],r=[];for(let i of n){let n;try{n=t(i)}catch(t){r.push(` ${i}: ${(0,e.getErrorMessage)(t).split(`
2
- `)[0]}`);continue}let a=n.payloadVersion?.();if(a!==10){r.push(` ${i}: payload version ${a??`unknown`} does not match the expected 10; rebuild the addon with \`bun run build-native\``);continue}return l=n,n}throw u=new c(`The code-gauge native addon is not available for ${p()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain).\n${r.join(`
2
+ `)[0]}`);continue}let a=n.payloadVersion?.();if(a!==12){r.push(` ${i}: payload version ${a??`unknown`} does not match the expected 12; rebuild the addon with \`bun run build-native\``);continue}return l=n,n}throw u=new c(`The code-gauge native addon is not available for ${p()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain).\n${r.join(`
3
3
  `)}`),u}function p(){let e=`${process.platform}-${process.arch}`;return process.platform===`win32`?`${e}-msvc`:process.platform===`linux`?(process.report?.getReport())?.header?.glibcVersionRuntime?`${e}-gnu`:`${e}-musl`:e}exports.NativeAddonError=c,exports.collectCrossFileDataNative=i,exports.collectFunctionTokenSequencesNative=a,exports.measureCodeNative=t,exports.measureCodeNativeAsync=n,exports.setNativeBinding=d;
4
4
  //# sourceMappingURL=nativeMetrics.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"nativeMetrics.cjs","names":["clamp","getErrorMessage"],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { clamp, getErrorMessage } from '@willbooster/shared-lib';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead' | 'cognitiveBlocks'> {\n /** Absent when the function has none. */\n cognitiveBlocks?: FunctionMetrics['cognitiveBlocks'];\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ntype NativeMeasureArguments = [\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens: number | undefined,\n maxGapTokens: number | undefined,\n minSimilarityPercent: number | undefined,\n includeCrossFileData: boolean,\n];\n\nexport interface NativeBinding {\n measureCodeNative(...args: NativeMeasureArguments): string;\n measureCodeNativeAsync(...args: NativeMeasureArguments): Promise<string>;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields, or lack newer binding functions, instead of failing with a clear rebuild message.\n */\nexport const expectedPayloadVersion = 10;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n ) as NativeMetricsPayload;\n}\n\n/**\n * measureCodeNative on the addon's worker threads, so several files can be measured in parallel.\n * A missing addon still throws synchronously, like measureCodeNative.\n */\nexport function measureCodeNativeAsync(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): Promise<NativeMetricsPayload> {\n return loadBinding()\n .measureCodeNativeAsync(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n .then((json) => JSON.parse(json) as NativeMetricsPayload);\n}\n\n/** The binding arguments shared by the sync and async measurements, so both measure alike. */\nfunction toNativeMeasureArguments(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication: DuplicationOptions | undefined,\n includeCrossFileData: boolean\n): NativeMeasureArguments {\n return [\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData,\n ];\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value) ? undefined : clamp(Math.trunc(value), 0, 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\n/** Replaces the N-API addon, e.g. with the WebAssembly build on runtimes without N-API. */\nexport function setNativeBinding(binding: NativeBinding): void {\n cachedBinding = binding;\n}\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n // node:module is loaded lazily so that runtimes without it can bundle this module.\n const requireNative = process.getBuiltinModule('node:module').createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${getErrorMessage(error).split('\\n')[0]}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n `toolchain).\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"sDAqEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CACF,CACF,CAMA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACQ,CAC/B,OAAO,EAAY,CAAC,CACjB,uBACC,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CAAC,CACA,KAAM,GAAS,KAAK,MAAM,CAAI,CAAyB,CAC5D,CAGA,SAAS,EACP,EACA,EACA,EACA,EACA,EACwB,CACxB,MAAO,CACL,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAAI,IAAA,IAAA,EAAYA,EAAAA,MAAAA,CAAM,KAAK,MAAM,CAAK,EAAG,EAAG,UAAa,CAC3G,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAGJ,SAAgB,EAAiB,EAA8B,CAC7D,EAAgB,CAClB,CAEA,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAIR,IAAM,EAAgB,QAAQ,iBAAiB,aAAa,CAAC,CAAC,cAAA,QAAA,KAAA,CAAA,CAAA,cAAA,UAAA,CAAA,CAAA,IAA6B,EACrF,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,KAAA,EAAIC,EAAAA,gBAAAA,CAAgB,CAAK,CAAC,CAAC,MAAM;CAAI,CAAC,CAAC,IAAI,EACxE,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAA,GAAoC,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,iFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAMA,KALA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,qHAEpD,EAAS,KAAK;CAAI,GACtC,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
1
+ {"version":3,"file":"nativeMetrics.cjs","names":["clamp","getErrorMessage"],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { clamp, getErrorMessage } from '@willbooster/shared-lib';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead' | 'cognitiveBlocks'> {\n /** Absent when the function has none. */\n cognitiveBlocks?: FunctionMetrics['cognitiveBlocks'];\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ntype NativeMeasureArguments = [\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens: number | undefined,\n maxGapTokens: number | undefined,\n minSimilarityPercent: number | undefined,\n includeCrossFileData: boolean,\n];\n\nexport interface NativeBinding {\n measureCodeNative(...args: NativeMeasureArguments): string;\n measureCodeNativeAsync(...args: NativeMeasureArguments): Promise<string>;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields, or lack newer binding functions, instead of failing with a clear rebuild message.\n */\nexport const expectedPayloadVersion = 12;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n ) as NativeMetricsPayload;\n}\n\n/**\n * measureCodeNative on the addon's worker threads, so several files can be measured in parallel.\n * A missing addon still throws synchronously, like measureCodeNative.\n */\nexport function measureCodeNativeAsync(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): Promise<NativeMetricsPayload> {\n return loadBinding()\n .measureCodeNativeAsync(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n .then((json) => JSON.parse(json) as NativeMetricsPayload);\n}\n\n/** The binding arguments shared by the sync and async measurements, so both measure alike. */\nfunction toNativeMeasureArguments(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication: DuplicationOptions | undefined,\n includeCrossFileData: boolean\n): NativeMeasureArguments {\n return [\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData,\n ];\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value) ? undefined : clamp(Math.trunc(value), 0, 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\n/** Replaces the N-API addon, e.g. with the WebAssembly build on runtimes without N-API. */\nexport function setNativeBinding(binding: NativeBinding): void {\n cachedBinding = binding;\n}\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n // node:module is loaded lazily so that runtimes without it can bundle this module.\n const requireNative = process.getBuiltinModule('node:module').createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${getErrorMessage(error).split('\\n')[0]}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n `toolchain).\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"sDAqEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CACF,CACF,CAMA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACQ,CAC/B,OAAO,EAAY,CAAC,CACjB,uBACC,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CAAC,CACA,KAAM,GAAS,KAAK,MAAM,CAAI,CAAyB,CAC5D,CAGA,SAAS,EACP,EACA,EACA,EACA,EACA,EACwB,CACxB,MAAO,CACL,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAAI,IAAA,IAAA,EAAYA,EAAAA,MAAAA,CAAM,KAAK,MAAM,CAAK,EAAG,EAAG,UAAa,CAC3G,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAGJ,SAAgB,EAAiB,EAA8B,CAC7D,EAAgB,CAClB,CAEA,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAIR,IAAM,EAAgB,QAAQ,iBAAiB,aAAa,CAAC,CAAC,cAAA,QAAA,KAAA,CAAA,CAAA,cAAA,UAAA,CAAA,CAAA,IAA6B,EACrF,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,KAAA,EAAIC,EAAAA,gBAAAA,CAAgB,CAAK,CAAC,CAAC,MAAM;CAAI,CAAC,CAAC,IAAI,EACxE,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAA,GAAoC,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,iFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAMA,KALA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,qHAEpD,EAAS,KAAK;CAAI,GACtC,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
@@ -53,7 +53,7 @@ export interface NativeBinding {
53
53
  * `git pull` untouched, so without this handshake it would silently return payloads missing
54
54
  * newer fields, or lack newer binding functions, instead of failing with a clear rebuild message.
55
55
  */
56
- export declare const expectedPayloadVersion = 10;
56
+ export declare const expectedPayloadVersion = 12;
57
57
  /**
58
58
  * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;
59
59
  * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.
@@ -1,4 +1,4 @@
1
1
  import{clamp as e,getErrorMessage as t}from"@willbooster/shared-lib";function n(e,t,n,r,a=!1){return JSON.parse(p().measureCodeNative(...i(e,t,n,r,a)))}function r(e,t,n,r,a=!1){return p().measureCodeNativeAsync(...i(e,t,n,r,a)).then(e=>JSON.parse(e))}function i(e,t,n,r,i){return[s(e),t,n,c(r?.minTokens),c(r?.maxGapTokens),c(r?.minSimilarityPercent),i]}function a(e,t,n){return JSON.parse(p().collectCrossFileDataNative(s(e),t,c(n)))}function o(e,t){return JSON.parse(p().collectFunctionTokenSequencesNative(s(e),t)).map(e=>Int32Array.from(e))}function s(e){return e.isWellFormed()?e:e.toWellFormed()}function c(t){return t===void 0||Number.isNaN(t)?void 0:e(Math.trunc(t),0,4294967295)}var l=class extends Error{};let u,d;function f(e){u=e}function p(){if(u)return u;if(d)throw d;let e=process.getBuiltinModule(`node:module`).createRequire(import.meta.url),n=[`code-gauge-${m()}`,`../native/code-gauge.node`],r=[];for(let i of n){let n;try{n=e(i)}catch(e){r.push(` ${i}: ${t(e).split(`
2
- `)[0]}`);continue}let a=n.payloadVersion?.();if(a!==10){r.push(` ${i}: payload version ${a??`unknown`} does not match the expected 10; rebuild the addon with \`bun run build-native\``);continue}return u=n,n}throw d=new l(`The code-gauge native addon is not available for ${m()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain).\n${r.join(`
2
+ `)[0]}`);continue}let a=n.payloadVersion?.();if(a!==12){r.push(` ${i}: payload version ${a??`unknown`} does not match the expected 12; rebuild the addon with \`bun run build-native\``);continue}return u=n,n}throw d=new l(`The code-gauge native addon is not available for ${m()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain).\n${r.join(`
3
3
  `)}`),d}function m(){let e=`${process.platform}-${process.arch}`;return process.platform===`win32`?`${e}-msvc`:process.platform===`linux`?(process.report?.getReport())?.header?.glibcVersionRuntime?`${e}-gnu`:`${e}-musl`:e}export{l as NativeAddonError,a as collectCrossFileDataNative,o as collectFunctionTokenSequencesNative,n as measureCodeNative,r as measureCodeNativeAsync,f as setNativeBinding};
4
4
  //# sourceMappingURL=nativeMetrics.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"nativeMetrics.js","names":[],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { clamp, getErrorMessage } from '@willbooster/shared-lib';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead' | 'cognitiveBlocks'> {\n /** Absent when the function has none. */\n cognitiveBlocks?: FunctionMetrics['cognitiveBlocks'];\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ntype NativeMeasureArguments = [\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens: number | undefined,\n maxGapTokens: number | undefined,\n minSimilarityPercent: number | undefined,\n includeCrossFileData: boolean,\n];\n\nexport interface NativeBinding {\n measureCodeNative(...args: NativeMeasureArguments): string;\n measureCodeNativeAsync(...args: NativeMeasureArguments): Promise<string>;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields, or lack newer binding functions, instead of failing with a clear rebuild message.\n */\nexport const expectedPayloadVersion = 10;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n ) as NativeMetricsPayload;\n}\n\n/**\n * measureCodeNative on the addon's worker threads, so several files can be measured in parallel.\n * A missing addon still throws synchronously, like measureCodeNative.\n */\nexport function measureCodeNativeAsync(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): Promise<NativeMetricsPayload> {\n return loadBinding()\n .measureCodeNativeAsync(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n .then((json) => JSON.parse(json) as NativeMetricsPayload);\n}\n\n/** The binding arguments shared by the sync and async measurements, so both measure alike. */\nfunction toNativeMeasureArguments(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication: DuplicationOptions | undefined,\n includeCrossFileData: boolean\n): NativeMeasureArguments {\n return [\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData,\n ];\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value) ? undefined : clamp(Math.trunc(value), 0, 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\n/** Replaces the N-API addon, e.g. with the WebAssembly build on runtimes without N-API. */\nexport function setNativeBinding(binding: NativeBinding): void {\n cachedBinding = binding;\n}\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n // node:module is loaded lazily so that runtimes without it can bundle this module.\n const requireNative = process.getBuiltinModule('node:module').createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${getErrorMessage(error).split('\\n')[0]}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n `toolchain).\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"qEAqEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CACF,CACF,CAMA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACQ,CAC/B,OAAO,EAAY,CAAC,CACjB,uBACC,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CAAC,CACA,KAAM,GAAS,KAAK,MAAM,CAAI,CAAyB,CAC5D,CAGA,SAAS,EACP,EACA,EACA,EACA,EACA,EACwB,CACxB,MAAO,CACL,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAAI,IAAA,GAAY,EAAM,KAAK,MAAM,CAAK,EAAG,EAAG,UAAa,CAC3G,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAGJ,SAAgB,EAAiB,EAA8B,CAC7D,EAAgB,CAClB,CAEA,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAIR,IAAM,EAAgB,QAAQ,iBAAiB,aAAa,CAAC,CAAC,cAAc,YAAY,GAAG,EACrF,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,IAAI,EAAgB,CAAK,CAAC,CAAC,MAAM;CAAI,CAAC,CAAC,IAAI,EACxE,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAA,GAAoC,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,iFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAMA,KALA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,qHAEpD,EAAS,KAAK;CAAI,GACtC,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
1
+ {"version":3,"file":"nativeMetrics.js","names":[],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { clamp, getErrorMessage } from '@willbooster/shared-lib';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead' | 'cognitiveBlocks'> {\n /** Absent when the function has none. */\n cognitiveBlocks?: FunctionMetrics['cognitiveBlocks'];\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ntype NativeMeasureArguments = [\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens: number | undefined,\n maxGapTokens: number | undefined,\n minSimilarityPercent: number | undefined,\n includeCrossFileData: boolean,\n];\n\nexport interface NativeBinding {\n measureCodeNative(...args: NativeMeasureArguments): string;\n measureCodeNativeAsync(...args: NativeMeasureArguments): Promise<string>;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields, or lack newer binding functions, instead of failing with a clear rebuild message.\n */\nexport const expectedPayloadVersion = 12;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n ) as NativeMetricsPayload;\n}\n\n/**\n * measureCodeNative on the addon's worker threads, so several files can be measured in parallel.\n * A missing addon still throws synchronously, like measureCodeNative.\n */\nexport function measureCodeNativeAsync(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): Promise<NativeMetricsPayload> {\n return loadBinding()\n .measureCodeNativeAsync(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n .then((json) => JSON.parse(json) as NativeMetricsPayload);\n}\n\n/** The binding arguments shared by the sync and async measurements, so both measure alike. */\nfunction toNativeMeasureArguments(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication: DuplicationOptions | undefined,\n includeCrossFileData: boolean\n): NativeMeasureArguments {\n return [\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData,\n ];\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value) ? undefined : clamp(Math.trunc(value), 0, 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\n/** Replaces the N-API addon, e.g. with the WebAssembly build on runtimes without N-API. */\nexport function setNativeBinding(binding: NativeBinding): void {\n cachedBinding = binding;\n}\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n // node:module is loaded lazily so that runtimes without it can bundle this module.\n const requireNative = process.getBuiltinModule('node:module').createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${getErrorMessage(error).split('\\n')[0]}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n `toolchain).\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"qEAqEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CACF,CACF,CAMA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACQ,CAC/B,OAAO,EAAY,CAAC,CACjB,uBACC,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CAAC,CACA,KAAM,GAAS,KAAK,MAAM,CAAI,CAAyB,CAC5D,CAGA,SAAS,EACP,EACA,EACA,EACA,EACA,EACwB,CACxB,MAAO,CACL,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAAI,IAAA,GAAY,EAAM,KAAK,MAAM,CAAK,EAAG,EAAG,UAAa,CAC3G,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAGJ,SAAgB,EAAiB,EAA8B,CAC7D,EAAgB,CAClB,CAEA,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAIR,IAAM,EAAgB,QAAQ,iBAAiB,aAAa,CAAC,CAAC,cAAc,YAAY,GAAG,EACrF,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,IAAI,EAAgB,CAAK,CAAC,CAAC,MAAM;CAAI,CAAC,CAAC,IAAI,EACxE,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAA,GAAoC,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,iFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAMA,KALA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,qHAEpD,EAAS,KAAK;CAAI,GACtC,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
@@ -1,2 +1,2 @@
1
- "use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./thresholds.cjs");let n=require("node:assert");n=e.__toESM(n,1);const r=[`file`,`function`,`duplication`],i=new Set(t.functionThresholds.filter(({includesNestedFunctions:e})=>e).map(e=>t.metricNameOf(e)));function a(e,n,r,i){let a=g(n,new Set(e.map(({file:e})=>e))),l=[],d=0;for(let n of e){let{file:e,metrics:o,hunks:m}=n,h=r(o.language),g=m?.length===0?[]:p(t.fileThresholds,o,h);g.length>0&&l.push(f({file:e,startLine:1,endLine:o.lines.total},`file`,g));let v=s(n);d+=v.length;let y=v.map(e=>({fn:e,exceeded:p(t.functionThresholds,e,h)})).filter(({exceeded:e})=>e.length>0);for(let{fn:t,exceeded:n}of y){let r=n.filter(e=>!c(e,t,y));r.length>0&&l.push(u(e,t,r))}l.push(..._(n,a.get(e)??[],h,i))}return{violations:l.toSorted(o),checkedFunctionCount:d}}function o(e,n){return t.levels.indexOf(n.level)-t.levels.indexOf(e.level)||x(e.file,n.file)||e.startLine-n.startLine||r.indexOf(e.kind)-r.indexOf(n.kind)||e.endLine-n.endLine}function s({metrics:e,hunks:t}){let n=e.language===`python`;return t?e.functions.filter(e=>t.some(t=>h(t,e.startLine,e.endLine,n))):e.functions}function c(e,t,n){return i.has(e.metric)?n.some(n=>n.fn!==t&&l(n.fn,t)&&n.exceeded.some(({metric:t})=>t===e.metric)):!1}function l(e,t){let n=e.startLine<t.startLine||e.startLine===t.startLine&&e.startColumn<=t.startColumn,r=e.endLine>t.endLine||e.endLine===t.endLine&&e.endColumn>=t.endColumn;return n&&r}function u(e,t,n){let{startLine:r,endLine:i}=t,a=n.some(({metric:e})=>e===`functionCognitiveComplexity`);return{...f({file:e,startLine:r,endLine:i},`function`,n),name:d(t),...a&&t.cognitiveBlocks.length>0&&{largestBlocks:t.cognitiveBlocks}}}function d({name:e=`<anonymous>`,containerName:t}){return t?`${t}.${e}`:e}function f(e,t,n){return{kind:t,level:n.some(e=>e.level===`error`)?`error`:`warning`,...e,exceeded:n}}function p(e,t,n){return e.flatMap(e=>m(e,t,n,(e,t)=>e>t))}function m(e,n,r,i){let a=e.measure(n),o=t.levels.findLast(t=>i(a,r[t][e.key]));return o?[{metric:t.metricNameOf(e),value:a,level:o,limit:r[o][e.key]}]:[]}function h(e,t,n,r=!1){return e.headCount===0?e.headStart>=t&&(e.headStart<n||r&&e.headStart===n):e.headStart<=n&&e.headStart+e.headCount>t}function g(e,t){let n=/* @__PURE__ */ new Map;for(let r of e?.groups??[])for(let e of r.files)if(t.has(e)){let t=n.get(e)??[];t.push(r),n.set(e,t)}return n}function _({file:e,metrics:n,hunks:r},i,a,o){let s=e=>m(t.duplicationThreshold,{codeLineCount:y(e,o)},a,(e,t)=>e>=t),c=[...n.duplication.duplicateBlockGroups.map(t=>t.map(t=>({...t,file:e}))),...i.map(e=>e.occurrences)],l=t=>t.file===e&&s(t).length>0&&(!r||r.some(e=>e.headCount>0&&h(e,t.startLine,t.endLine))),u=/* @__PURE__ */ new Map;for(let e of c)for(let t of e.filter(e=>l(e)))u.set(t,e);return v([...u.keys()]).map(({merged:e,sources:t})=>({...f(e,`duplication`,s(e)),partners:v(t.flatMap(e=>u.get(e)??[])).map(e=>e.merged).filter(t=>!b(t,e)).toSorted((e,t)=>y(t,o)-y(e,o))}))}function v(e){let t=[],n=e.toSorted((e,t)=>x(e.file,t.file)||e.startLine-t.startLine);for(let e of n){let n=t.at(-1);if(n&&b(n.merged,e))n.merged.endLine=Math.max(n.merged.endLine,e.endLine),n.sources.push(e);else{let{file:n,startLine:r,endLine:i}=e;t.push({merged:{file:n,startLine:r,endLine:i},sources:[e]})}}return t}function y(e,t){let r=t.get(e.file);n.default.ok(r,`No code lines for ${e.file}, which holds a duplicated block.`);let i=0;for(let t=e.startLine;t<=e.endLine;t++)r.has(t)&&(i+=1);return i}function b(e,t){return e.file===t.file&&e.startLine<=t.endLine&&t.startLine<=e.endLine}function x(e,t){return e<t?-1:+(e>t)}exports.checkThresholds=a,exports.qualifyFunctionName=d;
1
+ "use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./thresholds.cjs");let n=require("node:assert");n=e.__toESM(n,1);const r=[`file`,`function`,`duplication`],i=new Set(t.functionThresholds.filter(({includesNestedFunctions:e})=>e).map(e=>t.metricNameOf(e)));function a(e,n,r){let i=g(n.crossFile,new Set(e.map(({file:e})=>e))),a=[],l=0;for(let o of e){let{file:e,metrics:d,hunks:m}=o,h=r(d.language),g=m?.length===0?[]:p(t.fileThresholds,d,h);g.length>0&&a.push(f({file:e,startLine:1,endLine:d.lines.total},`file`,g));let v=s(o);l+=v.length;let y=v.map(e=>({fn:e,exceeded:p(t.functionThresholds,e,h)})).filter(({exceeded:e})=>e.length>0);for(let{fn:t,exceeded:n}of y){let r=n.filter(e=>!c(e,t,y));r.length>0&&a.push(u(e,t,r))}a.push(..._(o,i.get(e)??[],h,n))}return{violations:a.toSorted(o),checkedFunctionCount:l}}function o(e,n){return t.levels.indexOf(n.level)-t.levels.indexOf(e.level)||C(e.file,n.file)||e.startLine-n.startLine||r.indexOf(e.kind)-r.indexOf(n.kind)||e.endLine-n.endLine}function s({metrics:e,hunks:t}){let n=e.language===`python`;return t?e.functions.filter(e=>t.some(t=>h(t,e.startLine,e.endLine,n))):e.functions}function c(e,t,n){return i.has(e.metric)?n.some(n=>n.fn!==t&&l(n.fn,t)&&n.exceeded.some(({metric:t})=>t===e.metric)):!1}function l(e,t){let n=e.startLine<t.startLine||e.startLine===t.startLine&&e.startColumn<=t.startColumn,r=e.endLine>t.endLine||e.endLine===t.endLine&&e.endColumn>=t.endColumn;return n&&r}function u(e,t,n){let{startLine:r,endLine:i}=t,a=n.some(({metric:e})=>e===`functionCognitiveComplexity`);return{...f({file:e,startLine:r,endLine:i},`function`,n),name:d(t),...a&&t.cognitiveBlocks.length>0&&{largestBlocks:t.cognitiveBlocks}}}function d({name:e=`<anonymous>`,containerName:t}){return t?`${t}.${e}`:e}function f(e,t,n){return{kind:t,level:n.some(e=>e.level===`error`)?`error`:`warning`,...e,exceeded:n}}function p(e,t,n){return e.flatMap(e=>m(e,t,n,(e,t)=>e>t))}function m(e,n,r,i){let a=e.measure(n),o=t.levels.findLast(t=>i(a,r[t][e.key]));return o?[{metric:t.metricNameOf(e),value:a,level:o,limit:r[o][e.key]}]:[]}function h(e,t,n,r=!1){return e.headCount===0?e.headStart>=t&&(e.headStart<n||r&&e.headStart===n):e.headStart<=n&&e.headStart+e.headCount>t}function g(e,t){let n=/* @__PURE__ */ new Map;for(let r of e?.groups??[])for(let e of r.files)if(t.has(e)){let t=n.get(e)??[];t.push(r),n.set(e,t)}return n}function _({file:e,metrics:n,hunks:r},i,a,{minSimilarityPercent:o}){let s=e=>m(t.duplicationThreshold,{duplicatedLineCount:e},a,(e,t)=>e>=t),c=[...n.duplication.duplicateBlockGroups.map(t=>t.map(t=>({...t,file:e}))),...i.map(e=>e.occurrences.map(e=>v(e)))],l=t=>t.file===e&&s(t.lineNumbers.length).length>0&&(!r||r.some(e=>e.headCount>0&&h(e,t.startLine,t.endLine))),u=/* @__PURE__ */ new Map;for(let e of c)for(let t of e.filter(e=>l(e)))u.set(t,e);return b([...u.keys()]).map(({merged:e,sources:t})=>{let n=n=>b(t.flatMap(e=>{let r=Math.max(...t.filter(t=>x(t,e)).map(e=>e.tokenCount));return(u.get(e)??[]).filter(e=>e.tokenCount*100>=r*n)})).filter(t=>!S(t.merged,e)).toSorted((e,t)=>y(t.sources)-y(e.sources)).map(e=>e.merged),r=n(o);return{...f(e,`duplication`,s(new Set(t.flatMap(e=>e.lineNumbers)).size)),partners:r.length>0?r:n(0)}})}function v(e){let{lineNumbers:t}=e;return n.default.ok(t,`No matched lines for ${e.file}, a scanned file.`),{...e,lineNumbers:t}}function y(e){return e.reduce((e,t)=>e+t.tokenCount,0)}function b(e){let t=[],n=e.toSorted((e,t)=>C(e.file,t.file)||e.startLine-t.startLine);for(let e of n){let n=t.at(-1);if(n&&S(n.merged,e))n.merged.endLine=Math.max(n.merged.endLine,e.endLine),n.sources.push(e);else{let{file:n,startLine:r,endLine:i}=e;t.push({merged:{file:n,startLine:r,endLine:i},sources:[e]})}}return t}function x(e,t){return e.file===t.file&&e.startLine<=t.startLine&&t.endLine<=e.endLine}function S(e,t){return e.file===t.file&&e.startLine<=t.endLine&&t.startLine<=e.endLine}function C(e,t){return e<t?-1:+(e>t)}exports.checkThresholds=a,exports.qualifyFunctionName=d;
2
2
  //# sourceMappingURL=thresholdCheck.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"thresholdCheck.cjs","names":["functionThresholds","metricNameOf","fileThresholds","levels","duplicationThreshold"],"sources":["../src/thresholdCheck.ts"],"sourcesContent":["import assert from 'node:assert';\nimport type { CrossFileDuplicateBlockGroup, CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { LineHunk } from './git.js';\nimport {\n duplicationThreshold,\n fileThresholds,\n functionThresholds,\n levels,\n metricNameOf,\n type Level,\n type LimitsByLevel,\n type Threshold,\n} from './thresholds.js';\nimport type { CodeMetrics, CognitiveBlock, FunctionMetrics } from './types.js';\n\n/** A measured file to check, under the path cross-file duplication knows it by. */\nexport interface CheckedFile {\n file: string;\n metrics: CodeMetrics;\n /** The change to the file; when set, only what the change touches is checked. */\n hunks?: readonly LineHunk[];\n}\n\n/**\n * By file path, the 1-based lines that are neither blank nor comment-only, by which duplicated\n * blocks are measured and their copies ranked: of the checked files and of every other measured\n * file a copy may lie in.\n */\nexport type CodeLineNumbersByFile = ReadonlyMap<string, ReadonlySet<number>>;\n\nexport interface ExceededLimit {\n metric: string;\n value: number;\n /** The most severe level whose limit the value violates. */\n level: Level;\n /** That level's limit: the value is allowed up to it; a duplicated block is allowed below it. */\n limit: number;\n}\n\nexport interface BlockLocation {\n file: string;\n startLine: number;\n endLine: number;\n}\n\nexport interface Violation extends BlockLocation {\n kind: 'function' | 'file' | 'duplication';\n /** The most severe level among `exceeded`. */\n level: Level;\n /** The function's name, qualified with the class it is a member of, for kind `function`. */\n name?: string;\n exceeded: ExceededLimit[];\n /**\n * The parts adding the most to the cognitive complexity, largest first, for a function that\n * exceeds a limit of it and has such parts: operator sequences and jumps outside any branching\n * construct add to the complexity without being one.\n */\n largestBlocks?: CognitiveBlock[];\n /** The other copies of the block, most code lines first, for kind `duplication`. */\n partners?: BlockLocation[];\n}\n\nexport interface CheckResult {\n violations: Violation[];\n checkedFunctionCount: number;\n}\n\nconst violationKinds: readonly Violation['kind'][] = ['file', 'function', 'duplication'];\n\nconst cognitiveComplexityMetric = 'functionCognitiveComplexity';\n\nconst nestedInclusiveMetrics = new Set(\n functionThresholds\n .filter(({ includesNestedFunctions }) => includesNestedFunctions)\n .map((threshold) => metricNameOf(threshold))\n);\n\n/** Every limit the files exceed, errors first, then ordered by path, then line. */\nexport function checkThresholds(\n files: readonly CheckedFile[],\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n limitsOf: (language: string) => LimitsByLevel,\n codeLineNumbersByFile: CodeLineNumbersByFile\n): CheckResult {\n const crossFileGroupsByFile = indexGroupsByFile(crossFileDuplication, new Set(files.map(({ file }) => file)));\n const violations: Violation[] = [];\n let checkedFunctionCount = 0;\n for (const checkedFile of files) {\n const { file, metrics, hunks } = checkedFile;\n const limits = limitsOf(metrics.language);\n // A rename or a mode change lists the file as changed without touching any of its lines.\n const fileExceeded = hunks?.length === 0 ? [] : collectExceeded(fileThresholds, metrics, limits);\n if (fileExceeded.length > 0) {\n violations.push(toViolation({ file, startLine: 1, endLine: metrics.lines.total }, 'file', fileExceeded));\n }\n const checkedFunctions = listCheckedFunctions(checkedFile);\n checkedFunctionCount += checkedFunctions.length;\n const exceedingFunctions = checkedFunctions\n .map((fn) => ({ fn, exceeded: collectExceeded(functionThresholds, fn, limits) }))\n .filter(({ exceeded }) => exceeded.length > 0);\n for (const { fn, exceeded } of exceedingFunctions) {\n const ownExceeded = exceeded.filter((limit) => !isReportedByEnclosingFunction(limit, fn, exceedingFunctions));\n if (ownExceeded.length > 0) {\n violations.push(toFunctionViolation(file, fn, ownExceeded));\n }\n }\n violations.push(\n ...collectDuplicationViolations(checkedFile, crossFileGroupsByFile.get(file) ?? [], limits, codeLineNumbersByFile)\n );\n }\n return { violations: violations.toSorted(compareViolations), checkedFunctionCount };\n}\n\n// The kind is compared before the end line because a file violation's end line is corrected after\n// this ordering (to the last line that exists), and the order must not depend on that correction.\nfunction compareViolations(left: Violation, right: Violation): number {\n return (\n levels.indexOf(right.level) - levels.indexOf(left.level) ||\n compareStrings(left.file, right.file) ||\n left.startLine - right.startLine ||\n violationKinds.indexOf(left.kind) - violationKinds.indexOf(right.kind) ||\n left.endLine - right.endLine\n );\n}\n\n/** The file's functions or, for a changed file, those the change touches. */\nfunction listCheckedFunctions({ metrics, hunks }: CheckedFile): FunctionMetrics[] {\n const endsByIndentation = metrics.language === 'python';\n return hunks\n ? metrics.functions.filter((fn) =>\n hunks.some((hunk) => touchesSpan(hunk, fn.startLine, fn.endLine, endsByIndentation))\n )\n : metrics.functions;\n}\n\n/**\n * Whether a function enclosing `fn` exceeds the same limit with a value that covers `fn`, so\n * reporting it for `fn` too would count the same code twice. The enclosing value is at least that\n * of `fn` and both share the file's limits, so its level is never milder.\n */\nfunction isReportedByEnclosingFunction(\n limit: ExceededLimit,\n fn: FunctionMetrics,\n exceedingFunctions: readonly { fn: FunctionMetrics; exceeded: ExceededLimit[] }[]\n): boolean {\n if (!nestedInclusiveMetrics.has(limit.metric)) {\n return false;\n }\n return exceedingFunctions.some(\n (outer) => outer.fn !== fn && encloses(outer.fn, fn) && outer.exceeded.some(({ metric }) => metric === limit.metric)\n );\n}\n\nfunction encloses(outer: FunctionMetrics, inner: FunctionMetrics): boolean {\n const startsBefore =\n outer.startLine < inner.startLine ||\n (outer.startLine === inner.startLine && outer.startColumn <= inner.startColumn);\n const endsAfter =\n outer.endLine > inner.endLine || (outer.endLine === inner.endLine && outer.endColumn >= inner.endColumn);\n return startsBefore && endsAfter;\n}\n\nfunction toFunctionViolation(file: string, fn: FunctionMetrics, exceeded: ExceededLimit[]): Violation {\n const { startLine, endLine } = fn;\n const exceedsCognitiveComplexity = exceeded.some(({ metric }) => metric === cognitiveComplexityMetric);\n return {\n ...toViolation({ file, startLine, endLine }, 'function', exceeded),\n name: qualifyFunctionName(fn),\n ...(exceedsCognitiveComplexity && fn.cognitiveBlocks.length > 0 && { largestBlocks: fn.cognitiveBlocks }),\n };\n}\n\n/** How reports name a function: with the type or module it is a member of, so that it can be found. */\nexport function qualifyFunctionName({ name = '<anonymous>', containerName }: FunctionMetrics): string {\n return containerName ? `${containerName}.${name}` : name;\n}\n\nfunction toViolation(location: BlockLocation, kind: Violation['kind'], exceeded: ExceededLimit[]): Violation {\n const level = exceeded.some((limit) => limit.level === 'error') ? 'error' : 'warning';\n return { kind, level, ...location, exceeded };\n}\n\nfunction collectExceeded<Subject>(\n thresholds: readonly Threshold<Subject>[],\n subject: Subject,\n limits: LimitsByLevel\n): ExceededLimit[] {\n return thresholds.flatMap((threshold) => findExceeded(threshold, subject, limits, (value, limit) => value > limit));\n}\n\n/** The most severe violated limit of the threshold, if any. */\nfunction findExceeded<Subject>(\n threshold: Threshold<Subject>,\n subject: Subject,\n limits: LimitsByLevel,\n violates: (value: number, limit: number) => boolean\n): ExceededLimit[] {\n const value = threshold.measure(subject);\n const level = levels.findLast((candidate) => violates(value, limits[candidate][threshold.key] as number));\n return level\n ? [{ metric: metricNameOf(threshold), value, level, limit: limits[level][threshold.key] as number }]\n : [];\n}\n\n/**\n * Whether the hunk changes the head lines [startLine, endLine]. A hunk without head lines (a pure\n * deletion) sits between its headStart line and the next one. Where indentation ends a span, its\n * last line is a statement rather than a closing delimiter, so a deletion right after that line may\n * have removed the span's tail; `endsByIndentation` counts it as touching, since the head alone\n * cannot tell it from a deletion after the span.\n */\nfunction touchesSpan(hunk: LineHunk, startLine: number, endLine: number, endsByIndentation = false): boolean {\n return hunk.headCount === 0\n ? hunk.headStart >= startLine && (hunk.headStart < endLine || (endsByIndentation && hunk.headStart === endLine))\n : hunk.headStart <= endLine && hunk.headStart + hunk.headCount > startLine;\n}\n\n/** The cross-file groups with an occurrence in each of the given files. */\nfunction indexGroupsByFile(\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n files: ReadonlySet<string>\n): Map<string, CrossFileDuplicateBlockGroup[]> {\n const groupsByFile = new Map<string, CrossFileDuplicateBlockGroup[]>();\n for (const group of crossFileDuplication?.groups ?? []) {\n for (const file of group.files) {\n if (files.has(file)) {\n const groups = groupsByFile.get(file) ?? [];\n groups.push(group);\n groupsByFile.set(file, groups);\n }\n }\n }\n return groupsByFile;\n}\n\n/**\n * One violation per duplicated region of the file: its clone occurrences, within-file and\n * cross-file, that hold at least the code lines of a level (with `hunks`, only those overlapping\n * added lines), with overlapping occurrences merged into one region.\n */\nfunction collectDuplicationViolations(\n { file, metrics, hunks }: CheckedFile,\n crossFileGroups: readonly CrossFileDuplicateBlockGroup[],\n limits: LimitsByLevel,\n codeLineNumbersByFile: CodeLineNumbersByFile\n): Violation[] {\n const findExceededLines = (block: BlockLocation): ExceededLimit[] =>\n findExceeded(\n duplicationThreshold,\n { codeLineCount: countCodeLines(block, codeLineNumbersByFile) },\n limits,\n (value, limit) => value >= limit\n );\n const groups: BlockLocation[][] = [\n ...metrics.duplication.duplicateBlockGroups.map((group) => group.map((occurrence) => ({ ...occurrence, file }))),\n ...crossFileGroups.map((group) => group.occurrences),\n ];\n const isReported = (block: BlockLocation): boolean =>\n block.file === file &&\n findExceededLines(block).length > 0 &&\n (!hunks || hunks.some((hunk) => hunk.headCount > 0 && touchesSpan(hunk, block.startLine, block.endLine)));\n const copiesByBlock = new Map<BlockLocation, BlockLocation[]>();\n for (const group of groups) {\n for (const block of group.filter((occurrence) => isReported(occurrence))) {\n copiesByBlock.set(block, group);\n }\n }\n return mergeOverlapping([...copiesByBlock.keys()]).map(({ merged, sources }) => ({\n ...toViolation(merged, 'duplication', findExceededLines(merged)),\n // The region's own occurrences are among the copies of the groups it belongs to.\n // The region merges every clone that overlaps it, so its copies range from whole copies of it\n // to a few lines matching one fragment; the largest say the most about what to share.\n partners: mergeOverlapping(sources.flatMap((source) => copiesByBlock.get(source) ?? []))\n .map((partner) => partner.merged)\n .filter((partner) => !overlaps(partner, merged))\n .toSorted(\n (left, right) => countCodeLines(right, codeLineNumbersByFile) - countCodeLines(left, codeLineNumbersByFile)\n ),\n }));\n}\n\n/** The locations with those overlapping in the same file merged, ordered by file, then line. */\nfunction mergeOverlapping(locations: readonly BlockLocation[]): { merged: BlockLocation; sources: BlockLocation[] }[] {\n const regions: { merged: BlockLocation; sources: BlockLocation[] }[] = [];\n const ordered = locations.toSorted(\n (left, right) => compareStrings(left.file, right.file) || left.startLine - right.startLine\n );\n for (const location of ordered) {\n const last = regions.at(-1);\n if (last && overlaps(last.merged, location)) {\n last.merged.endLine = Math.max(last.merged.endLine, location.endLine);\n last.sources.push(location);\n } else {\n const { file, startLine, endLine } = location;\n regions.push({ merged: { file, startLine, endLine }, sources: [location] });\n }\n }\n return regions;\n}\n\n/** The length of a duplicated block. */\nfunction countCodeLines(block: BlockLocation, codeLineNumbersByFile: CodeLineNumbersByFile): number {\n const codeLineNumbers = codeLineNumbersByFile.get(block.file);\n assert.ok(codeLineNumbers, `No code lines for ${block.file}, which holds a duplicated block.`);\n let count = 0;\n for (let line = block.startLine; line <= block.endLine; line++) {\n if (codeLineNumbers.has(line)) count += 1;\n }\n return count;\n}\n\nfunction overlaps(left: BlockLocation, right: BlockLocation): boolean {\n return left.file === right.file && left.startLine <= right.endLine && right.startLine <= left.endLine;\n}\n\n/** Code-unit order, so reports do not depend on the locale. */\nfunction compareStrings(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"thresholdCheck.cjs","names":["functionThresholds","metricNameOf","fileThresholds","levels","duplicationThreshold"],"sources":["../src/thresholdCheck.ts"],"sourcesContent":["import assert from 'node:assert';\nimport type {\n CrossFileDuplicateBlockGroup,\n CrossFileDuplicateOccurrence,\n CrossFileDuplicationMetrics,\n} from './crossFileDuplication.js';\nimport type { LineHunk } from './git.js';\nimport {\n duplicationThreshold,\n fileThresholds,\n functionThresholds,\n levels,\n metricNameOf,\n type Level,\n type LimitsByLevel,\n type Threshold,\n} from './thresholds.js';\nimport type { CodeMetrics, CognitiveBlock, FunctionMetrics } from './types.js';\n\n/** A measured file to check, under the path cross-file duplication knows it by. */\nexport interface CheckedFile {\n file: string;\n metrics: CodeMetrics;\n /** The change to the file; when set, only what the change touches is checked. */\n hunks?: readonly LineHunk[];\n}\n\n/** The duplication of the measured files, in which duplicated blocks and their copies are found. */\nexport interface CheckedDuplication {\n crossFile?: CrossFileDuplicationMetrics;\n /** The detection's `minSimilarityPercent`, the share of a block a copy of it holds at least. */\n minSimilarityPercent: number;\n}\n\nexport interface ExceededLimit {\n metric: string;\n value: number;\n /** The most severe level whose limit the value violates. */\n level: Level;\n /** That level's limit: the value is allowed up to it; a duplicated block is allowed below it. */\n limit: number;\n}\n\nexport interface BlockLocation {\n file: string;\n startLine: number;\n endLine: number;\n}\n\n/** One copy of a clone group. */\ninterface CloneOccurrence extends BlockLocation {\n /**\n * Its code lines matched in another copy, whose number is the length of a duplicated block. Its\n * line span would also count the lines between the matched parts of one copy, which the copies\n * do not share; a block matched as a whole by a near-miss copy holds all its code lines.\n */\n lineNumbers: readonly number[];\n tokenCount: number;\n}\n\nexport interface Violation extends BlockLocation {\n kind: 'function' | 'file' | 'duplication';\n /** The most severe level among `exceeded`. */\n level: Level;\n /** The function's name, qualified with the class it is a member of, for kind `function`. */\n name?: string;\n exceeded: ExceededLimit[];\n /**\n * The parts adding the most to the cognitive complexity, largest first, for a function that\n * exceeds a limit of it and has such parts: operator sequences and jumps outside any branching\n * construct add to the complexity without being one.\n */\n largestBlocks?: CognitiveBlock[];\n /** The other copies of the block, most matched tokens first, for kind `duplication`. */\n partners?: BlockLocation[];\n}\n\nexport interface CheckResult {\n violations: Violation[];\n checkedFunctionCount: number;\n}\n\nconst violationKinds: readonly Violation['kind'][] = ['file', 'function', 'duplication'];\n\nconst cognitiveComplexityMetric = 'functionCognitiveComplexity';\n\nconst nestedInclusiveMetrics = new Set(\n functionThresholds\n .filter(({ includesNestedFunctions }) => includesNestedFunctions)\n .map((threshold) => metricNameOf(threshold))\n);\n\n/** Every limit the files exceed, errors first, then ordered by path, then line. */\nexport function checkThresholds(\n files: readonly CheckedFile[],\n duplication: CheckedDuplication,\n limitsOf: (language: string) => LimitsByLevel\n): CheckResult {\n const crossFileGroupsByFile = indexGroupsByFile(duplication.crossFile, new Set(files.map(({ file }) => file)));\n const violations: Violation[] = [];\n let checkedFunctionCount = 0;\n for (const checkedFile of files) {\n const { file, metrics, hunks } = checkedFile;\n const limits = limitsOf(metrics.language);\n // A rename or a mode change lists the file as changed without touching any of its lines.\n const fileExceeded = hunks?.length === 0 ? [] : collectExceeded(fileThresholds, metrics, limits);\n if (fileExceeded.length > 0) {\n violations.push(toViolation({ file, startLine: 1, endLine: metrics.lines.total }, 'file', fileExceeded));\n }\n const checkedFunctions = listCheckedFunctions(checkedFile);\n checkedFunctionCount += checkedFunctions.length;\n const exceedingFunctions = checkedFunctions\n .map((fn) => ({ fn, exceeded: collectExceeded(functionThresholds, fn, limits) }))\n .filter(({ exceeded }) => exceeded.length > 0);\n for (const { fn, exceeded } of exceedingFunctions) {\n const ownExceeded = exceeded.filter((limit) => !isReportedByEnclosingFunction(limit, fn, exceedingFunctions));\n if (ownExceeded.length > 0) {\n violations.push(toFunctionViolation(file, fn, ownExceeded));\n }\n }\n violations.push(\n ...collectDuplicationViolations(checkedFile, crossFileGroupsByFile.get(file) ?? [], limits, duplication)\n );\n }\n return { violations: violations.toSorted(compareViolations), checkedFunctionCount };\n}\n\n// The kind is compared before the end line because a file violation's end line is corrected after\n// this ordering (to the last line that exists), and the order must not depend on that correction.\nfunction compareViolations(left: Violation, right: Violation): number {\n return (\n levels.indexOf(right.level) - levels.indexOf(left.level) ||\n compareStrings(left.file, right.file) ||\n left.startLine - right.startLine ||\n violationKinds.indexOf(left.kind) - violationKinds.indexOf(right.kind) ||\n left.endLine - right.endLine\n );\n}\n\n/** The file's functions or, for a changed file, those the change touches. */\nfunction listCheckedFunctions({ metrics, hunks }: CheckedFile): FunctionMetrics[] {\n const endsByIndentation = metrics.language === 'python';\n return hunks\n ? metrics.functions.filter((fn) =>\n hunks.some((hunk) => touchesSpan(hunk, fn.startLine, fn.endLine, endsByIndentation))\n )\n : metrics.functions;\n}\n\n/**\n * Whether a function enclosing `fn` exceeds the same limit with a value that covers `fn`, so\n * reporting it for `fn` too would count the same code twice. The enclosing value is at least that\n * of `fn` and both share the file's limits, so its level is never milder.\n */\nfunction isReportedByEnclosingFunction(\n limit: ExceededLimit,\n fn: FunctionMetrics,\n exceedingFunctions: readonly { fn: FunctionMetrics; exceeded: ExceededLimit[] }[]\n): boolean {\n if (!nestedInclusiveMetrics.has(limit.metric)) {\n return false;\n }\n return exceedingFunctions.some(\n (outer) => outer.fn !== fn && encloses(outer.fn, fn) && outer.exceeded.some(({ metric }) => metric === limit.metric)\n );\n}\n\nfunction encloses(outer: FunctionMetrics, inner: FunctionMetrics): boolean {\n const startsBefore =\n outer.startLine < inner.startLine ||\n (outer.startLine === inner.startLine && outer.startColumn <= inner.startColumn);\n const endsAfter =\n outer.endLine > inner.endLine || (outer.endLine === inner.endLine && outer.endColumn >= inner.endColumn);\n return startsBefore && endsAfter;\n}\n\nfunction toFunctionViolation(file: string, fn: FunctionMetrics, exceeded: ExceededLimit[]): Violation {\n const { startLine, endLine } = fn;\n const exceedsCognitiveComplexity = exceeded.some(({ metric }) => metric === cognitiveComplexityMetric);\n return {\n ...toViolation({ file, startLine, endLine }, 'function', exceeded),\n name: qualifyFunctionName(fn),\n ...(exceedsCognitiveComplexity && fn.cognitiveBlocks.length > 0 && { largestBlocks: fn.cognitiveBlocks }),\n };\n}\n\n/** How reports name a function: with the type or module it is a member of, so that it can be found. */\nexport function qualifyFunctionName({ name = '<anonymous>', containerName }: FunctionMetrics): string {\n return containerName ? `${containerName}.${name}` : name;\n}\n\nfunction toViolation(location: BlockLocation, kind: Violation['kind'], exceeded: ExceededLimit[]): Violation {\n const level = exceeded.some((limit) => limit.level === 'error') ? 'error' : 'warning';\n return { kind, level, ...location, exceeded };\n}\n\nfunction collectExceeded<Subject>(\n thresholds: readonly Threshold<Subject>[],\n subject: Subject,\n limits: LimitsByLevel\n): ExceededLimit[] {\n return thresholds.flatMap((threshold) => findExceeded(threshold, subject, limits, (value, limit) => value > limit));\n}\n\n/** The most severe violated limit of the threshold, if any. */\nfunction findExceeded<Subject>(\n threshold: Threshold<Subject>,\n subject: Subject,\n limits: LimitsByLevel,\n violates: (value: number, limit: number) => boolean\n): ExceededLimit[] {\n const value = threshold.measure(subject);\n const level = levels.findLast((candidate) => violates(value, limits[candidate][threshold.key] as number));\n return level\n ? [{ metric: metricNameOf(threshold), value, level, limit: limits[level][threshold.key] as number }]\n : [];\n}\n\n/**\n * Whether the hunk changes the head lines [startLine, endLine]. A hunk without head lines (a pure\n * deletion) sits between its headStart line and the next one. Where indentation ends a span, its\n * last line is a statement rather than a closing delimiter, so a deletion right after that line may\n * have removed the span's tail; `endsByIndentation` counts it as touching, since the head alone\n * cannot tell it from a deletion after the span.\n */\nfunction touchesSpan(hunk: LineHunk, startLine: number, endLine: number, endsByIndentation = false): boolean {\n return hunk.headCount === 0\n ? hunk.headStart >= startLine && (hunk.headStart < endLine || (endsByIndentation && hunk.headStart === endLine))\n : hunk.headStart <= endLine && hunk.headStart + hunk.headCount > startLine;\n}\n\n/** The cross-file groups with an occurrence in each of the given files. */\nfunction indexGroupsByFile(\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n files: ReadonlySet<string>\n): Map<string, CrossFileDuplicateBlockGroup[]> {\n const groupsByFile = new Map<string, CrossFileDuplicateBlockGroup[]>();\n for (const group of crossFileDuplication?.groups ?? []) {\n for (const file of group.files) {\n if (files.has(file)) {\n const groups = groupsByFile.get(file) ?? [];\n groups.push(group);\n groupsByFile.set(file, groups);\n }\n }\n }\n return groupsByFile;\n}\n\n/**\n * One violation per duplicated region of the file: its clone occurrences, within-file and\n * cross-file, that hold at least the duplicated lines of a level (with `hunks`, only those\n * overlapping added lines), with overlapping occurrences merged into one region.\n */\nfunction collectDuplicationViolations(\n { file, metrics, hunks }: CheckedFile,\n crossFileGroups: readonly CrossFileDuplicateBlockGroup[],\n limits: LimitsByLevel,\n { minSimilarityPercent }: CheckedDuplication\n): Violation[] {\n const findExceededLines = (duplicatedLineCount: number): ExceededLimit[] =>\n findExceeded(duplicationThreshold, { duplicatedLineCount }, limits, (value, limit) => value >= limit);\n const groups: CloneOccurrence[][] = [\n ...metrics.duplication.duplicateBlockGroups.map((group) => group.map((occurrence) => ({ ...occurrence, file }))),\n ...crossFileGroups.map((group) => group.occurrences.map((occurrence) => toCloneOccurrence(occurrence))),\n ];\n const isReported = (block: CloneOccurrence): boolean =>\n block.file === file &&\n findExceededLines(block.lineNumbers.length).length > 0 &&\n (!hunks || hunks.some((hunk) => hunk.headCount > 0 && touchesSpan(hunk, block.startLine, block.endLine)));\n const groupByBlock = new Map<CloneOccurrence, CloneOccurrence[]>();\n for (const group of groups) {\n for (const block of group.filter((occurrence) => isReported(occurrence))) {\n groupByBlock.set(block, group);\n }\n }\n return mergeOverlapping([...groupByBlock.keys()]).map(({ merged, sources }) => {\n const listCopies = (minTokenPercent: number): BlockLocation[] =>\n mergeOverlapping(\n sources.flatMap((source) => {\n // The region merges every clone that overlaps it, so a copy of an occurrence lying\n // within a larger one is measured against the larger one.\n const tokenCount = Math.max(\n ...sources.filter((outer) => contains(outer, source)).map((outer) => outer.tokenCount)\n );\n return (groupByBlock.get(source) ?? []).filter(\n (copy) => copy.tokenCount * 100 >= tokenCount * minTokenPercent\n );\n })\n )\n // The region's own occurrences are among the copies of the groups it belongs to.\n .filter((partner) => !overlaps(partner.merged, merged))\n // The largest say the most about what to share.\n .toSorted((left, right) => sumTokens(right.sources) - sumTokens(left.sources))\n .map((partner) => partner.merged);\n // A group joins a block with every place repeating a part of it: a function another file\n // copies whole is grouped with each file holding a few of its lines. Such a fragment is not a\n // copy of the block: sharing it would not remove the block, and a report listing it reads as\n // one more copy. Sizes are compared in tokens, since the same code may be laid out over any\n // number of lines.\n const copies = listCopies(minSimilarityPercent);\n return {\n ...toViolation(\n merged,\n 'duplication',\n // Occurrences that overlap share lines, which count once.\n findExceededLines(new Set(sources.flatMap((source) => source.lineNumbers)).size)\n ),\n // A group does not always hold the copy a block was matched with as a whole: within a file,\n // a copy that already belongs to an exact group is listed as its part in that group. The\n // fragments then are all that locates the block's copies.\n partners: copies.length > 0 ? copies : listCopies(0),\n };\n });\n}\n\nfunction toCloneOccurrence(occurrence: CrossFileDuplicateOccurrence): CloneOccurrence {\n const { lineNumbers } = occurrence;\n assert.ok(lineNumbers, `No matched lines for ${occurrence.file}, a scanned file.`);\n return { ...occurrence, lineNumbers };\n}\n\nfunction sumTokens(occurrences: readonly CloneOccurrence[]): number {\n return occurrences.reduce((sum, occurrence) => sum + occurrence.tokenCount, 0);\n}\n\n/** The locations with those overlapping in the same file merged, ordered by file, then line. */\nfunction mergeOverlapping(\n locations: readonly CloneOccurrence[]\n): { merged: BlockLocation; sources: CloneOccurrence[] }[] {\n const regions: { merged: BlockLocation; sources: CloneOccurrence[] }[] = [];\n const ordered = locations.toSorted(\n (left, right) => compareStrings(left.file, right.file) || left.startLine - right.startLine\n );\n for (const location of ordered) {\n const last = regions.at(-1);\n if (last && overlaps(last.merged, location)) {\n last.merged.endLine = Math.max(last.merged.endLine, location.endLine);\n last.sources.push(location);\n } else {\n const { file, startLine, endLine } = location;\n regions.push({ merged: { file, startLine, endLine }, sources: [location] });\n }\n }\n return regions;\n}\n\n/** Whether `outer` spans every line of `inner`; a block contains itself. */\nfunction contains(outer: BlockLocation, inner: BlockLocation): boolean {\n return outer.file === inner.file && outer.startLine <= inner.startLine && inner.endLine <= outer.endLine;\n}\n\nfunction overlaps(left: BlockLocation, right: BlockLocation): boolean {\n return left.file === right.file && left.startLine <= right.endLine && right.startLine <= left.endLine;\n}\n\n/** Code-unit order, so reports do not depend on the locale. */\nfunction compareStrings(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"],"mappings":"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"}
@@ -9,12 +9,12 @@ export interface CheckedFile {
9
9
  /** The change to the file; when set, only what the change touches is checked. */
10
10
  hunks?: readonly LineHunk[];
11
11
  }
12
- /**
13
- * By file path, the 1-based lines that are neither blank nor comment-only, by which duplicated
14
- * blocks are measured and their copies ranked: of the checked files and of every other measured
15
- * file a copy may lie in.
16
- */
17
- export type CodeLineNumbersByFile = ReadonlyMap<string, ReadonlySet<number>>;
12
+ /** The duplication of the measured files, in which duplicated blocks and their copies are found. */
13
+ export interface CheckedDuplication {
14
+ crossFile?: CrossFileDuplicationMetrics;
15
+ /** The detection's `minSimilarityPercent`, the share of a block a copy of it holds at least. */
16
+ minSimilarityPercent: number;
17
+ }
18
18
  export interface ExceededLimit {
19
19
  metric: string;
20
20
  value: number;
@@ -41,7 +41,7 @@ export interface Violation extends BlockLocation {
41
41
  * construct add to the complexity without being one.
42
42
  */
43
43
  largestBlocks?: CognitiveBlock[];
44
- /** The other copies of the block, most code lines first, for kind `duplication`. */
44
+ /** The other copies of the block, most matched tokens first, for kind `duplication`. */
45
45
  partners?: BlockLocation[];
46
46
  }
47
47
  export interface CheckResult {
@@ -49,6 +49,6 @@ export interface CheckResult {
49
49
  checkedFunctionCount: number;
50
50
  }
51
51
  /** Every limit the files exceed, errors first, then ordered by path, then line. */
52
- export declare function checkThresholds(files: readonly CheckedFile[], crossFileDuplication: CrossFileDuplicationMetrics | undefined, limitsOf: (language: string) => LimitsByLevel, codeLineNumbersByFile: CodeLineNumbersByFile): CheckResult;
52
+ export declare function checkThresholds(files: readonly CheckedFile[], duplication: CheckedDuplication, limitsOf: (language: string) => LimitsByLevel): CheckResult;
53
53
  /** How reports name a function: with the type or module it is a member of, so that it can be found. */
54
54
  export declare function qualifyFunctionName({ name, containerName }: FunctionMetrics): string;
@@ -1,2 +1,2 @@
1
- import{duplicationThreshold as e,fileThresholds as t,functionThresholds as n,levels as r,metricNameOf as i}from"./thresholds.js";import a from"node:assert";const o=[`file`,`function`,`duplication`],s=new Set(n.filter(({includesNestedFunctions:e})=>e).map(e=>i(e)));function c(e,r,i,a){let o=y(r,new Set(e.map(({file:e})=>e))),s=[],c=0;for(let r of e){let{file:e,metrics:l,hunks:f}=r,m=i(l.language),_=f?.length===0?[]:g(t,l,m);_.length>0&&s.push(h({file:e,startLine:1,endLine:l.lines.total},`file`,_));let v=u(r);c+=v.length;let y=v.map(e=>({fn:e,exceeded:g(n,e,m)})).filter(({exceeded:e})=>e.length>0);for(let{fn:t,exceeded:n}of y){let r=n.filter(e=>!d(e,t,y));r.length>0&&s.push(p(e,t,r))}s.push(...b(r,o.get(e)??[],m,a))}return{violations:s.toSorted(l),checkedFunctionCount:c}}function l(e,t){return r.indexOf(t.level)-r.indexOf(e.level)||w(e.file,t.file)||e.startLine-t.startLine||o.indexOf(e.kind)-o.indexOf(t.kind)||e.endLine-t.endLine}function u({metrics:e,hunks:t}){let n=e.language===`python`;return t?e.functions.filter(e=>t.some(t=>v(t,e.startLine,e.endLine,n))):e.functions}function d(e,t,n){return s.has(e.metric)?n.some(n=>n.fn!==t&&f(n.fn,t)&&n.exceeded.some(({metric:t})=>t===e.metric)):!1}function f(e,t){let n=e.startLine<t.startLine||e.startLine===t.startLine&&e.startColumn<=t.startColumn,r=e.endLine>t.endLine||e.endLine===t.endLine&&e.endColumn>=t.endColumn;return n&&r}function p(e,t,n){let{startLine:r,endLine:i}=t,a=n.some(({metric:e})=>e===`functionCognitiveComplexity`);return{...h({file:e,startLine:r,endLine:i},`function`,n),name:m(t),...a&&t.cognitiveBlocks.length>0&&{largestBlocks:t.cognitiveBlocks}}}function m({name:e=`<anonymous>`,containerName:t}){return t?`${t}.${e}`:e}function h(e,t,n){return{kind:t,level:n.some(e=>e.level===`error`)?`error`:`warning`,...e,exceeded:n}}function g(e,t,n){return e.flatMap(e=>_(e,t,n,(e,t)=>e>t))}function _(e,t,n,a){let o=e.measure(t),s=r.findLast(t=>a(o,n[t][e.key]));return s?[{metric:i(e),value:o,level:s,limit:n[s][e.key]}]:[]}function v(e,t,n,r=!1){return e.headCount===0?e.headStart>=t&&(e.headStart<n||r&&e.headStart===n):e.headStart<=n&&e.headStart+e.headCount>t}function y(e,t){let n=/* @__PURE__ */ new Map;for(let r of e?.groups??[])for(let e of r.files)if(t.has(e)){let t=n.get(e)??[];t.push(r),n.set(e,t)}return n}function b({file:t,metrics:n,hunks:r},i,a,o){let s=t=>_(e,{codeLineCount:S(t,o)},a,(e,t)=>e>=t),c=[...n.duplication.duplicateBlockGroups.map(e=>e.map(e=>({...e,file:t}))),...i.map(e=>e.occurrences)],l=e=>e.file===t&&s(e).length>0&&(!r||r.some(t=>t.headCount>0&&v(t,e.startLine,e.endLine))),u=/* @__PURE__ */ new Map;for(let e of c)for(let t of e.filter(e=>l(e)))u.set(t,e);return x([...u.keys()]).map(({merged:e,sources:t})=>({...h(e,`duplication`,s(e)),partners:x(t.flatMap(e=>u.get(e)??[])).map(e=>e.merged).filter(t=>!C(t,e)).toSorted((e,t)=>S(t,o)-S(e,o))}))}function x(e){let t=[],n=e.toSorted((e,t)=>w(e.file,t.file)||e.startLine-t.startLine);for(let e of n){let n=t.at(-1);if(n&&C(n.merged,e))n.merged.endLine=Math.max(n.merged.endLine,e.endLine),n.sources.push(e);else{let{file:n,startLine:r,endLine:i}=e;t.push({merged:{file:n,startLine:r,endLine:i},sources:[e]})}}return t}function S(e,t){let n=t.get(e.file);a.ok(n,`No code lines for ${e.file}, which holds a duplicated block.`);let r=0;for(let t=e.startLine;t<=e.endLine;t++)n.has(t)&&(r+=1);return r}function C(e,t){return e.file===t.file&&e.startLine<=t.endLine&&t.startLine<=e.endLine}function w(e,t){return e<t?-1:+(e>t)}export{c as checkThresholds,m as qualifyFunctionName};
1
+ import{duplicationThreshold as e,fileThresholds as t,functionThresholds as n,levels as r,metricNameOf as i}from"./thresholds.js";import a from"node:assert";const o=[`file`,`function`,`duplication`],s=new Set(n.filter(({includesNestedFunctions:e})=>e).map(e=>i(e)));function c(e,r,i){let a=y(r.crossFile,new Set(e.map(({file:e})=>e))),o=[],s=0;for(let c of e){let{file:e,metrics:l,hunks:f}=c,m=i(l.language),_=f?.length===0?[]:g(t,l,m);_.length>0&&o.push(h({file:e,startLine:1,endLine:l.lines.total},`file`,_));let v=u(c);s+=v.length;let y=v.map(e=>({fn:e,exceeded:g(n,e,m)})).filter(({exceeded:e})=>e.length>0);for(let{fn:t,exceeded:n}of y){let r=n.filter(e=>!d(e,t,y));r.length>0&&o.push(p(e,t,r))}o.push(...b(c,a.get(e)??[],m,r))}return{violations:o.toSorted(l),checkedFunctionCount:s}}function l(e,t){return r.indexOf(t.level)-r.indexOf(e.level)||E(e.file,t.file)||e.startLine-t.startLine||o.indexOf(e.kind)-o.indexOf(t.kind)||e.endLine-t.endLine}function u({metrics:e,hunks:t}){let n=e.language===`python`;return t?e.functions.filter(e=>t.some(t=>v(t,e.startLine,e.endLine,n))):e.functions}function d(e,t,n){return s.has(e.metric)?n.some(n=>n.fn!==t&&f(n.fn,t)&&n.exceeded.some(({metric:t})=>t===e.metric)):!1}function f(e,t){let n=e.startLine<t.startLine||e.startLine===t.startLine&&e.startColumn<=t.startColumn,r=e.endLine>t.endLine||e.endLine===t.endLine&&e.endColumn>=t.endColumn;return n&&r}function p(e,t,n){let{startLine:r,endLine:i}=t,a=n.some(({metric:e})=>e===`functionCognitiveComplexity`);return{...h({file:e,startLine:r,endLine:i},`function`,n),name:m(t),...a&&t.cognitiveBlocks.length>0&&{largestBlocks:t.cognitiveBlocks}}}function m({name:e=`<anonymous>`,containerName:t}){return t?`${t}.${e}`:e}function h(e,t,n){return{kind:t,level:n.some(e=>e.level===`error`)?`error`:`warning`,...e,exceeded:n}}function g(e,t,n){return e.flatMap(e=>_(e,t,n,(e,t)=>e>t))}function _(e,t,n,a){let o=e.measure(t),s=r.findLast(t=>a(o,n[t][e.key]));return s?[{metric:i(e),value:o,level:s,limit:n[s][e.key]}]:[]}function v(e,t,n,r=!1){return e.headCount===0?e.headStart>=t&&(e.headStart<n||r&&e.headStart===n):e.headStart<=n&&e.headStart+e.headCount>t}function y(e,t){let n=/* @__PURE__ */ new Map;for(let r of e?.groups??[])for(let e of r.files)if(t.has(e)){let t=n.get(e)??[];t.push(r),n.set(e,t)}return n}function b({file:t,metrics:n,hunks:r},i,a,{minSimilarityPercent:o}){let s=t=>_(e,{duplicatedLineCount:t},a,(e,t)=>e>=t),c=[...n.duplication.duplicateBlockGroups.map(e=>e.map(e=>({...e,file:t}))),...i.map(e=>e.occurrences.map(e=>x(e)))],l=e=>e.file===t&&s(e.lineNumbers.length).length>0&&(!r||r.some(t=>t.headCount>0&&v(t,e.startLine,e.endLine))),u=/* @__PURE__ */ new Map;for(let e of c)for(let t of e.filter(e=>l(e)))u.set(t,e);return C([...u.keys()]).map(({merged:e,sources:t})=>{let n=n=>C(t.flatMap(e=>{let r=Math.max(...t.filter(t=>w(t,e)).map(e=>e.tokenCount));return(u.get(e)??[]).filter(e=>e.tokenCount*100>=r*n)})).filter(t=>!T(t.merged,e)).toSorted((e,t)=>S(t.sources)-S(e.sources)).map(e=>e.merged),r=n(o);return{...h(e,`duplication`,s(new Set(t.flatMap(e=>e.lineNumbers)).size)),partners:r.length>0?r:n(0)}})}function x(e){let{lineNumbers:t}=e;return a.ok(t,`No matched lines for ${e.file}, a scanned file.`),{...e,lineNumbers:t}}function S(e){return e.reduce((e,t)=>e+t.tokenCount,0)}function C(e){let t=[],n=e.toSorted((e,t)=>E(e.file,t.file)||e.startLine-t.startLine);for(let e of n){let n=t.at(-1);if(n&&T(n.merged,e))n.merged.endLine=Math.max(n.merged.endLine,e.endLine),n.sources.push(e);else{let{file:n,startLine:r,endLine:i}=e;t.push({merged:{file:n,startLine:r,endLine:i},sources:[e]})}}return t}function w(e,t){return e.file===t.file&&e.startLine<=t.startLine&&t.endLine<=e.endLine}function T(e,t){return e.file===t.file&&e.startLine<=t.endLine&&t.startLine<=e.endLine}function E(e,t){return e<t?-1:+(e>t)}export{c as checkThresholds,m as qualifyFunctionName};
2
2
  //# sourceMappingURL=thresholdCheck.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"thresholdCheck.js","names":[],"sources":["../src/thresholdCheck.ts"],"sourcesContent":["import assert from 'node:assert';\nimport type { CrossFileDuplicateBlockGroup, CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { LineHunk } from './git.js';\nimport {\n duplicationThreshold,\n fileThresholds,\n functionThresholds,\n levels,\n metricNameOf,\n type Level,\n type LimitsByLevel,\n type Threshold,\n} from './thresholds.js';\nimport type { CodeMetrics, CognitiveBlock, FunctionMetrics } from './types.js';\n\n/** A measured file to check, under the path cross-file duplication knows it by. */\nexport interface CheckedFile {\n file: string;\n metrics: CodeMetrics;\n /** The change to the file; when set, only what the change touches is checked. */\n hunks?: readonly LineHunk[];\n}\n\n/**\n * By file path, the 1-based lines that are neither blank nor comment-only, by which duplicated\n * blocks are measured and their copies ranked: of the checked files and of every other measured\n * file a copy may lie in.\n */\nexport type CodeLineNumbersByFile = ReadonlyMap<string, ReadonlySet<number>>;\n\nexport interface ExceededLimit {\n metric: string;\n value: number;\n /** The most severe level whose limit the value violates. */\n level: Level;\n /** That level's limit: the value is allowed up to it; a duplicated block is allowed below it. */\n limit: number;\n}\n\nexport interface BlockLocation {\n file: string;\n startLine: number;\n endLine: number;\n}\n\nexport interface Violation extends BlockLocation {\n kind: 'function' | 'file' | 'duplication';\n /** The most severe level among `exceeded`. */\n level: Level;\n /** The function's name, qualified with the class it is a member of, for kind `function`. */\n name?: string;\n exceeded: ExceededLimit[];\n /**\n * The parts adding the most to the cognitive complexity, largest first, for a function that\n * exceeds a limit of it and has such parts: operator sequences and jumps outside any branching\n * construct add to the complexity without being one.\n */\n largestBlocks?: CognitiveBlock[];\n /** The other copies of the block, most code lines first, for kind `duplication`. */\n partners?: BlockLocation[];\n}\n\nexport interface CheckResult {\n violations: Violation[];\n checkedFunctionCount: number;\n}\n\nconst violationKinds: readonly Violation['kind'][] = ['file', 'function', 'duplication'];\n\nconst cognitiveComplexityMetric = 'functionCognitiveComplexity';\n\nconst nestedInclusiveMetrics = new Set(\n functionThresholds\n .filter(({ includesNestedFunctions }) => includesNestedFunctions)\n .map((threshold) => metricNameOf(threshold))\n);\n\n/** Every limit the files exceed, errors first, then ordered by path, then line. */\nexport function checkThresholds(\n files: readonly CheckedFile[],\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n limitsOf: (language: string) => LimitsByLevel,\n codeLineNumbersByFile: CodeLineNumbersByFile\n): CheckResult {\n const crossFileGroupsByFile = indexGroupsByFile(crossFileDuplication, new Set(files.map(({ file }) => file)));\n const violations: Violation[] = [];\n let checkedFunctionCount = 0;\n for (const checkedFile of files) {\n const { file, metrics, hunks } = checkedFile;\n const limits = limitsOf(metrics.language);\n // A rename or a mode change lists the file as changed without touching any of its lines.\n const fileExceeded = hunks?.length === 0 ? [] : collectExceeded(fileThresholds, metrics, limits);\n if (fileExceeded.length > 0) {\n violations.push(toViolation({ file, startLine: 1, endLine: metrics.lines.total }, 'file', fileExceeded));\n }\n const checkedFunctions = listCheckedFunctions(checkedFile);\n checkedFunctionCount += checkedFunctions.length;\n const exceedingFunctions = checkedFunctions\n .map((fn) => ({ fn, exceeded: collectExceeded(functionThresholds, fn, limits) }))\n .filter(({ exceeded }) => exceeded.length > 0);\n for (const { fn, exceeded } of exceedingFunctions) {\n const ownExceeded = exceeded.filter((limit) => !isReportedByEnclosingFunction(limit, fn, exceedingFunctions));\n if (ownExceeded.length > 0) {\n violations.push(toFunctionViolation(file, fn, ownExceeded));\n }\n }\n violations.push(\n ...collectDuplicationViolations(checkedFile, crossFileGroupsByFile.get(file) ?? [], limits, codeLineNumbersByFile)\n );\n }\n return { violations: violations.toSorted(compareViolations), checkedFunctionCount };\n}\n\n// The kind is compared before the end line because a file violation's end line is corrected after\n// this ordering (to the last line that exists), and the order must not depend on that correction.\nfunction compareViolations(left: Violation, right: Violation): number {\n return (\n levels.indexOf(right.level) - levels.indexOf(left.level) ||\n compareStrings(left.file, right.file) ||\n left.startLine - right.startLine ||\n violationKinds.indexOf(left.kind) - violationKinds.indexOf(right.kind) ||\n left.endLine - right.endLine\n );\n}\n\n/** The file's functions or, for a changed file, those the change touches. */\nfunction listCheckedFunctions({ metrics, hunks }: CheckedFile): FunctionMetrics[] {\n const endsByIndentation = metrics.language === 'python';\n return hunks\n ? metrics.functions.filter((fn) =>\n hunks.some((hunk) => touchesSpan(hunk, fn.startLine, fn.endLine, endsByIndentation))\n )\n : metrics.functions;\n}\n\n/**\n * Whether a function enclosing `fn` exceeds the same limit with a value that covers `fn`, so\n * reporting it for `fn` too would count the same code twice. The enclosing value is at least that\n * of `fn` and both share the file's limits, so its level is never milder.\n */\nfunction isReportedByEnclosingFunction(\n limit: ExceededLimit,\n fn: FunctionMetrics,\n exceedingFunctions: readonly { fn: FunctionMetrics; exceeded: ExceededLimit[] }[]\n): boolean {\n if (!nestedInclusiveMetrics.has(limit.metric)) {\n return false;\n }\n return exceedingFunctions.some(\n (outer) => outer.fn !== fn && encloses(outer.fn, fn) && outer.exceeded.some(({ metric }) => metric === limit.metric)\n );\n}\n\nfunction encloses(outer: FunctionMetrics, inner: FunctionMetrics): boolean {\n const startsBefore =\n outer.startLine < inner.startLine ||\n (outer.startLine === inner.startLine && outer.startColumn <= inner.startColumn);\n const endsAfter =\n outer.endLine > inner.endLine || (outer.endLine === inner.endLine && outer.endColumn >= inner.endColumn);\n return startsBefore && endsAfter;\n}\n\nfunction toFunctionViolation(file: string, fn: FunctionMetrics, exceeded: ExceededLimit[]): Violation {\n const { startLine, endLine } = fn;\n const exceedsCognitiveComplexity = exceeded.some(({ metric }) => metric === cognitiveComplexityMetric);\n return {\n ...toViolation({ file, startLine, endLine }, 'function', exceeded),\n name: qualifyFunctionName(fn),\n ...(exceedsCognitiveComplexity && fn.cognitiveBlocks.length > 0 && { largestBlocks: fn.cognitiveBlocks }),\n };\n}\n\n/** How reports name a function: with the type or module it is a member of, so that it can be found. */\nexport function qualifyFunctionName({ name = '<anonymous>', containerName }: FunctionMetrics): string {\n return containerName ? `${containerName}.${name}` : name;\n}\n\nfunction toViolation(location: BlockLocation, kind: Violation['kind'], exceeded: ExceededLimit[]): Violation {\n const level = exceeded.some((limit) => limit.level === 'error') ? 'error' : 'warning';\n return { kind, level, ...location, exceeded };\n}\n\nfunction collectExceeded<Subject>(\n thresholds: readonly Threshold<Subject>[],\n subject: Subject,\n limits: LimitsByLevel\n): ExceededLimit[] {\n return thresholds.flatMap((threshold) => findExceeded(threshold, subject, limits, (value, limit) => value > limit));\n}\n\n/** The most severe violated limit of the threshold, if any. */\nfunction findExceeded<Subject>(\n threshold: Threshold<Subject>,\n subject: Subject,\n limits: LimitsByLevel,\n violates: (value: number, limit: number) => boolean\n): ExceededLimit[] {\n const value = threshold.measure(subject);\n const level = levels.findLast((candidate) => violates(value, limits[candidate][threshold.key] as number));\n return level\n ? [{ metric: metricNameOf(threshold), value, level, limit: limits[level][threshold.key] as number }]\n : [];\n}\n\n/**\n * Whether the hunk changes the head lines [startLine, endLine]. A hunk without head lines (a pure\n * deletion) sits between its headStart line and the next one. Where indentation ends a span, its\n * last line is a statement rather than a closing delimiter, so a deletion right after that line may\n * have removed the span's tail; `endsByIndentation` counts it as touching, since the head alone\n * cannot tell it from a deletion after the span.\n */\nfunction touchesSpan(hunk: LineHunk, startLine: number, endLine: number, endsByIndentation = false): boolean {\n return hunk.headCount === 0\n ? hunk.headStart >= startLine && (hunk.headStart < endLine || (endsByIndentation && hunk.headStart === endLine))\n : hunk.headStart <= endLine && hunk.headStart + hunk.headCount > startLine;\n}\n\n/** The cross-file groups with an occurrence in each of the given files. */\nfunction indexGroupsByFile(\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n files: ReadonlySet<string>\n): Map<string, CrossFileDuplicateBlockGroup[]> {\n const groupsByFile = new Map<string, CrossFileDuplicateBlockGroup[]>();\n for (const group of crossFileDuplication?.groups ?? []) {\n for (const file of group.files) {\n if (files.has(file)) {\n const groups = groupsByFile.get(file) ?? [];\n groups.push(group);\n groupsByFile.set(file, groups);\n }\n }\n }\n return groupsByFile;\n}\n\n/**\n * One violation per duplicated region of the file: its clone occurrences, within-file and\n * cross-file, that hold at least the code lines of a level (with `hunks`, only those overlapping\n * added lines), with overlapping occurrences merged into one region.\n */\nfunction collectDuplicationViolations(\n { file, metrics, hunks }: CheckedFile,\n crossFileGroups: readonly CrossFileDuplicateBlockGroup[],\n limits: LimitsByLevel,\n codeLineNumbersByFile: CodeLineNumbersByFile\n): Violation[] {\n const findExceededLines = (block: BlockLocation): ExceededLimit[] =>\n findExceeded(\n duplicationThreshold,\n { codeLineCount: countCodeLines(block, codeLineNumbersByFile) },\n limits,\n (value, limit) => value >= limit\n );\n const groups: BlockLocation[][] = [\n ...metrics.duplication.duplicateBlockGroups.map((group) => group.map((occurrence) => ({ ...occurrence, file }))),\n ...crossFileGroups.map((group) => group.occurrences),\n ];\n const isReported = (block: BlockLocation): boolean =>\n block.file === file &&\n findExceededLines(block).length > 0 &&\n (!hunks || hunks.some((hunk) => hunk.headCount > 0 && touchesSpan(hunk, block.startLine, block.endLine)));\n const copiesByBlock = new Map<BlockLocation, BlockLocation[]>();\n for (const group of groups) {\n for (const block of group.filter((occurrence) => isReported(occurrence))) {\n copiesByBlock.set(block, group);\n }\n }\n return mergeOverlapping([...copiesByBlock.keys()]).map(({ merged, sources }) => ({\n ...toViolation(merged, 'duplication', findExceededLines(merged)),\n // The region's own occurrences are among the copies of the groups it belongs to.\n // The region merges every clone that overlaps it, so its copies range from whole copies of it\n // to a few lines matching one fragment; the largest say the most about what to share.\n partners: mergeOverlapping(sources.flatMap((source) => copiesByBlock.get(source) ?? []))\n .map((partner) => partner.merged)\n .filter((partner) => !overlaps(partner, merged))\n .toSorted(\n (left, right) => countCodeLines(right, codeLineNumbersByFile) - countCodeLines(left, codeLineNumbersByFile)\n ),\n }));\n}\n\n/** The locations with those overlapping in the same file merged, ordered by file, then line. */\nfunction mergeOverlapping(locations: readonly BlockLocation[]): { merged: BlockLocation; sources: BlockLocation[] }[] {\n const regions: { merged: BlockLocation; sources: BlockLocation[] }[] = [];\n const ordered = locations.toSorted(\n (left, right) => compareStrings(left.file, right.file) || left.startLine - right.startLine\n );\n for (const location of ordered) {\n const last = regions.at(-1);\n if (last && overlaps(last.merged, location)) {\n last.merged.endLine = Math.max(last.merged.endLine, location.endLine);\n last.sources.push(location);\n } else {\n const { file, startLine, endLine } = location;\n regions.push({ merged: { file, startLine, endLine }, sources: [location] });\n }\n }\n return regions;\n}\n\n/** The length of a duplicated block. */\nfunction countCodeLines(block: BlockLocation, codeLineNumbersByFile: CodeLineNumbersByFile): number {\n const codeLineNumbers = codeLineNumbersByFile.get(block.file);\n assert.ok(codeLineNumbers, `No code lines for ${block.file}, which holds a duplicated block.`);\n let count = 0;\n for (let line = block.startLine; line <= block.endLine; line++) {\n if (codeLineNumbers.has(line)) count += 1;\n }\n return count;\n}\n\nfunction overlaps(left: BlockLocation, right: BlockLocation): boolean {\n return left.file === right.file && left.startLine <= right.endLine && right.startLine <= left.endLine;\n}\n\n/** Code-unit order, so reports do not depend on the locale. */\nfunction compareStrings(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"thresholdCheck.js","names":[],"sources":["../src/thresholdCheck.ts"],"sourcesContent":["import assert from 'node:assert';\nimport type {\n CrossFileDuplicateBlockGroup,\n CrossFileDuplicateOccurrence,\n CrossFileDuplicationMetrics,\n} from './crossFileDuplication.js';\nimport type { LineHunk } from './git.js';\nimport {\n duplicationThreshold,\n fileThresholds,\n functionThresholds,\n levels,\n metricNameOf,\n type Level,\n type LimitsByLevel,\n type Threshold,\n} from './thresholds.js';\nimport type { CodeMetrics, CognitiveBlock, FunctionMetrics } from './types.js';\n\n/** A measured file to check, under the path cross-file duplication knows it by. */\nexport interface CheckedFile {\n file: string;\n metrics: CodeMetrics;\n /** The change to the file; when set, only what the change touches is checked. */\n hunks?: readonly LineHunk[];\n}\n\n/** The duplication of the measured files, in which duplicated blocks and their copies are found. */\nexport interface CheckedDuplication {\n crossFile?: CrossFileDuplicationMetrics;\n /** The detection's `minSimilarityPercent`, the share of a block a copy of it holds at least. */\n minSimilarityPercent: number;\n}\n\nexport interface ExceededLimit {\n metric: string;\n value: number;\n /** The most severe level whose limit the value violates. */\n level: Level;\n /** That level's limit: the value is allowed up to it; a duplicated block is allowed below it. */\n limit: number;\n}\n\nexport interface BlockLocation {\n file: string;\n startLine: number;\n endLine: number;\n}\n\n/** One copy of a clone group. */\ninterface CloneOccurrence extends BlockLocation {\n /**\n * Its code lines matched in another copy, whose number is the length of a duplicated block. Its\n * line span would also count the lines between the matched parts of one copy, which the copies\n * do not share; a block matched as a whole by a near-miss copy holds all its code lines.\n */\n lineNumbers: readonly number[];\n tokenCount: number;\n}\n\nexport interface Violation extends BlockLocation {\n kind: 'function' | 'file' | 'duplication';\n /** The most severe level among `exceeded`. */\n level: Level;\n /** The function's name, qualified with the class it is a member of, for kind `function`. */\n name?: string;\n exceeded: ExceededLimit[];\n /**\n * The parts adding the most to the cognitive complexity, largest first, for a function that\n * exceeds a limit of it and has such parts: operator sequences and jumps outside any branching\n * construct add to the complexity without being one.\n */\n largestBlocks?: CognitiveBlock[];\n /** The other copies of the block, most matched tokens first, for kind `duplication`. */\n partners?: BlockLocation[];\n}\n\nexport interface CheckResult {\n violations: Violation[];\n checkedFunctionCount: number;\n}\n\nconst violationKinds: readonly Violation['kind'][] = ['file', 'function', 'duplication'];\n\nconst cognitiveComplexityMetric = 'functionCognitiveComplexity';\n\nconst nestedInclusiveMetrics = new Set(\n functionThresholds\n .filter(({ includesNestedFunctions }) => includesNestedFunctions)\n .map((threshold) => metricNameOf(threshold))\n);\n\n/** Every limit the files exceed, errors first, then ordered by path, then line. */\nexport function checkThresholds(\n files: readonly CheckedFile[],\n duplication: CheckedDuplication,\n limitsOf: (language: string) => LimitsByLevel\n): CheckResult {\n const crossFileGroupsByFile = indexGroupsByFile(duplication.crossFile, new Set(files.map(({ file }) => file)));\n const violations: Violation[] = [];\n let checkedFunctionCount = 0;\n for (const checkedFile of files) {\n const { file, metrics, hunks } = checkedFile;\n const limits = limitsOf(metrics.language);\n // A rename or a mode change lists the file as changed without touching any of its lines.\n const fileExceeded = hunks?.length === 0 ? [] : collectExceeded(fileThresholds, metrics, limits);\n if (fileExceeded.length > 0) {\n violations.push(toViolation({ file, startLine: 1, endLine: metrics.lines.total }, 'file', fileExceeded));\n }\n const checkedFunctions = listCheckedFunctions(checkedFile);\n checkedFunctionCount += checkedFunctions.length;\n const exceedingFunctions = checkedFunctions\n .map((fn) => ({ fn, exceeded: collectExceeded(functionThresholds, fn, limits) }))\n .filter(({ exceeded }) => exceeded.length > 0);\n for (const { fn, exceeded } of exceedingFunctions) {\n const ownExceeded = exceeded.filter((limit) => !isReportedByEnclosingFunction(limit, fn, exceedingFunctions));\n if (ownExceeded.length > 0) {\n violations.push(toFunctionViolation(file, fn, ownExceeded));\n }\n }\n violations.push(\n ...collectDuplicationViolations(checkedFile, crossFileGroupsByFile.get(file) ?? [], limits, duplication)\n );\n }\n return { violations: violations.toSorted(compareViolations), checkedFunctionCount };\n}\n\n// The kind is compared before the end line because a file violation's end line is corrected after\n// this ordering (to the last line that exists), and the order must not depend on that correction.\nfunction compareViolations(left: Violation, right: Violation): number {\n return (\n levels.indexOf(right.level) - levels.indexOf(left.level) ||\n compareStrings(left.file, right.file) ||\n left.startLine - right.startLine ||\n violationKinds.indexOf(left.kind) - violationKinds.indexOf(right.kind) ||\n left.endLine - right.endLine\n );\n}\n\n/** The file's functions or, for a changed file, those the change touches. */\nfunction listCheckedFunctions({ metrics, hunks }: CheckedFile): FunctionMetrics[] {\n const endsByIndentation = metrics.language === 'python';\n return hunks\n ? metrics.functions.filter((fn) =>\n hunks.some((hunk) => touchesSpan(hunk, fn.startLine, fn.endLine, endsByIndentation))\n )\n : metrics.functions;\n}\n\n/**\n * Whether a function enclosing `fn` exceeds the same limit with a value that covers `fn`, so\n * reporting it for `fn` too would count the same code twice. The enclosing value is at least that\n * of `fn` and both share the file's limits, so its level is never milder.\n */\nfunction isReportedByEnclosingFunction(\n limit: ExceededLimit,\n fn: FunctionMetrics,\n exceedingFunctions: readonly { fn: FunctionMetrics; exceeded: ExceededLimit[] }[]\n): boolean {\n if (!nestedInclusiveMetrics.has(limit.metric)) {\n return false;\n }\n return exceedingFunctions.some(\n (outer) => outer.fn !== fn && encloses(outer.fn, fn) && outer.exceeded.some(({ metric }) => metric === limit.metric)\n );\n}\n\nfunction encloses(outer: FunctionMetrics, inner: FunctionMetrics): boolean {\n const startsBefore =\n outer.startLine < inner.startLine ||\n (outer.startLine === inner.startLine && outer.startColumn <= inner.startColumn);\n const endsAfter =\n outer.endLine > inner.endLine || (outer.endLine === inner.endLine && outer.endColumn >= inner.endColumn);\n return startsBefore && endsAfter;\n}\n\nfunction toFunctionViolation(file: string, fn: FunctionMetrics, exceeded: ExceededLimit[]): Violation {\n const { startLine, endLine } = fn;\n const exceedsCognitiveComplexity = exceeded.some(({ metric }) => metric === cognitiveComplexityMetric);\n return {\n ...toViolation({ file, startLine, endLine }, 'function', exceeded),\n name: qualifyFunctionName(fn),\n ...(exceedsCognitiveComplexity && fn.cognitiveBlocks.length > 0 && { largestBlocks: fn.cognitiveBlocks }),\n };\n}\n\n/** How reports name a function: with the type or module it is a member of, so that it can be found. */\nexport function qualifyFunctionName({ name = '<anonymous>', containerName }: FunctionMetrics): string {\n return containerName ? `${containerName}.${name}` : name;\n}\n\nfunction toViolation(location: BlockLocation, kind: Violation['kind'], exceeded: ExceededLimit[]): Violation {\n const level = exceeded.some((limit) => limit.level === 'error') ? 'error' : 'warning';\n return { kind, level, ...location, exceeded };\n}\n\nfunction collectExceeded<Subject>(\n thresholds: readonly Threshold<Subject>[],\n subject: Subject,\n limits: LimitsByLevel\n): ExceededLimit[] {\n return thresholds.flatMap((threshold) => findExceeded(threshold, subject, limits, (value, limit) => value > limit));\n}\n\n/** The most severe violated limit of the threshold, if any. */\nfunction findExceeded<Subject>(\n threshold: Threshold<Subject>,\n subject: Subject,\n limits: LimitsByLevel,\n violates: (value: number, limit: number) => boolean\n): ExceededLimit[] {\n const value = threshold.measure(subject);\n const level = levels.findLast((candidate) => violates(value, limits[candidate][threshold.key] as number));\n return level\n ? [{ metric: metricNameOf(threshold), value, level, limit: limits[level][threshold.key] as number }]\n : [];\n}\n\n/**\n * Whether the hunk changes the head lines [startLine, endLine]. A hunk without head lines (a pure\n * deletion) sits between its headStart line and the next one. Where indentation ends a span, its\n * last line is a statement rather than a closing delimiter, so a deletion right after that line may\n * have removed the span's tail; `endsByIndentation` counts it as touching, since the head alone\n * cannot tell it from a deletion after the span.\n */\nfunction touchesSpan(hunk: LineHunk, startLine: number, endLine: number, endsByIndentation = false): boolean {\n return hunk.headCount === 0\n ? hunk.headStart >= startLine && (hunk.headStart < endLine || (endsByIndentation && hunk.headStart === endLine))\n : hunk.headStart <= endLine && hunk.headStart + hunk.headCount > startLine;\n}\n\n/** The cross-file groups with an occurrence in each of the given files. */\nfunction indexGroupsByFile(\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n files: ReadonlySet<string>\n): Map<string, CrossFileDuplicateBlockGroup[]> {\n const groupsByFile = new Map<string, CrossFileDuplicateBlockGroup[]>();\n for (const group of crossFileDuplication?.groups ?? []) {\n for (const file of group.files) {\n if (files.has(file)) {\n const groups = groupsByFile.get(file) ?? [];\n groups.push(group);\n groupsByFile.set(file, groups);\n }\n }\n }\n return groupsByFile;\n}\n\n/**\n * One violation per duplicated region of the file: its clone occurrences, within-file and\n * cross-file, that hold at least the duplicated lines of a level (with `hunks`, only those\n * overlapping added lines), with overlapping occurrences merged into one region.\n */\nfunction collectDuplicationViolations(\n { file, metrics, hunks }: CheckedFile,\n crossFileGroups: readonly CrossFileDuplicateBlockGroup[],\n limits: LimitsByLevel,\n { minSimilarityPercent }: CheckedDuplication\n): Violation[] {\n const findExceededLines = (duplicatedLineCount: number): ExceededLimit[] =>\n findExceeded(duplicationThreshold, { duplicatedLineCount }, limits, (value, limit) => value >= limit);\n const groups: CloneOccurrence[][] = [\n ...metrics.duplication.duplicateBlockGroups.map((group) => group.map((occurrence) => ({ ...occurrence, file }))),\n ...crossFileGroups.map((group) => group.occurrences.map((occurrence) => toCloneOccurrence(occurrence))),\n ];\n const isReported = (block: CloneOccurrence): boolean =>\n block.file === file &&\n findExceededLines(block.lineNumbers.length).length > 0 &&\n (!hunks || hunks.some((hunk) => hunk.headCount > 0 && touchesSpan(hunk, block.startLine, block.endLine)));\n const groupByBlock = new Map<CloneOccurrence, CloneOccurrence[]>();\n for (const group of groups) {\n for (const block of group.filter((occurrence) => isReported(occurrence))) {\n groupByBlock.set(block, group);\n }\n }\n return mergeOverlapping([...groupByBlock.keys()]).map(({ merged, sources }) => {\n const listCopies = (minTokenPercent: number): BlockLocation[] =>\n mergeOverlapping(\n sources.flatMap((source) => {\n // The region merges every clone that overlaps it, so a copy of an occurrence lying\n // within a larger one is measured against the larger one.\n const tokenCount = Math.max(\n ...sources.filter((outer) => contains(outer, source)).map((outer) => outer.tokenCount)\n );\n return (groupByBlock.get(source) ?? []).filter(\n (copy) => copy.tokenCount * 100 >= tokenCount * minTokenPercent\n );\n })\n )\n // The region's own occurrences are among the copies of the groups it belongs to.\n .filter((partner) => !overlaps(partner.merged, merged))\n // The largest say the most about what to share.\n .toSorted((left, right) => sumTokens(right.sources) - sumTokens(left.sources))\n .map((partner) => partner.merged);\n // A group joins a block with every place repeating a part of it: a function another file\n // copies whole is grouped with each file holding a few of its lines. Such a fragment is not a\n // copy of the block: sharing it would not remove the block, and a report listing it reads as\n // one more copy. Sizes are compared in tokens, since the same code may be laid out over any\n // number of lines.\n const copies = listCopies(minSimilarityPercent);\n return {\n ...toViolation(\n merged,\n 'duplication',\n // Occurrences that overlap share lines, which count once.\n findExceededLines(new Set(sources.flatMap((source) => source.lineNumbers)).size)\n ),\n // A group does not always hold the copy a block was matched with as a whole: within a file,\n // a copy that already belongs to an exact group is listed as its part in that group. The\n // fragments then are all that locates the block's copies.\n partners: copies.length > 0 ? copies : listCopies(0),\n };\n });\n}\n\nfunction toCloneOccurrence(occurrence: CrossFileDuplicateOccurrence): CloneOccurrence {\n const { lineNumbers } = occurrence;\n assert.ok(lineNumbers, `No matched lines for ${occurrence.file}, a scanned file.`);\n return { ...occurrence, lineNumbers };\n}\n\nfunction sumTokens(occurrences: readonly CloneOccurrence[]): number {\n return occurrences.reduce((sum, occurrence) => sum + occurrence.tokenCount, 0);\n}\n\n/** The locations with those overlapping in the same file merged, ordered by file, then line. */\nfunction mergeOverlapping(\n locations: readonly CloneOccurrence[]\n): { merged: BlockLocation; sources: CloneOccurrence[] }[] {\n const regions: { merged: BlockLocation; sources: CloneOccurrence[] }[] = [];\n const ordered = locations.toSorted(\n (left, right) => compareStrings(left.file, right.file) || left.startLine - right.startLine\n );\n for (const location of ordered) {\n const last = regions.at(-1);\n if (last && overlaps(last.merged, location)) {\n last.merged.endLine = Math.max(last.merged.endLine, location.endLine);\n last.sources.push(location);\n } else {\n const { file, startLine, endLine } = location;\n regions.push({ merged: { file, startLine, endLine }, sources: [location] });\n }\n }\n return regions;\n}\n\n/** Whether `outer` spans every line of `inner`; a block contains itself. */\nfunction contains(outer: BlockLocation, inner: BlockLocation): boolean {\n return outer.file === inner.file && outer.startLine <= inner.startLine && inner.endLine <= outer.endLine;\n}\n\nfunction overlaps(left: BlockLocation, right: BlockLocation): boolean {\n return left.file === right.file && left.startLine <= right.endLine && right.startLine <= left.endLine;\n}\n\n/** Code-unit order, so reports do not depend on the locale. */\nfunction compareStrings(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use strict";const e=[`warning`,`error`],t=[{key:`maxFunctionCognitiveComplexity`,label:`cognitive complexity`,defaultLimits:{warning:15,error:30},hint:`flatten nested branching with early returns and extract nested blocks into named functions.`,measure:e=>e.cognitiveComplexity,includesNestedFunctions:!0},{key:`maxFunctionCyclomaticComplexity`,label:`cyclomatic complexity`,defaultLimits:{warning:1/0,error:1/0},hint:`the function has too many independent paths; split it by decision or replace condition chains with a lookup table.`,measure:e=>e.cyclomaticComplexity},{key:`maxFunctionNcss`,label:`NCSS`,defaultLimits:{warning:30,error:60},hint:`move each group of statements that forms one step into a named function.`,measure:e=>e.ncss,includesNestedFunctions:!0},{key:`maxFunctionNestingDepth`,label:`nesting depth`,defaultLimits:{warning:1/0,error:1/0},hint:`replace nested conditions with guard clauses or move inner blocks into functions.`,measure:e=>e.nestingDepth},{key:`maxFunctionParameterCount`,label:`parameters`,defaultLimits:{warning:6,error:1/0},hint:`group related parameters into one object or split the function.`,measure:e=>e.parameterCount},{key:`maxFunctionHalsteadVolume`,label:`Halstead volume`,defaultLimits:{warning:1/0,error:1/0},hint:`the function holds too many operators and operands; move cohesive parts into helper functions.`,measure:e=>e.halstead.volume,includesNestedFunctions:!0},{key:`maxFunctionHalsteadDifficulty`,label:`Halstead difficulty`,defaultLimits:{warning:1/0,error:1/0},hint:`the function reuses the same operands many times; split it so each part handles fewer values.`,measure:e=>e.halstead.difficulty},{key:`maxFunctionHalsteadEffort`,label:`Halstead effort`,defaultLimits:{warning:1/0,error:1/0},hint:`the function is both large and dense; split it into smaller functions with fewer values each.`,measure:e=>e.halstead.effort},{key:`maxFunctionDepDegree`,label:`DepDegree`,defaultLimits:{warning:1/0,error:1/0},hint:`too many values flow between the variables of the function; split it so each part works on fewer variables.`,measure:e=>e.depDegree,includesNestedFunctions:!0}],n=[{key:`maxFileNcss`,label:`file NCSS`,defaultLimits:{warning:400,error:1e3},hint:`split the file into modules with one responsibility each, for example by moving a class or the helpers that serve only one function into a file of their own.`,measure:e=>e.ncssCount}],r={key:`minDuplicateLines`,label:`duplicated lines`,defaultLimits:{warning:15,error:1/0},hint:`extract the repeated code into one shared function or module and call it from every location.`,measure:e=>e.codeLineCount},i=[...t,...n,r];function a(e){let t=e.key.slice(3);return t.charAt(0).toLowerCase()+t.slice(1)}function o(e,t){let n=(n,r)=>{let a=t?.[n];return{...Object.fromEntries(i.map(({key:e,defaultLimits:t})=>[e,t[n]])),...a?.limits,...a&&Object.hasOwn(a.languages,r)?a.languages[r]:void 0,...e[n]}};return e=>({warning:n(`warning`,e),error:n(`error`,e)})}exports.duplicationThreshold=r,exports.fileThresholds=n,exports.functionThresholds=t,exports.levels=e,exports.metricNameOf=a,exports.resolveLimits=o,exports.thresholds=i;
1
+ "use strict";const e=[`warning`,`error`],t=[{key:`maxFunctionCognitiveComplexity`,label:`cognitive complexity`,defaultLimits:{warning:15,error:30},hint:`flatten nested branching with early returns and extract nested blocks into named functions.`,measure:e=>e.cognitiveComplexity,includesNestedFunctions:!0},{key:`maxFunctionCyclomaticComplexity`,label:`cyclomatic complexity`,defaultLimits:{warning:1/0,error:1/0},hint:`the function has too many independent paths; split it by decision or replace condition chains with a lookup table.`,measure:e=>e.cyclomaticComplexity},{key:`maxFunctionNcss`,label:`NCSS`,defaultLimits:{warning:30,error:60},hint:`move each group of statements that forms one step into a named function.`,measure:e=>e.ncss,includesNestedFunctions:!0},{key:`maxFunctionNestingDepth`,label:`nesting depth`,defaultLimits:{warning:1/0,error:1/0},hint:`replace nested conditions with guard clauses or move inner blocks into functions.`,measure:e=>e.nestingDepth},{key:`maxFunctionParameterCount`,label:`parameters`,defaultLimits:{warning:6,error:1/0},hint:`group related parameters into one object or split the function.`,measure:e=>e.parameterCount},{key:`maxFunctionHalsteadVolume`,label:`Halstead volume`,defaultLimits:{warning:1/0,error:1/0},hint:`the function holds too many operators and operands; move cohesive parts into helper functions.`,measure:e=>e.halstead.volume,includesNestedFunctions:!0},{key:`maxFunctionHalsteadDifficulty`,label:`Halstead difficulty`,defaultLimits:{warning:1/0,error:1/0},hint:`the function reuses the same operands many times; split it so each part handles fewer values.`,measure:e=>e.halstead.difficulty},{key:`maxFunctionHalsteadEffort`,label:`Halstead effort`,defaultLimits:{warning:1/0,error:1/0},hint:`the function is both large and dense; split it into smaller functions with fewer values each.`,measure:e=>e.halstead.effort},{key:`maxFunctionDepDegree`,label:`DepDegree`,defaultLimits:{warning:1/0,error:1/0},hint:`too many values flow between the variables of the function; split it so each part works on fewer variables.`,measure:e=>e.depDegree,includesNestedFunctions:!0}],n=[{key:`maxFileNcss`,label:`file NCSS`,defaultLimits:{warning:400,error:1e3},hint:`split the file into modules with one responsibility each, for example by moving a class or the helpers that serve only one function into a file of their own.`,measure:e=>e.ncssCount}],r={key:`minDuplicateLines`,label:`duplicated lines`,defaultLimits:{warning:15,error:1/0},hint:`extract the repeated code into one shared function or module and call it from every location.`,measure:e=>e.duplicatedLineCount},i=[...t,...n,r];function a(e){let t=e.key.slice(3);return t.charAt(0).toLowerCase()+t.slice(1)}function o(e,t){let n=(n,r)=>{let a=t?.[n];return{...Object.fromEntries(i.map(({key:e,defaultLimits:t})=>[e,t[n]])),...a?.limits,...a&&Object.hasOwn(a.languages,r)?a.languages[r]:void 0,...e[n]}};return e=>({warning:n(`warning`,e),error:n(`error`,e)})}exports.duplicationThreshold=r,exports.fileThresholds=n,exports.functionThresholds=t,exports.levels=e,exports.metricNameOf=a,exports.resolveLimits=o,exports.thresholds=i;
2
2
  //# sourceMappingURL=thresholds.cjs.map