code-gauge 4.8.0 → 5.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.
- package/README.md +34 -9
- package/THIRD-PARTY-NOTICES.txt +1152 -798
- package/dist/cli.cjs +3 -3
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/cliConfig.cjs +1 -1
- package/dist/cliConfig.cjs.map +1 -1
- package/dist/cliConfig.d.ts +13 -3
- package/dist/cliConfig.js +1 -1
- package/dist/cliConfig.js.map +1 -1
- package/dist/diffCommand.cjs +3 -3
- package/dist/diffCommand.cjs.map +1 -1
- package/dist/diffCommand.js +3 -3
- package/dist/diffCommand.js.map +1 -1
- package/dist/duplication.cjs +1 -1
- package/dist/duplication.cjs.map +1 -1
- package/dist/duplication.js +1 -1
- package/dist/duplication.js.map +1 -1
- package/dist/exclusion.cjs +2 -0
- package/dist/exclusion.cjs.map +1 -0
- package/dist/exclusion.d.ts +30 -0
- package/dist/exclusion.js +2 -0
- package/dist/exclusion.js.map +1 -0
- package/dist/git.cjs +1 -1
- package/dist/git.cjs.map +1 -1
- package/dist/git.d.ts +16 -0
- package/dist/git.js +1 -1
- package/dist/git.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/nativeMetrics.cjs +3 -3
- package/dist/nativeMetrics.cjs.map +1 -1
- package/dist/nativeMetrics.js +3 -3
- package/dist/nativeMetrics.js.map +1 -1
- package/dist/newDuplication.cjs +2 -0
- package/dist/newDuplication.cjs.map +1 -0
- package/dist/newDuplication.d.ts +40 -0
- package/dist/newDuplication.js +2 -0
- package/dist/newDuplication.js.map +1 -0
- package/dist/regressionGate.cjs +1 -1
- package/dist/regressionGate.cjs.map +1 -1
- package/dist/regressionGate.d.ts +8 -6
- package/dist/regressionGate.js +1 -1
- package/dist/regressionGate.js.map +1 -1
- package/dist/scan.cjs +1 -1
- package/dist/scan.cjs.map +1 -1
- package/dist/scan.d.ts +12 -4
- package/dist/scan.js +1 -1
- package/dist/scan.js.map +1 -1
- package/dist/worker.cjs +1 -1
- package/dist/worker.cjs.map +1 -1
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/native/Cargo.lock +73 -73
- package/native/Cargo.toml +16 -21
- package/native/code-gauge.wasm +0 -0
- package/native/src/dep_degree.rs +6 -6
- package/native/src/duplication.rs +143 -14
- package/native/src/functions.rs +10 -7
- package/native/src/languages.rs +14 -13
- package/native/src/measure.rs +16 -8
- package/native/src/tree_index.rs +4 -4
- package/native/src/util.rs +9 -2
- package/package.json +9 -8
package/dist/diffCommand.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./crossFileDuplication.cjs"),n=require("./metrics.cjs"),r=require("./regressionGate.cjs"),i=require("./cliConfig.cjs"),a=require("./git.cjs"),o=require("./scan.cjs");let s=require("node:fs/promises"),c=require("node:path");c=e.__toESM(c,1);let l=require("node:os");l=e.__toESM(l,1);async function u(e,t){try{await d(e,t)}catch(e){o.writeStderr(`Error: ${o.formatError(e)}\n`),process.exitCode=2}}async function d(e,t){let n=o.resolveTarget(e),c=await i.loadConfig(t.config,await o.configSearchDirectory(n)),l=i.resolveOptions(t,c),u=i.resolveGateOptions(c),d=await(0,s.realpath)(await a.resolveRepoRoot(await b(await o.configSearchDirectory(n)))),m=await a.resolveMergeBase(d,t.base),h=await a.listChangedFiles(d,m),g=await a.listRepositoryFiles(d),_=await a.listSymlinkPathsAtRevision(d,m),v=await o.scanListedFiles(d,g,l);if(v.fatalError)throw Error(v.fatalError);let y=v.files.map(e=>({relativePath:o.formatPath(e.file,v.displayRoot),file:e})),x=new Set(h.flatMap(e=>[e.headPath,...e.basePath===void 0?[]:[e.basePath]]).filter(e=>o.isScannedPath(e,l))),w=[],T=[...v.warnings];for(let e of v.errors)[...x].some(t=>e.startsWith(`${t}:`))?w.push(e):T.push(e);let{canonicalTarget:D,targetExists:O}=await f(n),k=await p(h,{repoRoot:d,mergeBase:m,canonicalTarget:D,options:l,scannedFiles:y,baseSymlinkPaths:_,scanErrors:[...w]},w,T);if(!O&&!k.some(e=>e.gated))throw Error(`target "${e}" does not exist and matches no changed file`);let{baseCross:j,headCross:M}=S(k,y,l),N=k.map(e=>C(e,j,M)),P=r.evaluateRegressionGate(N,u);t.json?A(t,m,P,N,w,T):E(t,m,P,w,T),w.length>0?process.exitCode=2:P.violations.length>0&&(process.exitCode=1)}async function f(e){try{return{canonicalTarget:await(0,s.realpath)(e),targetExists:!0}}catch{return{canonicalTarget:e,targetExists:!1}}}async function p(e,t,n,r){let i=new Map(t.scannedFiles.map(({relativePath:e,file:t})=>[e,t])),a=await m(e,l.default.availableParallelism()*2,async e=>{let n=[],r=[];return{file:await h(e,t,i,n,r),fileErrors:n,fileWarnings:r}}),o=[];for(let{file:e,fileErrors:t,fileWarnings:i}of a)n.push(...t),r.push(...i),e&&o.push(e);return o}async function m(e,t,n){let r=[],i=0;return await Promise.all(Array.from({length:Math.min(t,e.length)},async()=>{for(;i<e.length;){let t=i;i+=1,r[t]=await n(e[t])}})),r}async function h(e,t,n,r,i){let a=e.status!==`deleted`&&o.isScannedPath(e.headPath,t.options)&&!await y(c.default.join(t.repoRoot,e.headPath)),s=e.basePath!==void 0&&o.isScannedPath(e.basePath,t.options)&&!t.baseSymlinkPaths.has(e.basePath);if(!a&&!s)return;let l=e.status===`deleted`?e.basePath:e.headPath,u=a?n.get(e.headPath):void 0;if(a&&!u){g(e.headPath,t.scanErrors,r);return}let d={changed:e,displayFile:l,gated:a||e.status===`deleted`?x(c.default.join(t.repoRoot,l),t.canonicalTarget):!1,headFile:u};if(!s||await v(d,e.basePath,t,r,i))return u&&await _(d,u,t,i),d}function g(e,t,n){t.some(t=>t.startsWith(`${e}:`))||n.push(`${e}: changed file was not measured`)}async function _(e,t,r,i){try{let i=await(0,s.readFile)(t.file,`utf8`);e.headFunctionTokens=n.collectFunctionTokenSequences(i,{language:o.getLanguage(e.changed.headPath,r.options),duplication:r.options.duplication})}catch(t){i.push(`${e.displayFile}: function token sequences unavailable: ${o.formatError(t)}`)}}async function v(e,t,r,i,s){let c={language:o.getLanguage(t,r.options),duplication:r.options.duplication},l;try{l=await a.readFileAtRevision(r.repoRoot,r.mergeBase,t);let n=await o.measureWithCrossFileData(l,c);e.baseMetrics=n.metrics,e.baseCandidates=n.crossFileData,n.crossFileError!==void 0&&s.push(`${t} (at merge-base): duplication candidates unavailable: ${n.crossFileError}`)}catch(e){return i.push(`${t} (at merge-base): ${o.formatError(e)}`),!1}try{e.baseFunctionTokens=n.collectFunctionTokenSequences(l,c)}catch(e){s.push(`${t} (at merge-base): function token sequences unavailable: ${o.formatError(e)}`)}return!0}async function y(e){return(await(0,s.lstat)(e).catch(()=>{}))?.isSymbolicLink()??!1}async function b(e){let t=e;for(;;){if((await(0,s.stat)(t).catch(()=>{}))?.isDirectory())return t;let e=c.default.dirname(t);if(e===t)return t;t=e}}function x(e,t){let n=c.default.relative(t,e);return n===``||!n.startsWith(`..${c.default.sep}`)&&n!==`..`&&!c.default.isAbsolute(n)}function S(e,n,r){let i=n.flatMap(({relativePath:e,file:t})=>t.duplicationCandidates?[{file:e,...t.duplicationCandidates}]:[]),a=new Set(e.flatMap(e=>e.changed.status===`deleted`?[]:[e.changed.headPath])),o=i.filter(e=>!a.has(e.file));for(let t of e)t.baseCandidates&&t.changed.basePath!==void 0&&o.push({file:t.changed.basePath,...t.baseCandidates});return{baseCross:o.length>=2?t.measureCrossFileDuplication(o,r.duplication):void 0,headCross:i.length>=2?t.measureCrossFileDuplication(i,r.duplication):void 0}}function C(e,t,n){return{file:e.displayFile,baseMetrics:e.baseMetrics,headMetrics:e.headFile?.metrics,baseFunctionTokens:e.baseFunctionTokens,headFunctionTokens:e.headFunctionTokens,baseDuplicatedLineCount:e.baseMetrics===void 0||e.changed.basePath===void 0?0:w(e.baseMetrics,t,e.changed.basePath),headDuplicatedLineCount:e.changed.status===`deleted`?0:w(e.headFile?.metrics,n,e.changed.headPath),duplicationPartners:T(n,e.changed.headPath),gated:e.gated}}function w(e,t,n){return o.collectDuplicatedLineNumbers(e,t,n).size}function T(e,t){if(!e)return[];let n=/* @__PURE__ */ new Set;for(let r of e.groups)if(r.files.includes(t))for(let e of r.files)e!==t&&n.add(e);return[...n].toSorted()}function E(e,t,n,r,i){let a=t.slice(0,12);r.length>0?(o.writeStdout(`Regression gate could not complete: ${r.length} measurement failures (details on stderr)${n.violations.length>0?`; ${n.violations.length} violations in the measured files`:``} (base ${e.base}, merge-base ${a}).\n`),D(n)):n.violations.length===0?o.writeStdout(`Regression gate passed: ${n.checkedFileCount} changed files, ${n.checkedFunctionCount} functions checked (base ${e.base}, merge-base ${a}).\n`):(o.writeStdout(`Regression gate vs ${e.base} (merge-base ${a}): ${n.violations.length} violations\n`),D(n)),e.full&&O(n);for(let e of i)o.writeStderr(`Warning: ${e}\n`);for(let e of r)o.writeStderr(`Error: ${e}\n`)}function D(e){for(let[t,n]of e.violations.entries())o.writeStdout(`${t+1}. ${n.message}\n`)}function O(e){if(e.checkedFunctions.length!==0){o.writeStdout(`
|
|
1
|
+
"use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./crossFileDuplication.cjs"),n=require("./metrics.cjs"),r=require("./newDuplication.cjs"),i=require("./regressionGate.cjs"),a=require("./cliConfig.cjs"),o=require("./git.cjs"),s=require("./exclusion.cjs"),c=require("./scan.cjs");let l=require("@willbooster/shared-lib"),u=require("node:fs/promises"),d=require("node:path");d=e.__toESM(d,1);let f=require("node:os");f=e.__toESM(f,1);async function p(e,t){try{await m(e,t)}catch(e){c.writeStderr(`Error: ${(0,l.getErrorMessage)(e)}\n`),process.exitCode=2}}async function m(e,t){let n=c.resolveTarget(e),l=await a.loadConfig(t.config,await c.configSearchDirectory(n)),f=a.resolveOptions(t,l),p=a.resolveGateOptions(l),m=await(0,u.realpath)(await o.resolveRepoRoot(await O(await c.configSearchDirectory(n)))),h=await o.resolveMergeBase(m,t.base),b=await o.listChangedFiles(m,h),x=await o.listRepositoryFiles(m),S=await o.listSymlinkPathsAtRevision(m,h),{canonicalTarget:C,targetExists:w}=await v(n),T=(await(0,u.stat)(C).catch(()=>{}))?.isFile()?C:void 0,E=T&&d.default.relative(m,T).split(d.default.sep).join(`/`);E!==void 0&&!E.startsWith(`../`)&&!x.has(E)&&(x.add(E),b.push({status:`added`,headPath:E}));let D=_(T,m,b),k=b.some(e=>[e.headPath,e.basePath].some(e=>e!==void 0&&d.default.posix.basename(e)===`.gitattributes`)),N=b.flatMap(e=>e.basePath===void 0?[]:[e.basePath]),[P,F]=await Promise.all([s.loadRepositoryExclusion(m,x,f.exclude),s.loadRepositoryExclusion(m,k?[.../* @__PURE__ */ new Set([...N,...x])]:N,f.exclude,h)]),L=s.keepPaths(P,D),z=s.keepPaths(F,D),B=await c.scanListedFiles(m,x,{...f,loadExclusion:()=>Promise.resolve(L)},D);if(B.fatalError)throw Error(B.fatalError);let V=B.files.map(e=>({relativePath:c.formatPath(e.file,B.displayRoot),file:e}));k&&b.push(...await g(V,b,m,L,z));let U=new Set(b.flatMap(e=>[e.headPath,...e.basePath===void 0?[]:[e.basePath]]).filter(e=>c.isScannedPath(e,f,D.has(d.default.join(m,e))))),W=[],G=[...B.warnings];for(let e of B.errors)[...U].some(t=>e.startsWith(`${t}:`))?W.push(e):G.push(e);let K=await y(b,{repoRoot:m,mergeBase:h,canonicalTarget:C,options:f,scannedFiles:V,baseSymlinkPaths:S,scanErrors:[...W],headExclusion:L,baseExclusion:z,explicitFiles:D,generatedHeadFiles:new Set(B.generatedFiles)},W,G);if(!w&&!K.some(e=>e.gated))throw Error(`target "${e}" does not exist and matches no changed file`);let{baseCross:q,headCross:J}=A(K,V,f),Y=new Map(K.flatMap(({changed:e})=>e.status===`renamed`&&e.basePath!==void 0?[[e.basePath,e.headPath]]:[])),X=e=>Y.get(e)??e,Z=M(V),ee=r.findNewlyDuplicatedLines(K.map(e=>j(e,q,J,X,Z))),Q=K.map((e,t)=>I(e,ee[t]??[],J)),$=i.evaluateRegressionGate(Q,p);t.json?H(t,h,$,Q,W,G):R(t,h,$,W,G),W.length>0?process.exitCode=2:$.violations.length>0&&(process.exitCode=1)}const h=`/* @generated */`;async function g(e,t,n,r,i){let a=new Set(t.flatMap(e=>[e.headPath,e.basePath])),o=[];for(let{relativePath:t,file:s}of e){let e=d.default.join(n,t);if(a.has(t))continue;let c=i.isGeneratedCode(e,h)&&!r.isGeneratedCode(e,h);(i.isExcludedPath(e)||c&&i.isGeneratedCode(e,await(0,u.readFile)(s.file,`utf8`)))&&o.push({status:`modified`,basePath:t,headPath:t})}return o}function _(e,t,n){if(e===void 0)return/* @__PURE__ */ new Set;let r=/* @__PURE__ */ new Set([e]);for(let i of n)i.basePath!==void 0&&d.default.join(t,i.headPath)===e&&r.add(d.default.join(t,i.basePath));return r}async function v(e){try{return{canonicalTarget:await(0,u.realpath)(e),targetExists:!0}}catch{return{canonicalTarget:e,targetExists:!1}}}async function y(e,t,n,r){let i=new Map(t.scannedFiles.map(({relativePath:e,file:t})=>[e,t])),a=await(0,l.mapConcurrently)(e,f.default.availableParallelism()*2,async e=>{let n=[],r=[];return{file:await b(e,t,i,n,r),fileErrors:n,fileWarnings:r}}),o=[];for(let{file:e,fileErrors:t,fileWarnings:i}of a)n.push(...t),r.push(...i),e&&o.push(e);return o}async function b(e,t,n,r,i){let a=d.default.join(t.repoRoot,e.headPath),o=e.status!==`deleted`&&c.isScannedPath(e.headPath,t.options,t.explicitFiles.has(a))&&!t.headExclusion.isExcludedPath(a)&&!t.generatedHeadFiles.has(a)&&!await D(a),s=e.basePath===void 0?``:d.default.join(t.repoRoot,e.basePath),f=e.basePath!==void 0&&c.isScannedPath(e.basePath,t.options,t.explicitFiles.has(s))&&!t.baseExclusion.isExcludedPath(s)&&!t.baseSymlinkPaths.has(e.basePath);if(!o&&!f)return;let p=e.status===`deleted`?e.basePath:e.headPath,m=o?n.get(e.headPath):void 0;if(o&&!m){C(e.headPath,t.scanErrors,r);return}let h={changed:e,displayFile:p,gated:o||e.status===`deleted`?k(d.default.join(t.repoRoot,p),t.canonicalTarget):!1,headFile:m};if(f){let n=await T(h,e.basePath,t,r,i);if(n===`failed`||n===`generated`&&!m)return}try{m&&(h.headContent=await(0,u.readFile)(m.file,`utf8`),w(h,h.headContent,t,i)),h.hunks=await x(h,t)}catch(e){r.push(`${p}: ${(0,l.getErrorMessage)(e)}`);return}return h}async function x(e,t){let{changed:n,baseContent:r,headContent:i}=e;return r!==void 0&&i!==void 0?await o.listLineHunks(t.repoRoot,t.mergeBase,n.basePath,n.headPath):[{baseStart:1,baseCount:r===void 0?0:S(r).length,headStart:1,headCount:i===void 0?0:S(i).length}]}function S(e){return e.split(/\r\n|\n|\r/u)}function C(e,t,n){t.some(t=>t.startsWith(`${e}:`))||n.push(`${e}: changed file was not measured`)}function w(e,t,r,i){try{e.headFunctionTokens=n.collectFunctionTokenSequences(t,{language:E(e.changed.headPath,r),duplication:r.options.duplication})}catch(t){i.push(`${e.displayFile}: function token sequences unavailable: ${(0,l.getErrorMessage)(t)}`)}}async function T(e,t,r,i,a){let s={language:E(t,r),duplication:r.options.duplication},u;try{if(u=await o.readFileAtRevision(r.repoRoot,r.mergeBase,t),r.baseExclusion.isGeneratedCode(d.default.join(r.repoRoot,t),u))return`generated`;let n=await c.measureWithCrossFileData(u,s);e.baseMetrics=n.metrics,e.baseContent=u,e.baseCandidates=n.crossFileData,n.crossFileError!==void 0&&a.push(`${t} (at merge-base): duplication candidates unavailable: ${n.crossFileError}`)}catch(e){return i.push(`${t} (at merge-base): ${(0,l.getErrorMessage)(e)}`),`failed`}try{e.baseFunctionTokens=n.collectFunctionTokenSequences(u,s)}catch(e){a.push(`${t} (at merge-base): function token sequences unavailable: ${(0,l.getErrorMessage)(e)}`)}return`measured`}function E(e,t){let n=t.explicitFiles.has(d.default.join(t.repoRoot,e));return c.getLanguage(e,t.options,n)}async function D(e){return(await(0,u.lstat)(e).catch(()=>{}))?.isSymbolicLink()??!1}async function O(e){let t=e;for(;;){if((await(0,u.stat)(t).catch(()=>{}))?.isDirectory())return t;let e=d.default.dirname(t);if(e===t)return t;t=e}}function k(e,t){let n=d.default.relative(t,e);return n===``||!n.startsWith(`..${d.default.sep}`)&&n!==`..`&&!d.default.isAbsolute(n)}function A(e,n,r){let i=n.flatMap(({relativePath:e,file:t})=>t.duplicationCandidates?[{file:e,...t.duplicationCandidates}]:[]),a=new Set(e.flatMap(e=>e.changed.status===`deleted`?[]:[e.changed.headPath])),o=i.filter(e=>!a.has(e.file));for(let t of e)t.baseCandidates&&t.changed.basePath!==void 0&&o.push({file:t.changed.basePath,...t.baseCandidates});return{baseCross:o.length>=2?t.measureCrossFileDuplication(o,r.duplication):void 0,headCross:i.length>=2?t.measureCrossFileDuplication(i,r.duplication):void 0}}function j(e,t,n,r,i){let a=F(e.headFile?.metrics,n,e.changed.headPath);return{baseLines:e.baseContent===void 0?void 0:S(e.baseContent),headLines:e.headContent===void 0?void 0:S(e.headContent),baseDuplicatedLines:e.baseMetrics===void 0||e.changed.basePath===void 0?/* @__PURE__ */ new Set:c.collectDuplicatedLineNumbers(e.baseMetrics,t,e.changed.basePath),headDuplicatedLines:e.headFile===void 0?/* @__PURE__ */ new Set:P(c.collectDuplicatedLineNumbers(e.headFile.metrics,n,e.changed.headPath),e.changed.headPath,a,i),baseOccurrences:F(e.baseMetrics,t,e.changed.basePath,r),headOccurrences:a,hunks:e.hunks??[]}}function M(e){let t=new Map(e.map(({relativePath:e,file:t})=>[e,t.duplicationCandidates?.tokens])),n=/* @__PURE__ */ new Map;return e=>{if(!n.has(e)){let r=t.get(e);n.set(e,r&&N(r))}return n.get(e)}}function N(e){let t=/* @__PURE__ */ new Map,n=/* @__PURE__ */ new Map;for(let r of e){let e=r.startRow+1,i=r.text;if(r.kind===`id`){let t=n.get(e)??/* @__PURE__ */ new Map;n.set(e,t),t.has(r.text)||t.set(r.text,t.size),i=`\u0001${t.get(r.text)}`}t.set(e,`${t.get(e)??``}${i}\u0000`)}let r=/* @__PURE__ */ new Map;for(let e of t.values())r.set(e,(r.get(e)??0)+1);return{byLine:t,lineCounts:r}}function P(e,t,n,i){let a=i(t);if(a===void 0)return new Set(e);let o=(e,t)=>{if(t===``)return(a.lineCounts.get(e)??0)>1;let n=i(t);return n===void 0||n.lineCounts.has(e)},s=r.indexPartnersByLine(n);return new Set([...e].filter(e=>{let t=a.byLine.get(e);return t===void 0||[...s.get(e)??[]].some(e=>o(t,e))}))}function F(e,t,n,r=e=>e){if(e===void 0||n===void 0)return[];let i=e.duplication.duplicateBlockGroups.flatMap(e=>e.map(({startLine:e,endLine:t})=>({startLine:e,endLine:t,partners:[``]}))),a=(t?.groups??[]).flatMap(e=>{let t=e.occurrences.filter(e=>e.file===n),i=[...e.files.filter(e=>e!==n).map(r),...t.length>1?[``]:[]];return t.map(({startLine:e,endLine:t})=>({startLine:e,endLine:t,partners:i}))});return[...i,...a]}function I(e,t,n){return{file:e.displayFile,baseMetrics:e.baseMetrics,headMetrics:e.headFile?.metrics,baseFunctionTokens:e.baseFunctionTokens,headFunctionTokens:e.headFunctionTokens,newlyDuplicatedLines:t,duplicationPartners:L(e,t,n),gated:e.gated}}function L(e,t,n){let r=e.changed.headPath,i=e=>e.file===r&&t.some(t=>t>=e.startLine&&t<=e.endLine),a=/* @__PURE__ */ new Set;for(let e of n?.groups??[])if(e.occurrences.some(i))for(let t of e.files)t!==r&&a.add(t);return[...a].toSorted()}function R(e,t,n,r,i){let a=t.slice(0,12);r.length>0?(c.writeStdout(`Regression gate could not complete: ${r.length} measurement failures (details on stderr)${n.violations.length>0?`; ${n.violations.length} violations in the measured files`:``} (base ${e.base}, merge-base ${a}).\n`),z(n)):n.violations.length===0?c.writeStdout(`Regression gate passed: ${n.checkedFileCount} changed files, ${n.checkedFunctionCount} functions checked (base ${e.base}, merge-base ${a}).\n`):(c.writeStdout(`Regression gate vs ${e.base} (merge-base ${a}): ${n.violations.length} violations\n`),z(n)),e.full&&B(n);for(let e of i)c.writeStderr(`Warning: ${e}\n`);for(let e of r)c.writeStderr(`Error: ${e}\n`)}function z(e){for(let[t,n]of e.violations.entries())c.writeStdout(`${t+1}. ${n.message}\n`)}function B(e){if(e.checkedFunctions.length!==0){c.writeStdout(`
|
|
2
2
|
Checked functions (base -> head):
|
|
3
|
-
`);for(let t of e.checkedFunctions)
|
|
4
|
-
`)}exports.runDiffCommand=
|
|
3
|
+
`);for(let t of e.checkedFunctions)c.writeStdout(`- ${V(t)}\n`)}}function V(e){let t=(t,n=String)=>{let r=n(t(e.head));return e.base?`${n(t(e.base))} -> ${r}`:r},n=[`cognitive ${t(e=>e.cognitiveComplexity)}`,`NCSS ${t(e=>e.ncss)}`,`nesting ${t(e=>e.nestingDepth)}`,`DepDegree ${t(e=>e.depDegree)}`,`volume ${t(e=>e.halsteadVolume,e=>e.toFixed(1))}`];return`${e.file}:${e.startLine}-${e.endLine} ${e.name}${e.base?``:` (new)`}: ${n.join(`, `)}`}function H(e,t,n,r,i,a){let o={base:e.base,mergeBase:t,passed:n.violations.length===0&&i.length===0,violations:n.violations,checkedFileCount:n.checkedFileCount,checkedFunctionCount:n.checkedFunctionCount,newFunctionCount:n.newFunctionCount,errors:i,warnings:a};e.full&&(o.files=r.filter(e=>e.gated!==!1).map(e=>({file:e.file,baseFunctionCount:e.baseMetrics?.functions.length??0,headFunctionCount:e.headMetrics?.functions.length??0,baseNcss:e.baseMetrics?.ncssCount??0,headNcss:e.headMetrics?.ncssCount??0,baseMaxCognitiveComplexity:e.baseMetrics?.maxCognitiveComplexity??0,headMaxCognitiveComplexity:e.headMetrics?.maxCognitiveComplexity??0,newlyDuplicatedLines:e.newlyDuplicatedLines,duplicationPartners:e.duplicationPartners,functions:n.checkedFunctions.filter(t=>t.file===e.file)}))),c.writeStdout(JSON.stringify(o,void 0,2)+`
|
|
4
|
+
`)}exports.runDiffCommand=p;
|
|
5
5
|
//# sourceMappingURL=diffCommand.cjs.map
|
package/dist/diffCommand.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diffCommand.cjs","names":["formatError","resolveTarget","loadConfig","configSearchDirectory","resolveOptions","resolveGateOptions","realpath","resolveRepoRoot","resolveMergeBase","listChangedFiles","listRepositoryFiles","listSymlinkPathsAtRevision","scanListedFiles","formatPath","isScannedPath","evaluateRegressionGate","os","path","readFile","collectFunctionTokenSequences","getLanguage","readFileAtRevision","measureWithCrossFileData","lstat","stat","measureCrossFileDuplication","collectDuplicatedLineNumbers"],"sources":["../src/diffCommand.ts"],"sourcesContent":["import { lstat, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { loadConfig, resolveGateOptions, resolveOptions, type ResolvedOptions } from './cliConfig.js';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData } from './duplication.js';\nimport {\n listChangedFiles,\n listRepositoryFiles,\n listSymlinkPathsAtRevision,\n readFileAtRevision,\n resolveMergeBase,\n resolveRepoRoot,\n type ChangedFile,\n} from './git.js';\nimport { collectFunctionTokenSequences } from './metrics.js';\nimport {\n evaluateRegressionGate,\n type CheckedFunctionReport,\n type GateFileInput,\n type GateFunctionValues,\n type GateResult,\n} from './regressionGate.js';\nimport {\n collectDuplicatedLineNumbers,\n configSearchDirectory,\n formatError,\n formatPath,\n getLanguage,\n isScannedPath,\n measureWithCrossFileData,\n resolveTarget,\n scanListedFiles,\n writeStderr,\n writeStdout,\n type FileMetrics,\n} from './scan.js';\nimport type { CodeMetrics, LanguageName } from './types.js';\n\n/** Raw options of the `diff` subcommand; every field but base is undefined unless the flag was passed. */\nexport interface DiffCliOptions {\n base: string;\n config?: string;\n duplicationMinTokens?: number;\n duplicationMaxGapTokens?: number;\n duplicationMinSimilarityPercent?: number;\n includeTests?: boolean;\n json?: boolean;\n full?: boolean;\n}\n\n/** One changed file measured at both revisions, plus its duplication-universe contribution. */\ninterface PreparedFile {\n changed: ChangedFile;\n /** Repository-relative display path: the head path, or the base path for deleted files. */\n displayFile: string;\n /** Whether the file is gated (under the target directory); others only feed the base universe. */\n gated: boolean;\n headFile?: FileMetrics;\n baseMetrics?: CodeMetrics;\n baseCandidates?: CrossFileDuplicationFileData;\n baseFunctionTokens?: Int32Array[];\n headFunctionTokens?: Int32Array[];\n}\n\n/** A scanned file that git considers part of the project, keyed by its repository-relative path. */\ninterface ScannedFile {\n relativePath: string;\n file: FileMetrics;\n}\n\n/**\n * Runs the regression gate: measures the files changed relative to the merge-base with the base\n * ref, at both revisions (`git cat-file`; no checkout, no persisted baseline), and reports only\n * violations. Exit codes: 0 all gates passed, 1 violations, 2 changed files could not be measured.\n */\nexport async function runDiffCommand(target: string, cliOptions: DiffCliOptions): Promise<void> {\n try {\n await runGate(target, cliOptions);\n } catch (error) {\n writeStderr(`Error: ${formatError(error)}\\n`);\n process.exitCode = 2;\n }\n}\n\nasync function runGate(target: string, cliOptions: DiffCliOptions): Promise<void> {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const gateOptions = resolveGateOptions(config);\n\n // The target may be a typo'd path whose ancestors don't exist either; repository discovery must\n // still run so the mistyped target gets its own diagnostic instead of a git spawn failure.\n const repoRoot = await realpath(\n await resolveRepoRoot(await firstExistingDirectory(await configSearchDirectory(resolvedTarget)))\n );\n const mergeBase = await resolveMergeBase(repoRoot, cliOptions.base);\n const changedFiles = await listChangedFiles(repoRoot, mergeBase);\n\n // Every git-visible file (tracked or untracked non-ignored) is measured at head: that provides\n // the head metrics of changed files and the project-wide duplication universe, so copy-paste\n // from unchanged code into changed files is caught. Scanning the explicit git list (instead of\n // walking the tree) keeps ignored artifact directories from ever being parsed: they exist in\n // neither the base commit nor CI, so they would only cost time and skew duplication counts.\n // Unchanged files are byte-identical at both revisions, so the base universe is the same scan\n // with the changed files' contents swapped for their merge-base blobs.\n const repositoryFiles = await listRepositoryFiles(repoRoot);\n const baseSymlinkPaths = await listSymlinkPathsAtRevision(repoRoot, mergeBase);\n const scan = await scanListedFiles(repoRoot, repositoryFiles, options);\n // A run-wide failure (a missing native addon) invalidates the whole gate: surface it once as\n // the fatal error (exit 2) instead of diagnosing every changed file as unmeasured.\n if (scan.fatalError) {\n throw new Error(scan.fatalError);\n }\n const scannedFiles: ScannedFile[] = scan.files.map((file) => ({\n relativePath: formatPath(file.file, scan.displayRoot),\n file,\n }));\n\n // A measurement failure on ANY scannable changed file forces exit 2 — deliberately including\n // files outside a scoped target, because cross-file function matching and the base duplication\n // universe for the gated files depend on them. Failures elsewhere (unchanged files) and\n // unsupported changed paths degrade to warnings.\n const changedPaths = new Set(\n changedFiles\n .flatMap((changed) => [changed.headPath, ...(changed.basePath === undefined ? [] : [changed.basePath])])\n .filter((changedPath) => isScannedPath(changedPath, options))\n );\n const errors: string[] = [];\n const warnings = [...scan.warnings];\n for (const error of scan.errors) {\n if ([...changedPaths].some((changedPath) => error.startsWith(`${changedPath}:`))) {\n errors.push(error);\n } else {\n warnings.push(error);\n }\n }\n\n const { canonicalTarget, targetExists } = await canonicalizeTarget(resolvedTarget);\n const prepared = await prepareChangedFiles(\n changedFiles,\n { repoRoot, mergeBase, canonicalTarget, options, scannedFiles, baseSymlinkPaths, scanErrors: [...errors] },\n errors,\n warnings\n );\n // A gate must not fail open on a mistyped target: a nonexistent path is only acceptable when it\n // still matches changed files (e.g. a fully deleted directory).\n if (!targetExists && !prepared.some((file) => file.gated)) {\n throw new Error(`target \"${target}\" does not exist and matches no changed file`);\n }\n\n // Non-gated files (outside the target, or renamed out of scan scope) still feed function\n // matching and the duplication universes; the evaluator reports nothing for them.\n const { baseCross, headCross } = measureDuplicationUniverses(prepared, scannedFiles, options);\n const inputs = prepared.map((file) => toGateInput(file, baseCross, headCross));\n const result = evaluateRegressionGate(inputs, gateOptions);\n\n if (cliOptions.json) {\n printJsonReport(cliOptions, mergeBase, result, inputs, errors, warnings);\n } else {\n printTextReport(cliOptions, mergeBase, result, errors, warnings);\n }\n\n if (errors.length > 0) {\n process.exitCode = 2;\n } else if (result.violations.length > 0) {\n process.exitCode = 1;\n }\n}\n\n/** The target may not exist (e.g. only deleted files under it); fall back to the resolved path. */\nasync function canonicalizeTarget(resolvedTarget: string): Promise<{ canonicalTarget: string; targetExists: boolean }> {\n try {\n return { canonicalTarget: await realpath(resolvedTarget), targetExists: true };\n } catch {\n return { canonicalTarget: resolvedTarget, targetExists: false };\n }\n}\n\ninterface GateContext {\n repoRoot: string;\n mergeBase: string;\n canonicalTarget: string;\n options: ResolvedOptions;\n scannedFiles: ScannedFile[];\n /** Paths that are symbolic links at the merge-base; like head symlinks, they are not gated. */\n baseSymlinkPaths: Set<string>;\n /** Errors the head scan recorded against changed files. */\n scanErrors: readonly string[];\n}\n\nasync function prepareChangedFiles(\n changedFiles: ChangedFile[],\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile[]> {\n const headByPath = new Map(context.scannedFiles.map(({ relativePath, file }) => [relativePath, file]));\n // Files are prepared concurrently (git reads and base measurements); each records its diagnostics\n // on its own, and they are appended in changed-file order so the report stays deterministic.\n const results = await mapConcurrently(changedFiles, os.availableParallelism() * 2, async (changed) => {\n const fileErrors: string[] = [];\n const fileWarnings: string[] = [];\n const file = await prepareChangedFile(changed, context, headByPath, fileErrors, fileWarnings);\n return { file, fileErrors, fileWarnings };\n });\n const prepared: PreparedFile[] = [];\n for (const { file, fileErrors, fileWarnings } of results) {\n errors.push(...fileErrors);\n warnings.push(...fileWarnings);\n if (file) {\n prepared.push(file);\n }\n }\n return prepared;\n}\n\n/** `Promise.all(items.map(map))` with at most `limit` calls pending at once, results in input order. */\nasync function mapConcurrently<T, R>(items: readonly T[], limit: number, map: (item: T) => Promise<R>): Promise<R[]> {\n const results: R[] = [];\n let nextIndex = 0;\n const work = async (): Promise<void> => {\n while (nextIndex < items.length) {\n const index = nextIndex;\n nextIndex += 1;\n results[index] = await map(items[index] as T);\n }\n };\n await Promise.all(Array.from({ length: Math.min(limit, items.length) }, work));\n return results;\n}\n\nasync function prepareChangedFile(\n changed: ChangedFile,\n context: GateContext,\n headByPath: Map<string, FileMetrics>,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile | undefined> {\n // Symbolic links are skipped on both sides, mirroring scanListedFiles: git stores only the\n // target string, so a symlink blob is not measurable source.\n const headScannable =\n changed.status !== 'deleted' &&\n isScannedPath(changed.headPath, context.options) &&\n !(await isSymbolicLink(path.join(context.repoRoot, changed.headPath)));\n // A base path outside the scan scope (renamed from a test/ignored directory, or an unsupported\n // extension) was never measurable code: its content gates as new code instead of ratcheting\n // against a blob the scanner would not have measured.\n const baseScannable =\n changed.basePath !== undefined &&\n isScannedPath(changed.basePath, context.options) &&\n !context.baseSymlinkPaths.has(changed.basePath);\n if (!headScannable && !baseScannable) {\n return undefined;\n }\n\n const displayFile = changed.status === 'deleted' ? (changed.basePath as string) : changed.headPath;\n const headFile = headScannable ? headByPath.get(changed.headPath) : undefined;\n if (headScannable && !headFile) {\n reportUnmeasuredChangedFile(changed.headPath, context.scanErrors, errors);\n return undefined;\n }\n\n const file: PreparedFile = {\n changed,\n displayFile,\n // A file whose head left the scan scope still contributes its base functions to matching and\n // its base blob to the base universe, but nothing about it is gated or reported.\n gated:\n headScannable || changed.status === 'deleted'\n ? isWithinTarget(path.join(context.repoRoot, displayFile), context.canonicalTarget)\n : false,\n headFile,\n };\n\n if (baseScannable && !(await measureBaseRevision(file, changed.basePath as string, context, errors, warnings))) {\n return undefined;\n }\n\n if (headFile) {\n await collectHeadFunctionTokens(file, headFile, context, warnings);\n }\n\n return file;\n}\n\n/**\n * The scan covers exactly the git-visible list, so a scannable changed path can only be missing\n * after a measurement failure (already recorded as an error) or a silent exclusion (an alias of\n * an already-visited file, or absence from the git list). Failing loudly keeps the gate from\n * passing with the file unchecked.\n */\nfunction reportUnmeasuredChangedFile(headPath: string, scanErrors: readonly string[], errors: string[]): void {\n if (!scanErrors.some((error) => error.startsWith(`${headPath}:`))) {\n errors.push(`${headPath}: changed file was not measured`);\n }\n}\n\nasync function collectHeadFunctionTokens(\n file: PreparedFile,\n headFile: FileMetrics,\n context: GateContext,\n warnings: string[]\n): Promise<void> {\n try {\n const headContent = await readFile(headFile.file, 'utf8');\n file.headFunctionTokens = collectFunctionTokenSequences(headContent, {\n language: getLanguage(file.changed.headPath, context.options) as LanguageName,\n duplication: context.options.duplication,\n });\n } catch (error) {\n // Only rename re-matching degrades without token sequences; the head metrics still gate.\n warnings.push(`${file.displayFile}: function token sequences unavailable: ${formatError(error)}`);\n }\n}\n\n/**\n * Measures the merge-base blob into `file`; false (with an error recorded) only when the metrics\n * themselves cannot be measured. The auxiliary collections (duplication candidates, token\n * sequences) may fail independently of the metrics, so their failure only degrades duplication\n * data and rename re-matching — the function-level ratchets still run.\n */\nasync function measureBaseRevision(\n file: PreparedFile,\n basePath: string,\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<boolean> {\n const measureOptions = {\n language: getLanguage(basePath, context.options) as LanguageName,\n duplication: context.options.duplication,\n };\n let baseContent;\n try {\n baseContent = await readFileAtRevision(context.repoRoot, context.mergeBase, basePath);\n const measured = await measureWithCrossFileData(baseContent, measureOptions);\n file.baseMetrics = measured.metrics;\n file.baseCandidates = measured.crossFileData;\n if (measured.crossFileError !== undefined) {\n warnings.push(`${basePath} (at merge-base): duplication candidates unavailable: ${measured.crossFileError}`);\n }\n } catch (error) {\n errors.push(`${basePath} (at merge-base): ${formatError(error)}`);\n return false;\n }\n try {\n file.baseFunctionTokens = collectFunctionTokenSequences(baseContent, measureOptions);\n } catch (error) {\n warnings.push(`${basePath} (at merge-base): function token sequences unavailable: ${formatError(error)}`);\n }\n return true;\n}\n\nasync function isSymbolicLink(absolutePath: string): Promise<boolean> {\n const stats = await lstat(absolutePath).catch(() => {});\n return stats?.isSymbolicLink() ?? false;\n}\n\n/** Walks up to the nearest existing DIRECTORY, so git commands never spawn in a missing or non-directory cwd. */\nasync function firstExistingDirectory(directory: string): Promise<string> {\n let current = directory;\n while (true) {\n const stats = await stat(current).catch(() => {});\n if (stats?.isDirectory()) {\n return current;\n }\n const parent = path.dirname(current);\n if (parent === current) {\n return current;\n }\n current = parent;\n }\n}\n\nfunction isWithinTarget(candidate: string, targetDirectory: string): boolean {\n const relative = path.relative(targetDirectory, candidate);\n return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));\n}\n\nfunction measureDuplicationUniverses(\n prepared: PreparedFile[],\n scannedFiles: ScannedFile[],\n options: ResolvedOptions\n): { baseCross?: CrossFileDuplicationMetrics; headCross?: CrossFileDuplicationMetrics } {\n const headSources = scannedFiles.flatMap(({ relativePath, file }) =>\n file.duplicationCandidates ? [{ file: relativePath, ...file.duplicationCandidates }] : []\n );\n\n const changedHeadPaths = new Set(\n prepared.flatMap((file) => (file.changed.status === 'deleted' ? [] : [file.changed.headPath]))\n );\n const baseSources = headSources.filter((source) => !changedHeadPaths.has(source.file));\n for (const file of prepared) {\n if (file.baseCandidates && file.changed.basePath !== undefined) {\n baseSources.push({ file: file.changed.basePath, ...file.baseCandidates });\n }\n }\n\n return {\n baseCross: baseSources.length >= 2 ? measureCrossFileDuplication(baseSources, options.duplication) : undefined,\n headCross: headSources.length >= 2 ? measureCrossFileDuplication(headSources, options.duplication) : undefined,\n };\n}\n\nfunction toGateInput(\n file: PreparedFile,\n baseCross: CrossFileDuplicationMetrics | undefined,\n headCross: CrossFileDuplicationMetrics | undefined\n): GateFileInput {\n return {\n file: file.displayFile,\n baseMetrics: file.baseMetrics,\n headMetrics: file.headFile?.metrics,\n baseFunctionTokens: file.baseFunctionTokens,\n headFunctionTokens: file.headFunctionTokens,\n baseDuplicatedLineCount:\n file.baseMetrics === undefined || file.changed.basePath === undefined\n ? 0\n : countDuplicatedLines(file.baseMetrics, baseCross, file.changed.basePath),\n headDuplicatedLineCount:\n file.changed.status === 'deleted'\n ? 0\n : countDuplicatedLines(file.headFile?.metrics, headCross, file.changed.headPath),\n duplicationPartners: collectPartners(headCross, file.changed.headPath),\n gated: file.gated,\n };\n}\n\nfunction countDuplicatedLines(\n metrics: CodeMetrics | undefined,\n cross: CrossFileDuplicationMetrics | undefined,\n file: string\n): number {\n return collectDuplicatedLineNumbers(metrics, cross, file).size;\n}\n\nfunction collectPartners(cross: CrossFileDuplicationMetrics | undefined, file: string): string[] {\n if (!cross) {\n return [];\n }\n const partners = new Set<string>();\n for (const group of cross.groups) {\n if (group.files.includes(file)) {\n for (const partner of group.files) {\n if (partner !== file) {\n partners.add(partner);\n }\n }\n }\n }\n return [...partners].toSorted();\n}\n\nfunction printTextReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n errors: string[],\n warnings: string[]\n): void {\n const shortBase = mergeBase.slice(0, 12);\n if (errors.length > 0) {\n // Unmeasured files were not gated, so \"0 violations\" would be vacuous; never claim a pass.\n writeStdout(\n `Regression gate could not complete: ${errors.length} measurement failures (details on stderr)` +\n `${result.violations.length > 0 ? `; ${result.violations.length} violations in the measured files` : ''} (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n printViolations(result);\n } else if (result.violations.length === 0) {\n writeStdout(\n `Regression gate passed: ${result.checkedFileCount} changed files, ${result.checkedFunctionCount} functions checked (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n } else {\n writeStdout(\n `Regression gate vs ${cliOptions.base} (merge-base ${shortBase}): ${result.violations.length} violations\\n`\n );\n printViolations(result);\n }\n\n if (cliOptions.full) {\n printFullDetails(result);\n }\n\n for (const warning of warnings) {\n writeStderr(`Warning: ${warning}\\n`);\n }\n for (const error of errors) {\n writeStderr(`Error: ${error}\\n`);\n }\n}\n\nfunction printViolations(result: GateResult): void {\n for (const [index, violation] of result.violations.entries()) {\n writeStdout(`${index + 1}. ${violation.message}\\n`);\n }\n}\n\n/** Base -> head values of every checked function; kept behind --full for humans and trending. */\nfunction printFullDetails(result: GateResult): void {\n if (result.checkedFunctions.length === 0) {\n return;\n }\n writeStdout('\\nChecked functions (base -> head):\\n');\n for (const report of result.checkedFunctions) {\n writeStdout(`- ${formatFunctionReport(report)}\\n`);\n }\n}\n\nfunction formatFunctionReport(report: CheckedFunctionReport): string {\n const range = (\n select: (values: GateFunctionValues) => number,\n format: (value: number) => string = String\n ): string => {\n const head = format(select(report.head));\n return report.base ? `${format(select(report.base))} -> ${head}` : head;\n };\n const values = [\n `cognitive ${range((fn) => fn.cognitiveComplexity)}`,\n `NCSS ${range((fn) => fn.ncss)}`,\n `nesting ${range((fn) => fn.nestingDepth)}`,\n `DepDegree ${range((fn) => fn.depDegree)}`,\n `volume ${range(\n (fn) => fn.halsteadVolume,\n (value) => value.toFixed(1)\n )}`,\n ];\n return `${report.file}:${report.startLine}-${report.endLine} ${report.name}${report.base ? '' : ' (new)'}: ${values.join(', ')}`;\n}\n\nfunction printJsonReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n inputs: GateFileInput[],\n errors: string[],\n warnings: string[]\n): void {\n const report: Record<string, unknown> = {\n base: cliOptions.base,\n mergeBase,\n passed: result.violations.length === 0 && errors.length === 0,\n violations: result.violations,\n checkedFileCount: result.checkedFileCount,\n checkedFunctionCount: result.checkedFunctionCount,\n newFunctionCount: result.newFunctionCount,\n errors,\n warnings,\n };\n if (cliOptions.full) {\n report.files = inputs\n .filter((input) => input.gated !== false)\n .map((input) => ({\n file: input.file,\n baseFunctionCount: input.baseMetrics?.functions.length ?? 0,\n headFunctionCount: input.headMetrics?.functions.length ?? 0,\n baseNcss: input.baseMetrics?.ncssCount ?? 0,\n headNcss: input.headMetrics?.ncssCount ?? 0,\n baseMaxCognitiveComplexity: input.baseMetrics?.maxCognitiveComplexity ?? 0,\n headMaxCognitiveComplexity: input.headMetrics?.maxCognitiveComplexity ?? 0,\n baseDuplicatedLineCount: input.baseDuplicatedLineCount,\n headDuplicatedLineCount: input.headDuplicatedLineCount,\n duplicationPartners: input.duplicationPartners,\n functions: result.checkedFunctions.filter((fn) => fn.file === input.file),\n }));\n }\n writeStdout(JSON.stringify(report, undefined, 2) + '\\n');\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"diffCommand.cjs","names":["getErrorMessage","resolveTarget","loadConfig","configSearchDirectory","resolveOptions","resolveGateOptions","realpath","resolveRepoRoot","resolveMergeBase","listChangedFiles","listRepositoryFiles","listSymlinkPathsAtRevision","stat","path","loadRepositoryExclusion","keepPaths","scanListedFiles","formatPath","isScannedPath","findNewlyDuplicatedLines","evaluateRegressionGate","readFile","mapConcurrently","os","listLineHunks","collectFunctionTokenSequences","readFileAtRevision","measureWithCrossFileData","getLanguage","lstat","measureCrossFileDuplication","collectDuplicatedLineNumbers","indexPartnersByLine"],"sources":["../src/diffCommand.ts"],"sourcesContent":["import { lstat, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { getErrorMessage, mapConcurrently } from '@willbooster/shared-lib';\nimport { loadConfig, resolveGateOptions, resolveOptions, type ResolvedOptions } from './cliConfig.js';\nimport {\n measureCrossFileDuplication,\n type CrossFileDuplicateOccurrence,\n type CrossFileDuplicationMetrics,\n} from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData, Token } from './duplication.js';\nimport { keepPaths, loadRepositoryExclusion, type Exclusion } from './exclusion.js';\nimport {\n listChangedFiles,\n listLineHunks,\n listRepositoryFiles,\n listSymlinkPathsAtRevision,\n readFileAtRevision,\n resolveMergeBase,\n resolveRepoRoot,\n type ChangedFile,\n} from './git.js';\nimport { collectFunctionTokenSequences } from './metrics.js';\nimport {\n findNewlyDuplicatedLines,\n indexPartnersByLine,\n type CloneOccurrence,\n type DuplicationChange,\n type LineHunk,\n} from './newDuplication.js';\nimport {\n evaluateRegressionGate,\n type CheckedFunctionReport,\n type GateFileInput,\n type GateFunctionValues,\n type GateResult,\n} from './regressionGate.js';\nimport {\n collectDuplicatedLineNumbers,\n configSearchDirectory,\n formatPath,\n getLanguage,\n isScannedPath,\n measureWithCrossFileData,\n resolveTarget,\n scanListedFiles,\n writeStderr,\n writeStdout,\n type FileMetrics,\n} from './scan.js';\nimport type { CodeMetrics, LanguageName } from './types.js';\n\n/** Raw options of the `diff` subcommand; every field but base is undefined unless the flag was passed. */\nexport interface DiffCliOptions {\n base: string;\n config?: string;\n duplicationMinTokens?: number;\n duplicationMaxGapTokens?: number;\n duplicationMinSimilarityPercent?: number;\n includeTests?: boolean;\n json?: boolean;\n full?: boolean;\n}\n\n/** One changed file measured at both revisions, plus its duplication-universe contribution. */\ninterface PreparedFile {\n changed: ChangedFile;\n /** Repository-relative display path: the head path, or the base path for deleted files. */\n displayFile: string;\n /** Whether the file is gated (under the target directory); others only feed the base universe. */\n gated: boolean;\n headFile?: FileMetrics;\n headContent?: string;\n baseMetrics?: CodeMetrics;\n baseContent?: string;\n /**\n * The line diff from the measured base to the measured head; with one side unmeasured, every\n * line of the other counts as added or deleted.\n */\n hunks?: LineHunk[];\n baseCandidates?: CrossFileDuplicationFileData;\n baseFunctionTokens?: Int32Array[];\n headFunctionTokens?: Int32Array[];\n}\n\n/** A scanned file that git considers part of the project, keyed by its repository-relative path. */\ninterface ScannedFile {\n relativePath: string;\n file: FileMetrics;\n}\n\n/**\n * Runs the regression gate: measures the files changed relative to the merge-base with the base\n * ref, at both revisions (`git cat-file`; no checkout, no persisted baseline), and reports only\n * violations. Exit codes: 0 all gates passed, 1 violations, 2 changed files could not be measured.\n */\nexport async function runDiffCommand(target: string, cliOptions: DiffCliOptions): Promise<void> {\n try {\n await runGate(target, cliOptions);\n } catch (error) {\n writeStderr(`Error: ${getErrorMessage(error)}\\n`);\n process.exitCode = 2;\n }\n}\n\nasync function runGate(target: string, cliOptions: DiffCliOptions): Promise<void> {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const gateOptions = resolveGateOptions(config);\n\n // The target may be a typo'd path whose ancestors don't exist either; repository discovery must\n // still run so the mistyped target gets its own diagnostic instead of a git spawn failure.\n const repoRoot = await realpath(\n await resolveRepoRoot(await firstExistingDirectory(await configSearchDirectory(resolvedTarget)))\n );\n const mergeBase = await resolveMergeBase(repoRoot, cliOptions.base);\n const changedFiles = await listChangedFiles(repoRoot, mergeBase);\n\n // Every git-visible file (tracked or untracked non-ignored) is measured at head: that provides\n // the head metrics of changed files and the project-wide duplication universe, so copy-paste\n // from unchanged code into changed files is caught. Scanning the explicit git list (instead of\n // walking the tree) keeps ignored artifact directories from ever being parsed: they exist in\n // neither the base commit nor CI, so they would only cost time and skew duplication counts.\n // Unchanged files are byte-identical at both revisions, so the base universe is the same scan\n // with the changed files' contents swapped for their merge-base blobs.\n const repositoryFiles = await listRepositoryFiles(repoRoot);\n const baseSymlinkPaths = await listSymlinkPathsAtRevision(repoRoot, mergeBase);\n const { canonicalTarget, targetExists } = await canonicalizeTarget(resolvedTarget);\n const targetStat = await stat(canonicalTarget).catch(() => {});\n const targetFile = targetStat?.isFile() ? canonicalTarget : undefined;\n // A targeted file git does not list (an ignored one) is still measured, as an addition.\n const unlistedTarget = targetFile && path.relative(repoRoot, targetFile).split(path.sep).join('/');\n if (unlistedTarget !== undefined && !unlistedTarget.startsWith('../') && !repositoryFiles.has(unlistedTarget)) {\n repositoryFiles.add(unlistedTarget);\n changedFiles.push({ status: 'added', headPath: unlistedTarget });\n }\n const explicitFiles = listExplicitlyTargetedPaths(targetFile, repoRoot, changedFiles);\n // Base blobs are excluded by the attributes of the revision they come from. A changed\n // .gitattributes can re-include files whose bytes did not change, so then every file's base\n // attributes are needed to find them.\n const attributesChanged = changedFiles.some((changed) =>\n [changed.headPath, changed.basePath].some(\n (file) => file !== undefined && path.posix.basename(file) === '.gitattributes'\n )\n );\n const basePaths = changedFiles.flatMap((changed) => (changed.basePath === undefined ? [] : [changed.basePath]));\n const [headAttributesExclusion, baseAttributesExclusion] = await Promise.all([\n loadRepositoryExclusion(repoRoot, repositoryFiles, options.exclude),\n loadRepositoryExclusion(\n repoRoot,\n attributesChanged ? [...new Set([...basePaths, ...repositoryFiles])] : basePaths,\n options.exclude,\n mergeBase\n ),\n ]);\n const headExclusion = keepPaths(headAttributesExclusion, explicitFiles);\n const baseExclusion = keepPaths(baseAttributesExclusion, explicitFiles);\n const scan = await scanListedFiles(\n repoRoot,\n repositoryFiles,\n { ...options, loadExclusion: () => Promise.resolve(headExclusion) },\n explicitFiles\n );\n // A run-wide failure (a missing native addon) invalidates the whole gate: surface it once as\n // the fatal error (exit 2) instead of diagnosing every changed file as unmeasured.\n if (scan.fatalError) {\n throw new Error(scan.fatalError);\n }\n const scannedFiles: ScannedFile[] = scan.files.map((file) => ({\n relativePath: formatPath(file.file, scan.displayRoot),\n file,\n }));\n if (attributesChanged) {\n changedFiles.push(\n ...(await listReincludedFiles(scannedFiles, changedFiles, repoRoot, headExclusion, baseExclusion))\n );\n }\n\n // A measurement failure on ANY scannable changed file forces exit 2 — deliberately including\n // files outside a scoped target, because cross-file function matching and the base duplication\n // universe for the gated files depend on them. Failures elsewhere (unchanged files) and\n // unsupported changed paths degrade to warnings.\n const changedPaths = new Set(\n changedFiles\n .flatMap((changed) => [changed.headPath, ...(changed.basePath === undefined ? [] : [changed.basePath])])\n .filter((changedPath) => isScannedPath(changedPath, options, explicitFiles.has(path.join(repoRoot, changedPath))))\n );\n const errors: string[] = [];\n const warnings = [...scan.warnings];\n for (const error of scan.errors) {\n if ([...changedPaths].some((changedPath) => error.startsWith(`${changedPath}:`))) {\n errors.push(error);\n } else {\n warnings.push(error);\n }\n }\n\n const prepared = await prepareChangedFiles(\n changedFiles,\n {\n repoRoot,\n mergeBase,\n canonicalTarget,\n options,\n scannedFiles,\n baseSymlinkPaths,\n scanErrors: [...errors],\n headExclusion,\n baseExclusion,\n explicitFiles,\n generatedHeadFiles: new Set(scan.generatedFiles),\n },\n errors,\n warnings\n );\n // A gate must not fail open on a mistyped target: a nonexistent path is only acceptable when it\n // still matches changed files (e.g. a fully deleted directory).\n if (!targetExists && !prepared.some((file) => file.gated)) {\n throw new Error(`target \"${target}\" does not exist and matches no changed file`);\n }\n\n // Non-gated files (outside the target, or renamed out of scan scope) still feed function\n // matching and the duplication universes; the evaluator reports nothing for them.\n const { baseCross, headCross } = measureDuplicationUniverses(prepared, scannedFiles, options);\n const renamedPaths = new Map(\n prepared.flatMap(({ changed }) =>\n changed.status === 'renamed' && changed.basePath !== undefined\n ? [[changed.basePath, changed.headPath] as const]\n : []\n )\n );\n const headPathOf = (basePath: string): string => renamedPaths.get(basePath) ?? basePath;\n const lineSignaturesOf = createLineSignatureIndex(scannedFiles);\n const newlyDuplicatedLines = findNewlyDuplicatedLines(\n prepared.map((file) => toDuplicationChange(file, baseCross, headCross, headPathOf, lineSignaturesOf))\n );\n const inputs = prepared.map((file, index) => toGateInput(file, newlyDuplicatedLines[index] ?? [], headCross));\n const result = evaluateRegressionGate(inputs, gateOptions);\n\n if (cliOptions.json) {\n printJsonReport(cliOptions, mergeBase, result, inputs, errors, warnings);\n } else {\n printTextReport(cliOptions, mergeBase, result, errors, warnings);\n }\n\n if (errors.length > 0) {\n process.exitCode = 2;\n } else if (result.violations.length > 0) {\n process.exitCode = 1;\n }\n}\n\n/** Content with a generated-code marker, to ask whether an exclusion honors the marker for a path. */\nconst markedCode = '/* @generated */';\n\n/**\n * Unchanged files the head scan measured but the merge-base would not have: excluded by its\n * attributes, or generated by content without the `-linguist-generated` override the working tree\n * adds. Their code is newly measured, so it gates as a modification from an unmeasurable base. The\n * head side comes from the scan, which already applied scope, symbolic-link, and exclusion rules.\n */\nasync function listReincludedFiles(\n scannedFiles: readonly ScannedFile[],\n changedFiles: readonly ChangedFile[],\n repoRoot: string,\n headExclusion: Exclusion,\n baseExclusion: Exclusion\n): Promise<ChangedFile[]> {\n const changedPaths = new Set(changedFiles.flatMap((changed) => [changed.headPath, changed.basePath]));\n const reincluded: ChangedFile[] = [];\n for (const { relativePath, file } of scannedFiles) {\n const absolutePath = path.join(repoRoot, relativePath);\n if (changedPaths.has(relativePath)) {\n continue;\n }\n // The same content is generated at one revision only when its override differs, so the\n // content is read just for files the working tree newly marks -linguist-generated.\n const overrideAdded =\n baseExclusion.isGeneratedCode(absolutePath, markedCode) &&\n !headExclusion.isGeneratedCode(absolutePath, markedCode);\n if (\n baseExclusion.isExcludedPath(absolutePath) ||\n (overrideAdded && baseExclusion.isGeneratedCode(absolutePath, await readFile(file.file, 'utf8')))\n ) {\n reincluded.push({ status: 'modified', basePath: relativePath, headPath: relativePath });\n }\n }\n return reincluded;\n}\n\n/**\n * An explicitly targeted file is measured even when excluded, like the ranking command's single\n * file target: the file itself, and its base path when it was renamed.\n */\nfunction listExplicitlyTargetedPaths(\n targetFile: string | undefined,\n repoRoot: string,\n changedFiles: ChangedFile[]\n): Set<string> {\n if (targetFile === undefined) {\n return new Set();\n }\n const paths = new Set([targetFile]);\n for (const changed of changedFiles) {\n if (changed.basePath !== undefined && path.join(repoRoot, changed.headPath) === targetFile) {\n paths.add(path.join(repoRoot, changed.basePath));\n }\n }\n return paths;\n}\n\n/** The target may not exist (e.g. only deleted files under it); fall back to the resolved path. */\nasync function canonicalizeTarget(resolvedTarget: string): Promise<{ canonicalTarget: string; targetExists: boolean }> {\n try {\n return { canonicalTarget: await realpath(resolvedTarget), targetExists: true };\n } catch {\n return { canonicalTarget: resolvedTarget, targetExists: false };\n }\n}\n\ninterface GateContext {\n repoRoot: string;\n mergeBase: string;\n canonicalTarget: string;\n options: ResolvedOptions;\n scannedFiles: ScannedFile[];\n /** Paths that are symbolic links at the merge-base; like head symlinks, they are not gated. */\n baseSymlinkPaths: Set<string>;\n /** Errors the head scan recorded against changed files. */\n scanErrors: readonly string[];\n headExclusion: Exclusion;\n baseExclusion: Exclusion;\n /** The explicitly targeted file (and its renamed base path), measured despite every exclusion. */\n explicitFiles: ReadonlySet<string>;\n /** Absolute paths the head scan skipped as generated code. */\n generatedHeadFiles: ReadonlySet<string>;\n}\n\nasync function prepareChangedFiles(\n changedFiles: ChangedFile[],\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile[]> {\n const headByPath = new Map(context.scannedFiles.map(({ relativePath, file }) => [relativePath, file]));\n // Files are prepared concurrently (git reads and base measurements); each records its diagnostics\n // on its own, and they are appended in changed-file order so the report stays deterministic.\n const results = await mapConcurrently(changedFiles, os.availableParallelism() * 2, async (changed) => {\n const fileErrors: string[] = [];\n const fileWarnings: string[] = [];\n const file = await prepareChangedFile(changed, context, headByPath, fileErrors, fileWarnings);\n return { file, fileErrors, fileWarnings };\n });\n const prepared: PreparedFile[] = [];\n for (const { file, fileErrors, fileWarnings } of results) {\n errors.push(...fileErrors);\n warnings.push(...fileWarnings);\n if (file) {\n prepared.push(file);\n }\n }\n return prepared;\n}\n\nasync function prepareChangedFile(\n changed: ChangedFile,\n context: GateContext,\n headByPath: Map<string, FileMetrics>,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile | undefined> {\n // Symbolic links are skipped on both sides, mirroring scanListedFiles: git stores only the\n // target string, so a symlink blob is not measurable source.\n const absoluteHeadPath = path.join(context.repoRoot, changed.headPath);\n const headScannable =\n changed.status !== 'deleted' &&\n isScannedPath(changed.headPath, context.options, context.explicitFiles.has(absoluteHeadPath)) &&\n !context.headExclusion.isExcludedPath(absoluteHeadPath) &&\n !context.generatedHeadFiles.has(absoluteHeadPath) &&\n !(await isSymbolicLink(absoluteHeadPath));\n // A base path outside the scan scope (renamed from a test/ignored directory, or an unsupported\n // extension) was never measurable code: its content gates as new code instead of ratcheting\n // against a blob the scanner would not have measured.\n const absoluteBasePath = changed.basePath === undefined ? '' : path.join(context.repoRoot, changed.basePath);\n const baseScannable =\n changed.basePath !== undefined &&\n isScannedPath(changed.basePath, context.options, context.explicitFiles.has(absoluteBasePath)) &&\n !context.baseExclusion.isExcludedPath(absoluteBasePath) &&\n !context.baseSymlinkPaths.has(changed.basePath);\n if (!headScannable && !baseScannable) {\n return undefined;\n }\n\n const displayFile = changed.status === 'deleted' ? (changed.basePath as string) : changed.headPath;\n const headFile = headScannable ? headByPath.get(changed.headPath) : undefined;\n if (headScannable && !headFile) {\n reportUnmeasuredChangedFile(changed.headPath, context.scanErrors, errors);\n return undefined;\n }\n\n const file: PreparedFile = {\n changed,\n displayFile,\n // A file whose head left the scan scope still contributes its base functions to matching and\n // its base blob to the base universe, but nothing about it is gated or reported.\n gated:\n headScannable || changed.status === 'deleted'\n ? isWithinTarget(path.join(context.repoRoot, displayFile), context.canonicalTarget)\n : false,\n headFile,\n };\n\n if (baseScannable) {\n const outcome = await measureBaseRevision(file, changed.basePath as string, context, errors, warnings);\n // A base that turns out to be generated code gates its head as new code, like any other base\n // outside the scan scope.\n if (outcome === 'failed' || (outcome === 'generated' && !headFile)) {\n return undefined;\n }\n }\n\n try {\n if (headFile) {\n // The duplication gate needs the head lines, so an unreadable head fails like an unmeasured one.\n file.headContent = await readFile(headFile.file, 'utf8');\n collectHeadFunctionTokens(file, file.headContent, context, warnings);\n }\n file.hunks = await diffMeasuredRevisions(file, context);\n } catch (error) {\n errors.push(`${displayFile}: ${getErrorMessage(error)}`);\n return undefined;\n }\n return file;\n}\n\nasync function diffMeasuredRevisions(file: PreparedFile, context: GateContext): Promise<LineHunk[]> {\n const { changed, baseContent, headContent } = file;\n if (baseContent !== undefined && headContent !== undefined) {\n return await listLineHunks(context.repoRoot, context.mergeBase, changed.basePath as string, changed.headPath);\n }\n return [\n {\n baseStart: 1,\n baseCount: baseContent === undefined ? 0 : splitLines(baseContent).length,\n headStart: 1,\n headCount: headContent === undefined ? 0 : splitLines(headContent).length,\n },\n ];\n}\n\n/** Line numbering of the measured metrics (classifyLines splits on every line terminator). */\nfunction splitLines(content: string): string[] {\n return content.split(/\\r\\n|\\n|\\r/u);\n}\n\n/**\n * The scan covers exactly the git-visible list, so a scannable changed path can only be missing\n * after a measurement failure (already recorded as an error) or a silent exclusion (an alias of\n * an already-visited file, or absence from the git list). Failing loudly keeps the gate from\n * passing with the file unchecked.\n */\nfunction reportUnmeasuredChangedFile(headPath: string, scanErrors: readonly string[], errors: string[]): void {\n if (!scanErrors.some((error) => error.startsWith(`${headPath}:`))) {\n errors.push(`${headPath}: changed file was not measured`);\n }\n}\n\nfunction collectHeadFunctionTokens(\n file: PreparedFile,\n headContent: string,\n context: GateContext,\n warnings: string[]\n): void {\n try {\n file.headFunctionTokens = collectFunctionTokenSequences(headContent, {\n language: languageOf(file.changed.headPath, context),\n duplication: context.options.duplication,\n });\n } catch (error) {\n // Only rename re-matching degrades without token sequences; the head metrics still gate.\n warnings.push(`${file.displayFile}: function token sequences unavailable: ${getErrorMessage(error)}`);\n }\n}\n\n/**\n * Measures the merge-base blob into `file`; 'failed' (with an error recorded) only when the\n * metrics themselves cannot be measured, and 'generated' (nothing measured) for generated code.\n * The auxiliary collections (duplication candidates, token sequences) may fail independently of\n * the metrics, so their failure only degrades duplication data and rename re-matching — the\n * function-level ratchets still run.\n */\nasync function measureBaseRevision(\n file: PreparedFile,\n basePath: string,\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<'measured' | 'generated' | 'failed'> {\n const measureOptions = {\n language: languageOf(basePath, context),\n duplication: context.options.duplication,\n };\n let baseContent;\n try {\n baseContent = await readFileAtRevision(context.repoRoot, context.mergeBase, basePath);\n if (context.baseExclusion.isGeneratedCode(path.join(context.repoRoot, basePath), baseContent)) {\n return 'generated';\n }\n const measured = await measureWithCrossFileData(baseContent, measureOptions);\n file.baseMetrics = measured.metrics;\n file.baseContent = baseContent;\n file.baseCandidates = measured.crossFileData;\n if (measured.crossFileError !== undefined) {\n warnings.push(`${basePath} (at merge-base): duplication candidates unavailable: ${measured.crossFileError}`);\n }\n } catch (error) {\n errors.push(`${basePath} (at merge-base): ${getErrorMessage(error)}`);\n return 'failed';\n }\n try {\n file.baseFunctionTokens = collectFunctionTokenSequences(baseContent, measureOptions);\n } catch (error) {\n warnings.push(`${basePath} (at merge-base): function token sequences unavailable: ${getErrorMessage(error)}`);\n }\n return 'measured';\n}\n\n/** The language of a scannable path; an explicitly targeted file skips the test-file name rules. */\nfunction languageOf(relativePath: string, context: GateContext): LanguageName {\n const explicitTarget = context.explicitFiles.has(path.join(context.repoRoot, relativePath));\n return getLanguage(relativePath, context.options, explicitTarget) as LanguageName;\n}\n\nasync function isSymbolicLink(absolutePath: string): Promise<boolean> {\n const stats = await lstat(absolutePath).catch(() => {});\n return stats?.isSymbolicLink() ?? false;\n}\n\n/** Walks up to the nearest existing DIRECTORY, so git commands never spawn in a missing or non-directory cwd. */\nasync function firstExistingDirectory(directory: string): Promise<string> {\n let current = directory;\n while (true) {\n const stats = await stat(current).catch(() => {});\n if (stats?.isDirectory()) {\n return current;\n }\n const parent = path.dirname(current);\n if (parent === current) {\n return current;\n }\n current = parent;\n }\n}\n\nfunction isWithinTarget(candidate: string, targetDirectory: string): boolean {\n const relative = path.relative(targetDirectory, candidate);\n return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));\n}\n\nfunction measureDuplicationUniverses(\n prepared: PreparedFile[],\n scannedFiles: ScannedFile[],\n options: ResolvedOptions\n): { baseCross?: CrossFileDuplicationMetrics; headCross?: CrossFileDuplicationMetrics } {\n const headSources = scannedFiles.flatMap(({ relativePath, file }) =>\n file.duplicationCandidates ? [{ file: relativePath, ...file.duplicationCandidates }] : []\n );\n\n const changedHeadPaths = new Set(\n prepared.flatMap((file) => (file.changed.status === 'deleted' ? [] : [file.changed.headPath]))\n );\n const baseSources = headSources.filter((source) => !changedHeadPaths.has(source.file));\n for (const file of prepared) {\n if (file.baseCandidates && file.changed.basePath !== undefined) {\n baseSources.push({ file: file.changed.basePath, ...file.baseCandidates });\n }\n }\n\n return {\n baseCross: baseSources.length >= 2 ? measureCrossFileDuplication(baseSources, options.duplication) : undefined,\n headCross: headSources.length >= 2 ? measureCrossFileDuplication(headSources, options.duplication) : undefined,\n };\n}\n\nfunction toDuplicationChange(\n file: PreparedFile,\n baseCross: CrossFileDuplicationMetrics | undefined,\n headCross: CrossFileDuplicationMetrics | undefined,\n headPathOf: (basePath: string) => string,\n lineSignaturesOf: (file: string) => LineSignatures | undefined\n): DuplicationChange {\n const headOccurrences = collectOccurrences(file.headFile?.metrics, headCross, file.changed.headPath);\n return {\n baseLines: file.baseContent === undefined ? undefined : splitLines(file.baseContent),\n headLines: file.headContent === undefined ? undefined : splitLines(file.headContent),\n baseDuplicatedLines:\n file.baseMetrics === undefined || file.changed.basePath === undefined\n ? new Set()\n : collectDuplicatedLineNumbers(file.baseMetrics, baseCross, file.changed.basePath),\n headDuplicatedLines:\n file.headFile === undefined\n ? new Set()\n : keepRepeatedLines(\n collectDuplicatedLineNumbers(file.headFile.metrics, headCross, file.changed.headPath),\n file.changed.headPath,\n headOccurrences,\n lineSignaturesOf\n ),\n baseOccurrences: collectOccurrences(file.baseMetrics, baseCross, file.changed.basePath, headPathOf),\n headOccurrences,\n hunks: file.hunks ?? [],\n };\n}\n\n/** A file's per-line normalized token signatures and how many lines carry each. */\ninterface LineSignatures {\n byLine: Map<number, string>;\n lineCounts: Map<string, number>;\n}\n\n/** Line signatures of the scanned head files, computed on first use. */\nfunction createLineSignatureIndex(scannedFiles: ScannedFile[]): (file: string) => LineSignatures | undefined {\n const tokensByFile = new Map(\n scannedFiles.map(({ relativePath, file }) => [relativePath, file.duplicationCandidates?.tokens])\n );\n const signaturesByFile = new Map<string, LineSignatures | undefined>();\n return (file) => {\n if (!signaturesByFile.has(file)) {\n const tokens = tokensByFile.get(file);\n signaturesByFile.set(file, tokens && signLines(tokens));\n }\n return signaturesByFile.get(file);\n };\n}\n\n/**\n * Each line's tokens with identifiers numbered by first occurrence, as clone detection anonymizes\n * them.\n */\nfunction signLines(tokens: readonly Token[]): LineSignatures {\n const byLine = new Map<number, string>();\n const identifierIndexesByLine = new Map<number, Map<string, number>>();\n for (const token of tokens) {\n const line = token.startRow + 1;\n let text = token.text;\n if (token.kind === 'id') {\n // Numbered by first occurrence within the line, so `a + a` and `a + b` stay distinct.\n const identifierIndexes = identifierIndexesByLine.get(line) ?? new Map<string, number>();\n identifierIndexesByLine.set(line, identifierIndexes);\n if (!identifierIndexes.has(token.text)) {\n identifierIndexes.set(token.text, identifierIndexes.size);\n }\n text = `\\u0001${identifierIndexes.get(token.text)}`;\n }\n byLine.set(line, `${byLine.get(line) ?? ''}${text}\\u0000`);\n }\n const lineCounts = new Map<string, number>();\n for (const signature of byLine.values()) {\n lineCounts.set(signature, (lineCounts.get(signature) ?? 0) + 1);\n }\n return { byLine, lineCounts };\n}\n\n/**\n * The duplicated lines whose normalized tokens a partner file of their clones (the file itself,\n * on another line, for `''`) repeats on some line. Near-miss coverage also marks lines the copies\n * do not share, such as a new line inserted into an old near-miss clone, which copied nothing.\n */\nfunction keepRepeatedLines(\n lines: ReadonlySet<number>,\n file: string,\n occurrences: readonly CloneOccurrence[],\n lineSignaturesOf: (file: string) => LineSignatures | undefined\n): Set<number> {\n const own = lineSignaturesOf(file);\n if (own === undefined) {\n return new Set(lines);\n }\n const repeats = (signature: string, partner: string): boolean => {\n if (partner === '') {\n return (own.lineCounts.get(signature) ?? 0) > 1;\n }\n const partnerSignatures = lineSignaturesOf(partner);\n return partnerSignatures === undefined || partnerSignatures.lineCounts.has(signature);\n };\n const partnersByLine = indexPartnersByLine(occurrences);\n return new Set(\n [...lines].filter((line) => {\n const signature = own.byLine.get(line);\n return (\n signature === undefined || [...(partnersByLine.get(line) ?? [])].some((partner) => repeats(signature, partner))\n );\n })\n );\n}\n\n/**\n * The file's clone occurrences with their partner files (`''` for the file itself), partner paths\n * renamed by `renamePartner` so base partners compare with head ones.\n */\nfunction collectOccurrences(\n metrics: CodeMetrics | undefined,\n cross: CrossFileDuplicationMetrics | undefined,\n file: string | undefined,\n renamePartner: (partner: string) => string = (partner) => partner\n): CloneOccurrence[] {\n if (metrics === undefined || file === undefined) {\n return [];\n }\n const withinFile = metrics.duplication.duplicateBlockGroups.flatMap((group) =>\n group.map(({ startLine, endLine }) => ({ startLine, endLine, partners: [''] }))\n );\n const crossFile = (cross?.groups ?? []).flatMap((group) => {\n const own = group.occurrences.filter((occurrence) => occurrence.file === file);\n const partners = [\n ...group.files.filter((partner) => partner !== file).map(renamePartner),\n ...(own.length > 1 ? [''] : []),\n ];\n return own.map(({ startLine, endLine }) => ({ startLine, endLine, partners }));\n });\n return [...withinFile, ...crossFile];\n}\n\nfunction toGateInput(\n file: PreparedFile,\n newlyDuplicatedLines: number[],\n headCross: CrossFileDuplicationMetrics | undefined\n): GateFileInput {\n return {\n file: file.displayFile,\n baseMetrics: file.baseMetrics,\n headMetrics: file.headFile?.metrics,\n baseFunctionTokens: file.baseFunctionTokens,\n headFunctionTokens: file.headFunctionTokens,\n newlyDuplicatedLines,\n duplicationPartners: collectPartners(file, newlyDuplicatedLines, headCross),\n gated: file.gated,\n };\n}\n\n/** Other files sharing a cross-file clone that covers the given lines. */\nfunction collectPartners(\n file: PreparedFile,\n lines: number[],\n headCross: CrossFileDuplicationMetrics | undefined\n): string[] {\n const headPath = file.changed.headPath;\n const covers = (occurrence: CrossFileDuplicateOccurrence): boolean =>\n occurrence.file === headPath && lines.some((line) => line >= occurrence.startLine && line <= occurrence.endLine);\n const partners = new Set<string>();\n for (const group of headCross?.groups ?? []) {\n if (group.occurrences.some(covers)) {\n for (const partner of group.files) {\n if (partner !== headPath) {\n partners.add(partner);\n }\n }\n }\n }\n return [...partners].toSorted();\n}\n\nfunction printTextReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n errors: string[],\n warnings: string[]\n): void {\n const shortBase = mergeBase.slice(0, 12);\n if (errors.length > 0) {\n // Unmeasured files were not gated, so \"0 violations\" would be vacuous; never claim a pass.\n writeStdout(\n `Regression gate could not complete: ${errors.length} measurement failures (details on stderr)` +\n `${result.violations.length > 0 ? `; ${result.violations.length} violations in the measured files` : ''} (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n printViolations(result);\n } else if (result.violations.length === 0) {\n writeStdout(\n `Regression gate passed: ${result.checkedFileCount} changed files, ${result.checkedFunctionCount} functions checked (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n } else {\n writeStdout(\n `Regression gate vs ${cliOptions.base} (merge-base ${shortBase}): ${result.violations.length} violations\\n`\n );\n printViolations(result);\n }\n\n if (cliOptions.full) {\n printFullDetails(result);\n }\n\n for (const warning of warnings) {\n writeStderr(`Warning: ${warning}\\n`);\n }\n for (const error of errors) {\n writeStderr(`Error: ${error}\\n`);\n }\n}\n\nfunction printViolations(result: GateResult): void {\n for (const [index, violation] of result.violations.entries()) {\n writeStdout(`${index + 1}. ${violation.message}\\n`);\n }\n}\n\n/** Base -> head values of every checked function; kept behind --full for humans and trending. */\nfunction printFullDetails(result: GateResult): void {\n if (result.checkedFunctions.length === 0) {\n return;\n }\n writeStdout('\\nChecked functions (base -> head):\\n');\n for (const report of result.checkedFunctions) {\n writeStdout(`- ${formatFunctionReport(report)}\\n`);\n }\n}\n\nfunction formatFunctionReport(report: CheckedFunctionReport): string {\n const range = (\n select: (values: GateFunctionValues) => number,\n format: (value: number) => string = String\n ): string => {\n const head = format(select(report.head));\n return report.base ? `${format(select(report.base))} -> ${head}` : head;\n };\n const values = [\n `cognitive ${range((fn) => fn.cognitiveComplexity)}`,\n `NCSS ${range((fn) => fn.ncss)}`,\n `nesting ${range((fn) => fn.nestingDepth)}`,\n `DepDegree ${range((fn) => fn.depDegree)}`,\n `volume ${range(\n (fn) => fn.halsteadVolume,\n (value) => value.toFixed(1)\n )}`,\n ];\n return `${report.file}:${report.startLine}-${report.endLine} ${report.name}${report.base ? '' : ' (new)'}: ${values.join(', ')}`;\n}\n\nfunction printJsonReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n inputs: GateFileInput[],\n errors: string[],\n warnings: string[]\n): void {\n const report: Record<string, unknown> = {\n base: cliOptions.base,\n mergeBase,\n passed: result.violations.length === 0 && errors.length === 0,\n violations: result.violations,\n checkedFileCount: result.checkedFileCount,\n checkedFunctionCount: result.checkedFunctionCount,\n newFunctionCount: result.newFunctionCount,\n errors,\n warnings,\n };\n if (cliOptions.full) {\n report.files = inputs\n .filter((input) => input.gated !== false)\n .map((input) => ({\n file: input.file,\n baseFunctionCount: input.baseMetrics?.functions.length ?? 0,\n headFunctionCount: input.headMetrics?.functions.length ?? 0,\n baseNcss: input.baseMetrics?.ncssCount ?? 0,\n headNcss: input.headMetrics?.ncssCount ?? 0,\n baseMaxCognitiveComplexity: input.baseMetrics?.maxCognitiveComplexity ?? 0,\n headMaxCognitiveComplexity: input.headMetrics?.maxCognitiveComplexity ?? 0,\n newlyDuplicatedLines: input.newlyDuplicatedLines,\n duplicationPartners: input.duplicationPartners,\n functions: result.checkedFunctions.filter((fn) => fn.file === input.file),\n }));\n }\n writeStdout(JSON.stringify(report, undefined, 2) + '\\n');\n}\n"],"mappings":"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"}
|
package/dist/diffCommand.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{collectFunctionTokenSequences as t}from"./metrics.js";import{evaluateRegressionGate as n}from"./regressionGate.js";import{loadConfig as r,resolveGateOptions as i,resolveOptions as a}from"./cliConfig.js";import{listChangedFiles as o,listRepositoryFiles as s,listSymlinkPathsAtRevision as c,readFileAtRevision as l,resolveMergeBase as u,resolveRepoRoot as d}from"./git.js";import{collectDuplicatedLineNumbers as f,configSearchDirectory as p,formatError as m,formatPath as h,getLanguage as g,isScannedPath as _,measureWithCrossFileData as v,resolveTarget as y,scanListedFiles as b,writeStderr as x,writeStdout as S}from"./scan.js";import{lstat as C,readFile as w,realpath as T,stat as E}from"node:fs/promises";import D from"node:path";import O from"node:os";async function k(e,t){try{await A(e,t)}catch(e){x(`Error: ${m(e)}\n`),process.exitCode=2}}async function A(e,t){let l=y(e),f=await r(t.config,await p(l)),m=a(t,f),g=i(f),v=await T(await d(await z(await p(l)))),x=await u(v,t.base),S=await o(v,x),C=await s(v),w=await c(v,x),E=await b(v,C,m);if(E.fatalError)throw Error(E.fatalError);let D=E.files.map(e=>({relativePath:h(e.file,E.displayRoot),file:e})),O=new Set(S.flatMap(e=>[e.headPath,...e.basePath===void 0?[]:[e.basePath]]).filter(e=>_(e,m))),k=[],A=[...E.warnings];for(let e of E.errors)[...O].some(t=>e.startsWith(`${t}:`))?k.push(e):A.push(e);let{canonicalTarget:N,targetExists:P}=await j(l),F=await M(S,{repoRoot:v,mergeBase:x,canonicalTarget:N,options:m,scannedFiles:D,baseSymlinkPaths:w,scanErrors:[...k]},k,A);if(!P&&!F.some(e=>e.gated))throw Error(`target "${e}" does not exist and matches no changed file`);let{baseCross:I,headCross:L}=V(F,D,m),R=F.map(e=>H(e,I,L)),B=n(R,g);t.json?Y(t,x,B,R,k,A):G(t,x,B,k,A),k.length>0?process.exitCode=2:B.violations.length>0&&(process.exitCode=1)}async function j(e){try{return{canonicalTarget:await T(e),targetExists:!0}}catch{return{canonicalTarget:e,targetExists:!1}}}async function M(e,t,n,r){let i=new Map(t.scannedFiles.map(({relativePath:e,file:t})=>[e,t])),a=await N(e,O.availableParallelism()*2,async e=>{let n=[],r=[];return{file:await P(e,t,i,n,r),fileErrors:n,fileWarnings:r}}),o=[];for(let{file:e,fileErrors:t,fileWarnings:i}of a)n.push(...t),r.push(...i),e&&o.push(e);return o}async function N(e,t,n){let r=[],i=0;return await Promise.all(Array.from({length:Math.min(t,e.length)},async()=>{for(;i<e.length;){let t=i;i+=1,r[t]=await n(e[t])}})),r}async function P(e,t,n,r,i){let a=e.status!==`deleted`&&_(e.headPath,t.options)&&!await R(D.join(t.repoRoot,e.headPath)),o=e.basePath!==void 0&&_(e.basePath,t.options)&&!t.baseSymlinkPaths.has(e.basePath);if(!a&&!o)return;let s=e.status===`deleted`?e.basePath:e.headPath,c=a?n.get(e.headPath):void 0;if(a&&!c){F(e.headPath,t.scanErrors,r);return}let l={changed:e,displayFile:s,gated:a||e.status===`deleted`?B(D.join(t.repoRoot,s),t.canonicalTarget):!1,headFile:c};if(!o||await L(l,e.basePath,t,r,i))return c&&await I(l,c,t,i),l}function F(e,t,n){t.some(t=>t.startsWith(`${e}:`))||n.push(`${e}: changed file was not measured`)}async function I(e,n,r,i){try{let i=await w(n.file,`utf8`);e.headFunctionTokens=t(i,{language:g(e.changed.headPath,r.options),duplication:r.options.duplication})}catch(t){i.push(`${e.displayFile}: function token sequences unavailable: ${m(t)}`)}}async function L(e,n,r,i,a){let o={language:g(n,r.options),duplication:r.options.duplication},s;try{s=await l(r.repoRoot,r.mergeBase,n);let t=await v(s,o);e.baseMetrics=t.metrics,e.baseCandidates=t.crossFileData,t.crossFileError!==void 0&&a.push(`${n} (at merge-base): duplication candidates unavailable: ${t.crossFileError}`)}catch(e){return i.push(`${n} (at merge-base): ${m(e)}`),!1}try{e.baseFunctionTokens=t(s,o)}catch(e){a.push(`${n} (at merge-base): function token sequences unavailable: ${m(e)}`)}return!0}async function R(e){return(await C(e).catch(()=>{}))?.isSymbolicLink()??!1}async function z(e){let t=e;for(;;){if((await E(t).catch(()=>{}))?.isDirectory())return t;let e=D.dirname(t);if(e===t)return t;t=e}}function B(e,t){let n=D.relative(t,e);return n===``||!n.startsWith(`..${D.sep}`)&&n!==`..`&&!D.isAbsolute(n)}function V(t,n,r){let i=n.flatMap(({relativePath:e,file:t})=>t.duplicationCandidates?[{file:e,...t.duplicationCandidates}]:[]),a=new Set(t.flatMap(e=>e.changed.status===`deleted`?[]:[e.changed.headPath])),o=i.filter(e=>!a.has(e.file));for(let e of t)e.baseCandidates&&e.changed.basePath!==void 0&&o.push({file:e.changed.basePath,...e.baseCandidates});return{baseCross:o.length>=2?e(o,r.duplication):void 0,headCross:i.length>=2?e(i,r.duplication):void 0}}function H(e,t,n){return{file:e.displayFile,baseMetrics:e.baseMetrics,headMetrics:e.headFile?.metrics,baseFunctionTokens:e.baseFunctionTokens,headFunctionTokens:e.headFunctionTokens,baseDuplicatedLineCount:e.baseMetrics===void 0||e.changed.basePath===void 0?0:U(e.baseMetrics,t,e.changed.basePath),headDuplicatedLineCount:e.changed.status===`deleted`?0:U(e.headFile?.metrics,n,e.changed.headPath),duplicationPartners:W(n,e.changed.headPath),gated:e.gated}}function U(e,t,n){return f(e,t,n).size}function W(e,t){if(!e)return[];let n=/* @__PURE__ */ new Set;for(let r of e.groups)if(r.files.includes(t))for(let e of r.files)e!==t&&n.add(e);return[...n].toSorted()}function G(e,t,n,r,i){let a=t.slice(0,12);r.length>0?(S(`Regression gate could not complete: ${r.length} measurement failures (details on stderr)${n.violations.length>0?`; ${n.violations.length} violations in the measured files`:``} (base ${e.base}, merge-base ${a}).\n`),K(n)):n.violations.length===0?S(`Regression gate passed: ${n.checkedFileCount} changed files, ${n.checkedFunctionCount} functions checked (base ${e.base}, merge-base ${a}).\n`):(S(`Regression gate vs ${e.base} (merge-base ${a}): ${n.violations.length} violations\n`),K(n)),e.full&&q(n);for(let e of i)x(`Warning: ${e}\n`);for(let e of r)x(`Error: ${e}\n`)}function K(e){for(let[t,n]of e.violations.entries())S(`${t+1}. ${n.message}\n`)}function q(e){if(e.checkedFunctions.length!==0){S(`
|
|
1
|
+
import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{collectFunctionTokenSequences as t}from"./metrics.js";import{findNewlyDuplicatedLines as n,indexPartnersByLine as r}from"./newDuplication.js";import{evaluateRegressionGate as i}from"./regressionGate.js";import{loadConfig as a,resolveGateOptions as o,resolveOptions as s}from"./cliConfig.js";import{listChangedFiles as c,listLineHunks as l,listRepositoryFiles as u,listSymlinkPathsAtRevision as d,readFileAtRevision as f,resolveMergeBase as ee,resolveRepoRoot as te}from"./git.js";import{keepPaths as p,loadRepositoryExclusion as m}from"./exclusion.js";import{collectDuplicatedLineNumbers as h,configSearchDirectory as g,formatPath as ne,getLanguage as _,isScannedPath as v,measureWithCrossFileData as y,resolveTarget as re,scanListedFiles as ie,writeStderr as b,writeStdout as x}from"./scan.js";import{getErrorMessage as S,mapConcurrently as C}from"@willbooster/shared-lib";import{lstat as w,readFile as T,realpath as E,stat as D}from"node:fs/promises";import O from"node:path";import k from"node:os";async function A(e,t){try{await j(e,t)}catch(e){b(`Error: ${S(e)}\n`),process.exitCode=2}}async function j(e,t){let r=re(e),l=await a(t.config,await g(r)),f=s(t,l),h=o(l),_=await E(await te(await oe(await g(r)))),y=await ee(_,t.base),b=await c(_,y),x=await u(_),S=await d(_,y),{canonicalTarget:C,targetExists:w}=await P(r),T=(await D(C).catch(()=>{}))?.isFile()?C:void 0,k=T&&O.relative(_,T).split(O.sep).join(`/`);k!==void 0&&!k.startsWith(`../`)&&!x.has(k)&&(x.add(k),b.push({status:`added`,headPath:k}));let A=N(T,_,b),j=b.some(e=>[e.headPath,e.basePath].some(e=>e!==void 0&&O.posix.basename(e)===`.gitattributes`)),M=b.flatMap(e=>e.basePath===void 0?[]:[e.basePath]),[I,L]=await Promise.all([m(_,x,f.exclude),m(_,j?[.../* @__PURE__ */ new Set([...M,...x])]:M,f.exclude,y)]),R=p(I,A),z=p(L,A),B=await ie(_,x,{...f,loadExclusion:()=>Promise.resolve(R)},A);if(B.fatalError)throw Error(B.fatalError);let V=B.files.map(e=>({relativePath:ne(e.file,B.displayRoot),file:e}));j&&b.push(...await ae(V,b,_,R,z));let H=new Set(b.flatMap(e=>[e.headPath,...e.basePath===void 0?[]:[e.basePath]]).filter(e=>v(e,f,A.has(O.join(_,e))))),U=[],W=[...B.warnings];for(let e of B.errors)[...H].some(t=>e.startsWith(`${t}:`))?U.push(e):W.push(e);let G=await F(b,{repoRoot:_,mergeBase:y,canonicalTarget:C,options:f,scannedFiles:V,baseSymlinkPaths:S,scanErrors:[...U],headExclusion:R,baseExclusion:z,explicitFiles:A,generatedHeadFiles:new Set(B.generatedFiles)},U,W);if(!w&&!G.some(e=>e.gated))throw Error(`target "${e}" does not exist and matches no changed file`);let{baseCross:K,headCross:q}=se(G,V,f),J=new Map(G.flatMap(({changed:e})=>e.status===`renamed`&&e.basePath!==void 0?[[e.basePath,e.headPath]]:[])),Y=e=>J.get(e)??e,X=le(V),Z=n(G.map(e=>ce(e,K,q,Y,X))),Q=G.map((e,t)=>ue(e,Z[t]??[],q)),$=i(Q,h);t.json?fe(t,y,$,Q,U,W):de(t,y,$,U,W),U.length>0?process.exitCode=2:$.violations.length>0&&(process.exitCode=1)}const M=`/* @generated */`;async function ae(e,t,n,r,i){let a=new Set(t.flatMap(e=>[e.headPath,e.basePath])),o=[];for(let{relativePath:t,file:s}of e){let e=O.join(n,t);if(a.has(t))continue;let c=i.isGeneratedCode(e,M)&&!r.isGeneratedCode(e,M);(i.isExcludedPath(e)||c&&i.isGeneratedCode(e,await T(s.file,`utf8`)))&&o.push({status:`modified`,basePath:t,headPath:t})}return o}function N(e,t,n){if(e===void 0)return/* @__PURE__ */ new Set;let r=/* @__PURE__ */ new Set([e]);for(let i of n)i.basePath!==void 0&&O.join(t,i.headPath)===e&&r.add(O.join(t,i.basePath));return r}async function P(e){try{return{canonicalTarget:await E(e),targetExists:!0}}catch{return{canonicalTarget:e,targetExists:!1}}}async function F(e,t,n,r){let i=new Map(t.scannedFiles.map(({relativePath:e,file:t})=>[e,t])),a=await C(e,k.availableParallelism()*2,async e=>{let n=[],r=[];return{file:await I(e,t,i,n,r),fileErrors:n,fileWarnings:r}}),o=[];for(let{file:e,fileErrors:t,fileWarnings:i}of a)n.push(...t),r.push(...i),e&&o.push(e);return o}async function I(e,t,n,r,i){let a=O.join(t.repoRoot,e.headPath),o=e.status!==`deleted`&&v(e.headPath,t.options,t.explicitFiles.has(a))&&!t.headExclusion.isExcludedPath(a)&&!t.generatedHeadFiles.has(a)&&!await U(a),s=e.basePath===void 0?``:O.join(t.repoRoot,e.basePath),c=e.basePath!==void 0&&v(e.basePath,t.options,t.explicitFiles.has(s))&&!t.baseExclusion.isExcludedPath(s)&&!t.baseSymlinkPaths.has(e.basePath);if(!o&&!c)return;let l=e.status===`deleted`?e.basePath:e.headPath,u=o?n.get(e.headPath):void 0;if(o&&!u){z(e.headPath,t.scanErrors,r);return}let d={changed:e,displayFile:l,gated:o||e.status===`deleted`?W(O.join(t.repoRoot,l),t.canonicalTarget):!1,headFile:u};if(c){let n=await V(d,e.basePath,t,r,i);if(n===`failed`||n===`generated`&&!u)return}try{u&&(d.headContent=await T(u.file,`utf8`),B(d,d.headContent,t,i)),d.hunks=await L(d,t)}catch(e){r.push(`${l}: ${S(e)}`);return}return d}async function L(e,t){let{changed:n,baseContent:r,headContent:i}=e;return r!==void 0&&i!==void 0?await l(t.repoRoot,t.mergeBase,n.basePath,n.headPath):[{baseStart:1,baseCount:r===void 0?0:R(r).length,headStart:1,headCount:i===void 0?0:R(i).length}]}function R(e){return e.split(/\r\n|\n|\r/u)}function z(e,t,n){t.some(t=>t.startsWith(`${e}:`))||n.push(`${e}: changed file was not measured`)}function B(e,n,r,i){try{e.headFunctionTokens=t(n,{language:H(e.changed.headPath,r),duplication:r.options.duplication})}catch(t){i.push(`${e.displayFile}: function token sequences unavailable: ${S(t)}`)}}async function V(e,n,r,i,a){let o={language:H(n,r),duplication:r.options.duplication},s;try{if(s=await f(r.repoRoot,r.mergeBase,n),r.baseExclusion.isGeneratedCode(O.join(r.repoRoot,n),s))return`generated`;let t=await y(s,o);e.baseMetrics=t.metrics,e.baseContent=s,e.baseCandidates=t.crossFileData,t.crossFileError!==void 0&&a.push(`${n} (at merge-base): duplication candidates unavailable: ${t.crossFileError}`)}catch(e){return i.push(`${n} (at merge-base): ${S(e)}`),`failed`}try{e.baseFunctionTokens=t(s,o)}catch(e){a.push(`${n} (at merge-base): function token sequences unavailable: ${S(e)}`)}return`measured`}function H(e,t){let n=t.explicitFiles.has(O.join(t.repoRoot,e));return _(e,t.options,n)}async function U(e){return(await w(e).catch(()=>{}))?.isSymbolicLink()??!1}async function oe(e){let t=e;for(;;){if((await D(t).catch(()=>{}))?.isDirectory())return t;let e=O.dirname(t);if(e===t)return t;t=e}}function W(e,t){let n=O.relative(t,e);return n===``||!n.startsWith(`..${O.sep}`)&&n!==`..`&&!O.isAbsolute(n)}function se(t,n,r){let i=n.flatMap(({relativePath:e,file:t})=>t.duplicationCandidates?[{file:e,...t.duplicationCandidates}]:[]),a=new Set(t.flatMap(e=>e.changed.status===`deleted`?[]:[e.changed.headPath])),o=i.filter(e=>!a.has(e.file));for(let e of t)e.baseCandidates&&e.changed.basePath!==void 0&&o.push({file:e.changed.basePath,...e.baseCandidates});return{baseCross:o.length>=2?e(o,r.duplication):void 0,headCross:i.length>=2?e(i,r.duplication):void 0}}function ce(e,t,n,r,i){let a=q(e.headFile?.metrics,n,e.changed.headPath);return{baseLines:e.baseContent===void 0?void 0:R(e.baseContent),headLines:e.headContent===void 0?void 0:R(e.headContent),baseDuplicatedLines:e.baseMetrics===void 0||e.changed.basePath===void 0?/* @__PURE__ */ new Set:h(e.baseMetrics,t,e.changed.basePath),headDuplicatedLines:e.headFile===void 0?/* @__PURE__ */ new Set:K(h(e.headFile.metrics,n,e.changed.headPath),e.changed.headPath,a,i),baseOccurrences:q(e.baseMetrics,t,e.changed.basePath,r),headOccurrences:a,hunks:e.hunks??[]}}function le(e){let t=new Map(e.map(({relativePath:e,file:t})=>[e,t.duplicationCandidates?.tokens])),n=/* @__PURE__ */ new Map;return e=>{if(!n.has(e)){let r=t.get(e);n.set(e,r&&G(r))}return n.get(e)}}function G(e){let t=/* @__PURE__ */ new Map,n=/* @__PURE__ */ new Map;for(let r of e){let e=r.startRow+1,i=r.text;if(r.kind===`id`){let t=n.get(e)??/* @__PURE__ */ new Map;n.set(e,t),t.has(r.text)||t.set(r.text,t.size),i=`\u0001${t.get(r.text)}`}t.set(e,`${t.get(e)??``}${i}\u0000`)}let r=/* @__PURE__ */ new Map;for(let e of t.values())r.set(e,(r.get(e)??0)+1);return{byLine:t,lineCounts:r}}function K(e,t,n,i){let a=i(t);if(a===void 0)return new Set(e);let o=(e,t)=>{if(t===``)return(a.lineCounts.get(e)??0)>1;let n=i(t);return n===void 0||n.lineCounts.has(e)},s=r(n);return new Set([...e].filter(e=>{let t=a.byLine.get(e);return t===void 0||[...s.get(e)??[]].some(e=>o(t,e))}))}function q(e,t,n,r=e=>e){if(e===void 0||n===void 0)return[];let i=e.duplication.duplicateBlockGroups.flatMap(e=>e.map(({startLine:e,endLine:t})=>({startLine:e,endLine:t,partners:[``]}))),a=(t?.groups??[]).flatMap(e=>{let t=e.occurrences.filter(e=>e.file===n),i=[...e.files.filter(e=>e!==n).map(r),...t.length>1?[``]:[]];return t.map(({startLine:e,endLine:t})=>({startLine:e,endLine:t,partners:i}))});return[...i,...a]}function ue(e,t,n){return{file:e.displayFile,baseMetrics:e.baseMetrics,headMetrics:e.headFile?.metrics,baseFunctionTokens:e.baseFunctionTokens,headFunctionTokens:e.headFunctionTokens,newlyDuplicatedLines:t,duplicationPartners:J(e,t,n),gated:e.gated}}function J(e,t,n){let r=e.changed.headPath,i=e=>e.file===r&&t.some(t=>t>=e.startLine&&t<=e.endLine),a=/* @__PURE__ */ new Set;for(let e of n?.groups??[])if(e.occurrences.some(i))for(let t of e.files)t!==r&&a.add(t);return[...a].toSorted()}function de(e,t,n,r,i){let a=t.slice(0,12);r.length>0?(x(`Regression gate could not complete: ${r.length} measurement failures (details on stderr)${n.violations.length>0?`; ${n.violations.length} violations in the measured files`:``} (base ${e.base}, merge-base ${a}).\n`),Y(n)):n.violations.length===0?x(`Regression gate passed: ${n.checkedFileCount} changed files, ${n.checkedFunctionCount} functions checked (base ${e.base}, merge-base ${a}).\n`):(x(`Regression gate vs ${e.base} (merge-base ${a}): ${n.violations.length} violations\n`),Y(n)),e.full&&X(n);for(let e of i)b(`Warning: ${e}\n`);for(let e of r)b(`Error: ${e}\n`)}function Y(e){for(let[t,n]of e.violations.entries())x(`${t+1}. ${n.message}\n`)}function X(e){if(e.checkedFunctions.length!==0){x(`
|
|
2
2
|
Checked functions (base -> head):
|
|
3
|
-
`);for(let t of e.checkedFunctions)
|
|
4
|
-
`)}export{
|
|
3
|
+
`);for(let t of e.checkedFunctions)x(`- ${Z(t)}\n`)}}function Z(e){let t=(t,n=String)=>{let r=n(t(e.head));return e.base?`${n(t(e.base))} -> ${r}`:r},n=[`cognitive ${t(e=>e.cognitiveComplexity)}`,`NCSS ${t(e=>e.ncss)}`,`nesting ${t(e=>e.nestingDepth)}`,`DepDegree ${t(e=>e.depDegree)}`,`volume ${t(e=>e.halsteadVolume,e=>e.toFixed(1))}`];return`${e.file}:${e.startLine}-${e.endLine} ${e.name}${e.base?``:` (new)`}: ${n.join(`, `)}`}function fe(e,t,n,r,i,a){let o={base:e.base,mergeBase:t,passed:n.violations.length===0&&i.length===0,violations:n.violations,checkedFileCount:n.checkedFileCount,checkedFunctionCount:n.checkedFunctionCount,newFunctionCount:n.newFunctionCount,errors:i,warnings:a};e.full&&(o.files=r.filter(e=>e.gated!==!1).map(e=>({file:e.file,baseFunctionCount:e.baseMetrics?.functions.length??0,headFunctionCount:e.headMetrics?.functions.length??0,baseNcss:e.baseMetrics?.ncssCount??0,headNcss:e.headMetrics?.ncssCount??0,baseMaxCognitiveComplexity:e.baseMetrics?.maxCognitiveComplexity??0,headMaxCognitiveComplexity:e.headMetrics?.maxCognitiveComplexity??0,newlyDuplicatedLines:e.newlyDuplicatedLines,duplicationPartners:e.duplicationPartners,functions:n.checkedFunctions.filter(t=>t.file===e.file)}))),x(JSON.stringify(o,void 0,2)+`
|
|
4
|
+
`)}export{A as runDiffCommand};
|
|
5
5
|
//# sourceMappingURL=diffCommand.js.map
|
package/dist/diffCommand.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diffCommand.js","names":[],"sources":["../src/diffCommand.ts"],"sourcesContent":["import { lstat, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { loadConfig, resolveGateOptions, resolveOptions, type ResolvedOptions } from './cliConfig.js';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData } from './duplication.js';\nimport {\n listChangedFiles,\n listRepositoryFiles,\n listSymlinkPathsAtRevision,\n readFileAtRevision,\n resolveMergeBase,\n resolveRepoRoot,\n type ChangedFile,\n} from './git.js';\nimport { collectFunctionTokenSequences } from './metrics.js';\nimport {\n evaluateRegressionGate,\n type CheckedFunctionReport,\n type GateFileInput,\n type GateFunctionValues,\n type GateResult,\n} from './regressionGate.js';\nimport {\n collectDuplicatedLineNumbers,\n configSearchDirectory,\n formatError,\n formatPath,\n getLanguage,\n isScannedPath,\n measureWithCrossFileData,\n resolveTarget,\n scanListedFiles,\n writeStderr,\n writeStdout,\n type FileMetrics,\n} from './scan.js';\nimport type { CodeMetrics, LanguageName } from './types.js';\n\n/** Raw options of the `diff` subcommand; every field but base is undefined unless the flag was passed. */\nexport interface DiffCliOptions {\n base: string;\n config?: string;\n duplicationMinTokens?: number;\n duplicationMaxGapTokens?: number;\n duplicationMinSimilarityPercent?: number;\n includeTests?: boolean;\n json?: boolean;\n full?: boolean;\n}\n\n/** One changed file measured at both revisions, plus its duplication-universe contribution. */\ninterface PreparedFile {\n changed: ChangedFile;\n /** Repository-relative display path: the head path, or the base path for deleted files. */\n displayFile: string;\n /** Whether the file is gated (under the target directory); others only feed the base universe. */\n gated: boolean;\n headFile?: FileMetrics;\n baseMetrics?: CodeMetrics;\n baseCandidates?: CrossFileDuplicationFileData;\n baseFunctionTokens?: Int32Array[];\n headFunctionTokens?: Int32Array[];\n}\n\n/** A scanned file that git considers part of the project, keyed by its repository-relative path. */\ninterface ScannedFile {\n relativePath: string;\n file: FileMetrics;\n}\n\n/**\n * Runs the regression gate: measures the files changed relative to the merge-base with the base\n * ref, at both revisions (`git cat-file`; no checkout, no persisted baseline), and reports only\n * violations. Exit codes: 0 all gates passed, 1 violations, 2 changed files could not be measured.\n */\nexport async function runDiffCommand(target: string, cliOptions: DiffCliOptions): Promise<void> {\n try {\n await runGate(target, cliOptions);\n } catch (error) {\n writeStderr(`Error: ${formatError(error)}\\n`);\n process.exitCode = 2;\n }\n}\n\nasync function runGate(target: string, cliOptions: DiffCliOptions): Promise<void> {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const gateOptions = resolveGateOptions(config);\n\n // The target may be a typo'd path whose ancestors don't exist either; repository discovery must\n // still run so the mistyped target gets its own diagnostic instead of a git spawn failure.\n const repoRoot = await realpath(\n await resolveRepoRoot(await firstExistingDirectory(await configSearchDirectory(resolvedTarget)))\n );\n const mergeBase = await resolveMergeBase(repoRoot, cliOptions.base);\n const changedFiles = await listChangedFiles(repoRoot, mergeBase);\n\n // Every git-visible file (tracked or untracked non-ignored) is measured at head: that provides\n // the head metrics of changed files and the project-wide duplication universe, so copy-paste\n // from unchanged code into changed files is caught. Scanning the explicit git list (instead of\n // walking the tree) keeps ignored artifact directories from ever being parsed: they exist in\n // neither the base commit nor CI, so they would only cost time and skew duplication counts.\n // Unchanged files are byte-identical at both revisions, so the base universe is the same scan\n // with the changed files' contents swapped for their merge-base blobs.\n const repositoryFiles = await listRepositoryFiles(repoRoot);\n const baseSymlinkPaths = await listSymlinkPathsAtRevision(repoRoot, mergeBase);\n const scan = await scanListedFiles(repoRoot, repositoryFiles, options);\n // A run-wide failure (a missing native addon) invalidates the whole gate: surface it once as\n // the fatal error (exit 2) instead of diagnosing every changed file as unmeasured.\n if (scan.fatalError) {\n throw new Error(scan.fatalError);\n }\n const scannedFiles: ScannedFile[] = scan.files.map((file) => ({\n relativePath: formatPath(file.file, scan.displayRoot),\n file,\n }));\n\n // A measurement failure on ANY scannable changed file forces exit 2 — deliberately including\n // files outside a scoped target, because cross-file function matching and the base duplication\n // universe for the gated files depend on them. Failures elsewhere (unchanged files) and\n // unsupported changed paths degrade to warnings.\n const changedPaths = new Set(\n changedFiles\n .flatMap((changed) => [changed.headPath, ...(changed.basePath === undefined ? [] : [changed.basePath])])\n .filter((changedPath) => isScannedPath(changedPath, options))\n );\n const errors: string[] = [];\n const warnings = [...scan.warnings];\n for (const error of scan.errors) {\n if ([...changedPaths].some((changedPath) => error.startsWith(`${changedPath}:`))) {\n errors.push(error);\n } else {\n warnings.push(error);\n }\n }\n\n const { canonicalTarget, targetExists } = await canonicalizeTarget(resolvedTarget);\n const prepared = await prepareChangedFiles(\n changedFiles,\n { repoRoot, mergeBase, canonicalTarget, options, scannedFiles, baseSymlinkPaths, scanErrors: [...errors] },\n errors,\n warnings\n );\n // A gate must not fail open on a mistyped target: a nonexistent path is only acceptable when it\n // still matches changed files (e.g. a fully deleted directory).\n if (!targetExists && !prepared.some((file) => file.gated)) {\n throw new Error(`target \"${target}\" does not exist and matches no changed file`);\n }\n\n // Non-gated files (outside the target, or renamed out of scan scope) still feed function\n // matching and the duplication universes; the evaluator reports nothing for them.\n const { baseCross, headCross } = measureDuplicationUniverses(prepared, scannedFiles, options);\n const inputs = prepared.map((file) => toGateInput(file, baseCross, headCross));\n const result = evaluateRegressionGate(inputs, gateOptions);\n\n if (cliOptions.json) {\n printJsonReport(cliOptions, mergeBase, result, inputs, errors, warnings);\n } else {\n printTextReport(cliOptions, mergeBase, result, errors, warnings);\n }\n\n if (errors.length > 0) {\n process.exitCode = 2;\n } else if (result.violations.length > 0) {\n process.exitCode = 1;\n }\n}\n\n/** The target may not exist (e.g. only deleted files under it); fall back to the resolved path. */\nasync function canonicalizeTarget(resolvedTarget: string): Promise<{ canonicalTarget: string; targetExists: boolean }> {\n try {\n return { canonicalTarget: await realpath(resolvedTarget), targetExists: true };\n } catch {\n return { canonicalTarget: resolvedTarget, targetExists: false };\n }\n}\n\ninterface GateContext {\n repoRoot: string;\n mergeBase: string;\n canonicalTarget: string;\n options: ResolvedOptions;\n scannedFiles: ScannedFile[];\n /** Paths that are symbolic links at the merge-base; like head symlinks, they are not gated. */\n baseSymlinkPaths: Set<string>;\n /** Errors the head scan recorded against changed files. */\n scanErrors: readonly string[];\n}\n\nasync function prepareChangedFiles(\n changedFiles: ChangedFile[],\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile[]> {\n const headByPath = new Map(context.scannedFiles.map(({ relativePath, file }) => [relativePath, file]));\n // Files are prepared concurrently (git reads and base measurements); each records its diagnostics\n // on its own, and they are appended in changed-file order so the report stays deterministic.\n const results = await mapConcurrently(changedFiles, os.availableParallelism() * 2, async (changed) => {\n const fileErrors: string[] = [];\n const fileWarnings: string[] = [];\n const file = await prepareChangedFile(changed, context, headByPath, fileErrors, fileWarnings);\n return { file, fileErrors, fileWarnings };\n });\n const prepared: PreparedFile[] = [];\n for (const { file, fileErrors, fileWarnings } of results) {\n errors.push(...fileErrors);\n warnings.push(...fileWarnings);\n if (file) {\n prepared.push(file);\n }\n }\n return prepared;\n}\n\n/** `Promise.all(items.map(map))` with at most `limit` calls pending at once, results in input order. */\nasync function mapConcurrently<T, R>(items: readonly T[], limit: number, map: (item: T) => Promise<R>): Promise<R[]> {\n const results: R[] = [];\n let nextIndex = 0;\n const work = async (): Promise<void> => {\n while (nextIndex < items.length) {\n const index = nextIndex;\n nextIndex += 1;\n results[index] = await map(items[index] as T);\n }\n };\n await Promise.all(Array.from({ length: Math.min(limit, items.length) }, work));\n return results;\n}\n\nasync function prepareChangedFile(\n changed: ChangedFile,\n context: GateContext,\n headByPath: Map<string, FileMetrics>,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile | undefined> {\n // Symbolic links are skipped on both sides, mirroring scanListedFiles: git stores only the\n // target string, so a symlink blob is not measurable source.\n const headScannable =\n changed.status !== 'deleted' &&\n isScannedPath(changed.headPath, context.options) &&\n !(await isSymbolicLink(path.join(context.repoRoot, changed.headPath)));\n // A base path outside the scan scope (renamed from a test/ignored directory, or an unsupported\n // extension) was never measurable code: its content gates as new code instead of ratcheting\n // against a blob the scanner would not have measured.\n const baseScannable =\n changed.basePath !== undefined &&\n isScannedPath(changed.basePath, context.options) &&\n !context.baseSymlinkPaths.has(changed.basePath);\n if (!headScannable && !baseScannable) {\n return undefined;\n }\n\n const displayFile = changed.status === 'deleted' ? (changed.basePath as string) : changed.headPath;\n const headFile = headScannable ? headByPath.get(changed.headPath) : undefined;\n if (headScannable && !headFile) {\n reportUnmeasuredChangedFile(changed.headPath, context.scanErrors, errors);\n return undefined;\n }\n\n const file: PreparedFile = {\n changed,\n displayFile,\n // A file whose head left the scan scope still contributes its base functions to matching and\n // its base blob to the base universe, but nothing about it is gated or reported.\n gated:\n headScannable || changed.status === 'deleted'\n ? isWithinTarget(path.join(context.repoRoot, displayFile), context.canonicalTarget)\n : false,\n headFile,\n };\n\n if (baseScannable && !(await measureBaseRevision(file, changed.basePath as string, context, errors, warnings))) {\n return undefined;\n }\n\n if (headFile) {\n await collectHeadFunctionTokens(file, headFile, context, warnings);\n }\n\n return file;\n}\n\n/**\n * The scan covers exactly the git-visible list, so a scannable changed path can only be missing\n * after a measurement failure (already recorded as an error) or a silent exclusion (an alias of\n * an already-visited file, or absence from the git list). Failing loudly keeps the gate from\n * passing with the file unchecked.\n */\nfunction reportUnmeasuredChangedFile(headPath: string, scanErrors: readonly string[], errors: string[]): void {\n if (!scanErrors.some((error) => error.startsWith(`${headPath}:`))) {\n errors.push(`${headPath}: changed file was not measured`);\n }\n}\n\nasync function collectHeadFunctionTokens(\n file: PreparedFile,\n headFile: FileMetrics,\n context: GateContext,\n warnings: string[]\n): Promise<void> {\n try {\n const headContent = await readFile(headFile.file, 'utf8');\n file.headFunctionTokens = collectFunctionTokenSequences(headContent, {\n language: getLanguage(file.changed.headPath, context.options) as LanguageName,\n duplication: context.options.duplication,\n });\n } catch (error) {\n // Only rename re-matching degrades without token sequences; the head metrics still gate.\n warnings.push(`${file.displayFile}: function token sequences unavailable: ${formatError(error)}`);\n }\n}\n\n/**\n * Measures the merge-base blob into `file`; false (with an error recorded) only when the metrics\n * themselves cannot be measured. The auxiliary collections (duplication candidates, token\n * sequences) may fail independently of the metrics, so their failure only degrades duplication\n * data and rename re-matching — the function-level ratchets still run.\n */\nasync function measureBaseRevision(\n file: PreparedFile,\n basePath: string,\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<boolean> {\n const measureOptions = {\n language: getLanguage(basePath, context.options) as LanguageName,\n duplication: context.options.duplication,\n };\n let baseContent;\n try {\n baseContent = await readFileAtRevision(context.repoRoot, context.mergeBase, basePath);\n const measured = await measureWithCrossFileData(baseContent, measureOptions);\n file.baseMetrics = measured.metrics;\n file.baseCandidates = measured.crossFileData;\n if (measured.crossFileError !== undefined) {\n warnings.push(`${basePath} (at merge-base): duplication candidates unavailable: ${measured.crossFileError}`);\n }\n } catch (error) {\n errors.push(`${basePath} (at merge-base): ${formatError(error)}`);\n return false;\n }\n try {\n file.baseFunctionTokens = collectFunctionTokenSequences(baseContent, measureOptions);\n } catch (error) {\n warnings.push(`${basePath} (at merge-base): function token sequences unavailable: ${formatError(error)}`);\n }\n return true;\n}\n\nasync function isSymbolicLink(absolutePath: string): Promise<boolean> {\n const stats = await lstat(absolutePath).catch(() => {});\n return stats?.isSymbolicLink() ?? false;\n}\n\n/** Walks up to the nearest existing DIRECTORY, so git commands never spawn in a missing or non-directory cwd. */\nasync function firstExistingDirectory(directory: string): Promise<string> {\n let current = directory;\n while (true) {\n const stats = await stat(current).catch(() => {});\n if (stats?.isDirectory()) {\n return current;\n }\n const parent = path.dirname(current);\n if (parent === current) {\n return current;\n }\n current = parent;\n }\n}\n\nfunction isWithinTarget(candidate: string, targetDirectory: string): boolean {\n const relative = path.relative(targetDirectory, candidate);\n return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));\n}\n\nfunction measureDuplicationUniverses(\n prepared: PreparedFile[],\n scannedFiles: ScannedFile[],\n options: ResolvedOptions\n): { baseCross?: CrossFileDuplicationMetrics; headCross?: CrossFileDuplicationMetrics } {\n const headSources = scannedFiles.flatMap(({ relativePath, file }) =>\n file.duplicationCandidates ? [{ file: relativePath, ...file.duplicationCandidates }] : []\n );\n\n const changedHeadPaths = new Set(\n prepared.flatMap((file) => (file.changed.status === 'deleted' ? [] : [file.changed.headPath]))\n );\n const baseSources = headSources.filter((source) => !changedHeadPaths.has(source.file));\n for (const file of prepared) {\n if (file.baseCandidates && file.changed.basePath !== undefined) {\n baseSources.push({ file: file.changed.basePath, ...file.baseCandidates });\n }\n }\n\n return {\n baseCross: baseSources.length >= 2 ? measureCrossFileDuplication(baseSources, options.duplication) : undefined,\n headCross: headSources.length >= 2 ? measureCrossFileDuplication(headSources, options.duplication) : undefined,\n };\n}\n\nfunction toGateInput(\n file: PreparedFile,\n baseCross: CrossFileDuplicationMetrics | undefined,\n headCross: CrossFileDuplicationMetrics | undefined\n): GateFileInput {\n return {\n file: file.displayFile,\n baseMetrics: file.baseMetrics,\n headMetrics: file.headFile?.metrics,\n baseFunctionTokens: file.baseFunctionTokens,\n headFunctionTokens: file.headFunctionTokens,\n baseDuplicatedLineCount:\n file.baseMetrics === undefined || file.changed.basePath === undefined\n ? 0\n : countDuplicatedLines(file.baseMetrics, baseCross, file.changed.basePath),\n headDuplicatedLineCount:\n file.changed.status === 'deleted'\n ? 0\n : countDuplicatedLines(file.headFile?.metrics, headCross, file.changed.headPath),\n duplicationPartners: collectPartners(headCross, file.changed.headPath),\n gated: file.gated,\n };\n}\n\nfunction countDuplicatedLines(\n metrics: CodeMetrics | undefined,\n cross: CrossFileDuplicationMetrics | undefined,\n file: string\n): number {\n return collectDuplicatedLineNumbers(metrics, cross, file).size;\n}\n\nfunction collectPartners(cross: CrossFileDuplicationMetrics | undefined, file: string): string[] {\n if (!cross) {\n return [];\n }\n const partners = new Set<string>();\n for (const group of cross.groups) {\n if (group.files.includes(file)) {\n for (const partner of group.files) {\n if (partner !== file) {\n partners.add(partner);\n }\n }\n }\n }\n return [...partners].toSorted();\n}\n\nfunction printTextReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n errors: string[],\n warnings: string[]\n): void {\n const shortBase = mergeBase.slice(0, 12);\n if (errors.length > 0) {\n // Unmeasured files were not gated, so \"0 violations\" would be vacuous; never claim a pass.\n writeStdout(\n `Regression gate could not complete: ${errors.length} measurement failures (details on stderr)` +\n `${result.violations.length > 0 ? `; ${result.violations.length} violations in the measured files` : ''} (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n printViolations(result);\n } else if (result.violations.length === 0) {\n writeStdout(\n `Regression gate passed: ${result.checkedFileCount} changed files, ${result.checkedFunctionCount} functions checked (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n } else {\n writeStdout(\n `Regression gate vs ${cliOptions.base} (merge-base ${shortBase}): ${result.violations.length} violations\\n`\n );\n printViolations(result);\n }\n\n if (cliOptions.full) {\n printFullDetails(result);\n }\n\n for (const warning of warnings) {\n writeStderr(`Warning: ${warning}\\n`);\n }\n for (const error of errors) {\n writeStderr(`Error: ${error}\\n`);\n }\n}\n\nfunction printViolations(result: GateResult): void {\n for (const [index, violation] of result.violations.entries()) {\n writeStdout(`${index + 1}. ${violation.message}\\n`);\n }\n}\n\n/** Base -> head values of every checked function; kept behind --full for humans and trending. */\nfunction printFullDetails(result: GateResult): void {\n if (result.checkedFunctions.length === 0) {\n return;\n }\n writeStdout('\\nChecked functions (base -> head):\\n');\n for (const report of result.checkedFunctions) {\n writeStdout(`- ${formatFunctionReport(report)}\\n`);\n }\n}\n\nfunction formatFunctionReport(report: CheckedFunctionReport): string {\n const range = (\n select: (values: GateFunctionValues) => number,\n format: (value: number) => string = String\n ): string => {\n const head = format(select(report.head));\n return report.base ? `${format(select(report.base))} -> ${head}` : head;\n };\n const values = [\n `cognitive ${range((fn) => fn.cognitiveComplexity)}`,\n `NCSS ${range((fn) => fn.ncss)}`,\n `nesting ${range((fn) => fn.nestingDepth)}`,\n `DepDegree ${range((fn) => fn.depDegree)}`,\n `volume ${range(\n (fn) => fn.halsteadVolume,\n (value) => value.toFixed(1)\n )}`,\n ];\n return `${report.file}:${report.startLine}-${report.endLine} ${report.name}${report.base ? '' : ' (new)'}: ${values.join(', ')}`;\n}\n\nfunction printJsonReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n inputs: GateFileInput[],\n errors: string[],\n warnings: string[]\n): void {\n const report: Record<string, unknown> = {\n base: cliOptions.base,\n mergeBase,\n passed: result.violations.length === 0 && errors.length === 0,\n violations: result.violations,\n checkedFileCount: result.checkedFileCount,\n checkedFunctionCount: result.checkedFunctionCount,\n newFunctionCount: result.newFunctionCount,\n errors,\n warnings,\n };\n if (cliOptions.full) {\n report.files = inputs\n .filter((input) => input.gated !== false)\n .map((input) => ({\n file: input.file,\n baseFunctionCount: input.baseMetrics?.functions.length ?? 0,\n headFunctionCount: input.headMetrics?.functions.length ?? 0,\n baseNcss: input.baseMetrics?.ncssCount ?? 0,\n headNcss: input.headMetrics?.ncssCount ?? 0,\n baseMaxCognitiveComplexity: input.baseMetrics?.maxCognitiveComplexity ?? 0,\n headMaxCognitiveComplexity: input.headMetrics?.maxCognitiveComplexity ?? 0,\n baseDuplicatedLineCount: input.baseDuplicatedLineCount,\n headDuplicatedLineCount: input.headDuplicatedLineCount,\n duplicationPartners: input.duplicationPartners,\n functions: result.checkedFunctions.filter((fn) => fn.file === input.file),\n }));\n }\n writeStdout(JSON.stringify(report, undefined, 2) + '\\n');\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"diffCommand.js","names":[],"sources":["../src/diffCommand.ts"],"sourcesContent":["import { lstat, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { getErrorMessage, mapConcurrently } from '@willbooster/shared-lib';\nimport { loadConfig, resolveGateOptions, resolveOptions, type ResolvedOptions } from './cliConfig.js';\nimport {\n measureCrossFileDuplication,\n type CrossFileDuplicateOccurrence,\n type CrossFileDuplicationMetrics,\n} from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData, Token } from './duplication.js';\nimport { keepPaths, loadRepositoryExclusion, type Exclusion } from './exclusion.js';\nimport {\n listChangedFiles,\n listLineHunks,\n listRepositoryFiles,\n listSymlinkPathsAtRevision,\n readFileAtRevision,\n resolveMergeBase,\n resolveRepoRoot,\n type ChangedFile,\n} from './git.js';\nimport { collectFunctionTokenSequences } from './metrics.js';\nimport {\n findNewlyDuplicatedLines,\n indexPartnersByLine,\n type CloneOccurrence,\n type DuplicationChange,\n type LineHunk,\n} from './newDuplication.js';\nimport {\n evaluateRegressionGate,\n type CheckedFunctionReport,\n type GateFileInput,\n type GateFunctionValues,\n type GateResult,\n} from './regressionGate.js';\nimport {\n collectDuplicatedLineNumbers,\n configSearchDirectory,\n formatPath,\n getLanguage,\n isScannedPath,\n measureWithCrossFileData,\n resolveTarget,\n scanListedFiles,\n writeStderr,\n writeStdout,\n type FileMetrics,\n} from './scan.js';\nimport type { CodeMetrics, LanguageName } from './types.js';\n\n/** Raw options of the `diff` subcommand; every field but base is undefined unless the flag was passed. */\nexport interface DiffCliOptions {\n base: string;\n config?: string;\n duplicationMinTokens?: number;\n duplicationMaxGapTokens?: number;\n duplicationMinSimilarityPercent?: number;\n includeTests?: boolean;\n json?: boolean;\n full?: boolean;\n}\n\n/** One changed file measured at both revisions, plus its duplication-universe contribution. */\ninterface PreparedFile {\n changed: ChangedFile;\n /** Repository-relative display path: the head path, or the base path for deleted files. */\n displayFile: string;\n /** Whether the file is gated (under the target directory); others only feed the base universe. */\n gated: boolean;\n headFile?: FileMetrics;\n headContent?: string;\n baseMetrics?: CodeMetrics;\n baseContent?: string;\n /**\n * The line diff from the measured base to the measured head; with one side unmeasured, every\n * line of the other counts as added or deleted.\n */\n hunks?: LineHunk[];\n baseCandidates?: CrossFileDuplicationFileData;\n baseFunctionTokens?: Int32Array[];\n headFunctionTokens?: Int32Array[];\n}\n\n/** A scanned file that git considers part of the project, keyed by its repository-relative path. */\ninterface ScannedFile {\n relativePath: string;\n file: FileMetrics;\n}\n\n/**\n * Runs the regression gate: measures the files changed relative to the merge-base with the base\n * ref, at both revisions (`git cat-file`; no checkout, no persisted baseline), and reports only\n * violations. Exit codes: 0 all gates passed, 1 violations, 2 changed files could not be measured.\n */\nexport async function runDiffCommand(target: string, cliOptions: DiffCliOptions): Promise<void> {\n try {\n await runGate(target, cliOptions);\n } catch (error) {\n writeStderr(`Error: ${getErrorMessage(error)}\\n`);\n process.exitCode = 2;\n }\n}\n\nasync function runGate(target: string, cliOptions: DiffCliOptions): Promise<void> {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const gateOptions = resolveGateOptions(config);\n\n // The target may be a typo'd path whose ancestors don't exist either; repository discovery must\n // still run so the mistyped target gets its own diagnostic instead of a git spawn failure.\n const repoRoot = await realpath(\n await resolveRepoRoot(await firstExistingDirectory(await configSearchDirectory(resolvedTarget)))\n );\n const mergeBase = await resolveMergeBase(repoRoot, cliOptions.base);\n const changedFiles = await listChangedFiles(repoRoot, mergeBase);\n\n // Every git-visible file (tracked or untracked non-ignored) is measured at head: that provides\n // the head metrics of changed files and the project-wide duplication universe, so copy-paste\n // from unchanged code into changed files is caught. Scanning the explicit git list (instead of\n // walking the tree) keeps ignored artifact directories from ever being parsed: they exist in\n // neither the base commit nor CI, so they would only cost time and skew duplication counts.\n // Unchanged files are byte-identical at both revisions, so the base universe is the same scan\n // with the changed files' contents swapped for their merge-base blobs.\n const repositoryFiles = await listRepositoryFiles(repoRoot);\n const baseSymlinkPaths = await listSymlinkPathsAtRevision(repoRoot, mergeBase);\n const { canonicalTarget, targetExists } = await canonicalizeTarget(resolvedTarget);\n const targetStat = await stat(canonicalTarget).catch(() => {});\n const targetFile = targetStat?.isFile() ? canonicalTarget : undefined;\n // A targeted file git does not list (an ignored one) is still measured, as an addition.\n const unlistedTarget = targetFile && path.relative(repoRoot, targetFile).split(path.sep).join('/');\n if (unlistedTarget !== undefined && !unlistedTarget.startsWith('../') && !repositoryFiles.has(unlistedTarget)) {\n repositoryFiles.add(unlistedTarget);\n changedFiles.push({ status: 'added', headPath: unlistedTarget });\n }\n const explicitFiles = listExplicitlyTargetedPaths(targetFile, repoRoot, changedFiles);\n // Base blobs are excluded by the attributes of the revision they come from. A changed\n // .gitattributes can re-include files whose bytes did not change, so then every file's base\n // attributes are needed to find them.\n const attributesChanged = changedFiles.some((changed) =>\n [changed.headPath, changed.basePath].some(\n (file) => file !== undefined && path.posix.basename(file) === '.gitattributes'\n )\n );\n const basePaths = changedFiles.flatMap((changed) => (changed.basePath === undefined ? [] : [changed.basePath]));\n const [headAttributesExclusion, baseAttributesExclusion] = await Promise.all([\n loadRepositoryExclusion(repoRoot, repositoryFiles, options.exclude),\n loadRepositoryExclusion(\n repoRoot,\n attributesChanged ? [...new Set([...basePaths, ...repositoryFiles])] : basePaths,\n options.exclude,\n mergeBase\n ),\n ]);\n const headExclusion = keepPaths(headAttributesExclusion, explicitFiles);\n const baseExclusion = keepPaths(baseAttributesExclusion, explicitFiles);\n const scan = await scanListedFiles(\n repoRoot,\n repositoryFiles,\n { ...options, loadExclusion: () => Promise.resolve(headExclusion) },\n explicitFiles\n );\n // A run-wide failure (a missing native addon) invalidates the whole gate: surface it once as\n // the fatal error (exit 2) instead of diagnosing every changed file as unmeasured.\n if (scan.fatalError) {\n throw new Error(scan.fatalError);\n }\n const scannedFiles: ScannedFile[] = scan.files.map((file) => ({\n relativePath: formatPath(file.file, scan.displayRoot),\n file,\n }));\n if (attributesChanged) {\n changedFiles.push(\n ...(await listReincludedFiles(scannedFiles, changedFiles, repoRoot, headExclusion, baseExclusion))\n );\n }\n\n // A measurement failure on ANY scannable changed file forces exit 2 — deliberately including\n // files outside a scoped target, because cross-file function matching and the base duplication\n // universe for the gated files depend on them. Failures elsewhere (unchanged files) and\n // unsupported changed paths degrade to warnings.\n const changedPaths = new Set(\n changedFiles\n .flatMap((changed) => [changed.headPath, ...(changed.basePath === undefined ? [] : [changed.basePath])])\n .filter((changedPath) => isScannedPath(changedPath, options, explicitFiles.has(path.join(repoRoot, changedPath))))\n );\n const errors: string[] = [];\n const warnings = [...scan.warnings];\n for (const error of scan.errors) {\n if ([...changedPaths].some((changedPath) => error.startsWith(`${changedPath}:`))) {\n errors.push(error);\n } else {\n warnings.push(error);\n }\n }\n\n const prepared = await prepareChangedFiles(\n changedFiles,\n {\n repoRoot,\n mergeBase,\n canonicalTarget,\n options,\n scannedFiles,\n baseSymlinkPaths,\n scanErrors: [...errors],\n headExclusion,\n baseExclusion,\n explicitFiles,\n generatedHeadFiles: new Set(scan.generatedFiles),\n },\n errors,\n warnings\n );\n // A gate must not fail open on a mistyped target: a nonexistent path is only acceptable when it\n // still matches changed files (e.g. a fully deleted directory).\n if (!targetExists && !prepared.some((file) => file.gated)) {\n throw new Error(`target \"${target}\" does not exist and matches no changed file`);\n }\n\n // Non-gated files (outside the target, or renamed out of scan scope) still feed function\n // matching and the duplication universes; the evaluator reports nothing for them.\n const { baseCross, headCross } = measureDuplicationUniverses(prepared, scannedFiles, options);\n const renamedPaths = new Map(\n prepared.flatMap(({ changed }) =>\n changed.status === 'renamed' && changed.basePath !== undefined\n ? [[changed.basePath, changed.headPath] as const]\n : []\n )\n );\n const headPathOf = (basePath: string): string => renamedPaths.get(basePath) ?? basePath;\n const lineSignaturesOf = createLineSignatureIndex(scannedFiles);\n const newlyDuplicatedLines = findNewlyDuplicatedLines(\n prepared.map((file) => toDuplicationChange(file, baseCross, headCross, headPathOf, lineSignaturesOf))\n );\n const inputs = prepared.map((file, index) => toGateInput(file, newlyDuplicatedLines[index] ?? [], headCross));\n const result = evaluateRegressionGate(inputs, gateOptions);\n\n if (cliOptions.json) {\n printJsonReport(cliOptions, mergeBase, result, inputs, errors, warnings);\n } else {\n printTextReport(cliOptions, mergeBase, result, errors, warnings);\n }\n\n if (errors.length > 0) {\n process.exitCode = 2;\n } else if (result.violations.length > 0) {\n process.exitCode = 1;\n }\n}\n\n/** Content with a generated-code marker, to ask whether an exclusion honors the marker for a path. */\nconst markedCode = '/* @generated */';\n\n/**\n * Unchanged files the head scan measured but the merge-base would not have: excluded by its\n * attributes, or generated by content without the `-linguist-generated` override the working tree\n * adds. Their code is newly measured, so it gates as a modification from an unmeasurable base. The\n * head side comes from the scan, which already applied scope, symbolic-link, and exclusion rules.\n */\nasync function listReincludedFiles(\n scannedFiles: readonly ScannedFile[],\n changedFiles: readonly ChangedFile[],\n repoRoot: string,\n headExclusion: Exclusion,\n baseExclusion: Exclusion\n): Promise<ChangedFile[]> {\n const changedPaths = new Set(changedFiles.flatMap((changed) => [changed.headPath, changed.basePath]));\n const reincluded: ChangedFile[] = [];\n for (const { relativePath, file } of scannedFiles) {\n const absolutePath = path.join(repoRoot, relativePath);\n if (changedPaths.has(relativePath)) {\n continue;\n }\n // The same content is generated at one revision only when its override differs, so the\n // content is read just for files the working tree newly marks -linguist-generated.\n const overrideAdded =\n baseExclusion.isGeneratedCode(absolutePath, markedCode) &&\n !headExclusion.isGeneratedCode(absolutePath, markedCode);\n if (\n baseExclusion.isExcludedPath(absolutePath) ||\n (overrideAdded && baseExclusion.isGeneratedCode(absolutePath, await readFile(file.file, 'utf8')))\n ) {\n reincluded.push({ status: 'modified', basePath: relativePath, headPath: relativePath });\n }\n }\n return reincluded;\n}\n\n/**\n * An explicitly targeted file is measured even when excluded, like the ranking command's single\n * file target: the file itself, and its base path when it was renamed.\n */\nfunction listExplicitlyTargetedPaths(\n targetFile: string | undefined,\n repoRoot: string,\n changedFiles: ChangedFile[]\n): Set<string> {\n if (targetFile === undefined) {\n return new Set();\n }\n const paths = new Set([targetFile]);\n for (const changed of changedFiles) {\n if (changed.basePath !== undefined && path.join(repoRoot, changed.headPath) === targetFile) {\n paths.add(path.join(repoRoot, changed.basePath));\n }\n }\n return paths;\n}\n\n/** The target may not exist (e.g. only deleted files under it); fall back to the resolved path. */\nasync function canonicalizeTarget(resolvedTarget: string): Promise<{ canonicalTarget: string; targetExists: boolean }> {\n try {\n return { canonicalTarget: await realpath(resolvedTarget), targetExists: true };\n } catch {\n return { canonicalTarget: resolvedTarget, targetExists: false };\n }\n}\n\ninterface GateContext {\n repoRoot: string;\n mergeBase: string;\n canonicalTarget: string;\n options: ResolvedOptions;\n scannedFiles: ScannedFile[];\n /** Paths that are symbolic links at the merge-base; like head symlinks, they are not gated. */\n baseSymlinkPaths: Set<string>;\n /** Errors the head scan recorded against changed files. */\n scanErrors: readonly string[];\n headExclusion: Exclusion;\n baseExclusion: Exclusion;\n /** The explicitly targeted file (and its renamed base path), measured despite every exclusion. */\n explicitFiles: ReadonlySet<string>;\n /** Absolute paths the head scan skipped as generated code. */\n generatedHeadFiles: ReadonlySet<string>;\n}\n\nasync function prepareChangedFiles(\n changedFiles: ChangedFile[],\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile[]> {\n const headByPath = new Map(context.scannedFiles.map(({ relativePath, file }) => [relativePath, file]));\n // Files are prepared concurrently (git reads and base measurements); each records its diagnostics\n // on its own, and they are appended in changed-file order so the report stays deterministic.\n const results = await mapConcurrently(changedFiles, os.availableParallelism() * 2, async (changed) => {\n const fileErrors: string[] = [];\n const fileWarnings: string[] = [];\n const file = await prepareChangedFile(changed, context, headByPath, fileErrors, fileWarnings);\n return { file, fileErrors, fileWarnings };\n });\n const prepared: PreparedFile[] = [];\n for (const { file, fileErrors, fileWarnings } of results) {\n errors.push(...fileErrors);\n warnings.push(...fileWarnings);\n if (file) {\n prepared.push(file);\n }\n }\n return prepared;\n}\n\nasync function prepareChangedFile(\n changed: ChangedFile,\n context: GateContext,\n headByPath: Map<string, FileMetrics>,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile | undefined> {\n // Symbolic links are skipped on both sides, mirroring scanListedFiles: git stores only the\n // target string, so a symlink blob is not measurable source.\n const absoluteHeadPath = path.join(context.repoRoot, changed.headPath);\n const headScannable =\n changed.status !== 'deleted' &&\n isScannedPath(changed.headPath, context.options, context.explicitFiles.has(absoluteHeadPath)) &&\n !context.headExclusion.isExcludedPath(absoluteHeadPath) &&\n !context.generatedHeadFiles.has(absoluteHeadPath) &&\n !(await isSymbolicLink(absoluteHeadPath));\n // A base path outside the scan scope (renamed from a test/ignored directory, or an unsupported\n // extension) was never measurable code: its content gates as new code instead of ratcheting\n // against a blob the scanner would not have measured.\n const absoluteBasePath = changed.basePath === undefined ? '' : path.join(context.repoRoot, changed.basePath);\n const baseScannable =\n changed.basePath !== undefined &&\n isScannedPath(changed.basePath, context.options, context.explicitFiles.has(absoluteBasePath)) &&\n !context.baseExclusion.isExcludedPath(absoluteBasePath) &&\n !context.baseSymlinkPaths.has(changed.basePath);\n if (!headScannable && !baseScannable) {\n return undefined;\n }\n\n const displayFile = changed.status === 'deleted' ? (changed.basePath as string) : changed.headPath;\n const headFile = headScannable ? headByPath.get(changed.headPath) : undefined;\n if (headScannable && !headFile) {\n reportUnmeasuredChangedFile(changed.headPath, context.scanErrors, errors);\n return undefined;\n }\n\n const file: PreparedFile = {\n changed,\n displayFile,\n // A file whose head left the scan scope still contributes its base functions to matching and\n // its base blob to the base universe, but nothing about it is gated or reported.\n gated:\n headScannable || changed.status === 'deleted'\n ? isWithinTarget(path.join(context.repoRoot, displayFile), context.canonicalTarget)\n : false,\n headFile,\n };\n\n if (baseScannable) {\n const outcome = await measureBaseRevision(file, changed.basePath as string, context, errors, warnings);\n // A base that turns out to be generated code gates its head as new code, like any other base\n // outside the scan scope.\n if (outcome === 'failed' || (outcome === 'generated' && !headFile)) {\n return undefined;\n }\n }\n\n try {\n if (headFile) {\n // The duplication gate needs the head lines, so an unreadable head fails like an unmeasured one.\n file.headContent = await readFile(headFile.file, 'utf8');\n collectHeadFunctionTokens(file, file.headContent, context, warnings);\n }\n file.hunks = await diffMeasuredRevisions(file, context);\n } catch (error) {\n errors.push(`${displayFile}: ${getErrorMessage(error)}`);\n return undefined;\n }\n return file;\n}\n\nasync function diffMeasuredRevisions(file: PreparedFile, context: GateContext): Promise<LineHunk[]> {\n const { changed, baseContent, headContent } = file;\n if (baseContent !== undefined && headContent !== undefined) {\n return await listLineHunks(context.repoRoot, context.mergeBase, changed.basePath as string, changed.headPath);\n }\n return [\n {\n baseStart: 1,\n baseCount: baseContent === undefined ? 0 : splitLines(baseContent).length,\n headStart: 1,\n headCount: headContent === undefined ? 0 : splitLines(headContent).length,\n },\n ];\n}\n\n/** Line numbering of the measured metrics (classifyLines splits on every line terminator). */\nfunction splitLines(content: string): string[] {\n return content.split(/\\r\\n|\\n|\\r/u);\n}\n\n/**\n * The scan covers exactly the git-visible list, so a scannable changed path can only be missing\n * after a measurement failure (already recorded as an error) or a silent exclusion (an alias of\n * an already-visited file, or absence from the git list). Failing loudly keeps the gate from\n * passing with the file unchecked.\n */\nfunction reportUnmeasuredChangedFile(headPath: string, scanErrors: readonly string[], errors: string[]): void {\n if (!scanErrors.some((error) => error.startsWith(`${headPath}:`))) {\n errors.push(`${headPath}: changed file was not measured`);\n }\n}\n\nfunction collectHeadFunctionTokens(\n file: PreparedFile,\n headContent: string,\n context: GateContext,\n warnings: string[]\n): void {\n try {\n file.headFunctionTokens = collectFunctionTokenSequences(headContent, {\n language: languageOf(file.changed.headPath, context),\n duplication: context.options.duplication,\n });\n } catch (error) {\n // Only rename re-matching degrades without token sequences; the head metrics still gate.\n warnings.push(`${file.displayFile}: function token sequences unavailable: ${getErrorMessage(error)}`);\n }\n}\n\n/**\n * Measures the merge-base blob into `file`; 'failed' (with an error recorded) only when the\n * metrics themselves cannot be measured, and 'generated' (nothing measured) for generated code.\n * The auxiliary collections (duplication candidates, token sequences) may fail independently of\n * the metrics, so their failure only degrades duplication data and rename re-matching — the\n * function-level ratchets still run.\n */\nasync function measureBaseRevision(\n file: PreparedFile,\n basePath: string,\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<'measured' | 'generated' | 'failed'> {\n const measureOptions = {\n language: languageOf(basePath, context),\n duplication: context.options.duplication,\n };\n let baseContent;\n try {\n baseContent = await readFileAtRevision(context.repoRoot, context.mergeBase, basePath);\n if (context.baseExclusion.isGeneratedCode(path.join(context.repoRoot, basePath), baseContent)) {\n return 'generated';\n }\n const measured = await measureWithCrossFileData(baseContent, measureOptions);\n file.baseMetrics = measured.metrics;\n file.baseContent = baseContent;\n file.baseCandidates = measured.crossFileData;\n if (measured.crossFileError !== undefined) {\n warnings.push(`${basePath} (at merge-base): duplication candidates unavailable: ${measured.crossFileError}`);\n }\n } catch (error) {\n errors.push(`${basePath} (at merge-base): ${getErrorMessage(error)}`);\n return 'failed';\n }\n try {\n file.baseFunctionTokens = collectFunctionTokenSequences(baseContent, measureOptions);\n } catch (error) {\n warnings.push(`${basePath} (at merge-base): function token sequences unavailable: ${getErrorMessage(error)}`);\n }\n return 'measured';\n}\n\n/** The language of a scannable path; an explicitly targeted file skips the test-file name rules. */\nfunction languageOf(relativePath: string, context: GateContext): LanguageName {\n const explicitTarget = context.explicitFiles.has(path.join(context.repoRoot, relativePath));\n return getLanguage(relativePath, context.options, explicitTarget) as LanguageName;\n}\n\nasync function isSymbolicLink(absolutePath: string): Promise<boolean> {\n const stats = await lstat(absolutePath).catch(() => {});\n return stats?.isSymbolicLink() ?? false;\n}\n\n/** Walks up to the nearest existing DIRECTORY, so git commands never spawn in a missing or non-directory cwd. */\nasync function firstExistingDirectory(directory: string): Promise<string> {\n let current = directory;\n while (true) {\n const stats = await stat(current).catch(() => {});\n if (stats?.isDirectory()) {\n return current;\n }\n const parent = path.dirname(current);\n if (parent === current) {\n return current;\n }\n current = parent;\n }\n}\n\nfunction isWithinTarget(candidate: string, targetDirectory: string): boolean {\n const relative = path.relative(targetDirectory, candidate);\n return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));\n}\n\nfunction measureDuplicationUniverses(\n prepared: PreparedFile[],\n scannedFiles: ScannedFile[],\n options: ResolvedOptions\n): { baseCross?: CrossFileDuplicationMetrics; headCross?: CrossFileDuplicationMetrics } {\n const headSources = scannedFiles.flatMap(({ relativePath, file }) =>\n file.duplicationCandidates ? [{ file: relativePath, ...file.duplicationCandidates }] : []\n );\n\n const changedHeadPaths = new Set(\n prepared.flatMap((file) => (file.changed.status === 'deleted' ? [] : [file.changed.headPath]))\n );\n const baseSources = headSources.filter((source) => !changedHeadPaths.has(source.file));\n for (const file of prepared) {\n if (file.baseCandidates && file.changed.basePath !== undefined) {\n baseSources.push({ file: file.changed.basePath, ...file.baseCandidates });\n }\n }\n\n return {\n baseCross: baseSources.length >= 2 ? measureCrossFileDuplication(baseSources, options.duplication) : undefined,\n headCross: headSources.length >= 2 ? measureCrossFileDuplication(headSources, options.duplication) : undefined,\n };\n}\n\nfunction toDuplicationChange(\n file: PreparedFile,\n baseCross: CrossFileDuplicationMetrics | undefined,\n headCross: CrossFileDuplicationMetrics | undefined,\n headPathOf: (basePath: string) => string,\n lineSignaturesOf: (file: string) => LineSignatures | undefined\n): DuplicationChange {\n const headOccurrences = collectOccurrences(file.headFile?.metrics, headCross, file.changed.headPath);\n return {\n baseLines: file.baseContent === undefined ? undefined : splitLines(file.baseContent),\n headLines: file.headContent === undefined ? undefined : splitLines(file.headContent),\n baseDuplicatedLines:\n file.baseMetrics === undefined || file.changed.basePath === undefined\n ? new Set()\n : collectDuplicatedLineNumbers(file.baseMetrics, baseCross, file.changed.basePath),\n headDuplicatedLines:\n file.headFile === undefined\n ? new Set()\n : keepRepeatedLines(\n collectDuplicatedLineNumbers(file.headFile.metrics, headCross, file.changed.headPath),\n file.changed.headPath,\n headOccurrences,\n lineSignaturesOf\n ),\n baseOccurrences: collectOccurrences(file.baseMetrics, baseCross, file.changed.basePath, headPathOf),\n headOccurrences,\n hunks: file.hunks ?? [],\n };\n}\n\n/** A file's per-line normalized token signatures and how many lines carry each. */\ninterface LineSignatures {\n byLine: Map<number, string>;\n lineCounts: Map<string, number>;\n}\n\n/** Line signatures of the scanned head files, computed on first use. */\nfunction createLineSignatureIndex(scannedFiles: ScannedFile[]): (file: string) => LineSignatures | undefined {\n const tokensByFile = new Map(\n scannedFiles.map(({ relativePath, file }) => [relativePath, file.duplicationCandidates?.tokens])\n );\n const signaturesByFile = new Map<string, LineSignatures | undefined>();\n return (file) => {\n if (!signaturesByFile.has(file)) {\n const tokens = tokensByFile.get(file);\n signaturesByFile.set(file, tokens && signLines(tokens));\n }\n return signaturesByFile.get(file);\n };\n}\n\n/**\n * Each line's tokens with identifiers numbered by first occurrence, as clone detection anonymizes\n * them.\n */\nfunction signLines(tokens: readonly Token[]): LineSignatures {\n const byLine = new Map<number, string>();\n const identifierIndexesByLine = new Map<number, Map<string, number>>();\n for (const token of tokens) {\n const line = token.startRow + 1;\n let text = token.text;\n if (token.kind === 'id') {\n // Numbered by first occurrence within the line, so `a + a` and `a + b` stay distinct.\n const identifierIndexes = identifierIndexesByLine.get(line) ?? new Map<string, number>();\n identifierIndexesByLine.set(line, identifierIndexes);\n if (!identifierIndexes.has(token.text)) {\n identifierIndexes.set(token.text, identifierIndexes.size);\n }\n text = `\\u0001${identifierIndexes.get(token.text)}`;\n }\n byLine.set(line, `${byLine.get(line) ?? ''}${text}\\u0000`);\n }\n const lineCounts = new Map<string, number>();\n for (const signature of byLine.values()) {\n lineCounts.set(signature, (lineCounts.get(signature) ?? 0) + 1);\n }\n return { byLine, lineCounts };\n}\n\n/**\n * The duplicated lines whose normalized tokens a partner file of their clones (the file itself,\n * on another line, for `''`) repeats on some line. Near-miss coverage also marks lines the copies\n * do not share, such as a new line inserted into an old near-miss clone, which copied nothing.\n */\nfunction keepRepeatedLines(\n lines: ReadonlySet<number>,\n file: string,\n occurrences: readonly CloneOccurrence[],\n lineSignaturesOf: (file: string) => LineSignatures | undefined\n): Set<number> {\n const own = lineSignaturesOf(file);\n if (own === undefined) {\n return new Set(lines);\n }\n const repeats = (signature: string, partner: string): boolean => {\n if (partner === '') {\n return (own.lineCounts.get(signature) ?? 0) > 1;\n }\n const partnerSignatures = lineSignaturesOf(partner);\n return partnerSignatures === undefined || partnerSignatures.lineCounts.has(signature);\n };\n const partnersByLine = indexPartnersByLine(occurrences);\n return new Set(\n [...lines].filter((line) => {\n const signature = own.byLine.get(line);\n return (\n signature === undefined || [...(partnersByLine.get(line) ?? [])].some((partner) => repeats(signature, partner))\n );\n })\n );\n}\n\n/**\n * The file's clone occurrences with their partner files (`''` for the file itself), partner paths\n * renamed by `renamePartner` so base partners compare with head ones.\n */\nfunction collectOccurrences(\n metrics: CodeMetrics | undefined,\n cross: CrossFileDuplicationMetrics | undefined,\n file: string | undefined,\n renamePartner: (partner: string) => string = (partner) => partner\n): CloneOccurrence[] {\n if (metrics === undefined || file === undefined) {\n return [];\n }\n const withinFile = metrics.duplication.duplicateBlockGroups.flatMap((group) =>\n group.map(({ startLine, endLine }) => ({ startLine, endLine, partners: [''] }))\n );\n const crossFile = (cross?.groups ?? []).flatMap((group) => {\n const own = group.occurrences.filter((occurrence) => occurrence.file === file);\n const partners = [\n ...group.files.filter((partner) => partner !== file).map(renamePartner),\n ...(own.length > 1 ? [''] : []),\n ];\n return own.map(({ startLine, endLine }) => ({ startLine, endLine, partners }));\n });\n return [...withinFile, ...crossFile];\n}\n\nfunction toGateInput(\n file: PreparedFile,\n newlyDuplicatedLines: number[],\n headCross: CrossFileDuplicationMetrics | undefined\n): GateFileInput {\n return {\n file: file.displayFile,\n baseMetrics: file.baseMetrics,\n headMetrics: file.headFile?.metrics,\n baseFunctionTokens: file.baseFunctionTokens,\n headFunctionTokens: file.headFunctionTokens,\n newlyDuplicatedLines,\n duplicationPartners: collectPartners(file, newlyDuplicatedLines, headCross),\n gated: file.gated,\n };\n}\n\n/** Other files sharing a cross-file clone that covers the given lines. */\nfunction collectPartners(\n file: PreparedFile,\n lines: number[],\n headCross: CrossFileDuplicationMetrics | undefined\n): string[] {\n const headPath = file.changed.headPath;\n const covers = (occurrence: CrossFileDuplicateOccurrence): boolean =>\n occurrence.file === headPath && lines.some((line) => line >= occurrence.startLine && line <= occurrence.endLine);\n const partners = new Set<string>();\n for (const group of headCross?.groups ?? []) {\n if (group.occurrences.some(covers)) {\n for (const partner of group.files) {\n if (partner !== headPath) {\n partners.add(partner);\n }\n }\n }\n }\n return [...partners].toSorted();\n}\n\nfunction printTextReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n errors: string[],\n warnings: string[]\n): void {\n const shortBase = mergeBase.slice(0, 12);\n if (errors.length > 0) {\n // Unmeasured files were not gated, so \"0 violations\" would be vacuous; never claim a pass.\n writeStdout(\n `Regression gate could not complete: ${errors.length} measurement failures (details on stderr)` +\n `${result.violations.length > 0 ? `; ${result.violations.length} violations in the measured files` : ''} (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n printViolations(result);\n } else if (result.violations.length === 0) {\n writeStdout(\n `Regression gate passed: ${result.checkedFileCount} changed files, ${result.checkedFunctionCount} functions checked (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n } else {\n writeStdout(\n `Regression gate vs ${cliOptions.base} (merge-base ${shortBase}): ${result.violations.length} violations\\n`\n );\n printViolations(result);\n }\n\n if (cliOptions.full) {\n printFullDetails(result);\n }\n\n for (const warning of warnings) {\n writeStderr(`Warning: ${warning}\\n`);\n }\n for (const error of errors) {\n writeStderr(`Error: ${error}\\n`);\n }\n}\n\nfunction printViolations(result: GateResult): void {\n for (const [index, violation] of result.violations.entries()) {\n writeStdout(`${index + 1}. ${violation.message}\\n`);\n }\n}\n\n/** Base -> head values of every checked function; kept behind --full for humans and trending. */\nfunction printFullDetails(result: GateResult): void {\n if (result.checkedFunctions.length === 0) {\n return;\n }\n writeStdout('\\nChecked functions (base -> head):\\n');\n for (const report of result.checkedFunctions) {\n writeStdout(`- ${formatFunctionReport(report)}\\n`);\n }\n}\n\nfunction formatFunctionReport(report: CheckedFunctionReport): string {\n const range = (\n select: (values: GateFunctionValues) => number,\n format: (value: number) => string = String\n ): string => {\n const head = format(select(report.head));\n return report.base ? `${format(select(report.base))} -> ${head}` : head;\n };\n const values = [\n `cognitive ${range((fn) => fn.cognitiveComplexity)}`,\n `NCSS ${range((fn) => fn.ncss)}`,\n `nesting ${range((fn) => fn.nestingDepth)}`,\n `DepDegree ${range((fn) => fn.depDegree)}`,\n `volume ${range(\n (fn) => fn.halsteadVolume,\n (value) => value.toFixed(1)\n )}`,\n ];\n return `${report.file}:${report.startLine}-${report.endLine} ${report.name}${report.base ? '' : ' (new)'}: ${values.join(', ')}`;\n}\n\nfunction printJsonReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n inputs: GateFileInput[],\n errors: string[],\n warnings: string[]\n): void {\n const report: Record<string, unknown> = {\n base: cliOptions.base,\n mergeBase,\n passed: result.violations.length === 0 && errors.length === 0,\n violations: result.violations,\n checkedFileCount: result.checkedFileCount,\n checkedFunctionCount: result.checkedFunctionCount,\n newFunctionCount: result.newFunctionCount,\n errors,\n warnings,\n };\n if (cliOptions.full) {\n report.files = inputs\n .filter((input) => input.gated !== false)\n .map((input) => ({\n file: input.file,\n baseFunctionCount: input.baseMetrics?.functions.length ?? 0,\n headFunctionCount: input.headMetrics?.functions.length ?? 0,\n baseNcss: input.baseMetrics?.ncssCount ?? 0,\n headNcss: input.headMetrics?.ncssCount ?? 0,\n baseMaxCognitiveComplexity: input.baseMetrics?.maxCognitiveComplexity ?? 0,\n headMaxCognitiveComplexity: input.headMetrics?.maxCognitiveComplexity ?? 0,\n newlyDuplicatedLines: input.newlyDuplicatedLines,\n duplicationPartners: input.duplicationPartners,\n functions: result.checkedFunctions.filter((fn) => fn.file === input.file),\n }));\n }\n writeStdout(JSON.stringify(report, undefined, 2) + '\\n');\n}\n"],"mappings":"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"}
|
package/dist/duplication.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";
|
|
1
|
+
"use strict";let e=require("@willbooster/shared-lib");const t={minTokens:40,maxGapTokens:30,minSimilarityPercent:70};function n(e){return{minTokens:r(e?.minTokens,t.minTokens),maxGapTokens:r(e?.maxGapTokens,t.maxGapTokens),minSimilarityPercent:r(e?.minSimilarityPercent,t.minSimilarityPercent)}}function r(t,n){return t===void 0||Number.isNaN(t)?n:(0,e.clamp)(Math.trunc(t),0,4294967295)}function i(e,t){return e*5>=t}function a(e){let t=new Int32Array(e.length+1);for(let[n,r]of e.entries())t[n+1]=(t[n]??0)+(r.literalHash===void 0?0:1);return t}function o(e,t,n){let r=[],i=[],a=[];for(let[t,n]of e.entries())for(let e of n.containers)i.push(t),a.push(e);let o=t=>e[i[t]??0],l=/* @__PURE__ */ new Map,u=a.map((e,n)=>c(o(n)?.tokens??[],e,t));for(let[e,t]of u.entries()){let n=i[e]??0;for(let[r,i]of t.windowKeysByStart.entries())for(let t of i){if(t===void 0)continue;let i=l.get(t);i?(i.count+=1,i.containerIndex!==e&&(i.containerIndex=-1),i.contextIndex!==n&&(i.contextIndex=-1),i.minStart=Math.min(i.minStart,r),i.maxStart=Math.max(i.maxStart,r)):l.set(t,{count:1,containerIndex:e,contextIndex:n,minStart:r,maxStart:r})}}let d=(e,t)=>{if(e===void 0)return!1;let r=l.get(e);return r===void 0||r.count<2?!1:n?r.contextIndex===-1:r.containerIndex===-1||r.maxStart-r.minStart>=t},p=e=>{let t=u[e.containerIndex]?.statementHashes??[],n=t[e.start];for(let r=e.start+1;r<e.start+e.length;r+=1)if(t[r]!==n)return!0;return!1},m=[];for(let[e,t]of u.entries())for(let[n,r]of t.windowKeysByStart.entries())for(let[i,a]of r.entries()){if(!d(a,i)||!p({containerIndex:e,start:n,length:i}))continue;let r=t.windowKeysByStart[n]?.[i+1],o=t.windowKeysByStart[n-1]?.[i+1];d(r,i+1)||d(o,i+1)||m.push({containerIndex:e,start:n,length:i})}let h=new Set(m.map(s)),g=m;for(;g.length>0;){let e=[];for(let t of g){let n=a[t.containerIndex],s=n?.[t.start],c=n?.[t.start+t.length-1],l=o(t.containerIndex);if(!s||!c||!l)continue;let u=`s:${_(l.tokens,l.literalCountPrefix,s.startTokenIndex,c.endTokenIndex)}`;r.push({candidate:f(u,s.startTokenIndex,c.endTokenIndex,s,c),contextIndex:i[t.containerIndex]??0}),e.push(t)}g=[];for(let t of e)for(let e of[t.start,t.start+1]){let n={containerIndex:t.containerIndex,start:e,length:t.length-1},r=u[t.containerIndex]?.windowKeysByStart[e]?.[n.length];!h.has(s(n))&&d(r,n.length)&&p(n)&&(h.add(s(n)),g.push(n))}}return r}function s(e){return`${e.containerIndex}:${e.start}:${e.length}`}function c(e,t,n){let r=t.map(t=>v(e,t.startTokenIndex,t.endTokenIndex)),i=[];for(let e=0;e<t.length;e+=1){let a=[],o=5381,s=0,c=Math.min(t.length,e+100);for(let i=e;i<c;i+=1){let c=t[i],l=r[i];if(!c||l===void 0)break;o=S(o,l),s+=c.endTokenIndex-c.startTokenIndex;let u=i-e+1;a[u]=u>=2&&s>=n?S(o,u):void 0}i.push(a)}return{windowKeysByStart:i,statementHashes:r}}function l(e,t){return u(e)(t)}function u(e){let t=e.length+31>>>5,n=/* @__PURE__ */ new Map;for(let[r,i]of e.entries()){let e=n.get(i);e||(e=new Uint32Array(t),n.set(i,e));let a=r>>>5;e[a]=(e[a]??0)|1<<(r&31)}let r=new Uint32Array(t);return e=>{r.fill(0);for(let i of e){let e=n.get(i),a=1,o=0;for(let n=0;n<t;n+=1){let t=r[n]??0,i=((e?.[n]??0)|t)>>>0,s=(t<<1|a)>>>0;a=t>>>31;let c=i-s-o;o=+(c<0),r[n]=i&~c}}let i=0;for(let e of r)i+=d(e);return i}}function d(e){let t=e-(e>>>1&1431655765);return t=(t&858993459)+(t>>>2&858993459),Math.imul(t+(t>>>4)&252645135,16843009)>>>24&255}function f(e,t,n,r,i){return{fingerprint:e,tokenCount:n-t,startTokenIndex:t,endTokenIndex:n,startIndex:r.startIndex,endIndex:i.endIndex,startLine:r.startLine,endLine:i.endLine}}const p=[],m=[];function h(e){let t=p[e];return t===void 0&&(t=b(`$${e}`),p[e]=t),t}function g(e){let t=m[e];return t===void 0&&(t=x(`$${e}`),m[e]=t),t}function _(e,t,n,r){let[a,o]=y(e,n,r,i((t[r]??0)-(t[n]??0),r-n));return`${a}:${o}:${r-n}`}function v(e,t,n){let[r,i]=y(e,t,n,!1);return r^Math.imul(i,31)}function y(e,t,n,r){let i=/* @__PURE__ */ new Map,a=5381,o=52711;for(let s=t;s<n;s+=1){let t=e[s];if(!t)continue;let n,c;if(t.kind===`id`){let e=i.get(t.text);e===void 0&&(e=i.size,i.set(t.text,e)),n=h(e),c=g(e)}else n=t.textHash,c=t.textHash2;a=Math.imul(a,31)+n|0,o=Math.imul(o,37)^c,r&&t.literalHash!==void 0&&t.literalHash2!==void 0&&(a=Math.imul(a,31)+t.literalHash|0,o=Math.imul(o,37)^t.literalHash2)}return[a,o]}function b(e){let t=5381;for(let n=0;n<e.length;n+=1)t=Math.imul(t,33)^e.charCodeAt(n);return t}function x(e){let t=-2128831035;for(let n=0;n<e.length;n+=1)t=Math.imul(t^e.charCodeAt(n),16777619);return t}function S(e,t){return Math.imul(e,31)+t}function C(e,t,n=()=>!0){if(t<=0||e.length<2)return e;e.sort(E);for(let r=!0;r;){r=!1;let i=w(e,t);for(let a=0;a<e.length&&!r;a+=1)for(let o of i[a]??[]){let i=e[a],s=e[o];if(!i||!s)continue;let c=D(i,s,t,n),l=c??D(s,i,t,n);if(!l)continue;let u=c?l.firstReplaced:l.secondReplaced,d=c?l.secondReplaced:l.firstReplaced;u&&d?(e[a]=l.merged,e.splice(o,1)):d?e[o]=l.merged:u?e[a]=l.merged:e.push(l.merged);for(let e of l.pairedRetained)e.spanCountedElsewhere=!0;e.sort(E),r=!0;break}}return e}function w(e,t){let n=[];for(let[t,r]of e.entries())for(let e of r)n.push({startTokenIndex:e.startTokenIndex,groupIndex:t});n.sort((e,t)=>e.startTokenIndex-t.startTokenIndex);let r=e.map(()=>/* @__PURE__ */ new Set);for(let[i,a]of e.entries())for(let e of a)for(let a=T(n,e.endTokenIndex);a<n.length&&(n[a]?.startTokenIndex??0)<=e.endTokenIndex+t;a+=1){let e=n[a]?.groupIndex??i;e!==i&&r[Math.min(e,i)]?.add(Math.max(e,i))}return r.map(e=>[...e].toSorted((e,t)=>e-t))}function T(e,t){let n=0,r=e.length;for(;n<r;){let i=n+r>>>1;(e[i]?.startTokenIndex??0)<t?n=i+1:r=i}return n}function E(e,t){let n=e[0],r=t[0];return(n?.startTokenIndex??0)-(r?.startTokenIndex??0)||(n?.endTokenIndex??0)-(r?.endTokenIndex??0)}function D(e,t,n,r){let[i,a]=O(e),[o,s]=O(t),c=[],l=0,u=-1;for(let e of o){for(;l<i.length;){let t=i[l];if(t&&t.endTokenIndex+n<e.startTokenIndex)l+=1;else break}let t=i[l];t&&t.endTokenIndex<=e.startTokenIndex&&t.startTokenIndex>=u&&(c.push([t,e]),u=e.endTokenIndex,l+=1)}let d=c.length===a,f=c.length===s;if(c.length<2||!d&&!f)return;let p=d&&!e.some(e=>e.nestedInLargerGroup),m=f&&!t.some(e=>e.nestedInLargerGroup),h=c.map(([e,t])=>({...e,spanCountedElsewhere:void 0,nestedInLargerGroup:void 0,segments:[...e.segments,...t.segments],tokenCount:e.tokenCount+t.tokenCount,endTokenIndex:t.endTokenIndex,endIndex:t.endIndex,endLine:t.endLine}));if(r(h))return{merged:h,firstReplaced:p,secondReplaced:m,pairedRetained:[...p?[]:c.map(([e])=>e),...m?[]:c.map(([,e])=>e)]}}function O(e){let t=[],n=0;for(let r of e)r.nestedInLargerGroup||(n+=1),!r.spanCountedElsewhere&&!r.nestedInLargerGroup&&t.push(r);return[t,n]}function k(e){let t=0,n=0,r=!1;for(let i of e){if(i.spanCountedElsewhere){r=!0;continue}t+=i.segments.length,n=Math.max(n,i.segments.length)}return r?t:t-n}function A(e,t,n,r){for(let i=e.startTokenIndex;i<e.endTokenIndex;i+=1){let e=t[i];for(let t=e?.startRow??0;t<=(e?.endRow??-1);t+=1)(!n||n.has(t+1))&&r.add(t+1)}}exports.buildLiteralCountPrefix=a,exports.collectSegmentLines=A,exports.collectSequenceWindowCandidates=o,exports.countRedundantFragments=k,exports.createLcsLengthCounter=u,exports.defaultDuplicationOptions=t,exports.lcsLength=l,exports.mergeAdjacentGroups=C,exports.resolveDuplicationOptions=n;
|
|
2
2
|
//# sourceMappingURL=duplication.cjs.map
|