code-gauge 5.1.0 → 6.0.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 +204 -72
- package/dist/checkCommand.cjs +5 -0
- package/dist/checkCommand.cjs.map +1 -0
- package/dist/checkCommand.d.ts +12 -0
- package/dist/checkCommand.js +5 -0
- package/dist/checkCommand.js.map +1 -0
- 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 +3 -11
- package/dist/cliConfig.js +1 -1
- package/dist/cliConfig.js.map +1 -1
- package/dist/exclusion.cjs +1 -1
- package/dist/exclusion.cjs.map +1 -1
- package/dist/exclusion.d.ts +2 -2
- package/dist/exclusion.js +1 -1
- package/dist/exclusion.js.map +1 -1
- package/dist/git.cjs +1 -1
- package/dist/git.cjs.map +1 -1
- package/dist/git.d.ts +13 -12
- package/dist/git.js +1 -1
- package/dist/git.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.js +1 -1
- package/dist/metrics.cjs +1 -1
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.ts +1 -2
- package/dist/metrics.js +1 -1
- package/dist/metrics.js.map +1 -1
- package/dist/scan.cjs +1 -1
- package/dist/scan.cjs.map +1 -1
- package/dist/scan.d.ts +10 -25
- package/dist/scan.js +1 -1
- package/dist/scan.js.map +1 -1
- package/dist/thresholdCheck.cjs +2 -0
- package/dist/thresholdCheck.cjs.map +1 -0
- package/dist/thresholdCheck.d.ts +36 -0
- package/dist/thresholdCheck.js +2 -0
- package/dist/thresholdCheck.js.map +1 -0
- package/dist/thresholds.cjs +2 -0
- package/dist/thresholds.cjs.map +1 -0
- package/dist/thresholds.d.ts +38 -0
- package/dist/thresholds.js +2 -0
- package/dist/thresholds.js.map +1 -0
- package/dist/types.d.ts +8 -4
- 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/package.json +9 -9
- package/dist/diffCommand.cjs +0 -5
- package/dist/diffCommand.cjs.map +0 -1
- package/dist/diffCommand.d.ts +0 -17
- package/dist/diffCommand.js +0 -5
- package/dist/diffCommand.js.map +0 -1
- package/dist/newDuplication.cjs +0 -2
- package/dist/newDuplication.cjs.map +0 -1
- package/dist/newDuplication.d.ts +0 -40
- package/dist/newDuplication.js +0 -2
- package/dist/newDuplication.js.map +0 -1
- package/dist/regressionGate.cjs +0 -2
- package/dist/regressionGate.cjs.map +0 -1
- package/dist/regressionGate.d.ts +0 -108
- package/dist/regressionGate.js +0 -2
- package/dist/regressionGate.js.map +0 -1
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=[{key:`maxFunctionCognitiveComplexity`,label:`cognitive complexity`,defaultLimit:15,hint:`flatten nested branching with early returns and extract nested blocks into named functions.`,measure:e=>e.cognitiveComplexity},{key:`maxFunctionCyclomaticComplexity`,label:`cyclomatic complexity`,defaultLimit:10,hint:`the function has too many independent paths; split it by decision or replace condition chains with a lookup table.`,measure:e=>e.cyclomaticComplexity},{key:`maxFunctionNcss`,label:`NCSS`,defaultLimit:60,hint:`split the function into smaller functions that each do one step.`,measure:e=>e.ncss},{key:`maxFunctionNestingDepth`,label:`nesting depth`,defaultLimit:4,hint:`replace nested conditions with guard clauses or move inner blocks into functions.`,measure:e=>e.nestingDepth},{key:`maxFunctionParameterCount`,label:`parameters`,defaultLimit:7,hint:`group related parameters into one object or split the function.`,measure:e=>e.parameterCount},{key:`maxFunctionHalsteadVolume`,label:`Halstead volume`,defaultLimit:2e3,hint:`the function holds too many operators and operands; move cohesive parts into helper functions.`,measure:e=>e.halstead.volume},{key:`maxFunctionHalsteadDifficulty`,label:`Halstead difficulty`,defaultLimit:20,hint:`the function reuses the same operands many times; split it so each part handles fewer values.`,measure:e=>e.halstead.difficulty},{key:`maxFunctionHalsteadEffort`,label:`Halstead effort`,defaultLimit:3e4,hint:`the function is both large and dense; split it into smaller functions with fewer values each.`,measure:e=>e.halstead.effort},{key:`maxFunctionDepDegree`,label:`DepDegree`,defaultLimit:50,hint:`too many values flow between the variables of the function; split it so each part works on fewer variables.`,measure:e=>e.depDegree}],t=[{key:`maxFileNcss`,label:`file NCSS`,defaultLimit:500,hint:`split the file into smaller modules with one responsibility each.`,measure:e=>e.ncssCount}],n={key:`minDuplicateLines`,label:`duplicated lines`,defaultLimit:10,hint:`extract the repeated code into one shared function or module and call it from every location.`,measure:e=>e.endLine-e.startLine+1},r=[...e,...t,n];function i(e){let t=e.key.slice(3);return t.charAt(0).toLowerCase()+t.slice(1)}function a(e,t){let n={...Object.fromEntries(r.map(({key:e,defaultLimit:t})=>[e,t])),...t?.limits};return r=>({...n,...t&&Object.hasOwn(t.languages,r)?t.languages[r]:void 0,...e})}export{n as duplicationThreshold,t as fileThresholds,e as functionThresholds,i as metricNameOf,a as resolveLimits,r as thresholds};
|
|
2
|
+
//# sourceMappingURL=thresholds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thresholds.js","names":[],"sources":["../src/thresholds.ts"],"sourcesContent":["import type { CodeMetrics, FunctionMetrics } from './types.js';\n\n/**\n * One limit of `code-gauge check`. Its `key` is the config setting, the per-language override\n * setting, and (kebab-cased) the command-line option, so a new entry here is all a new thresholded\n * metric needs.\n */\nexport interface Threshold<Subject = never> {\n key: string;\n /** How the report names the measured value. */\n label: string;\n defaultLimit: number;\n /** What to change in the code when the limit is exceeded. */\n hint: string;\n measure: (subject: Subject) => number;\n}\n\n/** Limits by threshold key; `Infinity` disables a limit. */\nexport type Limits = Readonly<Record<string, number>>;\n\nexport const functionThresholds: readonly Threshold<FunctionMetrics>[] = [\n {\n key: 'maxFunctionCognitiveComplexity',\n label: 'cognitive complexity',\n defaultLimit: 15,\n hint: 'flatten nested branching with early returns and extract nested blocks into named functions.',\n measure: (fn) => fn.cognitiveComplexity,\n },\n {\n key: 'maxFunctionCyclomaticComplexity',\n label: 'cyclomatic complexity',\n defaultLimit: 10,\n hint: 'the function has too many independent paths; split it by decision or replace condition chains with a lookup table.',\n measure: (fn) => fn.cyclomaticComplexity,\n },\n {\n key: 'maxFunctionNcss',\n label: 'NCSS',\n defaultLimit: 60,\n hint: 'split the function into smaller functions that each do one step.',\n measure: (fn) => fn.ncss,\n },\n {\n key: 'maxFunctionNestingDepth',\n label: 'nesting depth',\n defaultLimit: 4,\n hint: 'replace nested conditions with guard clauses or move inner blocks into functions.',\n measure: (fn) => fn.nestingDepth,\n },\n {\n key: 'maxFunctionParameterCount',\n label: 'parameters',\n defaultLimit: 7,\n hint: 'group related parameters into one object or split the function.',\n measure: (fn) => fn.parameterCount,\n },\n {\n key: 'maxFunctionHalsteadVolume',\n label: 'Halstead volume',\n defaultLimit: 2000,\n hint: 'the function holds too many operators and operands; move cohesive parts into helper functions.',\n measure: (fn) => fn.halstead.volume,\n },\n {\n key: 'maxFunctionHalsteadDifficulty',\n label: 'Halstead difficulty',\n defaultLimit: 20,\n hint: 'the function reuses the same operands many times; split it so each part handles fewer values.',\n measure: (fn) => fn.halstead.difficulty,\n },\n {\n key: 'maxFunctionHalsteadEffort',\n label: 'Halstead effort',\n defaultLimit: 30_000,\n hint: 'the function is both large and dense; split it into smaller functions with fewer values each.',\n measure: (fn) => fn.halstead.effort,\n },\n {\n key: 'maxFunctionDepDegree',\n label: 'DepDegree',\n defaultLimit: 50,\n hint: 'too many values flow between the variables of the function; split it so each part works on fewer variables.',\n measure: (fn) => fn.depDegree,\n },\n];\n\nexport const fileThresholds: readonly Threshold<CodeMetrics>[] = [\n {\n key: 'maxFileNcss',\n label: 'file NCSS',\n defaultLimit: 500,\n hint: 'split the file into smaller modules with one responsibility each.',\n measure: (file) => file.ncssCount,\n },\n];\n\n/** A duplicated block violates when it spans at least this many lines. */\nexport const duplicationThreshold: Threshold<{ startLine: number; endLine: number }> = {\n key: 'minDuplicateLines',\n label: 'duplicated lines',\n defaultLimit: 10,\n hint: 'extract the repeated code into one shared function or module and call it from every location.',\n measure: (block) => block.endLine - block.startLine + 1,\n};\n\nexport const thresholds: readonly Threshold[] = [...functionThresholds, ...fileThresholds, duplicationThreshold];\n\n/** The name a report gives a threshold's measured value: its key without the `max`/`min` prefix. */\nexport function metricNameOf(threshold: Threshold): string {\n const name = threshold.key.slice(3);\n return name.charAt(0).toLowerCase() + name.slice(1);\n}\n\n/** The `thresholds` config section; a missing key falls back to the less specific layer. */\nexport interface ThresholdsConfig {\n limits: Limits;\n /** Overrides for the files of one language, keyed by language name. */\n languages: Readonly<Record<string, Limits>>;\n}\n\n/**\n * The limits applying to files of each language, with precedence command-line options > the\n * config's per-language overrides > the config's limits > built-in defaults.\n */\nexport function resolveLimits(cliLimits: Limits, config: ThresholdsConfig | undefined): (language: string) => Limits {\n const defaults = Object.fromEntries(thresholds.map(({ key, defaultLimit }) => [key, defaultLimit]));\n const shared = { ...defaults, ...config?.limits };\n return (language) => ({\n ...shared,\n // Object.hasOwn: a language named like an Object.prototype member must not read an inherited value.\n ...(config && Object.hasOwn(config.languages, language) ? config.languages[language] : undefined),\n ...cliLimits,\n });\n}\n"],"mappings":"AAoBA,MAAa,EAA4D,CACvE,CACE,IAAK,iCACL,MAAO,uBACP,aAAc,GACd,KAAM,8FACN,QAAU,GAAO,EAAG,mBACtB,EACA,CACE,IAAK,kCACL,MAAO,wBACP,aAAc,GACd,KAAM,qHACN,QAAU,GAAO,EAAG,oBACtB,EACA,CACE,IAAK,kBACL,MAAO,OACP,aAAc,GACd,KAAM,mEACN,QAAU,GAAO,EAAG,IACtB,EACA,CACE,IAAK,0BACL,MAAO,gBACP,aAAc,EACd,KAAM,oFACN,QAAU,GAAO,EAAG,YACtB,EACA,CACE,IAAK,4BACL,MAAO,aACP,aAAc,EACd,KAAM,kEACN,QAAU,GAAO,EAAG,cACtB,EACA,CACE,IAAK,4BACL,MAAO,kBACP,aAAc,IACd,KAAM,iGACN,QAAU,GAAO,EAAG,SAAS,MAC/B,EACA,CACE,IAAK,gCACL,MAAO,sBACP,aAAc,GACd,KAAM,gGACN,QAAU,GAAO,EAAG,SAAS,UAC/B,EACA,CACE,IAAK,4BACL,MAAO,kBACP,aAAc,IACd,KAAM,gGACN,QAAU,GAAO,EAAG,SAAS,MAC/B,EACA,CACE,IAAK,uBACL,MAAO,YACP,aAAc,GACd,KAAM,8GACN,QAAU,GAAO,EAAG,SACtB,CACF,EAEa,EAAoD,CAC/D,CACE,IAAK,cACL,MAAO,YACP,aAAc,IACd,KAAM,oEACN,QAAU,GAAS,EAAK,SAC1B,CACF,EAGa,EAA0E,CACrF,IAAK,oBACL,MAAO,mBACP,aAAc,GACd,KAAM,gGACN,QAAU,GAAU,EAAM,QAAU,EAAM,UAAY,CACxD,EAEa,EAAmC,CAAC,GAAG,EAAoB,GAAG,EAAgB,CAAoB,EAG/G,SAAgB,EAAa,EAA8B,CACzD,IAAM,EAAO,EAAU,IAAI,MAAM,CAAC,EAClC,OAAO,EAAK,OAAO,CAAC,CAAC,CAAC,YAAY,EAAI,EAAK,MAAM,CAAC,CACpD,CAaA,SAAgB,EAAc,EAAmB,EAAoE,CAEnH,IAAM,EAAS,CAAE,GADA,OAAO,YAAY,EAAW,KAAK,CAAE,MAAK,kBAAmB,CAAC,EAAK,CAAY,CAAC,CACtE,EAAG,GAAG,GAAQ,MAAO,EAChD,MAAQ,KAAc,CACpB,GAAG,EAEH,GAAI,GAAU,OAAO,OAAO,EAAO,UAAW,CAAQ,EAAI,EAAO,UAAU,GAAY,IAAA,GACvF,GAAG,CACL,EACF"}
|
package/dist/types.d.ts
CHANGED
|
@@ -49,6 +49,11 @@ export interface HalsteadMetrics {
|
|
|
49
49
|
vocabulary: number;
|
|
50
50
|
length: number;
|
|
51
51
|
volume: number;
|
|
52
|
+
/**
|
|
53
|
+
* (distinctOperators / 2) * (totalOperands / distinctOperands), i.e. effort / volume; 0 when
|
|
54
|
+
* there are no operands.
|
|
55
|
+
*/
|
|
56
|
+
difficulty: number;
|
|
52
57
|
effort: number;
|
|
53
58
|
}
|
|
54
59
|
export interface FunctionMetrics {
|
|
@@ -72,7 +77,7 @@ export interface FunctionMetrics {
|
|
|
72
77
|
* short-circuit operator (`&&`, `||`, `and`, `or`) wherever it appears, and per case-labelled
|
|
73
78
|
* statement or match arm (stacked labels share one; default and catch-all arms add none).
|
|
74
79
|
* Full-evaluation operators and `throw` add nothing. Nested function bodies are excluded, so
|
|
75
|
-
* summing over functions counts each decision once.
|
|
80
|
+
* summing over functions counts each decision once.
|
|
76
81
|
*/
|
|
77
82
|
cyclomaticComplexity: number;
|
|
78
83
|
cognitiveComplexity: number;
|
|
@@ -92,9 +97,8 @@ export interface FunctionMetrics {
|
|
|
92
97
|
/**
|
|
93
98
|
* Approximate def-use dependency degree (DepDegree, Beyer & Fararooy 2010): the number of
|
|
94
99
|
* variable reads whose name has a preceding definition (declaration, assignment, or parameter)
|
|
95
|
-
* within the same function. A file-local single-assignment approximation
|
|
96
|
-
* one reaching definition
|
|
97
|
-
* delta matters.
|
|
100
|
+
* within the same function. A file-local single-assignment approximation: each read is charged
|
|
101
|
+
* one reaching definition.
|
|
98
102
|
*/
|
|
99
103
|
depDegree: number;
|
|
100
104
|
}
|
package/dist/worker.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./crossFileDuplication.cjs"),n=require("./languages.cjs"),r=require("./nativeMetrics.cjs"),i=require("./metrics.cjs")
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./crossFileDuplication.cjs"),n=require("./languages.cjs"),r=require("./nativeMetrics.cjs"),i=require("./metrics.cjs");require("./index.cjs");const a=require("./wasmBinding.cjs");let o=require("../native/code-gauge.wasm");o=e.__toESM(o,1),r.setNativeBinding(a.createWasmBinding(o.default)),exports.TreeMeasurer=i.TreeMeasurer,exports.collectCrossFileDuplicationFileData=i.collectCrossFileDuplicationFileData,exports.collectDuplicationCandidates=i.collectDuplicationCandidates,exports.collectFunctionTokenSequences=i.collectFunctionTokenSequences,exports.defaultLanguages=n.defaultLanguages,exports.defaultMeasurer=i.defaultMeasurer,exports.detectLanguage=n.detectLanguage,exports.measureCode=i.measureCode,exports.measureCrossFileDuplication=t.measureCrossFileDuplication,exports.supportedLanguages=n.supportedLanguages;
|
|
2
2
|
//# sourceMappingURL=worker.cjs.map
|
package/dist/worker.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.cjs","names":["setNativeBinding","createWasmBinding","wasmModule"],"sources":["../src/worker.ts"],"sourcesContent":["// The entry point for Cloudflare Workers (the `workerd` export condition), which cannot load N-API\n// addons: the same API runs on the WebAssembly build of the native addon.\nimport wasmModule from '../native/code-gauge.wasm';\nimport { setNativeBinding } from './nativeMetrics.js';\nimport { createWasmBinding } from './wasmBinding.js';\n\nsetNativeBinding(createWasmBinding(wasmModule));\n\nexport * from './index.js';\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"worker.cjs","names":["setNativeBinding","createWasmBinding","wasmModule"],"sources":["../src/worker.ts"],"sourcesContent":["// The entry point for Cloudflare Workers (the `workerd` export condition), which cannot load N-API\n// addons: the same API runs on the WebAssembly build of the native addon.\nimport wasmModule from '../native/code-gauge.wasm';\nimport { setNativeBinding } from './nativeMetrics.js';\nimport { createWasmBinding } from './wasmBinding.js';\n\nsetNativeBinding(createWasmBinding(wasmModule));\n\nexport * from './index.js';\n"],"mappings":"6XAMAA,EAAAA,iBAAiBC,EAAAA,kBAAkBC,EAAAA,OAAU,CAAC"}
|
package/dist/worker.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{defaultLanguages as t,detectLanguage as n,supportedLanguages as r}from"./languages.js";import{setNativeBinding as i}from"./nativeMetrics.js";import{TreeMeasurer as a,collectCrossFileDuplicationFileData as o,collectDuplicationCandidates as s,collectFunctionTokenSequences as c,defaultMeasurer as l,measureCode as u}from"./metrics.js";import
|
|
1
|
+
import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{defaultLanguages as t,detectLanguage as n,supportedLanguages as r}from"./languages.js";import{setNativeBinding as i}from"./nativeMetrics.js";import{TreeMeasurer as a,collectCrossFileDuplicationFileData as o,collectDuplicationCandidates as s,collectFunctionTokenSequences as c,defaultMeasurer as l,measureCode as u}from"./metrics.js";import"./index.js";import{createWasmBinding as d}from"./wasmBinding.js";import f from"../native/code-gauge.wasm";i(d(f));export{a as TreeMeasurer,o as collectCrossFileDuplicationFileData,s as collectDuplicationCandidates,c as collectFunctionTokenSequences,t as defaultLanguages,l as defaultMeasurer,n as detectLanguage,u as measureCode,e as measureCrossFileDuplication,r as supportedLanguages};
|
|
2
2
|
//# sourceMappingURL=worker.js.map
|
package/dist/worker.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.js","names":[],"sources":["../src/worker.ts"],"sourcesContent":["// The entry point for Cloudflare Workers (the `workerd` export condition), which cannot load N-API\n// addons: the same API runs on the WebAssembly build of the native addon.\nimport wasmModule from '../native/code-gauge.wasm';\nimport { setNativeBinding } from './nativeMetrics.js';\nimport { createWasmBinding } from './wasmBinding.js';\n\nsetNativeBinding(createWasmBinding(wasmModule));\n\nexport * from './index.js';\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"worker.js","names":[],"sources":["../src/worker.ts"],"sourcesContent":["// The entry point for Cloudflare Workers (the `workerd` export condition), which cannot load N-API\n// addons: the same API runs on the WebAssembly build of the native addon.\nimport wasmModule from '../native/code-gauge.wasm';\nimport { setNativeBinding } from './nativeMetrics.js';\nimport { createWasmBinding } from './wasmBinding.js';\n\nsetNativeBinding(createWasmBinding(wasmModule));\n\nexport * from './index.js';\n"],"mappings":"6gBAMA,EAAiB,EAAkB,CAAU,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-gauge",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Measure code metrics with tree-sitter.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"lint-fix": "bun wb lint --fix",
|
|
57
57
|
"prepare": "(bun lefthook install || true) && (test ! -f .lefthook/normalize-bun-lockfile.sh || bash .lefthook/normalize-bun-lockfile.sh)",
|
|
58
58
|
"test": "bun wb test",
|
|
59
|
-
"test/ci": "bun
|
|
59
|
+
"test/ci": "bun wb test-on-ci",
|
|
60
60
|
"typecheck": "bun wb typecheck",
|
|
61
61
|
"verify": "bun wb verify",
|
|
62
62
|
"verify-full": "bun wb verify --full"
|
|
@@ -95,12 +95,12 @@
|
|
|
95
95
|
"registry": "https://registry.npmjs.org/"
|
|
96
96
|
},
|
|
97
97
|
"optionalDependencies": {
|
|
98
|
-
"code-gauge-linux-x64-gnu": "
|
|
99
|
-
"code-gauge-linux-arm64-gnu": "
|
|
100
|
-
"code-gauge-linux-x64-musl": "
|
|
101
|
-
"code-gauge-linux-arm64-musl": "
|
|
102
|
-
"code-gauge-darwin-x64": "
|
|
103
|
-
"code-gauge-darwin-arm64": "
|
|
104
|
-
"code-gauge-win32-x64-msvc": "
|
|
98
|
+
"code-gauge-linux-x64-gnu": "6.0.0",
|
|
99
|
+
"code-gauge-linux-arm64-gnu": "6.0.0",
|
|
100
|
+
"code-gauge-linux-x64-musl": "6.0.0",
|
|
101
|
+
"code-gauge-linux-arm64-musl": "6.0.0",
|
|
102
|
+
"code-gauge-darwin-x64": "6.0.0",
|
|
103
|
+
"code-gauge-darwin-arm64": "6.0.0",
|
|
104
|
+
"code-gauge-win32-x64-msvc": "6.0.0"
|
|
105
105
|
}
|
|
106
106
|
}
|
package/dist/diffCommand.cjs
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
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
|
-
Checked functions (base -> head):
|
|
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
|
-
//# sourceMappingURL=diffCommand.cjs.map
|
package/dist/diffCommand.cjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/** Raw options of the `diff` subcommand; every field but base is undefined unless the flag was passed. */
|
|
2
|
-
export interface DiffCliOptions {
|
|
3
|
-
base: string;
|
|
4
|
-
config?: string;
|
|
5
|
-
duplicationMinTokens?: number;
|
|
6
|
-
duplicationMaxGapTokens?: number;
|
|
7
|
-
duplicationMinSimilarityPercent?: number;
|
|
8
|
-
includeTests?: boolean;
|
|
9
|
-
json?: boolean;
|
|
10
|
-
full?: boolean;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Runs the regression gate: measures the files changed relative to the merge-base with the base
|
|
14
|
-
* ref, at both revisions (`git cat-file`; no checkout, no persisted baseline), and reports only
|
|
15
|
-
* violations. Exit codes: 0 all gates passed, 1 violations, 2 changed files could not be measured.
|
|
16
|
-
*/
|
|
17
|
-
export declare function runDiffCommand(target: string, cliOptions: DiffCliOptions): Promise<void>;
|
package/dist/diffCommand.js
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
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
|
-
Checked functions (base -> head):
|
|
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
|
-
//# sourceMappingURL=diffCommand.js.map
|
package/dist/diffCommand.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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/newDuplication.cjs
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
"use strict";function e(e){let i=e.map(e=>{let i={base:n(e.baseOccurrences),head:n(e.headOccurrences)},a=r(e,i.base);return e.hunks.map(n=>t(e,n,a,i))}),o=/* @__PURE__ */ new Map;for(let{deletedContents:e}of i.flat())for(let t of e)o.set(t,(o.get(t)??0)+1);return e.map((e,t)=>(i[t]??[]).flatMap(({addedLines:e})=>e).filter(t=>{let n=a(e.headLines?.[t-1]),r=o.get(n)??0;return r===0||(o.set(n,r-1),!1)}).toSorted((e,t)=>e-t))}function t(e,t,n,r){let i=[];for(let n=t.baseStart;n<t.baseStart+t.baseCount;n++)e.baseDuplicatedLines.has(n)&&i.push({content:a(e.baseLines?.[n-1]),partners:r.base.get(n)??/* @__PURE__ */ new Set});let o=[];for(let r=t.headStart;r<t.headStart+t.headCount;r++){if(!e.headDuplicatedLines.has(r)||!n.has(r))continue;let t=a(e.headLines?.[r-1]),s=i.findIndex(e=>e.content===t);s===-1?o.push(r):i.splice(s,1)}return{addedLines:o.filter(e=>{let t=r.head.get(e),n=i.findIndex(e=>[...e.partners].some(e=>t?.has(e)===!0));return n===-1||(i.splice(n,1),!1)}),deletedContents:i.map(e=>e.content)}}function n(e){let t=/* @__PURE__ */ new Map;for(let{startLine:n,endLine:r,partners:i}of e)for(let e=n;e<=r;e++){let n=t.get(e)??/* @__PURE__ */ new Set;for(let e of i)n.add(e);t.set(e,n)}return t}function r(e,t){let n=i(e),r=/* @__PURE__ */ new Set;for(let{startLine:i,endLine:a,partners:o}of e.headOccurrences){let s=r=>{let i=n[r]??0;if(i===0||!e.headDuplicatedLines.has(r)||!e.baseDuplicatedLines.has(i))return!1;let a=t.get(i);return o.some(e=>a?.has(e)===!0)},c=1/0,l=-1/0;for(let e=i;e<=a;e++)s(e)&&(c=Math.min(c,e),l=e);for(let e=i;e<=a;e++)(e<c||e>l)&&r.add(e)}return r}function i(e){let t=e.headLines?.length??0,n=new Int32Array(t+1),r=e.hunks.toSorted((e,t)=>e.headStart-t.headStart),i=0,a=0;for(let e=1;e<=t;e++){let t=r[i];for(;t&&e>=t.headStart+Math.max(t.headCount,1);)a+=t.baseCount-t.headCount,i+=1,t=r[i];let o=t!==void 0&&t.headCount>0&&e>=t.headStart;n[e]=o?0:e+a}return n}function a(e){return e?.trim()??``}exports.findNewlyDuplicatedLines=e,exports.indexPartnersByLine=n;
|
|
2
|
-
//# sourceMappingURL=newDuplication.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"newDuplication.cjs","names":[],"sources":["../src/newDuplication.ts"],"sourcesContent":["/** One region of a line diff: base lines [baseStart, baseStart + baseCount) became head lines [headStart, headStart + headCount). */\nexport interface LineHunk {\n baseStart: number;\n baseCount: number;\n headStart: number;\n headCount: number;\n}\n\n/** A clone occurrence in a file and the files it duplicates, `''` standing for the file itself. */\nexport interface CloneOccurrence {\n startLine: number;\n endLine: number;\n partners: readonly string[];\n}\n\n/** One changed file's line diff and the duplicated lines of each revision (1-based). */\nexport interface DuplicationChange {\n /** Absent for added files. */\n baseLines?: readonly string[];\n /** Absent for deleted files. */\n headLines?: readonly string[];\n baseDuplicatedLines: ReadonlySet<number>;\n headDuplicatedLines: ReadonlySet<number>;\n /** Every clone occurrence in the file at each revision, within-file and cross-file. */\n baseOccurrences: readonly CloneOccurrence[];\n headOccurrences: readonly CloneOccurrence[];\n hunks: readonly LineHunk[];\n}\n\n/**\n * The head lines, per change, that the change newly made duplicated. Only added lines count, so a\n * copy is attributed to where it was pasted, never to the unchanged code it copies. An added line\n * is not new duplication when\n * - every clone occurrence covering it holds it between unchanged lines that were duplicated at\n * base with a file the occurrence duplicates (an edit inside pre-existing duplication, which\n * near-miss coverage reports even for tokens the copies do not share),\n * - it replaces a deleted duplicated line of the same hunk that was duplicated with a file it is\n * duplicated with too (a clone rewritten in place), or\n * - it reappears from a deleted duplicated line elsewhere in the change (the clone was moved).\n */\nexport function findNewlyDuplicatedLines(changes: readonly DuplicationChange[]): number[][] {\n // Same-hunk replacements settle first, identical content pairing up before any other line, so\n // only deleted lines no rewrite needed are left to back moves, whatever the order of changes.\n const unreplaced = changes.map((change) => {\n const partners = {\n base: indexPartnersByLine(change.baseOccurrences),\n head: indexPartnersByLine(change.headOccurrences),\n };\n const newCloneLines = collectNewCloneLines(change, partners.base);\n return change.hunks.map((hunk) => settleReplacements(change, hunk, newCloneLines, partners));\n });\n\n const movableByContent = new Map<string, number>();\n for (const { deletedContents } of unreplaced.flat()) {\n for (const content of deletedContents) {\n movableByContent.set(content, (movableByContent.get(content) ?? 0) + 1);\n }\n }\n\n return changes.map((change, changeIndex) =>\n (unreplaced[changeIndex] ?? [])\n .flatMap(({ addedLines }) => addedLines)\n .filter((line) => {\n const content = normalizeLine(change.headLines?.[line - 1]);\n const movable = movableByContent.get(content) ?? 0;\n if (movable === 0) {\n return true;\n }\n movableByContent.set(content, movable - 1);\n return false;\n })\n .toSorted((left, right) => left - right)\n );\n}\n\n/**\n * The hunk's added duplicated lines (of new clones) and deleted duplicated lines left once each\n * deleted line has credited one added line of the hunk: one with the same content first, then one\n * duplicating a common partner file, so replacing a clone with an unrelated one is not credited.\n */\nfunction settleReplacements(\n change: DuplicationChange,\n hunk: LineHunk,\n newCloneLines: ReadonlySet<number>,\n partners: { base: ReadonlyMap<number, ReadonlySet<string>>; head: ReadonlyMap<number, ReadonlySet<string>> }\n): { addedLines: number[]; deletedContents: string[] } {\n const deleted: { content: string; partners: ReadonlySet<string> }[] = [];\n for (let line = hunk.baseStart; line < hunk.baseStart + hunk.baseCount; line++) {\n if (change.baseDuplicatedLines.has(line)) {\n deleted.push({\n content: normalizeLine(change.baseLines?.[line - 1]),\n partners: partners.base.get(line) ?? new Set<string>(),\n });\n }\n }\n const unmatched: number[] = [];\n for (let line = hunk.headStart; line < hunk.headStart + hunk.headCount; line++) {\n if (!change.headDuplicatedLines.has(line) || !newCloneLines.has(line)) {\n continue;\n }\n const content = normalizeLine(change.headLines?.[line - 1]);\n const sameContentIndex = deleted.findIndex((entry) => entry.content === content);\n if (sameContentIndex === -1) {\n unmatched.push(line);\n } else {\n deleted.splice(sameContentIndex, 1);\n }\n }\n const addedLines = unmatched.filter((line) => {\n const linePartners = partners.head.get(line);\n const replacedIndex = deleted.findIndex((entry) =>\n [...entry.partners].some((partner) => linePartners?.has(partner) === true)\n );\n if (replacedIndex === -1) {\n return true;\n }\n deleted.splice(replacedIndex, 1);\n return false;\n });\n return { addedLines, deletedContents: deleted.map((entry) => entry.content) };\n}\n\n/** The partner files of the clone occurrences covering each line. */\nexport function indexPartnersByLine(occurrences: readonly CloneOccurrence[]): Map<number, Set<string>> {\n const partnersByLine = new Map<number, Set<string>>();\n for (const { startLine, endLine, partners } of occurrences) {\n for (let line = startLine; line <= endLine; line++) {\n const linePartners = partnersByLine.get(line) ?? new Set<string>();\n for (const partner of partners) {\n linePartners.add(partner);\n }\n partnersByLine.set(line, linePartners);\n }\n }\n return partnersByLine;\n}\n\n/**\n * Lines of clone occurrences that are not edits inside pre-existing duplication: all lines of an\n * occurrence with no unchanged line that was duplicated at base with one of its partner files, and\n * otherwise the lines outside the span between its first and last such line, so code pasted next\n * to an old clone, or a clone replaced by one of unrelated code, still counts.\n */\nfunction collectNewCloneLines(\n change: DuplicationChange,\n basePartnersByLine: ReadonlyMap<number, ReadonlySet<string>>\n): Set<number> {\n const baseLineOf = mapUnchangedLines(change);\n const lines = new Set<number>();\n for (const { startLine, endLine, partners } of change.headOccurrences) {\n const isPreexisting = (line: number): boolean => {\n const baseLine = baseLineOf[line] ?? 0;\n if (baseLine === 0 || !change.headDuplicatedLines.has(line) || !change.baseDuplicatedLines.has(baseLine)) {\n return false;\n }\n const basePartners = basePartnersByLine.get(baseLine);\n return partners.some((partner) => basePartners?.has(partner) === true);\n };\n let firstPreexisting = Infinity;\n let lastPreexisting = -Infinity;\n for (let line = startLine; line <= endLine; line++) {\n if (isPreexisting(line)) {\n firstPreexisting = Math.min(firstPreexisting, line);\n lastPreexisting = line;\n }\n }\n for (let line = startLine; line <= endLine; line++) {\n if (line < firstPreexisting || line > lastPreexisting) {\n lines.add(line);\n }\n }\n }\n return lines;\n}\n\n/** The base line of each unchanged head line (index = head line), 0 for added lines. */\nfunction mapUnchangedLines(change: DuplicationChange): Int32Array {\n const headLineCount = change.headLines?.length ?? 0;\n const baseLineOf = new Int32Array(headLineCount + 1);\n const hunks = change.hunks.toSorted((left, right) => left.headStart - right.headStart);\n let hunkIndex = 0;\n let shift = 0;\n for (let line = 1; line <= headLineCount; line++) {\n let hunk = hunks[hunkIndex];\n // A hunk without head lines (a pure deletion) sits after its headStart line.\n while (hunk && line >= hunk.headStart + Math.max(hunk.headCount, 1)) {\n shift += hunk.baseCount - hunk.headCount;\n hunkIndex += 1;\n hunk = hunks[hunkIndex];\n }\n const added = hunk !== undefined && hunk.headCount > 0 && line >= hunk.headStart;\n baseLineOf[line] = added ? 0 : line + shift;\n }\n return baseLineOf;\n}\n\n/** Moves re-indent code, so lines compare without surrounding whitespace. */\nfunction normalizeLine(line: string | undefined): string {\n return line?.trim() ?? '';\n}\n"],"mappings":"aAwCA,SAAgB,EAAyB,EAAmD,CAG1F,IAAM,EAAa,EAAQ,IAAK,GAAW,CACzC,IAAM,EAAW,CACf,KAAM,EAAoB,EAAO,eAAe,EAChD,KAAM,EAAoB,EAAO,eAAe,CAClD,EACM,EAAgB,EAAqB,EAAQ,EAAS,IAAI,EAChE,OAAO,EAAO,MAAM,IAAK,GAAS,EAAmB,EAAQ,EAAM,EAAe,CAAQ,CAAC,CAC7F,CAAC,EAEK,kBAAmB,IAAI,IAC7B,IAAK,GAAM,CAAE,qBAAqB,EAAW,KAAK,EAChD,IAAK,IAAM,KAAW,EACpB,EAAiB,IAAI,GAAU,EAAiB,IAAI,CAAO,GAAK,GAAK,CAAC,EAI1E,OAAO,EAAQ,KAAK,EAAQ,KACzB,EAAW,IAAgB,CAAC,EAAA,CAC1B,SAAS,CAAE,gBAAiB,CAAU,CAAC,CACvC,OAAQ,GAAS,CAChB,IAAM,EAAU,EAAc,EAAO,YAAY,EAAO,EAAE,EACpD,EAAU,EAAiB,IAAI,CAAO,GAAK,EAKjD,OAJI,IAAY,IAGhB,EAAiB,IAAI,EAAS,EAAU,CAAC,EAClC,GACT,CAAC,CAAC,CACD,UAAU,EAAM,IAAU,EAAO,CAAK,CAC3C,CACF,CAOA,SAAS,EACP,EACA,EACA,EACA,EACqD,CACrD,IAAM,EAAgE,CAAC,EACvE,IAAK,IAAI,EAAO,EAAK,UAAW,EAAO,EAAK,UAAY,EAAK,UAAW,IAClE,EAAO,oBAAoB,IAAI,CAAI,GACrC,EAAQ,KAAK,CACX,QAAS,EAAc,EAAO,YAAY,EAAO,EAAE,EACnD,SAAU,EAAS,KAAK,IAAI,CAAI,mBAAK,IAAI,GAC3C,CAAC,EAGL,IAAM,EAAsB,CAAC,EAC7B,IAAK,IAAI,EAAO,EAAK,UAAW,EAAO,EAAK,UAAY,EAAK,UAAW,IAAQ,CAC9E,GAAI,CAAC,EAAO,oBAAoB,IAAI,CAAI,GAAK,CAAC,EAAc,IAAI,CAAI,EAClE,SAEF,IAAM,EAAU,EAAc,EAAO,YAAY,EAAO,EAAE,EACpD,EAAmB,EAAQ,UAAW,GAAU,EAAM,UAAY,CAAO,EAC3E,IAAqB,GACvB,EAAU,KAAK,CAAI,EAEnB,EAAQ,OAAO,EAAkB,CAAC,CAEtC,CAYA,MAAO,CAAE,WAXU,EAAU,OAAQ,GAAS,CAC5C,IAAM,EAAe,EAAS,KAAK,IAAI,CAAI,EACrC,EAAgB,EAAQ,UAAW,GACvC,CAAC,GAAG,EAAM,QAAQ,CAAC,CAAC,KAAM,GAAY,GAAc,IAAI,CAAO,IAAM,EAAI,CAC3E,EAKA,OAJI,IAAkB,KAGtB,EAAQ,OAAO,EAAe,CAAC,EACxB,GACT,CACkB,EAAG,gBAAiB,EAAQ,IAAK,GAAU,EAAM,OAAO,CAAE,CAC9E,CAGA,SAAgB,EAAoB,EAAmE,CACrG,IAAM,kBAAiB,IAAI,IAC3B,IAAK,GAAM,CAAE,YAAW,UAAS,cAAc,EAC7C,IAAK,IAAI,EAAO,EAAW,GAAQ,EAAS,IAAQ,CAClD,IAAM,EAAe,EAAe,IAAI,CAAI,mBAAK,IAAI,IACrD,IAAK,IAAM,KAAW,EACpB,EAAa,IAAI,CAAO,EAE1B,EAAe,IAAI,EAAM,CAAY,CACvC,CAEF,OAAO,CACT,CAQA,SAAS,EACP,EACA,EACa,CACb,IAAM,EAAa,EAAkB,CAAM,EACrC,kBAAQ,IAAI,IAClB,IAAK,GAAM,CAAE,YAAW,UAAS,cAAc,EAAO,gBAAiB,CACrE,IAAM,EAAiB,GAA0B,CAC/C,IAAM,EAAW,EAAW,IAAS,EACrC,GAAI,IAAa,GAAK,CAAC,EAAO,oBAAoB,IAAI,CAAI,GAAK,CAAC,EAAO,oBAAoB,IAAI,CAAQ,EACrG,MAAO,GAET,IAAM,EAAe,EAAmB,IAAI,CAAQ,EACpD,OAAO,EAAS,KAAM,GAAY,GAAc,IAAI,CAAO,IAAM,EAAI,CACvE,EACI,EAAmB,IACnB,EAAkB,KACtB,IAAK,IAAI,EAAO,EAAW,GAAQ,EAAS,IACtC,EAAc,CAAI,IACpB,EAAmB,KAAK,IAAI,EAAkB,CAAI,EAClD,EAAkB,GAGtB,IAAK,IAAI,EAAO,EAAW,GAAQ,EAAS,KACtC,EAAO,GAAoB,EAAO,IACpC,EAAM,IAAI,CAAI,CAGpB,CACA,OAAO,CACT,CAGA,SAAS,EAAkB,EAAuC,CAChE,IAAM,EAAgB,EAAO,WAAW,QAAU,EAC5C,EAAa,IAAI,WAAW,EAAgB,CAAC,EAC7C,EAAQ,EAAO,MAAM,UAAU,EAAM,IAAU,EAAK,UAAY,EAAM,SAAS,EACjF,EAAY,EACZ,EAAQ,EACZ,IAAK,IAAI,EAAO,EAAG,GAAQ,EAAe,IAAQ,CAChD,IAAI,EAAO,EAAM,GAEjB,KAAO,GAAQ,GAAQ,EAAK,UAAY,KAAK,IAAI,EAAK,UAAW,CAAC,GAChE,GAAS,EAAK,UAAY,EAAK,UAC/B,GAAa,EACb,EAAO,EAAM,GAEf,IAAM,EAAQ,IAAS,IAAA,IAAa,EAAK,UAAY,GAAK,GAAQ,EAAK,UACvE,EAAW,GAAQ,EAAQ,EAAI,EAAO,CACxC,CACA,OAAO,CACT,CAGA,SAAS,EAAc,EAAkC,CACvD,OAAO,GAAM,KAAK,GAAK,EACzB"}
|
package/dist/newDuplication.d.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/** One region of a line diff: base lines [baseStart, baseStart + baseCount) became head lines [headStart, headStart + headCount). */
|
|
2
|
-
export interface LineHunk {
|
|
3
|
-
baseStart: number;
|
|
4
|
-
baseCount: number;
|
|
5
|
-
headStart: number;
|
|
6
|
-
headCount: number;
|
|
7
|
-
}
|
|
8
|
-
/** A clone occurrence in a file and the files it duplicates, `''` standing for the file itself. */
|
|
9
|
-
export interface CloneOccurrence {
|
|
10
|
-
startLine: number;
|
|
11
|
-
endLine: number;
|
|
12
|
-
partners: readonly string[];
|
|
13
|
-
}
|
|
14
|
-
/** One changed file's line diff and the duplicated lines of each revision (1-based). */
|
|
15
|
-
export interface DuplicationChange {
|
|
16
|
-
/** Absent for added files. */
|
|
17
|
-
baseLines?: readonly string[];
|
|
18
|
-
/** Absent for deleted files. */
|
|
19
|
-
headLines?: readonly string[];
|
|
20
|
-
baseDuplicatedLines: ReadonlySet<number>;
|
|
21
|
-
headDuplicatedLines: ReadonlySet<number>;
|
|
22
|
-
/** Every clone occurrence in the file at each revision, within-file and cross-file. */
|
|
23
|
-
baseOccurrences: readonly CloneOccurrence[];
|
|
24
|
-
headOccurrences: readonly CloneOccurrence[];
|
|
25
|
-
hunks: readonly LineHunk[];
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* The head lines, per change, that the change newly made duplicated. Only added lines count, so a
|
|
29
|
-
* copy is attributed to where it was pasted, never to the unchanged code it copies. An added line
|
|
30
|
-
* is not new duplication when
|
|
31
|
-
* - every clone occurrence covering it holds it between unchanged lines that were duplicated at
|
|
32
|
-
* base with a file the occurrence duplicates (an edit inside pre-existing duplication, which
|
|
33
|
-
* near-miss coverage reports even for tokens the copies do not share),
|
|
34
|
-
* - it replaces a deleted duplicated line of the same hunk that was duplicated with a file it is
|
|
35
|
-
* duplicated with too (a clone rewritten in place), or
|
|
36
|
-
* - it reappears from a deleted duplicated line elsewhere in the change (the clone was moved).
|
|
37
|
-
*/
|
|
38
|
-
export declare function findNewlyDuplicatedLines(changes: readonly DuplicationChange[]): number[][];
|
|
39
|
-
/** The partner files of the clone occurrences covering each line. */
|
|
40
|
-
export declare function indexPartnersByLine(occurrences: readonly CloneOccurrence[]): Map<number, Set<string>>;
|
package/dist/newDuplication.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
function e(e){let i=e.map(e=>{let i={base:n(e.baseOccurrences),head:n(e.headOccurrences)},a=r(e,i.base);return e.hunks.map(n=>t(e,n,a,i))}),o=/* @__PURE__ */ new Map;for(let{deletedContents:e}of i.flat())for(let t of e)o.set(t,(o.get(t)??0)+1);return e.map((e,t)=>(i[t]??[]).flatMap(({addedLines:e})=>e).filter(t=>{let n=a(e.headLines?.[t-1]),r=o.get(n)??0;return r===0||(o.set(n,r-1),!1)}).toSorted((e,t)=>e-t))}function t(e,t,n,r){let i=[];for(let n=t.baseStart;n<t.baseStart+t.baseCount;n++)e.baseDuplicatedLines.has(n)&&i.push({content:a(e.baseLines?.[n-1]),partners:r.base.get(n)??/* @__PURE__ */ new Set});let o=[];for(let r=t.headStart;r<t.headStart+t.headCount;r++){if(!e.headDuplicatedLines.has(r)||!n.has(r))continue;let t=a(e.headLines?.[r-1]),s=i.findIndex(e=>e.content===t);s===-1?o.push(r):i.splice(s,1)}return{addedLines:o.filter(e=>{let t=r.head.get(e),n=i.findIndex(e=>[...e.partners].some(e=>t?.has(e)===!0));return n===-1||(i.splice(n,1),!1)}),deletedContents:i.map(e=>e.content)}}function n(e){let t=/* @__PURE__ */ new Map;for(let{startLine:n,endLine:r,partners:i}of e)for(let e=n;e<=r;e++){let n=t.get(e)??/* @__PURE__ */ new Set;for(let e of i)n.add(e);t.set(e,n)}return t}function r(e,t){let n=i(e),r=/* @__PURE__ */ new Set;for(let{startLine:i,endLine:a,partners:o}of e.headOccurrences){let s=r=>{let i=n[r]??0;if(i===0||!e.headDuplicatedLines.has(r)||!e.baseDuplicatedLines.has(i))return!1;let a=t.get(i);return o.some(e=>a?.has(e)===!0)},c=1/0,l=-1/0;for(let e=i;e<=a;e++)s(e)&&(c=Math.min(c,e),l=e);for(let e=i;e<=a;e++)(e<c||e>l)&&r.add(e)}return r}function i(e){let t=e.headLines?.length??0,n=new Int32Array(t+1),r=e.hunks.toSorted((e,t)=>e.headStart-t.headStart),i=0,a=0;for(let e=1;e<=t;e++){let t=r[i];for(;t&&e>=t.headStart+Math.max(t.headCount,1);)a+=t.baseCount-t.headCount,i+=1,t=r[i];let o=t!==void 0&&t.headCount>0&&e>=t.headStart;n[e]=o?0:e+a}return n}function a(e){return e?.trim()??``}export{e as findNewlyDuplicatedLines,n as indexPartnersByLine};
|
|
2
|
-
//# sourceMappingURL=newDuplication.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"newDuplication.js","names":[],"sources":["../src/newDuplication.ts"],"sourcesContent":["/** One region of a line diff: base lines [baseStart, baseStart + baseCount) became head lines [headStart, headStart + headCount). */\nexport interface LineHunk {\n baseStart: number;\n baseCount: number;\n headStart: number;\n headCount: number;\n}\n\n/** A clone occurrence in a file and the files it duplicates, `''` standing for the file itself. */\nexport interface CloneOccurrence {\n startLine: number;\n endLine: number;\n partners: readonly string[];\n}\n\n/** One changed file's line diff and the duplicated lines of each revision (1-based). */\nexport interface DuplicationChange {\n /** Absent for added files. */\n baseLines?: readonly string[];\n /** Absent for deleted files. */\n headLines?: readonly string[];\n baseDuplicatedLines: ReadonlySet<number>;\n headDuplicatedLines: ReadonlySet<number>;\n /** Every clone occurrence in the file at each revision, within-file and cross-file. */\n baseOccurrences: readonly CloneOccurrence[];\n headOccurrences: readonly CloneOccurrence[];\n hunks: readonly LineHunk[];\n}\n\n/**\n * The head lines, per change, that the change newly made duplicated. Only added lines count, so a\n * copy is attributed to where it was pasted, never to the unchanged code it copies. An added line\n * is not new duplication when\n * - every clone occurrence covering it holds it between unchanged lines that were duplicated at\n * base with a file the occurrence duplicates (an edit inside pre-existing duplication, which\n * near-miss coverage reports even for tokens the copies do not share),\n * - it replaces a deleted duplicated line of the same hunk that was duplicated with a file it is\n * duplicated with too (a clone rewritten in place), or\n * - it reappears from a deleted duplicated line elsewhere in the change (the clone was moved).\n */\nexport function findNewlyDuplicatedLines(changes: readonly DuplicationChange[]): number[][] {\n // Same-hunk replacements settle first, identical content pairing up before any other line, so\n // only deleted lines no rewrite needed are left to back moves, whatever the order of changes.\n const unreplaced = changes.map((change) => {\n const partners = {\n base: indexPartnersByLine(change.baseOccurrences),\n head: indexPartnersByLine(change.headOccurrences),\n };\n const newCloneLines = collectNewCloneLines(change, partners.base);\n return change.hunks.map((hunk) => settleReplacements(change, hunk, newCloneLines, partners));\n });\n\n const movableByContent = new Map<string, number>();\n for (const { deletedContents } of unreplaced.flat()) {\n for (const content of deletedContents) {\n movableByContent.set(content, (movableByContent.get(content) ?? 0) + 1);\n }\n }\n\n return changes.map((change, changeIndex) =>\n (unreplaced[changeIndex] ?? [])\n .flatMap(({ addedLines }) => addedLines)\n .filter((line) => {\n const content = normalizeLine(change.headLines?.[line - 1]);\n const movable = movableByContent.get(content) ?? 0;\n if (movable === 0) {\n return true;\n }\n movableByContent.set(content, movable - 1);\n return false;\n })\n .toSorted((left, right) => left - right)\n );\n}\n\n/**\n * The hunk's added duplicated lines (of new clones) and deleted duplicated lines left once each\n * deleted line has credited one added line of the hunk: one with the same content first, then one\n * duplicating a common partner file, so replacing a clone with an unrelated one is not credited.\n */\nfunction settleReplacements(\n change: DuplicationChange,\n hunk: LineHunk,\n newCloneLines: ReadonlySet<number>,\n partners: { base: ReadonlyMap<number, ReadonlySet<string>>; head: ReadonlyMap<number, ReadonlySet<string>> }\n): { addedLines: number[]; deletedContents: string[] } {\n const deleted: { content: string; partners: ReadonlySet<string> }[] = [];\n for (let line = hunk.baseStart; line < hunk.baseStart + hunk.baseCount; line++) {\n if (change.baseDuplicatedLines.has(line)) {\n deleted.push({\n content: normalizeLine(change.baseLines?.[line - 1]),\n partners: partners.base.get(line) ?? new Set<string>(),\n });\n }\n }\n const unmatched: number[] = [];\n for (let line = hunk.headStart; line < hunk.headStart + hunk.headCount; line++) {\n if (!change.headDuplicatedLines.has(line) || !newCloneLines.has(line)) {\n continue;\n }\n const content = normalizeLine(change.headLines?.[line - 1]);\n const sameContentIndex = deleted.findIndex((entry) => entry.content === content);\n if (sameContentIndex === -1) {\n unmatched.push(line);\n } else {\n deleted.splice(sameContentIndex, 1);\n }\n }\n const addedLines = unmatched.filter((line) => {\n const linePartners = partners.head.get(line);\n const replacedIndex = deleted.findIndex((entry) =>\n [...entry.partners].some((partner) => linePartners?.has(partner) === true)\n );\n if (replacedIndex === -1) {\n return true;\n }\n deleted.splice(replacedIndex, 1);\n return false;\n });\n return { addedLines, deletedContents: deleted.map((entry) => entry.content) };\n}\n\n/** The partner files of the clone occurrences covering each line. */\nexport function indexPartnersByLine(occurrences: readonly CloneOccurrence[]): Map<number, Set<string>> {\n const partnersByLine = new Map<number, Set<string>>();\n for (const { startLine, endLine, partners } of occurrences) {\n for (let line = startLine; line <= endLine; line++) {\n const linePartners = partnersByLine.get(line) ?? new Set<string>();\n for (const partner of partners) {\n linePartners.add(partner);\n }\n partnersByLine.set(line, linePartners);\n }\n }\n return partnersByLine;\n}\n\n/**\n * Lines of clone occurrences that are not edits inside pre-existing duplication: all lines of an\n * occurrence with no unchanged line that was duplicated at base with one of its partner files, and\n * otherwise the lines outside the span between its first and last such line, so code pasted next\n * to an old clone, or a clone replaced by one of unrelated code, still counts.\n */\nfunction collectNewCloneLines(\n change: DuplicationChange,\n basePartnersByLine: ReadonlyMap<number, ReadonlySet<string>>\n): Set<number> {\n const baseLineOf = mapUnchangedLines(change);\n const lines = new Set<number>();\n for (const { startLine, endLine, partners } of change.headOccurrences) {\n const isPreexisting = (line: number): boolean => {\n const baseLine = baseLineOf[line] ?? 0;\n if (baseLine === 0 || !change.headDuplicatedLines.has(line) || !change.baseDuplicatedLines.has(baseLine)) {\n return false;\n }\n const basePartners = basePartnersByLine.get(baseLine);\n return partners.some((partner) => basePartners?.has(partner) === true);\n };\n let firstPreexisting = Infinity;\n let lastPreexisting = -Infinity;\n for (let line = startLine; line <= endLine; line++) {\n if (isPreexisting(line)) {\n firstPreexisting = Math.min(firstPreexisting, line);\n lastPreexisting = line;\n }\n }\n for (let line = startLine; line <= endLine; line++) {\n if (line < firstPreexisting || line > lastPreexisting) {\n lines.add(line);\n }\n }\n }\n return lines;\n}\n\n/** The base line of each unchanged head line (index = head line), 0 for added lines. */\nfunction mapUnchangedLines(change: DuplicationChange): Int32Array {\n const headLineCount = change.headLines?.length ?? 0;\n const baseLineOf = new Int32Array(headLineCount + 1);\n const hunks = change.hunks.toSorted((left, right) => left.headStart - right.headStart);\n let hunkIndex = 0;\n let shift = 0;\n for (let line = 1; line <= headLineCount; line++) {\n let hunk = hunks[hunkIndex];\n // A hunk without head lines (a pure deletion) sits after its headStart line.\n while (hunk && line >= hunk.headStart + Math.max(hunk.headCount, 1)) {\n shift += hunk.baseCount - hunk.headCount;\n hunkIndex += 1;\n hunk = hunks[hunkIndex];\n }\n const added = hunk !== undefined && hunk.headCount > 0 && line >= hunk.headStart;\n baseLineOf[line] = added ? 0 : line + shift;\n }\n return baseLineOf;\n}\n\n/** Moves re-indent code, so lines compare without surrounding whitespace. */\nfunction normalizeLine(line: string | undefined): string {\n return line?.trim() ?? '';\n}\n"],"mappings":"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"}
|