code-gauge 4.8.0 → 5.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 +34 -9
- package/dist/cli.cjs +3 -3
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/cliConfig.cjs +1 -1
- package/dist/cliConfig.cjs.map +1 -1
- package/dist/cliConfig.d.ts +13 -3
- package/dist/cliConfig.js +1 -1
- package/dist/cliConfig.js.map +1 -1
- package/dist/diffCommand.cjs +3 -3
- package/dist/diffCommand.cjs.map +1 -1
- package/dist/diffCommand.js +3 -3
- package/dist/diffCommand.js.map +1 -1
- package/dist/exclusion.cjs +2 -0
- package/dist/exclusion.cjs.map +1 -0
- package/dist/exclusion.d.ts +30 -0
- package/dist/exclusion.js +2 -0
- package/dist/exclusion.js.map +1 -0
- package/dist/git.cjs +1 -1
- package/dist/git.cjs.map +1 -1
- package/dist/git.d.ts +16 -0
- package/dist/git.js +1 -1
- package/dist/git.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/newDuplication.cjs +2 -0
- package/dist/newDuplication.cjs.map +1 -0
- package/dist/newDuplication.d.ts +40 -0
- package/dist/newDuplication.js +2 -0
- package/dist/newDuplication.js.map +1 -0
- package/dist/regressionGate.cjs +1 -1
- package/dist/regressionGate.cjs.map +1 -1
- package/dist/regressionGate.d.ts +8 -6
- package/dist/regressionGate.js +1 -1
- package/dist/regressionGate.js.map +1 -1
- package/dist/scan.cjs +1 -1
- package/dist/scan.cjs.map +1 -1
- package/dist/scan.d.ts +12 -3
- package/dist/scan.js +1 -1
- package/dist/scan.js.map +1 -1
- package/dist/worker.cjs +1 -1
- package/dist/worker.cjs.map +1 -1
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/native/code-gauge.wasm +0 -0
- package/native/src/duplication.rs +105 -2
- package/package.json +8 -8
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./git.cjs");let n=require("node:fs/promises"),r=require("node:path");r=e.__toESM(r,1);const i=/(?:^|[^\w@])@generated\b/u,a=/\bgenerated\b.*\bdo not (?:edit|modify)\b/iu;function o(e,t=/* @__PURE__ */ new Map){return{isExcludedPath(n){let i=t.get(n);if(i?.generated===!0||i?.vendored===!0)return!0;let a=r.default.relative(e.root,n),o=a.split(r.default.sep).join(`/`),s=o===`..`||o.startsWith(`../`)||r.default.isAbsolute(a);return e.patterns.some(e=>(!s||e.startsWith(`..`))&&r.default.posix.matchesGlob(o,e))},isGeneratedCode(e,n){return t.get(e)?.generated!==!1&&n.split(/\r\n|\n|\r/u,5).some(e=>i.test(e)||a.test(e))}}}function s(e,t){return{isExcludedPath:n=>!t.has(n)&&e.isExcludedPath(n),isGeneratedCode:(n,r)=>!t.has(n)&&e.isGeneratedCode(n,r)}}async function c(e,i,a){let s;try{s=await(0,n.realpath)(await t.resolveRepoRoot(e))}catch{return o(await u(i))}let c=a.map(e=>r.default.relative(s,e).split(r.default.sep).join(`/`)).filter(e=>e!==`..`&&!e.startsWith(`../`));return await l(s,c,i)}async function l(e,n,i,a){let s=await t.readLinguistAttributes(e,n,a);return o(await u(i),new Map([...s].map(([t,n])=>[r.default.join(e,t),n])))}async function u(e){return{...e,root:await(0,n.realpath)(e.root).catch(()=>e.root)}}exports.createExclusion=o,exports.keepPaths=s,exports.loadExclusion=c,exports.loadRepositoryExclusion=l;
|
|
2
|
+
//# sourceMappingURL=exclusion.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exclusion.cjs","names":["path","realpath","resolveRepoRoot","readLinguistAttributes"],"sources":["../src/exclusion.ts"],"sourcesContent":["import { realpath } from 'node:fs/promises';\nimport path from 'node:path';\nimport { readLinguistAttributes, resolveRepoRoot, type LinguistAttributes } from './git.js';\n\n/**\n * Files left out of measurement beyond the built-in directory and file-name rules: configured\n * glob patterns, files git attributes mark `linguist-generated` or `linguist-vendored`, and files\n * whose header carries a generated-code marker. All paths are absolute.\n */\nexport interface Exclusion {\n isExcludedPath(absolutePath: string): boolean;\n /** Whether the content is generated code, unless git attributes mark the file `-linguist-generated`. */\n isGeneratedCode(absolutePath: string, code: string): boolean;\n}\n\n/** Glob patterns relative to `root` (the directory of the config file that declared them). */\nexport interface ExcludePatterns {\n patterns: string[];\n root: string;\n}\n\nconst generatedMarkerLineCount = 5;\n// Meta's `@generated` convention (used by tree-sitter, Relay, and Buck) and the wording of Go's\n// `// Code generated ... DO NOT EDIT.` convention, which protoc, Prisma, and others share.\nconst generatedTagPattern = /(?:^|[^\\w@])@generated\\b/u;\nconst doNotEditPattern = /\\bgenerated\\b.*\\bdo not (?:edit|modify)\\b/iu;\n\nexport function createExclusion(\n excludePatterns: ExcludePatterns,\n attributesByPath: ReadonlyMap<string, LinguistAttributes> = new Map()\n): Exclusion {\n return {\n isExcludedPath(absolutePath) {\n const attributes = attributesByPath.get(absolutePath);\n if (attributes?.generated === true || attributes?.vendored === true) {\n return true;\n }\n const nativeRelativePath = path.relative(excludePatterns.root, absolutePath);\n const relativePath = nativeRelativePath.split(path.sep).join('/');\n // Only a pattern spelling out `..` reaches outside the root: whether `*` and `**` match `..`\n // segments differs between runtimes (Node's matchesGlob says no, Bun's says yes). A path on\n // another Windows drive has no relative form, so path.relative returns it absolute.\n const outsideRoot =\n relativePath === '..' || relativePath.startsWith('../') || path.isAbsolute(nativeRelativePath);\n return excludePatterns.patterns.some(\n (pattern) => (!outsideRoot || pattern.startsWith('..')) && path.posix.matchesGlob(relativePath, pattern)\n );\n },\n isGeneratedCode(absolutePath, code) {\n if (attributesByPath.get(absolutePath)?.generated === false) {\n return false;\n }\n return code\n .split(/\\r\\n|\\n|\\r/u, generatedMarkerLineCount)\n .some((line) => generatedTagPattern.test(line) || doNotEditPattern.test(line));\n },\n };\n}\n\n/** The exclusion that never excludes the given absolute paths (explicitly targeted files). */\nexport function keepPaths(exclusion: Exclusion, keptPaths: ReadonlySet<string>): Exclusion {\n return {\n isExcludedPath: (absolutePath) => !keptPaths.has(absolutePath) && exclusion.isExcludedPath(absolutePath),\n isGeneratedCode: (absolutePath, code) =>\n !keptPaths.has(absolutePath) && exclusion.isGeneratedCode(absolutePath, code),\n };\n}\n\n/**\n * The exclusion for the given absolute paths found under `directory`, with git attributes from\n * the enclosing repository (which apply to ignored files too); outside any repository only\n * patterns and markers apply.\n */\nexport async function loadExclusion(\n directory: string,\n excludePatterns: ExcludePatterns,\n absolutePaths: readonly string[]\n): Promise<Exclusion> {\n let repoRoot;\n try {\n repoRoot = await realpath(await resolveRepoRoot(directory));\n } catch {\n return createExclusion(await canonicalizeRoot(excludePatterns));\n }\n const relativePaths = absolutePaths\n .map((absolutePath) => path.relative(repoRoot, absolutePath).split(path.sep).join('/'))\n .filter((relativePath) => relativePath !== '..' && !relativePath.startsWith('../'));\n return await loadRepositoryExclusion(repoRoot, relativePaths, excludePatterns);\n}\n\n/**\n * The exclusion for repository-relative `paths` of the canonical `repoRoot`, with git attributes\n * from the working tree or, with `source`, from that revision.\n */\nexport async function loadRepositoryExclusion(\n repoRoot: string,\n paths: Iterable<string>,\n excludePatterns: ExcludePatterns,\n source?: string\n): Promise<Exclusion> {\n const attributes = await readLinguistAttributes(repoRoot, paths, source);\n return createExclusion(\n await canonicalizeRoot(excludePatterns),\n new Map([...attributes].map(([relativePath, value]) => [path.join(repoRoot, relativePath), value]))\n );\n}\n\n/** Scanned paths are canonical, so a pattern root reached through a symbolic link must be too. */\nasync function canonicalizeRoot(excludePatterns: ExcludePatterns): Promise<ExcludePatterns> {\n return { ...excludePatterns, root: await realpath(excludePatterns.root).catch(() => excludePatterns.root) };\n}\n"],"mappings":"kKAqBA,MAGM,EAAsB,4BACtB,EAAmB,8CAEzB,SAAgB,EACd,EACA,kBAA4D,IAAI,IACrD,CACX,MAAO,CACL,eAAe,EAAc,CAC3B,IAAM,EAAa,EAAiB,IAAI,CAAY,EACpD,GAAI,GAAY,YAAc,IAAQ,GAAY,WAAa,GAC7D,MAAO,GAET,IAAM,EAAqBA,EAAAA,QAAK,SAAS,EAAgB,KAAM,CAAY,EACrE,EAAe,EAAmB,MAAMA,EAAAA,QAAK,GAAG,CAAC,CAAC,KAAK,GAAG,EAI1D,EACJ,IAAiB,MAAQ,EAAa,WAAW,KAAK,GAAKA,EAAAA,QAAK,WAAW,CAAkB,EAC/F,OAAO,EAAgB,SAAS,KAC7B,IAAa,CAAC,GAAe,EAAQ,WAAW,IAAI,IAAMA,EAAAA,QAAK,MAAM,YAAY,EAAc,CAAO,CACzG,CACF,EACA,gBAAgB,EAAc,EAAM,CAIlC,OAHI,EAAiB,IAAI,CAAY,CAAC,EAAE,YAAc,IAG/C,EACJ,MAAM,cAAe,CAAwB,CAAC,CAC9C,KAAM,GAAS,EAAoB,KAAK,CAAI,GAAK,EAAiB,KAAK,CAAI,CAAC,CACjF,CACF,CACF,CAGA,SAAgB,EAAU,EAAsB,EAA2C,CACzF,MAAO,CACL,eAAiB,GAAiB,CAAC,EAAU,IAAI,CAAY,GAAK,EAAU,eAAe,CAAY,EACvG,iBAAkB,EAAc,IAC9B,CAAC,EAAU,IAAI,CAAY,GAAK,EAAU,gBAAgB,EAAc,CAAI,CAChF,CACF,CAOA,eAAsB,EACpB,EACA,EACA,EACoB,CACpB,IAAI,EACJ,GAAI,CACF,EAAW,MAAA,EAAMC,EAAAA,SAAAA,CAAS,MAAMC,EAAAA,gBAAgB,CAAS,CAAC,CAC5D,MAAQ,CACN,OAAO,EAAgB,MAAM,EAAiB,CAAe,CAAC,CAChE,CACA,IAAM,EAAgB,EACnB,IAAK,GAAiBF,EAAAA,QAAK,SAAS,EAAU,CAAY,CAAC,CAAC,MAAMA,EAAAA,QAAK,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CACtF,OAAQ,GAAiB,IAAiB,MAAQ,CAAC,EAAa,WAAW,KAAK,CAAC,EACpF,OAAO,MAAM,EAAwB,EAAU,EAAe,CAAe,CAC/E,CAMA,eAAsB,EACpB,EACA,EACA,EACA,EACoB,CACpB,IAAM,EAAa,MAAMG,EAAAA,uBAAuB,EAAU,EAAO,CAAM,EACvE,OAAO,EACL,MAAM,EAAiB,CAAe,EACtC,IAAI,IAAI,CAAC,GAAG,CAAU,CAAC,CAAC,KAAK,CAAC,EAAc,KAAW,CAACH,EAAAA,QAAK,KAAK,EAAU,CAAY,EAAG,CAAK,CAAC,CAAC,CACpG,CACF,CAGA,eAAe,EAAiB,EAA4D,CAC1F,MAAO,CAAE,GAAG,EAAiB,KAAM,MAAA,EAAMC,EAAAA,SAAAA,CAAS,EAAgB,IAAI,CAAC,CAAC,UAAY,EAAgB,IAAI,CAAE,CAC5G"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type LinguistAttributes } from './git.js';
|
|
2
|
+
/**
|
|
3
|
+
* Files left out of measurement beyond the built-in directory and file-name rules: configured
|
|
4
|
+
* glob patterns, files git attributes mark `linguist-generated` or `linguist-vendored`, and files
|
|
5
|
+
* whose header carries a generated-code marker. All paths are absolute.
|
|
6
|
+
*/
|
|
7
|
+
export interface Exclusion {
|
|
8
|
+
isExcludedPath(absolutePath: string): boolean;
|
|
9
|
+
/** Whether the content is generated code, unless git attributes mark the file `-linguist-generated`. */
|
|
10
|
+
isGeneratedCode(absolutePath: string, code: string): boolean;
|
|
11
|
+
}
|
|
12
|
+
/** Glob patterns relative to `root` (the directory of the config file that declared them). */
|
|
13
|
+
export interface ExcludePatterns {
|
|
14
|
+
patterns: string[];
|
|
15
|
+
root: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function createExclusion(excludePatterns: ExcludePatterns, attributesByPath?: ReadonlyMap<string, LinguistAttributes>): Exclusion;
|
|
18
|
+
/** The exclusion that never excludes the given absolute paths (explicitly targeted files). */
|
|
19
|
+
export declare function keepPaths(exclusion: Exclusion, keptPaths: ReadonlySet<string>): Exclusion;
|
|
20
|
+
/**
|
|
21
|
+
* The exclusion for the given absolute paths found under `directory`, with git attributes from
|
|
22
|
+
* the enclosing repository (which apply to ignored files too); outside any repository only
|
|
23
|
+
* patterns and markers apply.
|
|
24
|
+
*/
|
|
25
|
+
export declare function loadExclusion(directory: string, excludePatterns: ExcludePatterns, absolutePaths: readonly string[]): Promise<Exclusion>;
|
|
26
|
+
/**
|
|
27
|
+
* The exclusion for repository-relative `paths` of the canonical `repoRoot`, with git attributes
|
|
28
|
+
* from the working tree or, with `source`, from that revision.
|
|
29
|
+
*/
|
|
30
|
+
export declare function loadRepositoryExclusion(repoRoot: string, paths: Iterable<string>, excludePatterns: ExcludePatterns, source?: string): Promise<Exclusion>;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{readLinguistAttributes as e,resolveRepoRoot as t}from"./git.js";import{realpath as n}from"node:fs/promises";import r from"node:path";const i=/(?:^|[^\w@])@generated\b/u,a=/\bgenerated\b.*\bdo not (?:edit|modify)\b/iu;function o(e,t=/* @__PURE__ */ new Map){return{isExcludedPath(n){let i=t.get(n);if(i?.generated===!0||i?.vendored===!0)return!0;let a=r.relative(e.root,n),o=a.split(r.sep).join(`/`),s=o===`..`||o.startsWith(`../`)||r.isAbsolute(a);return e.patterns.some(e=>(!s||e.startsWith(`..`))&&r.posix.matchesGlob(o,e))},isGeneratedCode(e,n){return t.get(e)?.generated!==!1&&n.split(/\r\n|\n|\r/u,5).some(e=>i.test(e)||a.test(e))}}}function s(e,t){return{isExcludedPath:n=>!t.has(n)&&e.isExcludedPath(n),isGeneratedCode:(n,r)=>!t.has(n)&&e.isGeneratedCode(n,r)}}async function c(e,i,a){let s;try{s=await n(await t(e))}catch{return o(await u(i))}let c=a.map(e=>r.relative(s,e).split(r.sep).join(`/`)).filter(e=>e!==`..`&&!e.startsWith(`../`));return await l(s,c,i)}async function l(t,n,i,a){let s=await e(t,n,a);return o(await u(i),new Map([...s].map(([e,n])=>[r.join(t,e),n])))}async function u(e){return{...e,root:await n(e.root).catch(()=>e.root)}}export{o as createExclusion,s as keepPaths,c as loadExclusion,l as loadRepositoryExclusion};
|
|
2
|
+
//# sourceMappingURL=exclusion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exclusion.js","names":[],"sources":["../src/exclusion.ts"],"sourcesContent":["import { realpath } from 'node:fs/promises';\nimport path from 'node:path';\nimport { readLinguistAttributes, resolveRepoRoot, type LinguistAttributes } from './git.js';\n\n/**\n * Files left out of measurement beyond the built-in directory and file-name rules: configured\n * glob patterns, files git attributes mark `linguist-generated` or `linguist-vendored`, and files\n * whose header carries a generated-code marker. All paths are absolute.\n */\nexport interface Exclusion {\n isExcludedPath(absolutePath: string): boolean;\n /** Whether the content is generated code, unless git attributes mark the file `-linguist-generated`. */\n isGeneratedCode(absolutePath: string, code: string): boolean;\n}\n\n/** Glob patterns relative to `root` (the directory of the config file that declared them). */\nexport interface ExcludePatterns {\n patterns: string[];\n root: string;\n}\n\nconst generatedMarkerLineCount = 5;\n// Meta's `@generated` convention (used by tree-sitter, Relay, and Buck) and the wording of Go's\n// `// Code generated ... DO NOT EDIT.` convention, which protoc, Prisma, and others share.\nconst generatedTagPattern = /(?:^|[^\\w@])@generated\\b/u;\nconst doNotEditPattern = /\\bgenerated\\b.*\\bdo not (?:edit|modify)\\b/iu;\n\nexport function createExclusion(\n excludePatterns: ExcludePatterns,\n attributesByPath: ReadonlyMap<string, LinguistAttributes> = new Map()\n): Exclusion {\n return {\n isExcludedPath(absolutePath) {\n const attributes = attributesByPath.get(absolutePath);\n if (attributes?.generated === true || attributes?.vendored === true) {\n return true;\n }\n const nativeRelativePath = path.relative(excludePatterns.root, absolutePath);\n const relativePath = nativeRelativePath.split(path.sep).join('/');\n // Only a pattern spelling out `..` reaches outside the root: whether `*` and `**` match `..`\n // segments differs between runtimes (Node's matchesGlob says no, Bun's says yes). A path on\n // another Windows drive has no relative form, so path.relative returns it absolute.\n const outsideRoot =\n relativePath === '..' || relativePath.startsWith('../') || path.isAbsolute(nativeRelativePath);\n return excludePatterns.patterns.some(\n (pattern) => (!outsideRoot || pattern.startsWith('..')) && path.posix.matchesGlob(relativePath, pattern)\n );\n },\n isGeneratedCode(absolutePath, code) {\n if (attributesByPath.get(absolutePath)?.generated === false) {\n return false;\n }\n return code\n .split(/\\r\\n|\\n|\\r/u, generatedMarkerLineCount)\n .some((line) => generatedTagPattern.test(line) || doNotEditPattern.test(line));\n },\n };\n}\n\n/** The exclusion that never excludes the given absolute paths (explicitly targeted files). */\nexport function keepPaths(exclusion: Exclusion, keptPaths: ReadonlySet<string>): Exclusion {\n return {\n isExcludedPath: (absolutePath) => !keptPaths.has(absolutePath) && exclusion.isExcludedPath(absolutePath),\n isGeneratedCode: (absolutePath, code) =>\n !keptPaths.has(absolutePath) && exclusion.isGeneratedCode(absolutePath, code),\n };\n}\n\n/**\n * The exclusion for the given absolute paths found under `directory`, with git attributes from\n * the enclosing repository (which apply to ignored files too); outside any repository only\n * patterns and markers apply.\n */\nexport async function loadExclusion(\n directory: string,\n excludePatterns: ExcludePatterns,\n absolutePaths: readonly string[]\n): Promise<Exclusion> {\n let repoRoot;\n try {\n repoRoot = await realpath(await resolveRepoRoot(directory));\n } catch {\n return createExclusion(await canonicalizeRoot(excludePatterns));\n }\n const relativePaths = absolutePaths\n .map((absolutePath) => path.relative(repoRoot, absolutePath).split(path.sep).join('/'))\n .filter((relativePath) => relativePath !== '..' && !relativePath.startsWith('../'));\n return await loadRepositoryExclusion(repoRoot, relativePaths, excludePatterns);\n}\n\n/**\n * The exclusion for repository-relative `paths` of the canonical `repoRoot`, with git attributes\n * from the working tree or, with `source`, from that revision.\n */\nexport async function loadRepositoryExclusion(\n repoRoot: string,\n paths: Iterable<string>,\n excludePatterns: ExcludePatterns,\n source?: string\n): Promise<Exclusion> {\n const attributes = await readLinguistAttributes(repoRoot, paths, source);\n return createExclusion(\n await canonicalizeRoot(excludePatterns),\n new Map([...attributes].map(([relativePath, value]) => [path.join(repoRoot, relativePath), value]))\n );\n}\n\n/** Scanned paths are canonical, so a pattern root reached through a symbolic link must be too. */\nasync function canonicalizeRoot(excludePatterns: ExcludePatterns): Promise<ExcludePatterns> {\n return { ...excludePatterns, root: await realpath(excludePatterns.root).catch(() => excludePatterns.root) };\n}\n"],"mappings":"4IAqBA,MAGM,EAAsB,4BACtB,EAAmB,8CAEzB,SAAgB,EACd,EACA,kBAA4D,IAAI,IACrD,CACX,MAAO,CACL,eAAe,EAAc,CAC3B,IAAM,EAAa,EAAiB,IAAI,CAAY,EACpD,GAAI,GAAY,YAAc,IAAQ,GAAY,WAAa,GAC7D,MAAO,GAET,IAAM,EAAqB,EAAK,SAAS,EAAgB,KAAM,CAAY,EACrE,EAAe,EAAmB,MAAM,EAAK,GAAG,CAAC,CAAC,KAAK,GAAG,EAI1D,EACJ,IAAiB,MAAQ,EAAa,WAAW,KAAK,GAAK,EAAK,WAAW,CAAkB,EAC/F,OAAO,EAAgB,SAAS,KAC7B,IAAa,CAAC,GAAe,EAAQ,WAAW,IAAI,IAAM,EAAK,MAAM,YAAY,EAAc,CAAO,CACzG,CACF,EACA,gBAAgB,EAAc,EAAM,CAIlC,OAHI,EAAiB,IAAI,CAAY,CAAC,EAAE,YAAc,IAG/C,EACJ,MAAM,cAAe,CAAwB,CAAC,CAC9C,KAAM,GAAS,EAAoB,KAAK,CAAI,GAAK,EAAiB,KAAK,CAAI,CAAC,CACjF,CACF,CACF,CAGA,SAAgB,EAAU,EAAsB,EAA2C,CACzF,MAAO,CACL,eAAiB,GAAiB,CAAC,EAAU,IAAI,CAAY,GAAK,EAAU,eAAe,CAAY,EACvG,iBAAkB,EAAc,IAC9B,CAAC,EAAU,IAAI,CAAY,GAAK,EAAU,gBAAgB,EAAc,CAAI,CAChF,CACF,CAOA,eAAsB,EACpB,EACA,EACA,EACoB,CACpB,IAAI,EACJ,GAAI,CACF,EAAW,MAAM,EAAS,MAAM,EAAgB,CAAS,CAAC,CAC5D,MAAQ,CACN,OAAO,EAAgB,MAAM,EAAiB,CAAe,CAAC,CAChE,CACA,IAAM,EAAgB,EACnB,IAAK,GAAiB,EAAK,SAAS,EAAU,CAAY,CAAC,CAAC,MAAM,EAAK,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CACtF,OAAQ,GAAiB,IAAiB,MAAQ,CAAC,EAAa,WAAW,KAAK,CAAC,EACpF,OAAO,MAAM,EAAwB,EAAU,EAAe,CAAe,CAC/E,CAMA,eAAsB,EACpB,EACA,EACA,EACA,EACoB,CACpB,IAAM,EAAa,MAAM,EAAuB,EAAU,EAAO,CAAM,EACvE,OAAO,EACL,MAAM,EAAiB,CAAe,EACtC,IAAI,IAAI,CAAC,GAAG,CAAU,CAAC,CAAC,KAAK,CAAC,EAAc,KAAW,CAAC,EAAK,KAAK,EAAU,CAAY,EAAG,CAAK,CAAC,CAAC,CACpG,CACF,CAGA,eAAe,EAAiB,EAA4D,CAC1F,MAAO,CAAE,GAAG,EAAiB,KAAM,MAAM,EAAS,EAAgB,IAAI,CAAC,CAAC,UAAY,EAAgB,IAAI,CAAE,CAC5G"}
|
package/dist/git.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";let e=require("node:child_process");const
|
|
1
|
+
"use strict";const e=require("./_virtual/_rolldown/runtime.cjs");let t=require("node:fs/promises"),n=require("node:path");n=e.__toESM(n,1);let r=require("node:os");r=e.__toESM(r,1);let i=require("node:child_process");const a=(0,require("node:util").promisify)(i.execFile);async function o(e){return(await v(e,[`rev-parse`,`--show-toplevel`])).trim()}async function s(e,t){return(await v(e,[`merge-base`,t,`HEAD`])).trim()}async function c(e,t){let[n,r]=await Promise.all([v(e,[`diff`,`--name-status`,`--find-renames`,`-z`,t]),v(e,[`ls-files`,`--others`,`--exclude-standard`,`-z`])]),i=l(n);for(let e of r.split(`\0`))e!==``&&i.push({status:`added`,headPath:e});return i}function l(e){let t=[],n=e.split(`\0`),r=0;for(;r<n.length-1;){let e=n[r].charAt(0),i=e===`R`||e===`C`?2:1,a=u(e,n.slice(r+1,r+1+i));a&&t.push(a),r+=1+i}return t}function u(e,t){let[n,r]=t;if(n!==void 0)switch(e){case`R`:return r===void 0?void 0:{status:`renamed`,basePath:n,headPath:r};case`C`:return r===void 0?void 0:{status:`added`,headPath:r};case`A`:return{status:`added`,headPath:n};case`D`:return{status:`deleted`,basePath:n,headPath:n};case`M`:case`T`:return{status:`modified`,basePath:n,headPath:n};default:return}}async function d(e){let[t,n]=await Promise.all([v(e,[`ls-files`,`--cached`,`--others`,`--exclude-standard`,`-z`]),v(e,[`ls-files`,`--deleted`,`-z`])]),r=new Set(t.split(`\0`).filter(e=>e!==``));for(let e of n.split(`\0`))r.delete(e);return r}async function f(e,t){let n=await v(e,[`ls-tree`,`-r`,`-z`,t]),r=/* @__PURE__ */ new Set;for(let e of n.split(`\0`)){let t=e.indexOf(` `);e.startsWith(`120000 `)&&t!==-1&&r.add(e.slice(t+1))}return r}async function p(e,t,n,r){let i=await v(e,[`-c`,`diff.algorithm=myers`,`-c`,`diff.indentHeuristic=true`,`diff`,`--unified=0`,`--inter-hunk-context=0`,`--text`,`--no-color`,`--no-ext-diff`,`--no-textconv`,`--find-renames`,t,`--`,...(n===r?[r]:[n,r]).map(e=>`:(literal)${e}`)]),a=[];for(let e of i.matchAll(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/gmu)){let[,t,n,r,i]=e;a.push({baseStart:Number(t),baseCount:n===void 0?1:Number(n),headStart:Number(r),headCount:i===void 0?1:Number(i)})}return a}async function m(e,i,a){let o=[...i].join(`\0`);if(o===``)return/* @__PURE__ */ new Map;let s=[`--stdin`,`-z`,`linguist-generated`,`linguist-vendored`],c;if(a===void 0)c=await _(e,[`check-attr`,...s],o);else{let i=await(0,t.mkdtemp)(n.default.join(r.default.tmpdir(),`code-gauge-index-`));try{let t={...process.env,GIT_INDEX_FILE:n.default.join(i,`index`)};await _(e,[`read-tree`,a],``,t),c=await _(e,[`check-attr`,`--cached`,...s],o,t)}finally{await(0,t.rm)(i,{recursive:!0,force:!0})}}let l=/* @__PURE__ */ new Map,u=c.split(`\0`);for(let e=0;e+2<u.length;e+=3){let[t,n,r]=u.slice(e,e+3),i=h(r);if(i===void 0)continue;let a=l.get(t)??{};n===`linguist-generated`?a.generated=i:a.vendored=i,l.set(t,a)}return l}function h(e){if(e===`set`||e===`true`)return!0;if(e===`unset`||e===`false`)return!1}async function g(e,t,n){return await v(e,[`cat-file`,`blob`,`${t}:${n}`])}async function _(e,t,n,r=process.env){return await new Promise((a,o)=>{let s=(0,i.spawn)(`git`,t,{cwd:e,env:r});s.stdin.on(`error`,()=>{});let c=[],l=[];s.stdout.on(`data`,e=>c.push(e)),s.stderr.on(`data`,e=>l.push(e)),s.on(`error`,o),s.on(`close`,e=>{if(e===0){a(Buffer.concat(c).toString(`utf8`));return}let n=Buffer.concat(l).toString(`utf8`).trim();o(/* @__PURE__ */ Error(`git ${y(t)} failed${n?`: ${n}`:``}`))}),s.stdin.end(n)})}async function v(e,t){try{let{stdout:n}=await a(`git`,t,{cwd:e,maxBuffer:536870912,encoding:`utf8`});return n}catch(e){let n=e.stderr?.trim();throw Error(`git ${y(t)} failed${n?`: ${n}`:``}`)}}function y(e){let t=0;for(;e[t]===`-c`;)t+=2;return e.slice(t,t+2).join(` `)}exports.listChangedFiles=c,exports.listLineHunks=p,exports.listRepositoryFiles=d,exports.listSymlinkPathsAtRevision=f,exports.readFileAtRevision=g,exports.readLinguistAttributes=m,exports.resolveMergeBase=s,exports.resolveRepoRoot=o;
|
|
2
2
|
//# sourceMappingURL=git.cjs.map
|
package/dist/git.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git.cjs","names":["promisify","execFile"],"sources":["../src/git.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport { promisify } from 'node:util';\n\nconst execFileAsync = promisify(execFile);\n\n/** Large enough for whole-file blobs; execFile's 1 MiB default truncates real sources. */\nconst maxOutputBytes = 512 * 1024 * 1024;\n\n/** One entry of the working tree's diff against the merge-base commit. */\nexport interface ChangedFile {\n status: 'added' | 'modified' | 'deleted' | 'renamed';\n /** Path at the merge-base, absent for added files; differs from headPath only for renames. */\n basePath?: string;\n /** Path in the working tree; for deleted files, the (now missing) base path. */\n headPath: string;\n}\n\nexport async function resolveRepoRoot(directory: string): Promise<string> {\n const output = await runGit(directory, ['rev-parse', '--show-toplevel']);\n return output.trim();\n}\n\nexport async function resolveMergeBase(repoRoot: string, baseRef: string): Promise<string> {\n const output = await runGit(repoRoot, ['merge-base', baseRef, 'HEAD']);\n return output.trim();\n}\n\n/**\n * Files whose working-tree content differs from the merge-base commit (staged or not), with\n * rename detection, plus untracked (non-ignored) files as additions. Copies gate like additions:\n * only the new path is measured against the new-code thresholds.\n */\nexport async function listChangedFiles(repoRoot: string, mergeBase: string): Promise<ChangedFile[]> {\n const [diffOutput, untrackedOutput] = await Promise.all([\n runGit(repoRoot, ['diff', '--name-status', '--find-renames', '-z', mergeBase]),\n runGit(repoRoot, ['ls-files', '--others', '--exclude-standard', '-z']),\n ]);\n\n const files = parseNameStatusEntries(diffOutput);\n for (const path of untrackedOutput.split('\\0')) {\n if (path !== '') {\n files.push({ status: 'added', headPath: path });\n }\n }\n return files;\n}\n\n/** Parses `git diff --name-status -z` output: `<status>\\0<path>\\0`, with two paths for R/C. */\nfunction parseNameStatusEntries(output: string): ChangedFile[] {\n const files: ChangedFile[] = [];\n const parts = output.split('\\0');\n let index = 0;\n while (index < parts.length - 1) {\n const kind = (parts[index] as string).charAt(0);\n const pathCount = kind === 'R' || kind === 'C' ? 2 : 1;\n const changed = toChangedFile(kind, parts.slice(index + 1, index + 1 + pathCount));\n if (changed) {\n files.push(changed);\n }\n index += 1 + pathCount;\n }\n return files;\n}\n\n/** Undefined for unmerged (U) and unknown statuses, which have nothing to gate. */\nfunction toChangedFile(kind: string, paths: (string | undefined)[]): ChangedFile | undefined {\n const [first, second] = paths;\n if (first === undefined) {\n return undefined;\n }\n switch (kind) {\n case 'R': {\n return second === undefined ? undefined : { status: 'renamed', basePath: first, headPath: second };\n }\n // A copy gates like an addition: only the new path is measured against the new-code thresholds.\n case 'C': {\n return second === undefined ? undefined : { status: 'added', headPath: second };\n }\n case 'A': {\n return { status: 'added', headPath: first };\n }\n case 'D': {\n return { status: 'deleted', basePath: first, headPath: first };\n }\n case 'M':\n case 'T': {\n return { status: 'modified', basePath: first, headPath: first };\n }\n default: {\n return undefined;\n }\n }\n}\n\n/**\n * Repository-relative paths git considers part of the project AND that exist in the working tree:\n * tracked files (minus worktree deletions) plus untracked non-ignored ones. The diff gate scans\n * exactly these, so local ignored artifacts (build output, generated copies) are neither measured\n * nor allowed to skew the duplication universes.\n */\nexport async function listRepositoryFiles(repoRoot: string): Promise<Set<string>> {\n const [listed, deleted] = await Promise.all([\n runGit(repoRoot, ['ls-files', '--cached', '--others', '--exclude-standard', '-z']),\n runGit(repoRoot, ['ls-files', '--deleted', '-z']),\n ]);\n const files = new Set(listed.split('\\0').filter((path) => path !== ''));\n for (const path of deleted.split('\\0')) {\n files.delete(path);\n }\n return files;\n}\n\n/** Paths that are symbolic links (blob mode 120000) at the given revision. */\nexport async function listSymlinkPathsAtRevision(repoRoot: string, revision: string): Promise<Set<string>> {\n const output = await runGit(repoRoot, ['ls-tree', '-r', '-z', revision]);\n const links = new Set<string>();\n for (const entry of output.split('\\0')) {\n const tabIndex = entry.indexOf('\\t');\n if (entry.startsWith('120000 ') && tabIndex !== -1) {\n links.add(entry.slice(tabIndex + 1));\n }\n }\n return links;\n}\n\n/** The file's content at the given commit; the path is repository-relative with forward slashes. */\nexport async function readFileAtRevision(repoRoot: string, revision: string, path: string): Promise<string> {\n return await runGit(repoRoot, ['cat-file', 'blob', `${revision}:${path}`]);\n}\n\nasync function runGit(cwd: string, args: string[]): Promise<string> {\n try {\n const { stdout } = await execFileAsync('git', args, { cwd, maxBuffer: maxOutputBytes, encoding: 'utf8' });\n return stdout;\n } catch (error) {\n const stderr = (error as { stderr?: string }).stderr?.trim();\n throw new Error(`git ${args.slice(0, 2).join(' ')} failed${stderr ? `: ${stderr}` : ''}`);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"git.cjs","names":["promisify","execFile","mkdtemp","path","os","rm","spawn"],"sources":["../src/git.ts"],"sourcesContent":["import { execFile, spawn } from 'node:child_process';\nimport { mkdtemp, rm } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\nimport type { LineHunk } from './newDuplication.js';\n\nconst execFileAsync = promisify(execFile);\n\n/** Large enough for whole-file blobs; execFile's 1 MiB default truncates real sources. */\nconst maxOutputBytes = 512 * 1024 * 1024;\n\n/** One entry of the working tree's diff against the merge-base commit. */\nexport interface ChangedFile {\n status: 'added' | 'modified' | 'deleted' | 'renamed';\n /** Path at the merge-base, absent for added files; differs from headPath only for renames. */\n basePath?: string;\n /** Path in the working tree; for deleted files, the (now missing) base path. */\n headPath: string;\n}\n\nexport async function resolveRepoRoot(directory: string): Promise<string> {\n const output = await runGit(directory, ['rev-parse', '--show-toplevel']);\n return output.trim();\n}\n\nexport async function resolveMergeBase(repoRoot: string, baseRef: string): Promise<string> {\n const output = await runGit(repoRoot, ['merge-base', baseRef, 'HEAD']);\n return output.trim();\n}\n\n/**\n * Files whose working-tree content differs from the merge-base commit (staged or not), with\n * rename detection, plus untracked (non-ignored) files as additions. Copies gate like additions:\n * only the new path is measured against the new-code thresholds.\n */\nexport async function listChangedFiles(repoRoot: string, mergeBase: string): Promise<ChangedFile[]> {\n const [diffOutput, untrackedOutput] = await Promise.all([\n runGit(repoRoot, ['diff', '--name-status', '--find-renames', '-z', mergeBase]),\n runGit(repoRoot, ['ls-files', '--others', '--exclude-standard', '-z']),\n ]);\n\n const files = parseNameStatusEntries(diffOutput);\n for (const path of untrackedOutput.split('\\0')) {\n if (path !== '') {\n files.push({ status: 'added', headPath: path });\n }\n }\n return files;\n}\n\n/** Parses `git diff --name-status -z` output: `<status>\\0<path>\\0`, with two paths for R/C. */\nfunction parseNameStatusEntries(output: string): ChangedFile[] {\n const files: ChangedFile[] = [];\n const parts = output.split('\\0');\n let index = 0;\n while (index < parts.length - 1) {\n const kind = (parts[index] as string).charAt(0);\n const pathCount = kind === 'R' || kind === 'C' ? 2 : 1;\n const changed = toChangedFile(kind, parts.slice(index + 1, index + 1 + pathCount));\n if (changed) {\n files.push(changed);\n }\n index += 1 + pathCount;\n }\n return files;\n}\n\n/** Undefined for unmerged (U) and unknown statuses, which have nothing to gate. */\nfunction toChangedFile(kind: string, paths: (string | undefined)[]): ChangedFile | undefined {\n const [first, second] = paths;\n if (first === undefined) {\n return undefined;\n }\n switch (kind) {\n case 'R': {\n return second === undefined ? undefined : { status: 'renamed', basePath: first, headPath: second };\n }\n // A copy gates like an addition: only the new path is measured against the new-code thresholds.\n case 'C': {\n return second === undefined ? undefined : { status: 'added', headPath: second };\n }\n case 'A': {\n return { status: 'added', headPath: first };\n }\n case 'D': {\n return { status: 'deleted', basePath: first, headPath: first };\n }\n case 'M':\n case 'T': {\n return { status: 'modified', basePath: first, headPath: first };\n }\n default: {\n return undefined;\n }\n }\n}\n\n/**\n * Repository-relative paths git considers part of the project AND that exist in the working tree:\n * tracked files (minus worktree deletions) plus untracked non-ignored ones. The diff gate scans\n * exactly these, so local ignored artifacts (build output, generated copies) are neither measured\n * nor allowed to skew the duplication universes.\n */\nexport async function listRepositoryFiles(repoRoot: string): Promise<Set<string>> {\n const [listed, deleted] = await Promise.all([\n runGit(repoRoot, ['ls-files', '--cached', '--others', '--exclude-standard', '-z']),\n runGit(repoRoot, ['ls-files', '--deleted', '-z']),\n ]);\n const files = new Set(listed.split('\\0').filter((path) => path !== ''));\n for (const path of deleted.split('\\0')) {\n files.delete(path);\n }\n return files;\n}\n\n/** Paths that are symbolic links (blob mode 120000) at the given revision. */\nexport async function listSymlinkPathsAtRevision(repoRoot: string, revision: string): Promise<Set<string>> {\n const output = await runGit(repoRoot, ['ls-tree', '-r', '-z', revision]);\n const links = new Set<string>();\n for (const entry of output.split('\\0')) {\n const tabIndex = entry.indexOf('\\t');\n if (entry.startsWith('120000 ') && tabIndex !== -1) {\n links.add(entry.slice(tabIndex + 1));\n }\n }\n return links;\n}\n\n/**\n * The line hunks turning the merge-base blob at `basePath` into the working-tree file at\n * `headPath` (equal unless renamed), as `git diff` computes them for review.\n */\nexport async function listLineHunks(\n repoRoot: string,\n mergeBase: string,\n basePath: string,\n headPath: string\n): Promise<LineHunk[]> {\n // Literal pathspecs: a name like `app/[slug]/page.tsx` must not glob-match other files.\n const paths = (basePath === headPath ? [headPath] : [basePath, headPath]).map((file) => `:(literal)${file}`);\n const output = await runGit(repoRoot, [\n // The algorithm and indent heuristic decide which lines count as added, so user config must\n // not change them; `-c` values an older git does not know are ignored rather than rejected.\n '-c',\n 'diff.algorithm=myers',\n '-c',\n 'diff.indentHeuristic=true',\n 'diff',\n '--unified=0',\n // diff.interHunkContext would otherwise merge nearby hunks, and the unchanged lines between\n // them would count as added.\n '--inter-hunk-context=0',\n // A file git would show as binary (a `-diff` attribute, a NUL byte) must still yield hunks.\n '--text',\n '--no-color',\n '--no-ext-diff',\n '--no-textconv',\n '--find-renames',\n mergeBase,\n '--',\n ...paths,\n ]);\n const hunks: LineHunk[] = [];\n for (const match of output.matchAll(/^@@ -(\\d+)(?:,(\\d+))? \\+(\\d+)(?:,(\\d+))? @@/gmu)) {\n const [, baseStart, baseCount, headStart, headCount] = match;\n hunks.push({\n baseStart: Number(baseStart),\n baseCount: baseCount === undefined ? 1 : Number(baseCount),\n headStart: Number(headStart),\n headCount: headCount === undefined ? 1 : Number(headCount),\n });\n }\n return hunks;\n}\n\n/** A file's `linguist-generated` / `linguist-vendored` attributes; undefined when unspecified. */\nexport interface LinguistAttributes {\n generated?: boolean;\n vendored?: boolean;\n}\n\n/**\n * The linguist attributes of repository-relative paths, from the working tree's `.gitattributes`\n * or, with `source`, from that revision's. Paths with neither attribute specified are omitted.\n */\nexport async function readLinguistAttributes(\n repoRoot: string,\n paths: Iterable<string>,\n source?: string\n): Promise<Map<string, LinguistAttributes>> {\n const input = [...paths].join('\\0');\n if (input === '') {\n return new Map();\n }\n const attributeArgs = ['--stdin', '-z', 'linguist-generated', 'linguist-vendored'];\n let output;\n if (source === undefined) {\n output = await runGitWithInput(repoRoot, ['check-attr', ...attributeArgs], input);\n } else {\n // `check-attr --source` needs git 2.40, so the revision's attributes are read from a\n // throwaway index holding its tree instead, which `--cached` supports on any git.\n const indexDirectory = await mkdtemp(path.join(os.tmpdir(), 'code-gauge-index-'));\n try {\n const env = { ...process.env, GIT_INDEX_FILE: path.join(indexDirectory, 'index') };\n await runGitWithInput(repoRoot, ['read-tree', source], '', env);\n output = await runGitWithInput(repoRoot, ['check-attr', '--cached', ...attributeArgs], input, env);\n } finally {\n await rm(indexDirectory, { recursive: true, force: true });\n }\n }\n const attributesByPath = new Map<string, LinguistAttributes>();\n const fields = output.split('\\0');\n for (let index = 0; index + 2 < fields.length; index += 3) {\n const [file, attribute, value] = fields.slice(index, index + 3) as [string, string, string];\n const enabled = toAttributeFlag(value);\n if (enabled === undefined) {\n continue;\n }\n const attributes = attributesByPath.get(file) ?? {};\n if (attribute === 'linguist-generated') {\n attributes.generated = enabled;\n } else {\n attributes.vendored = enabled;\n }\n attributesByPath.set(file, attributes);\n }\n return attributesByPath;\n}\n\n/** Linguist reads `attr`/`attr=true` as set and `-attr`/`attr=false` as explicitly unset. */\nfunction toAttributeFlag(value: string): boolean | undefined {\n if (value === 'set' || value === 'true') {\n return true;\n }\n if (value === 'unset' || value === 'false') {\n return false;\n }\n return undefined;\n}\n\n/** The file's content at the given commit; the path is repository-relative with forward slashes. */\nexport async function readFileAtRevision(repoRoot: string, revision: string, path: string): Promise<string> {\n return await runGit(repoRoot, ['cat-file', 'blob', `${revision}:${path}`]);\n}\n\nasync function runGitWithInput(\n cwd: string,\n args: string[],\n input: string,\n env: NodeJS.ProcessEnv = process.env\n): Promise<string> {\n return await new Promise((resolve, reject) => {\n const child = spawn('git', args, { cwd, env });\n // git exiting before it reads all input (e.g. on a usage error) makes the write fail with\n // EPIPE; its exit code and stderr, reported on close, already describe the failure.\n child.stdin.on('error', () => {});\n const stdout: Buffer[] = [];\n const stderr: Buffer[] = [];\n child.stdout.on('data', (chunk: Buffer) => stdout.push(chunk));\n child.stderr.on('data', (chunk: Buffer) => stderr.push(chunk));\n child.on('error', reject);\n child.on('close', (code) => {\n if (code === 0) {\n resolve(Buffer.concat(stdout).toString('utf8'));\n return;\n }\n const message = Buffer.concat(stderr).toString('utf8').trim();\n reject(new Error(`git ${describeCommand(args)} failed${message ? `: ${message}` : ''}`));\n });\n child.stdin.end(input);\n });\n}\n\nasync function runGit(cwd: string, args: string[]): Promise<string> {\n try {\n const { stdout } = await execFileAsync('git', args, { cwd, maxBuffer: maxOutputBytes, encoding: 'utf8' });\n return stdout;\n } catch (error) {\n const stderr = (error as { stderr?: string }).stderr?.trim();\n throw new Error(`git ${describeCommand(args)} failed${stderr ? `: ${stderr}` : ''}`);\n }\n}\n\n/** The subcommand and its first argument, skipping leading `-c key=value` overrides. */\nfunction describeCommand(args: readonly string[]): string {\n let start = 0;\n while (args[start] === '-c') {\n start += 2;\n }\n return args.slice(start, start + 2).join(' ');\n}\n"],"mappings":"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"}
|
package/dist/git.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LineHunk } from './newDuplication.js';
|
|
1
2
|
/** One entry of the working tree's diff against the merge-base commit. */
|
|
2
3
|
export interface ChangedFile {
|
|
3
4
|
status: 'added' | 'modified' | 'deleted' | 'renamed';
|
|
@@ -23,5 +24,20 @@ export declare function listChangedFiles(repoRoot: string, mergeBase: string): P
|
|
|
23
24
|
export declare function listRepositoryFiles(repoRoot: string): Promise<Set<string>>;
|
|
24
25
|
/** Paths that are symbolic links (blob mode 120000) at the given revision. */
|
|
25
26
|
export declare function listSymlinkPathsAtRevision(repoRoot: string, revision: string): Promise<Set<string>>;
|
|
27
|
+
/**
|
|
28
|
+
* The line hunks turning the merge-base blob at `basePath` into the working-tree file at
|
|
29
|
+
* `headPath` (equal unless renamed), as `git diff` computes them for review.
|
|
30
|
+
*/
|
|
31
|
+
export declare function listLineHunks(repoRoot: string, mergeBase: string, basePath: string, headPath: string): Promise<LineHunk[]>;
|
|
32
|
+
/** A file's `linguist-generated` / `linguist-vendored` attributes; undefined when unspecified. */
|
|
33
|
+
export interface LinguistAttributes {
|
|
34
|
+
generated?: boolean;
|
|
35
|
+
vendored?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The linguist attributes of repository-relative paths, from the working tree's `.gitattributes`
|
|
39
|
+
* or, with `source`, from that revision's. Paths with neither attribute specified are omitted.
|
|
40
|
+
*/
|
|
41
|
+
export declare function readLinguistAttributes(repoRoot: string, paths: Iterable<string>, source?: string): Promise<Map<string, LinguistAttributes>>;
|
|
26
42
|
/** The file's content at the given commit; the path is repository-relative with forward slashes. */
|
|
27
43
|
export declare function readFileAtRevision(repoRoot: string, revision: string, path: string): Promise<string>;
|
package/dist/git.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{mkdtemp as e,rm as t}from"node:fs/promises";import n from"node:path";import r from"node:os";import{execFile as i,spawn as a}from"node:child_process";import{promisify as o}from"node:util";const s=o(i);async function c(e){return(await b(e,[`rev-parse`,`--show-toplevel`])).trim()}async function l(e,t){return(await b(e,[`merge-base`,t,`HEAD`])).trim()}async function u(e,t){let[n,r]=await Promise.all([b(e,[`diff`,`--name-status`,`--find-renames`,`-z`,t]),b(e,[`ls-files`,`--others`,`--exclude-standard`,`-z`])]),i=d(n);for(let e of r.split(`\0`))e!==``&&i.push({status:`added`,headPath:e});return i}function d(e){let t=[],n=e.split(`\0`),r=0;for(;r<n.length-1;){let e=n[r].charAt(0),i=e===`R`||e===`C`?2:1,a=f(e,n.slice(r+1,r+1+i));a&&t.push(a),r+=1+i}return t}function f(e,t){let[n,r]=t;if(n!==void 0)switch(e){case`R`:return r===void 0?void 0:{status:`renamed`,basePath:n,headPath:r};case`C`:return r===void 0?void 0:{status:`added`,headPath:r};case`A`:return{status:`added`,headPath:n};case`D`:return{status:`deleted`,basePath:n,headPath:n};case`M`:case`T`:return{status:`modified`,basePath:n,headPath:n};default:return}}async function p(e){let[t,n]=await Promise.all([b(e,[`ls-files`,`--cached`,`--others`,`--exclude-standard`,`-z`]),b(e,[`ls-files`,`--deleted`,`-z`])]),r=new Set(t.split(`\0`).filter(e=>e!==``));for(let e of n.split(`\0`))r.delete(e);return r}async function m(e,t){let n=await b(e,[`ls-tree`,`-r`,`-z`,t]),r=/* @__PURE__ */ new Set;for(let e of n.split(`\0`)){let t=e.indexOf(` `);e.startsWith(`120000 `)&&t!==-1&&r.add(e.slice(t+1))}return r}async function h(e,t,n,r){let i=await b(e,[`-c`,`diff.algorithm=myers`,`-c`,`diff.indentHeuristic=true`,`diff`,`--unified=0`,`--inter-hunk-context=0`,`--text`,`--no-color`,`--no-ext-diff`,`--no-textconv`,`--find-renames`,t,`--`,...(n===r?[r]:[n,r]).map(e=>`:(literal)${e}`)]),a=[];for(let e of i.matchAll(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/gmu)){let[,t,n,r,i]=e;a.push({baseStart:Number(t),baseCount:n===void 0?1:Number(n),headStart:Number(r),headCount:i===void 0?1:Number(i)})}return a}async function g(i,a,o){let s=[...a].join(`\0`);if(s===``)return/* @__PURE__ */ new Map;let c=[`--stdin`,`-z`,`linguist-generated`,`linguist-vendored`],l;if(o===void 0)l=await y(i,[`check-attr`,...c],s);else{let a=await e(n.join(r.tmpdir(),`code-gauge-index-`));try{let e={...process.env,GIT_INDEX_FILE:n.join(a,`index`)};await y(i,[`read-tree`,o],``,e),l=await y(i,[`check-attr`,`--cached`,...c],s,e)}finally{await t(a,{recursive:!0,force:!0})}}let u=/* @__PURE__ */ new Map,d=l.split(`\0`);for(let e=0;e+2<d.length;e+=3){let[t,n,r]=d.slice(e,e+3),i=_(r);if(i===void 0)continue;let a=u.get(t)??{};n===`linguist-generated`?a.generated=i:a.vendored=i,u.set(t,a)}return u}function _(e){if(e===`set`||e===`true`)return!0;if(e===`unset`||e===`false`)return!1}async function v(e,t,n){return await b(e,[`cat-file`,`blob`,`${t}:${n}`])}async function y(e,t,n,r=process.env){return await new Promise((i,o)=>{let s=a(`git`,t,{cwd:e,env:r});s.stdin.on(`error`,()=>{});let c=[],l=[];s.stdout.on(`data`,e=>c.push(e)),s.stderr.on(`data`,e=>l.push(e)),s.on(`error`,o),s.on(`close`,e=>{if(e===0){i(Buffer.concat(c).toString(`utf8`));return}let n=Buffer.concat(l).toString(`utf8`).trim();o(/* @__PURE__ */ Error(`git ${x(t)} failed${n?`: ${n}`:``}`))}),s.stdin.end(n)})}async function b(e,t){try{let{stdout:n}=await s(`git`,t,{cwd:e,maxBuffer:536870912,encoding:`utf8`});return n}catch(e){let n=e.stderr?.trim();throw Error(`git ${x(t)} failed${n?`: ${n}`:``}`)}}function x(e){let t=0;for(;e[t]===`-c`;)t+=2;return e.slice(t,t+2).join(` `)}export{u as listChangedFiles,h as listLineHunks,p as listRepositoryFiles,m as listSymlinkPathsAtRevision,v as readFileAtRevision,g as readLinguistAttributes,l as resolveMergeBase,c as resolveRepoRoot};
|
|
2
2
|
//# sourceMappingURL=git.js.map
|
package/dist/git.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git.js","names":[],"sources":["../src/git.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport { promisify } from 'node:util';\n\nconst execFileAsync = promisify(execFile);\n\n/** Large enough for whole-file blobs; execFile's 1 MiB default truncates real sources. */\nconst maxOutputBytes = 512 * 1024 * 1024;\n\n/** One entry of the working tree's diff against the merge-base commit. */\nexport interface ChangedFile {\n status: 'added' | 'modified' | 'deleted' | 'renamed';\n /** Path at the merge-base, absent for added files; differs from headPath only for renames. */\n basePath?: string;\n /** Path in the working tree; for deleted files, the (now missing) base path. */\n headPath: string;\n}\n\nexport async function resolveRepoRoot(directory: string): Promise<string> {\n const output = await runGit(directory, ['rev-parse', '--show-toplevel']);\n return output.trim();\n}\n\nexport async function resolveMergeBase(repoRoot: string, baseRef: string): Promise<string> {\n const output = await runGit(repoRoot, ['merge-base', baseRef, 'HEAD']);\n return output.trim();\n}\n\n/**\n * Files whose working-tree content differs from the merge-base commit (staged or not), with\n * rename detection, plus untracked (non-ignored) files as additions. Copies gate like additions:\n * only the new path is measured against the new-code thresholds.\n */\nexport async function listChangedFiles(repoRoot: string, mergeBase: string): Promise<ChangedFile[]> {\n const [diffOutput, untrackedOutput] = await Promise.all([\n runGit(repoRoot, ['diff', '--name-status', '--find-renames', '-z', mergeBase]),\n runGit(repoRoot, ['ls-files', '--others', '--exclude-standard', '-z']),\n ]);\n\n const files = parseNameStatusEntries(diffOutput);\n for (const path of untrackedOutput.split('\\0')) {\n if (path !== '') {\n files.push({ status: 'added', headPath: path });\n }\n }\n return files;\n}\n\n/** Parses `git diff --name-status -z` output: `<status>\\0<path>\\0`, with two paths for R/C. */\nfunction parseNameStatusEntries(output: string): ChangedFile[] {\n const files: ChangedFile[] = [];\n const parts = output.split('\\0');\n let index = 0;\n while (index < parts.length - 1) {\n const kind = (parts[index] as string).charAt(0);\n const pathCount = kind === 'R' || kind === 'C' ? 2 : 1;\n const changed = toChangedFile(kind, parts.slice(index + 1, index + 1 + pathCount));\n if (changed) {\n files.push(changed);\n }\n index += 1 + pathCount;\n }\n return files;\n}\n\n/** Undefined for unmerged (U) and unknown statuses, which have nothing to gate. */\nfunction toChangedFile(kind: string, paths: (string | undefined)[]): ChangedFile | undefined {\n const [first, second] = paths;\n if (first === undefined) {\n return undefined;\n }\n switch (kind) {\n case 'R': {\n return second === undefined ? undefined : { status: 'renamed', basePath: first, headPath: second };\n }\n // A copy gates like an addition: only the new path is measured against the new-code thresholds.\n case 'C': {\n return second === undefined ? undefined : { status: 'added', headPath: second };\n }\n case 'A': {\n return { status: 'added', headPath: first };\n }\n case 'D': {\n return { status: 'deleted', basePath: first, headPath: first };\n }\n case 'M':\n case 'T': {\n return { status: 'modified', basePath: first, headPath: first };\n }\n default: {\n return undefined;\n }\n }\n}\n\n/**\n * Repository-relative paths git considers part of the project AND that exist in the working tree:\n * tracked files (minus worktree deletions) plus untracked non-ignored ones. The diff gate scans\n * exactly these, so local ignored artifacts (build output, generated copies) are neither measured\n * nor allowed to skew the duplication universes.\n */\nexport async function listRepositoryFiles(repoRoot: string): Promise<Set<string>> {\n const [listed, deleted] = await Promise.all([\n runGit(repoRoot, ['ls-files', '--cached', '--others', '--exclude-standard', '-z']),\n runGit(repoRoot, ['ls-files', '--deleted', '-z']),\n ]);\n const files = new Set(listed.split('\\0').filter((path) => path !== ''));\n for (const path of deleted.split('\\0')) {\n files.delete(path);\n }\n return files;\n}\n\n/** Paths that are symbolic links (blob mode 120000) at the given revision. */\nexport async function listSymlinkPathsAtRevision(repoRoot: string, revision: string): Promise<Set<string>> {\n const output = await runGit(repoRoot, ['ls-tree', '-r', '-z', revision]);\n const links = new Set<string>();\n for (const entry of output.split('\\0')) {\n const tabIndex = entry.indexOf('\\t');\n if (entry.startsWith('120000 ') && tabIndex !== -1) {\n links.add(entry.slice(tabIndex + 1));\n }\n }\n return links;\n}\n\n/** The file's content at the given commit; the path is repository-relative with forward slashes. */\nexport async function readFileAtRevision(repoRoot: string, revision: string, path: string): Promise<string> {\n return await runGit(repoRoot, ['cat-file', 'blob', `${revision}:${path}`]);\n}\n\nasync function runGit(cwd: string, args: string[]): Promise<string> {\n try {\n const { stdout } = await execFileAsync('git', args, { cwd, maxBuffer: maxOutputBytes, encoding: 'utf8' });\n return stdout;\n } catch (error) {\n const stderr = (error as { stderr?: string }).stderr?.trim();\n throw new Error(`git ${args.slice(0, 2).join(' ')} failed${stderr ? `: ${stderr}` : ''}`);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"git.js","names":[],"sources":["../src/git.ts"],"sourcesContent":["import { execFile, spawn } from 'node:child_process';\nimport { mkdtemp, rm } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\nimport type { LineHunk } from './newDuplication.js';\n\nconst execFileAsync = promisify(execFile);\n\n/** Large enough for whole-file blobs; execFile's 1 MiB default truncates real sources. */\nconst maxOutputBytes = 512 * 1024 * 1024;\n\n/** One entry of the working tree's diff against the merge-base commit. */\nexport interface ChangedFile {\n status: 'added' | 'modified' | 'deleted' | 'renamed';\n /** Path at the merge-base, absent for added files; differs from headPath only for renames. */\n basePath?: string;\n /** Path in the working tree; for deleted files, the (now missing) base path. */\n headPath: string;\n}\n\nexport async function resolveRepoRoot(directory: string): Promise<string> {\n const output = await runGit(directory, ['rev-parse', '--show-toplevel']);\n return output.trim();\n}\n\nexport async function resolveMergeBase(repoRoot: string, baseRef: string): Promise<string> {\n const output = await runGit(repoRoot, ['merge-base', baseRef, 'HEAD']);\n return output.trim();\n}\n\n/**\n * Files whose working-tree content differs from the merge-base commit (staged or not), with\n * rename detection, plus untracked (non-ignored) files as additions. Copies gate like additions:\n * only the new path is measured against the new-code thresholds.\n */\nexport async function listChangedFiles(repoRoot: string, mergeBase: string): Promise<ChangedFile[]> {\n const [diffOutput, untrackedOutput] = await Promise.all([\n runGit(repoRoot, ['diff', '--name-status', '--find-renames', '-z', mergeBase]),\n runGit(repoRoot, ['ls-files', '--others', '--exclude-standard', '-z']),\n ]);\n\n const files = parseNameStatusEntries(diffOutput);\n for (const path of untrackedOutput.split('\\0')) {\n if (path !== '') {\n files.push({ status: 'added', headPath: path });\n }\n }\n return files;\n}\n\n/** Parses `git diff --name-status -z` output: `<status>\\0<path>\\0`, with two paths for R/C. */\nfunction parseNameStatusEntries(output: string): ChangedFile[] {\n const files: ChangedFile[] = [];\n const parts = output.split('\\0');\n let index = 0;\n while (index < parts.length - 1) {\n const kind = (parts[index] as string).charAt(0);\n const pathCount = kind === 'R' || kind === 'C' ? 2 : 1;\n const changed = toChangedFile(kind, parts.slice(index + 1, index + 1 + pathCount));\n if (changed) {\n files.push(changed);\n }\n index += 1 + pathCount;\n }\n return files;\n}\n\n/** Undefined for unmerged (U) and unknown statuses, which have nothing to gate. */\nfunction toChangedFile(kind: string, paths: (string | undefined)[]): ChangedFile | undefined {\n const [first, second] = paths;\n if (first === undefined) {\n return undefined;\n }\n switch (kind) {\n case 'R': {\n return second === undefined ? undefined : { status: 'renamed', basePath: first, headPath: second };\n }\n // A copy gates like an addition: only the new path is measured against the new-code thresholds.\n case 'C': {\n return second === undefined ? undefined : { status: 'added', headPath: second };\n }\n case 'A': {\n return { status: 'added', headPath: first };\n }\n case 'D': {\n return { status: 'deleted', basePath: first, headPath: first };\n }\n case 'M':\n case 'T': {\n return { status: 'modified', basePath: first, headPath: first };\n }\n default: {\n return undefined;\n }\n }\n}\n\n/**\n * Repository-relative paths git considers part of the project AND that exist in the working tree:\n * tracked files (minus worktree deletions) plus untracked non-ignored ones. The diff gate scans\n * exactly these, so local ignored artifacts (build output, generated copies) are neither measured\n * nor allowed to skew the duplication universes.\n */\nexport async function listRepositoryFiles(repoRoot: string): Promise<Set<string>> {\n const [listed, deleted] = await Promise.all([\n runGit(repoRoot, ['ls-files', '--cached', '--others', '--exclude-standard', '-z']),\n runGit(repoRoot, ['ls-files', '--deleted', '-z']),\n ]);\n const files = new Set(listed.split('\\0').filter((path) => path !== ''));\n for (const path of deleted.split('\\0')) {\n files.delete(path);\n }\n return files;\n}\n\n/** Paths that are symbolic links (blob mode 120000) at the given revision. */\nexport async function listSymlinkPathsAtRevision(repoRoot: string, revision: string): Promise<Set<string>> {\n const output = await runGit(repoRoot, ['ls-tree', '-r', '-z', revision]);\n const links = new Set<string>();\n for (const entry of output.split('\\0')) {\n const tabIndex = entry.indexOf('\\t');\n if (entry.startsWith('120000 ') && tabIndex !== -1) {\n links.add(entry.slice(tabIndex + 1));\n }\n }\n return links;\n}\n\n/**\n * The line hunks turning the merge-base blob at `basePath` into the working-tree file at\n * `headPath` (equal unless renamed), as `git diff` computes them for review.\n */\nexport async function listLineHunks(\n repoRoot: string,\n mergeBase: string,\n basePath: string,\n headPath: string\n): Promise<LineHunk[]> {\n // Literal pathspecs: a name like `app/[slug]/page.tsx` must not glob-match other files.\n const paths = (basePath === headPath ? [headPath] : [basePath, headPath]).map((file) => `:(literal)${file}`);\n const output = await runGit(repoRoot, [\n // The algorithm and indent heuristic decide which lines count as added, so user config must\n // not change them; `-c` values an older git does not know are ignored rather than rejected.\n '-c',\n 'diff.algorithm=myers',\n '-c',\n 'diff.indentHeuristic=true',\n 'diff',\n '--unified=0',\n // diff.interHunkContext would otherwise merge nearby hunks, and the unchanged lines between\n // them would count as added.\n '--inter-hunk-context=0',\n // A file git would show as binary (a `-diff` attribute, a NUL byte) must still yield hunks.\n '--text',\n '--no-color',\n '--no-ext-diff',\n '--no-textconv',\n '--find-renames',\n mergeBase,\n '--',\n ...paths,\n ]);\n const hunks: LineHunk[] = [];\n for (const match of output.matchAll(/^@@ -(\\d+)(?:,(\\d+))? \\+(\\d+)(?:,(\\d+))? @@/gmu)) {\n const [, baseStart, baseCount, headStart, headCount] = match;\n hunks.push({\n baseStart: Number(baseStart),\n baseCount: baseCount === undefined ? 1 : Number(baseCount),\n headStart: Number(headStart),\n headCount: headCount === undefined ? 1 : Number(headCount),\n });\n }\n return hunks;\n}\n\n/** A file's `linguist-generated` / `linguist-vendored` attributes; undefined when unspecified. */\nexport interface LinguistAttributes {\n generated?: boolean;\n vendored?: boolean;\n}\n\n/**\n * The linguist attributes of repository-relative paths, from the working tree's `.gitattributes`\n * or, with `source`, from that revision's. Paths with neither attribute specified are omitted.\n */\nexport async function readLinguistAttributes(\n repoRoot: string,\n paths: Iterable<string>,\n source?: string\n): Promise<Map<string, LinguistAttributes>> {\n const input = [...paths].join('\\0');\n if (input === '') {\n return new Map();\n }\n const attributeArgs = ['--stdin', '-z', 'linguist-generated', 'linguist-vendored'];\n let output;\n if (source === undefined) {\n output = await runGitWithInput(repoRoot, ['check-attr', ...attributeArgs], input);\n } else {\n // `check-attr --source` needs git 2.40, so the revision's attributes are read from a\n // throwaway index holding its tree instead, which `--cached` supports on any git.\n const indexDirectory = await mkdtemp(path.join(os.tmpdir(), 'code-gauge-index-'));\n try {\n const env = { ...process.env, GIT_INDEX_FILE: path.join(indexDirectory, 'index') };\n await runGitWithInput(repoRoot, ['read-tree', source], '', env);\n output = await runGitWithInput(repoRoot, ['check-attr', '--cached', ...attributeArgs], input, env);\n } finally {\n await rm(indexDirectory, { recursive: true, force: true });\n }\n }\n const attributesByPath = new Map<string, LinguistAttributes>();\n const fields = output.split('\\0');\n for (let index = 0; index + 2 < fields.length; index += 3) {\n const [file, attribute, value] = fields.slice(index, index + 3) as [string, string, string];\n const enabled = toAttributeFlag(value);\n if (enabled === undefined) {\n continue;\n }\n const attributes = attributesByPath.get(file) ?? {};\n if (attribute === 'linguist-generated') {\n attributes.generated = enabled;\n } else {\n attributes.vendored = enabled;\n }\n attributesByPath.set(file, attributes);\n }\n return attributesByPath;\n}\n\n/** Linguist reads `attr`/`attr=true` as set and `-attr`/`attr=false` as explicitly unset. */\nfunction toAttributeFlag(value: string): boolean | undefined {\n if (value === 'set' || value === 'true') {\n return true;\n }\n if (value === 'unset' || value === 'false') {\n return false;\n }\n return undefined;\n}\n\n/** The file's content at the given commit; the path is repository-relative with forward slashes. */\nexport async function readFileAtRevision(repoRoot: string, revision: string, path: string): Promise<string> {\n return await runGit(repoRoot, ['cat-file', 'blob', `${revision}:${path}`]);\n}\n\nasync function runGitWithInput(\n cwd: string,\n args: string[],\n input: string,\n env: NodeJS.ProcessEnv = process.env\n): Promise<string> {\n return await new Promise((resolve, reject) => {\n const child = spawn('git', args, { cwd, env });\n // git exiting before it reads all input (e.g. on a usage error) makes the write fail with\n // EPIPE; its exit code and stderr, reported on close, already describe the failure.\n child.stdin.on('error', () => {});\n const stdout: Buffer[] = [];\n const stderr: Buffer[] = [];\n child.stdout.on('data', (chunk: Buffer) => stdout.push(chunk));\n child.stderr.on('data', (chunk: Buffer) => stderr.push(chunk));\n child.on('error', reject);\n child.on('close', (code) => {\n if (code === 0) {\n resolve(Buffer.concat(stdout).toString('utf8'));\n return;\n }\n const message = Buffer.concat(stderr).toString('utf8').trim();\n reject(new Error(`git ${describeCommand(args)} failed${message ? `: ${message}` : ''}`));\n });\n child.stdin.end(input);\n });\n}\n\nasync function runGit(cwd: string, args: string[]): Promise<string> {\n try {\n const { stdout } = await execFileAsync('git', args, { cwd, maxBuffer: maxOutputBytes, encoding: 'utf8' });\n return stdout;\n } catch (error) {\n const stderr = (error as { stderr?: string }).stderr?.trim();\n throw new Error(`git ${describeCommand(args)} failed${stderr ? `: ${stderr}` : ''}`);\n }\n}\n\n/** The subcommand and its first argument, skipping leading `-c key=value` overrides. */\nfunction describeCommand(args: readonly string[]): string {\n let start = 0;\n while (args[start] === '-c') {\n start += 2;\n }\n return args.slice(start, start + 2).join(' ');\n}\n"],"mappings":"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"}
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./crossFileDuplication.cjs"),t=require("./languages.cjs"),n=require("./metrics.cjs"),r=require("./regressionGate.cjs");exports.TreeMeasurer=n.TreeMeasurer,exports.collectCrossFileDuplicationFileData=n.collectCrossFileDuplicationFileData,exports.collectDuplicationCandidates=n.collectDuplicationCandidates,exports.collectFunctionTokenSequences=n.collectFunctionTokenSequences,exports.defaultGateOptions=
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./crossFileDuplication.cjs"),t=require("./languages.cjs"),n=require("./metrics.cjs"),r=require("./newDuplication.cjs"),i=require("./regressionGate.cjs");exports.TreeMeasurer=n.TreeMeasurer,exports.collectCrossFileDuplicationFileData=n.collectCrossFileDuplicationFileData,exports.collectDuplicationCandidates=n.collectDuplicationCandidates,exports.collectFunctionTokenSequences=n.collectFunctionTokenSequences,exports.defaultGateOptions=i.defaultGateOptions,exports.defaultLanguages=t.defaultLanguages,exports.defaultMeasurer=n.defaultMeasurer,exports.detectLanguage=t.detectLanguage,exports.evaluateRegressionGate=i.evaluateRegressionGate,exports.findNewlyDuplicatedLines=r.findNewlyDuplicatedLines,exports.measureCode=n.measureCode,exports.measureCrossFileDuplication=e.measureCrossFileDuplication,exports.supportedLanguages=t.supportedLanguages;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export type { CrossFileDuplicateBlockGroup, CrossFileDuplicateOccurrence, CrossF
|
|
|
3
3
|
export type { CrossFileDuplicateCandidate, CrossFileDuplicationFileData } from './duplication.js';
|
|
4
4
|
export { defaultLanguages, detectLanguage, supportedLanguages } from './languages.js';
|
|
5
5
|
export { TreeMeasurer, collectCrossFileDuplicationFileData, collectDuplicationCandidates, collectFunctionTokenSequences, defaultMeasurer, measureCode, } from './metrics.js';
|
|
6
|
+
export { findNewlyDuplicatedLines } from './newDuplication.js';
|
|
7
|
+
export type { CloneOccurrence, DuplicationChange, LineHunk } from './newDuplication.js';
|
|
6
8
|
export { defaultGateOptions, evaluateRegressionGate } from './regressionGate.js';
|
|
7
9
|
export type { GateFileInput, GateOptions, GateResult, GateTolerances, GateViolation, NewFunctionThresholds, } from './regressionGate.js';
|
|
8
10
|
export type { CodeMetrics, DuplicationMetrics, DuplicationOptions, FunctionMetrics, HalsteadMetrics, LanguageDefinition, LanguageName, LineMetrics, MeasureOptions, SupportedLanguage, } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{defaultLanguages as t,detectLanguage as n,supportedLanguages as r}from"./languages.js";import{TreeMeasurer as i,collectCrossFileDuplicationFileData as a,collectDuplicationCandidates as o,collectFunctionTokenSequences as s,defaultMeasurer as c,measureCode as l}from"./metrics.js";import{defaultGateOptions as
|
|
1
|
+
import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{defaultLanguages as t,detectLanguage as n,supportedLanguages as r}from"./languages.js";import{TreeMeasurer as i,collectCrossFileDuplicationFileData as a,collectDuplicationCandidates as o,collectFunctionTokenSequences as s,defaultMeasurer as c,measureCode as l}from"./metrics.js";import{findNewlyDuplicatedLines as u}from"./newDuplication.js";import{defaultGateOptions as d,evaluateRegressionGate as f}from"./regressionGate.js";export{i as TreeMeasurer,a as collectCrossFileDuplicationFileData,o as collectDuplicationCandidates,s as collectFunctionTokenSequences,d as defaultGateOptions,t as defaultLanguages,c as defaultMeasurer,n as detectLanguage,f as evaluateRegressionGate,u as findNewlyDuplicatedLines,l as measureCode,e as measureCrossFileDuplication,r as supportedLanguages};
|
|
@@ -0,0 +1,2 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
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>>;
|
|
@@ -0,0 +1,2 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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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"}
|
package/dist/regressionGate.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const e=require("./duplication.cjs"),t={newFunction:{maxCognitiveComplexity:15,maxNcss:60,maxNestingDepth:4},tolerance:{cognitiveComplexity:2,ncss:5,nestingDepth:1,depDegree:10,halsteadVolume:150,fileNcss:20,duplicateLines:0},matchSimilarityPercent:70},n=String,r=[{metric:`cognitive complexity`,value:e=>e.cognitiveComplexity,allowance:e=>e.cognitiveComplexity,remediation:`Simplify the added branching or split the function.`,format:n},{metric:`NCSS`,value:e=>e.ncss,allowance:e=>e.ncss,remediation:`Extract the added statements into helper functions or remove them.`,format:n},{metric:`max nesting depth`,value:e=>e.nestingDepth,allowance:e=>e.nestingDepth,remediation:`Flatten the added nesting with early returns or extracted helpers.`,format:n},{metric:`DepDegree`,value:e=>e.depDegree,allowance:e=>e.depDegree,remediation:`Reduce how many earlier definitions the code reads, e.g. by splitting the data flow.`,format:n},{metric:`Halstead volume`,value:e=>e.halstead.volume,allowance:(e,t)=>Math.max(e.halsteadVolume,t*.25),remediation:`Shrink the function: fewer distinct names and operations.`,format:e=>e.toFixed(1)}],i=[{metric:`cognitive complexity`,value:e=>e.cognitiveComplexity,limit:e=>e.maxCognitiveComplexity,remediation:`Split the new function or simplify its branching.`},{metric:`NCSS`,value:e=>e.ncss,limit:e=>e.maxNcss,remediation:`Split the new function into smaller ones.`},{metric:`max nesting depth`,value:e=>e.nestingDepth,limit:e=>e.maxNestingDepth,remediation:`Flatten the new function with early returns or extracted helpers.`}];function a(e,t){let n=l(e,t.matchSimilarityPercent),r=[],i=[],a=0,c=0;for(let o of n.pairs){let n=e[o.headFileIndex];n.gated!==!1&&(a+=1,r.push(...y(n.file,o.base,o.head,t.tolerance)),i.push(s(n.file,o.head,o.base)))}let u=0;for(let[a,l]of e.entries()){if(l.gated===!1)continue;u+=1;let e=l.headMetrics?.functions??[],d=n.headMatched[a];for(let[n,a]of e.entries())d[n]||(c+=1,r.push(...b(l.file,a,t.newFunction)),i.push(s(l.file,a)));r.push(...x(l,o(l,a,n),r,t)),r.push(...C(l,t.tolerance))}return r.sort((e,t)=>e.file.localeCompare(t.file)||e.startLine-t.startLine),i.sort((e,t)=>e.file.localeCompare(t.file)||e.startLine-t.startLine),{violations:r,checkedFileCount:u,checkedFunctionCount:a+c,newFunctionCount:c,checkedFunctions:i}}function o(e,t,n){let r=e.baseMetrics?.functions??[],i=n.baseMatched[t],a=n.headMatched[t],o=r.flatMap((e,t)=>!i[t]&&e.name!==void 0?[t]:[]);if(o.length===0)return!1;let s=(e.headMetrics?.functions??[]).flatMap((e,t)=>a[t]?[]:[t]);return o.length*s.length>f?!1:o.some(t=>s.some(n=>v(e.baseFunctionTokens?.[t],e.headFunctionTokens?.[n],30)>=30))}function s(e,t,n){return{file:e,name:t.name??`<anonymous>`,startLine:t.startLine,endLine:t.endLine,base:n?c(n):void 0,head:c(t)}}function c(e){return{cognitiveComplexity:e.cognitiveComplexity,ncss:e.ncss,nestingDepth:e.nestingDepth,depDegree:e.depDegree,halsteadVolume:e.halstead.volume}}function l(e,t){let n=e.map(e=>(e.baseMetrics?.functions??[]).map(()=>!1)),r=e.map(e=>(e.headMetrics?.functions??[]).map(()=>!1)),i=[];for(let t of e.keys()){let a=a=>{let o=d(u(e,n,`base`,t),a),s=d(u(e,r,`head`,t),a);for(let[e,t]of o)p(t,s.get(e)??[],n,r,i)};a(e=>e.name===void 0?void 0:`${e.name}\u0000${e.parameterCount}`),a(e=>e.name)}return m(e,n,r,i,t),{pairs:i,baseMatched:n,headMatched:r}}function u(e,t,n,r){let i=[];for(let[a,o]of e.entries()){if(r!==void 0&&a!==r)continue;let e=t[a],s=n===`base`?o.baseMetrics:o.headMetrics,c=n===`base`?o.baseFunctionTokens:o.headFunctionTokens;for(let[t,n]of(s?.functions??[]).entries())e[t]||i.push({fileIndex:a,functionIndex:t,fn:n,tokens:c?.[t]})}return i}function d(e,t){let n=/* @__PURE__ */ new Map;for(let r of e){let e=t(r.fn);if(e===void 0)continue;let i=n.get(e)??[];i.push(r),n.set(e,i)}return n}const f=1e4;function p(e,t,n,r,i){let a=e.length+t.length>2&&e.length*t.length<=f,o=[];for(let[n,r]of e.entries())for(let[e,i]of t.entries())o.push({similarity:a?v(r.tokens,i.tokens,0):0,basePosition:n,headPosition:e});h(o,e,t,n,r,i)}function m(e,t,n,r,i){let a=u(e,t,`base`),o=u(e,n,`head`);if(a.length*o.length>f)return;let s=[];for(let[e,t]of a.entries())for(let[n,r]of o.entries()){let a=v(t.tokens,r.tokens,i);a>=i&&s.push({similarity:a,basePosition:e,headPosition:n})}h(s,a,o,t,n,r)}function h(e,t,n,r,i,a){e.sort((e,t)=>t.similarity-e.similarity||e.basePosition-t.basePosition||e.headPosition-t.headPosition);let o=/* @__PURE__ */ new Map,s=/* @__PURE__ */ new Map,c=/* @__PURE__ */ new Set,l=g(e,o,s,c);for(let e of[...o.keys()].toSorted((e,t)=>e-t))l.has(e)||_(e,o,s,c,/* @__PURE__ */ new Set);for(let[e,o]of[...s.entries()].toSorted((e,t)=>e[0]-t[0])){let s=t[o],c=n[e];r[s.fileIndex][s.functionIndex]=!0,i[c.fileIndex][c.functionIndex]=!0,a.push({base:s.fn,head:c.fn,headFileIndex:c.fileIndex})}}function g(e,t,n,r){let i=/* @__PURE__ */ new Set;for(let a of e){let e=t.get(a.basePosition)??[];e.push(a.headPosition),t.set(a.basePosition,e),!i.has(a.basePosition)&&!n.has(a.headPosition)&&(n.set(a.headPosition,a.basePosition),i.add(a.basePosition),a.similarity===100&&r.add(a.headPosition))}return i}function _(e,t,n,r,i){for(let a of t.get(e)??[]){if(i.has(a)||r.has(a))continue;i.add(a);let o=n.get(a);if(o===void 0||_(o,t,n,r,i))return n.set(a,e),!0}return!1}function v(t,n,r){if(!t||!n||t.length===0||n.length===0)return 0;let i=Math.max(t.length,n.length);return Math.min(t.length,n.length)*100<r*i?0:e.lcsLength(t,n)*100/i}function y(e,t,n,i){let a=[];for(let o of r){let r=o.value(t),s=o.value(n),c=r+o.allowance(i,r);if(s>c){let t=n.name??`<anonymous>`;a.push({gate:`function-regression`,metric:o.metric,file:e,functionName:t,startLine:n.startLine,endLine:n.endLine,baseValue:r,headValue:s,allowedValue:c,message:`${e}:${n.startLine}-${n.endLine} ${t}: ${o.metric} worsened ${o.format(r)} -> ${o.format(s)} (allowed <= ${o.format(c)}). `+o.remediation})}}return a}function b(e,t,n){let r=[];for(let a of i){let i=a.value(t),o=a.limit(n);if(i>o){let n=t.name??`<anonymous>`;r.push({gate:`new-function`,metric:a.metric,file:e,functionName:n,startLine:t.startLine,endLine:t.endLine,headValue:i,allowedValue:o,message:`${e}:${t.startLine}-${t.endLine} new function ${n}: ${a.metric} ${i} exceeds the new-code limit ${o}. ${a.remediation}`})}}return r}function x(e,t,n,r){let{baseMetrics:i,headMetrics:a}=e;if(!i||!a||!t)return[];let o=[],s=r.tolerance,c=n.some(t=>t.file===e.file&&t.metric===`cognitive complexity`&&(t.gate===`function-regression`||t.gate===`new-function`)&&t.headValue===a.maxCognitiveComplexity),l=i.maxCognitiveComplexity+s.cognitiveComplexity;if(!c&&a.maxCognitiveComplexity>l){let t=S(a.functions),n=t?.startLine??1,r=t?.endLine??a.lines.total;o.push({gate:`file-regression`,metric:`file max cognitive complexity`,file:e.file,functionName:t?.name??void 0,startLine:n,endLine:r,baseValue:i.maxCognitiveComplexity,headValue:a.maxCognitiveComplexity,allowedValue:l,message:`${e.file}:${n}-${r}: the file's max cognitive complexity worsened ${i.maxCognitiveComplexity} -> ${a.maxCognitiveComplexity} (allowed <= ${l}). Simplify or split the most complex function${t?` (${t.name??`<anonymous>`})`:``}.`})}let u=i.ncssCount+s.fileNcss;return a.ncssCount>u&&o.push({gate:`file-regression`,metric:`file NCSS`,file:e.file,startLine:1,endLine:a.lines.total,baseValue:i.ncssCount,headValue:a.ncssCount,allowedValue:u,message:`${e.file}:1-${a.lines.total}: functions were removed or split while the file grew from NCSS ${i.ncssCount} to ${a.ncssCount} (allowed <= ${u}). Remove the added statements or move genuinely new code into its own module.`}),o}function S(e){let t;for(let n of e)(!t||n.cognitiveComplexity>t.cognitiveComplexity)&&(t=n);return t}function C(e,t){let n=e.
|
|
1
|
+
"use strict";const e=require("./duplication.cjs"),t={newFunction:{maxCognitiveComplexity:15,maxNcss:60,maxNestingDepth:4},tolerance:{cognitiveComplexity:2,ncss:5,nestingDepth:1,depDegree:10,halsteadVolume:150,fileNcss:20,duplicateLines:0},matchSimilarityPercent:70},n=String,r=[{metric:`cognitive complexity`,value:e=>e.cognitiveComplexity,allowance:e=>e.cognitiveComplexity,remediation:`Simplify the added branching or split the function.`,format:n},{metric:`NCSS`,value:e=>e.ncss,allowance:e=>e.ncss,remediation:`Extract the added statements into helper functions or remove them.`,format:n},{metric:`max nesting depth`,value:e=>e.nestingDepth,allowance:e=>e.nestingDepth,remediation:`Flatten the added nesting with early returns or extracted helpers.`,format:n},{metric:`DepDegree`,value:e=>e.depDegree,allowance:e=>e.depDegree,remediation:`Reduce how many earlier definitions the code reads, e.g. by splitting the data flow.`,format:n},{metric:`Halstead volume`,value:e=>e.halstead.volume,allowance:(e,t)=>Math.max(e.halsteadVolume,t*.25),remediation:`Shrink the function: fewer distinct names and operations.`,format:e=>e.toFixed(1)}],i=[{metric:`cognitive complexity`,value:e=>e.cognitiveComplexity,limit:e=>e.maxCognitiveComplexity,remediation:`Split the new function or simplify its branching.`},{metric:`NCSS`,value:e=>e.ncss,limit:e=>e.maxNcss,remediation:`Split the new function into smaller ones.`},{metric:`max nesting depth`,value:e=>e.nestingDepth,limit:e=>e.maxNestingDepth,remediation:`Flatten the new function with early returns or extracted helpers.`}];function a(e,t){let n=l(e,t.matchSimilarityPercent),r=[],i=[],a=0,c=0;for(let o of n.pairs){let n=e[o.headFileIndex];n.gated!==!1&&(a+=1,r.push(...y(n.file,o.base,o.head,t.tolerance)),i.push(s(n.file,o.head,o.base)))}let u=0;for(let[a,l]of e.entries()){if(l.gated===!1)continue;u+=1;let e=l.headMetrics?.functions??[],d=n.headMatched[a];for(let[n,a]of e.entries())d[n]||(c+=1,r.push(...b(l.file,a,t.newFunction)),i.push(s(l.file,a)));r.push(...x(l,o(l,a,n),r,t)),r.push(...C(l,t.tolerance))}return r.sort((e,t)=>e.file.localeCompare(t.file)||e.startLine-t.startLine),i.sort((e,t)=>e.file.localeCompare(t.file)||e.startLine-t.startLine),{violations:r,checkedFileCount:u,checkedFunctionCount:a+c,newFunctionCount:c,checkedFunctions:i}}function o(e,t,n){let r=e.baseMetrics?.functions??[],i=n.baseMatched[t],a=n.headMatched[t],o=r.flatMap((e,t)=>!i[t]&&e.name!==void 0?[t]:[]);if(o.length===0)return!1;let s=(e.headMetrics?.functions??[]).flatMap((e,t)=>a[t]?[]:[t]);return o.length*s.length>f?!1:o.some(t=>s.some(n=>v(e.baseFunctionTokens?.[t],e.headFunctionTokens?.[n],30)>=30))}function s(e,t,n){return{file:e,name:t.name??`<anonymous>`,startLine:t.startLine,endLine:t.endLine,base:n?c(n):void 0,head:c(t)}}function c(e){return{cognitiveComplexity:e.cognitiveComplexity,ncss:e.ncss,nestingDepth:e.nestingDepth,depDegree:e.depDegree,halsteadVolume:e.halstead.volume}}function l(e,t){let n=e.map(e=>(e.baseMetrics?.functions??[]).map(()=>!1)),r=e.map(e=>(e.headMetrics?.functions??[]).map(()=>!1)),i=[];for(let t of e.keys()){let a=a=>{let o=d(u(e,n,`base`,t),a),s=d(u(e,r,`head`,t),a);for(let[e,t]of o)p(t,s.get(e)??[],n,r,i)};a(e=>e.name===void 0?void 0:`${e.name}\u0000${e.parameterCount}`),a(e=>e.name)}return m(e,n,r,i,t),{pairs:i,baseMatched:n,headMatched:r}}function u(e,t,n,r){let i=[];for(let[a,o]of e.entries()){if(r!==void 0&&a!==r)continue;let e=t[a],s=n===`base`?o.baseMetrics:o.headMetrics,c=n===`base`?o.baseFunctionTokens:o.headFunctionTokens;for(let[t,n]of(s?.functions??[]).entries())e[t]||i.push({fileIndex:a,functionIndex:t,fn:n,tokens:c?.[t]})}return i}function d(e,t){let n=/* @__PURE__ */ new Map;for(let r of e){let e=t(r.fn);if(e===void 0)continue;let i=n.get(e)??[];i.push(r),n.set(e,i)}return n}const f=1e4;function p(e,t,n,r,i){let a=e.length+t.length>2&&e.length*t.length<=f,o=[];for(let[n,r]of e.entries())for(let[e,i]of t.entries())o.push({similarity:a?v(r.tokens,i.tokens,0):0,basePosition:n,headPosition:e});h(o,e,t,n,r,i)}function m(e,t,n,r,i){let a=u(e,t,`base`),o=u(e,n,`head`);if(a.length*o.length>f)return;let s=[];for(let[e,t]of a.entries())for(let[n,r]of o.entries()){let a=v(t.tokens,r.tokens,i);a>=i&&s.push({similarity:a,basePosition:e,headPosition:n})}h(s,a,o,t,n,r)}function h(e,t,n,r,i,a){e.sort((e,t)=>t.similarity-e.similarity||e.basePosition-t.basePosition||e.headPosition-t.headPosition);let o=/* @__PURE__ */ new Map,s=/* @__PURE__ */ new Map,c=/* @__PURE__ */ new Set,l=g(e,o,s,c);for(let e of[...o.keys()].toSorted((e,t)=>e-t))l.has(e)||_(e,o,s,c,/* @__PURE__ */ new Set);for(let[e,o]of[...s.entries()].toSorted((e,t)=>e[0]-t[0])){let s=t[o],c=n[e];r[s.fileIndex][s.functionIndex]=!0,i[c.fileIndex][c.functionIndex]=!0,a.push({base:s.fn,head:c.fn,headFileIndex:c.fileIndex})}}function g(e,t,n,r){let i=/* @__PURE__ */ new Set;for(let a of e){let e=t.get(a.basePosition)??[];e.push(a.headPosition),t.set(a.basePosition,e),!i.has(a.basePosition)&&!n.has(a.headPosition)&&(n.set(a.headPosition,a.basePosition),i.add(a.basePosition),a.similarity===100&&r.add(a.headPosition))}return i}function _(e,t,n,r,i){for(let a of t.get(e)??[]){if(i.has(a)||r.has(a))continue;i.add(a);let o=n.get(a);if(o===void 0||_(o,t,n,r,i))return n.set(a,e),!0}return!1}function v(t,n,r){if(!t||!n||t.length===0||n.length===0)return 0;let i=Math.max(t.length,n.length);return Math.min(t.length,n.length)*100<r*i?0:e.lcsLength(t,n)*100/i}function y(e,t,n,i){let a=[];for(let o of r){let r=o.value(t),s=o.value(n),c=r+o.allowance(i,r);if(s>c){let t=n.name??`<anonymous>`;a.push({gate:`function-regression`,metric:o.metric,file:e,functionName:t,startLine:n.startLine,endLine:n.endLine,baseValue:r,headValue:s,allowedValue:c,message:`${e}:${n.startLine}-${n.endLine} ${t}: ${o.metric} worsened ${o.format(r)} -> ${o.format(s)} (allowed <= ${o.format(c)}). `+o.remediation})}}return a}function b(e,t,n){let r=[];for(let a of i){let i=a.value(t),o=a.limit(n);if(i>o){let n=t.name??`<anonymous>`;r.push({gate:`new-function`,metric:a.metric,file:e,functionName:n,startLine:t.startLine,endLine:t.endLine,headValue:i,allowedValue:o,message:`${e}:${t.startLine}-${t.endLine} new function ${n}: ${a.metric} ${i} exceeds the new-code limit ${o}. ${a.remediation}`})}}return r}function x(e,t,n,r){let{baseMetrics:i,headMetrics:a}=e;if(!i||!a||!t)return[];let o=[],s=r.tolerance,c=n.some(t=>t.file===e.file&&t.metric===`cognitive complexity`&&(t.gate===`function-regression`||t.gate===`new-function`)&&t.headValue===a.maxCognitiveComplexity),l=i.maxCognitiveComplexity+s.cognitiveComplexity;if(!c&&a.maxCognitiveComplexity>l){let t=S(a.functions),n=t?.startLine??1,r=t?.endLine??a.lines.total;o.push({gate:`file-regression`,metric:`file max cognitive complexity`,file:e.file,functionName:t?.name??void 0,startLine:n,endLine:r,baseValue:i.maxCognitiveComplexity,headValue:a.maxCognitiveComplexity,allowedValue:l,message:`${e.file}:${n}-${r}: the file's max cognitive complexity worsened ${i.maxCognitiveComplexity} -> ${a.maxCognitiveComplexity} (allowed <= ${l}). Simplify or split the most complex function${t?` (${t.name??`<anonymous>`})`:``}.`})}let u=i.ncssCount+s.fileNcss;return a.ncssCount>u&&o.push({gate:`file-regression`,metric:`file NCSS`,file:e.file,startLine:1,endLine:a.lines.total,baseValue:i.ncssCount,headValue:a.ncssCount,allowedValue:u,message:`${e.file}:1-${a.lines.total}: functions were removed or split while the file grew from NCSS ${i.ncssCount} to ${a.ncssCount} (allowed <= ${u}). Remove the added statements or move genuinely new code into its own module.`}),o}function S(e){let t;for(let n of e)(!t||n.cognitiveComplexity>t.cognitiveComplexity)&&(t=n);return t}function C(e,t){let n=e.newlyDuplicatedLines,r=t.duplicateLines;if(n.length<=r)return[];let i=n[0],a=n.at(-1),o=e.duplicationPartners.slice(0,3).join(`, `);return[{gate:`duplication`,metric:`newly duplicated lines`,file:e.file,startLine:i,endLine:a,headValue:n.length,allowedValue:r,message:`${e.file}:${i}-${a}: ${n.length} changed lines duplicate existing code (lines ${w(n)}; allowed <= ${r}). Deduplicate${o?` against ${o}`:` the repeated code`} by extracting a shared helper.`}]}function w(e){let t=[],n=e[0];for(let[r,i]of e.entries()){let a=e[r+1];a!==i+1&&(t.push(n===i?String(i):`${n}-${i}`),n=a)}return t.join(`, `)}exports.defaultGateOptions=t,exports.evaluateRegressionGate=a;
|
|
2
2
|
//# sourceMappingURL=regressionGate.cjs.map
|