code-gauge 3.0.0 → 4.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 +83 -21
- 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 +11 -0
- package/dist/cliConfig.js +1 -1
- package/dist/cliConfig.js.map +1 -1
- package/dist/crossFileDuplication.cjs +1 -1
- package/dist/crossFileDuplication.cjs.map +1 -1
- package/dist/crossFileDuplication.js +1 -1
- package/dist/crossFileDuplication.js.map +1 -1
- package/dist/diffCommand.cjs +5 -0
- package/dist/diffCommand.cjs.map +1 -0
- package/dist/diffCommand.d.ts +17 -0
- package/dist/diffCommand.js +5 -0
- package/dist/diffCommand.js.map +1 -0
- package/dist/duplication.cjs +1 -1
- package/dist/duplication.cjs.map +1 -1
- package/dist/duplication.d.ts +20 -24
- package/dist/duplication.js +1 -1
- package/dist/duplication.js.map +1 -1
- package/dist/git.cjs +2 -0
- package/dist/git.cjs.map +1 -0
- package/dist/git.d.ts +27 -0
- package/dist/git.js +2 -0
- package/dist/git.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/languages.cjs +1 -1
- package/dist/languages.cjs.map +1 -1
- package/dist/languages.d.ts +5 -0
- package/dist/languages.js +1 -1
- package/dist/languages.js.map +1 -1
- package/dist/metrics.cjs +1 -1
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.ts +18 -5
- package/dist/metrics.js +1 -1
- package/dist/metrics.js.map +1 -1
- package/dist/nativeMetrics.cjs +3 -1
- package/dist/nativeMetrics.cjs.map +1 -1
- package/dist/nativeMetrics.d.ts +29 -10
- package/dist/nativeMetrics.js +3 -1
- package/dist/nativeMetrics.js.map +1 -1
- package/dist/regressionGate.cjs +2 -0
- package/dist/regressionGate.cjs.map +1 -0
- package/dist/regressionGate.d.ts +106 -0
- package/dist/regressionGate.js +2 -0
- package/dist/regressionGate.js.map +1 -0
- package/dist/scan.cjs +2 -0
- package/dist/scan.cjs.map +1 -0
- package/dist/scan.d.ts +55 -0
- package/dist/scan.js +2 -0
- package/dist/scan.js.map +1 -0
- package/dist/types.d.ts +18 -13
- package/native/Cargo.lock +523 -0
- package/native/Cargo.toml +45 -0
- package/native/build.rs +3 -0
- package/native/src/complexity.rs +627 -0
- package/native/src/dep_degree.rs +253 -0
- package/native/src/duplication.rs +2007 -0
- package/native/src/functions.rs +345 -0
- package/native/src/languages.rs +647 -0
- package/native/src/lib.rs +101 -0
- package/native/src/measure.rs +590 -0
- package/native/src/ncss.rs +263 -0
- package/native/src/types.rs +135 -0
- package/native/src/util.rs +139 -0
- package/package.json +16 -19
- package/scripts/buildNative.mjs +25 -0
- package/scripts/installNative.mjs +96 -0
- package/dist/ncss.cjs +0 -2
- package/dist/ncss.cjs.map +0 -1
- package/dist/ncss.d.ts +0 -17
- package/dist/ncss.js +0 -2
- package/dist/ncss.js.map +0 -1
package/dist/cliConfig.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cliConfig.cjs","names":["defaultDuplicationOptions","readFile","path","stat"],"sources":["../src/cliConfig.ts"],"sourcesContent":["import { readFile, stat } from 'node:fs/promises';\nimport path from 'node:path';\nimport { defaultDuplicationOptions } from './duplication.js';\nimport type { DuplicationOptions } from './types.js';\n\nexport const configFileName = 'code-gauge.config.json';\nexport const defaultTopFileCount = 10;\n\n/** Shape of the JSON configuration file. All fields are optional and fall back to the built-in defaults. */\nexport interface CodeGaugeConfig {\n /** Duplication detection settings applied to every measured file. */\n duplication?: DuplicationOptions;\n /** Refactoring-candidate ranking settings. */\n rank?: { top?: number };\n includeTests?: boolean;\n failOnError?: boolean;\n}\n\n/** Raw command-line options; every field is undefined unless the user passed the flag. */\nexport interface CliOptions {\n config?: string;\n top?: number;\n duplicationMinTokens?: number;\n duplicationMaxGapTokens?: number;\n duplicationMinSimilarityPercent?: number;\n includeTests?: boolean;\n failOnError?: boolean;\n json?: boolean;\n}\n\n/** Options after merging command-line flags, the configuration file, and the built-in defaults. */\nexport interface ResolvedOptions {\n duplication: Required<DuplicationOptions>;\n /** Number of top-ranked files to report. */\n top: number;\n includeTests: boolean;\n failOnError: boolean;\n json: boolean;\n}\n\n/** Resolves options with precedence command-line flags > configuration file > built-in defaults. */\nexport function resolveOptions(cli: CliOptions, config: CodeGaugeConfig): ResolvedOptions {\n return {\n duplication: {\n minTokens: cli.duplicationMinTokens ?? config.duplication?.minTokens ?? defaultDuplicationOptions.minTokens,\n maxGapTokens:\n cli.duplicationMaxGapTokens ?? config.duplication?.maxGapTokens ?? defaultDuplicationOptions.maxGapTokens,\n minSimilarityPercent:\n cli.duplicationMinSimilarityPercent ??\n config.duplication?.minSimilarityPercent ??\n defaultDuplicationOptions.minSimilarityPercent,\n },\n top: cli.top ?? config.rank?.top ?? defaultTopFileCount,\n includeTests: cli.includeTests ?? config.includeTests ?? false,\n failOnError: cli.failOnError ?? config.failOnError ?? false,\n json: cli.json ?? false,\n };\n}\n\n/**\n * Loads the configuration file. An explicit path must exist; otherwise the nearest\n * `code-gauge.config.json` is searched by walking up from the target directory.\n */\nexport async function loadConfig(explicitPath: string | undefined, targetDirectory: string): Promise<CodeGaugeConfig> {\n const configFile = explicitPath ?? (await findNearestConfig(targetDirectory));\n if (!configFile) {\n return {};\n }\n\n let content;\n try {\n content = await readFile(configFile, 'utf8');\n } catch (error) {\n if (explicitPath) {\n throw new Error(`Cannot read config file \"${configFile}\": ${formatError(error)}`);\n }\n return {};\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch (error) {\n throw new Error(`Invalid JSON in config file \"${configFile}\": ${formatError(error)}`);\n }\n\n return validateConfig(parsed, configFile);\n}\n\nasync function findNearestConfig(targetDirectory: string): Promise<string | undefined> {\n let currentDirectory = targetDirectory;\n while (true) {\n const configFile = path.join(currentDirectory, configFileName);\n if (await fileExists(configFile)) {\n return configFile;\n }\n\n const parentDirectory = path.dirname(currentDirectory);\n if (parentDirectory === currentDirectory) {\n return undefined;\n }\n currentDirectory = parentDirectory;\n }\n}\n\nasync function fileExists(file: string): Promise<boolean> {\n try {\n const fileStat = await stat(file);\n return fileStat.isFile();\n } catch {\n return false;\n }\n}\n\nfunction validateConfig(value: unknown, configFile: string): CodeGaugeConfig {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\" must contain a JSON object.`);\n }\n\n const raw = value as Record<string, unknown>;\n const knownKeys = new Set(['duplication', 'rank', 'includeTests', 'failOnError']);\n for (const key of Object.keys(raw)) {\n if (!knownKeys.has(key)) {\n throw new Error(`Config file \"${configFile}\": unknown setting \"${key}\" (expected ${[...knownKeys].join(', ')}).`);\n }\n }\n const config: CodeGaugeConfig = {};\n\n if (raw.duplication !== undefined) {\n config.duplication = validateDuplicationObject(raw.duplication, configFile);\n }\n\n if (raw.rank !== undefined) {\n config.rank = validateRankObject(raw.rank, configFile);\n }\n\n for (const key of ['includeTests', 'failOnError'] as const) {\n if (raw[key] !== undefined) {\n config[key] = requireBoolean(raw[key], key, configFile);\n }\n }\n\n return config;\n}\n\nfunction validateRankObject(value: unknown, configFile: string): { top?: number } {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"rank\" must be an object.`);\n }\n const rank: { top?: number } = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key !== 'top') {\n throw new Error(`Config file \"${configFile}\": unknown setting \"${key}\" in \"rank\" (expected top).`);\n }\n rank.top = requirePositiveInteger(setting, 'rank.top', configFile);\n }\n return rank;\n}\n\nfunction validateDuplicationObject(value: unknown, configFile: string): DuplicationOptions {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"duplication\" must be an object.`);\n }\n const duplication: DuplicationOptions = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key === 'minTokens') {\n duplication.minTokens = requirePositiveInteger(setting, 'duplication.minTokens', configFile);\n } else if (key === 'maxGapTokens') {\n // 0 is meaningful: it disables gapped-clone merging.\n duplication.maxGapTokens = requireNonNegativeInteger(setting, 'duplication.maxGapTokens', configFile);\n } else if (key === 'minSimilarityPercent') {\n const parsed = requirePositiveInteger(setting, 'duplication.minSimilarityPercent', configFile);\n if (parsed > 100) {\n throw new Error(`Config file \"${configFile}\": \"duplication.minSimilarityPercent\" must be between 1 and 100.`);\n }\n duplication.minSimilarityPercent = parsed;\n } else {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"duplication\" (expected minTokens, maxGapTokens, or minSimilarityPercent).`\n );\n }\n }\n return duplication;\n}\n\nfunction requireNonNegativeInteger(value: unknown, key: string, configFile: string): number {\n if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be a non-negative integer.`);\n }\n return value;\n}\n\nfunction requirePositiveInteger(value: unknown, key: string, configFile: string): number {\n if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be a positive integer.`);\n }\n return value;\n}\n\nfunction requireBoolean(value: unknown, key: string, configFile: string): boolean {\n if (typeof value !== 'boolean') {\n throw new TypeError(`Config file \"${configFile}\": \"${key}\" must be a boolean.`);\n }\n return value;\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cliConfig.cjs","names":["defaultDuplicationOptions","defaultGateOptions","readFile","path","stat"],"sources":["../src/cliConfig.ts"],"sourcesContent":["import { readFile, stat } from 'node:fs/promises';\nimport path from 'node:path';\nimport { defaultDuplicationOptions } from './duplication.js';\nimport {\n defaultGateOptions,\n type GateOptions,\n type GateTolerances,\n type NewFunctionThresholds,\n} from './regressionGate.js';\nimport type { DuplicationOptions } from './types.js';\n\nexport const configFileName = 'code-gauge.config.json';\nexport const defaultTopFileCount = 10;\n\n/** Regression-gate settings for `code-gauge diff`; unset fields use the built-in defaults. */\nexport interface GateConfig {\n newFunction?: Partial<NewFunctionThresholds>;\n tolerance?: Partial<GateTolerances>;\n matchSimilarityPercent?: number;\n}\n\n/** Shape of the JSON configuration file. All fields are optional and fall back to the built-in defaults. */\nexport interface CodeGaugeConfig {\n /** Duplication detection settings applied to every measured file. */\n duplication?: DuplicationOptions;\n /** Refactoring-candidate ranking settings. */\n rank?: { top?: number };\n /** Regression-gate settings for `code-gauge diff`. */\n gate?: GateConfig;\n includeTests?: boolean;\n failOnError?: boolean;\n}\n\n/** Raw command-line options; every field is undefined unless the user passed the flag. */\nexport interface CliOptions {\n config?: string;\n top?: number;\n duplicationMinTokens?: number;\n duplicationMaxGapTokens?: number;\n duplicationMinSimilarityPercent?: number;\n includeTests?: boolean;\n failOnError?: boolean;\n json?: boolean;\n}\n\n/** Options after merging command-line flags, the configuration file, and the built-in defaults. */\nexport interface ResolvedOptions {\n duplication: Required<DuplicationOptions>;\n /** Number of top-ranked files to report. */\n top: number;\n includeTests: boolean;\n failOnError: boolean;\n json: boolean;\n}\n\n/** Resolves options with precedence command-line flags > configuration file > built-in defaults. */\nexport function resolveOptions(cli: CliOptions, config: CodeGaugeConfig): ResolvedOptions {\n return {\n duplication: {\n minTokens: cli.duplicationMinTokens ?? config.duplication?.minTokens ?? defaultDuplicationOptions.minTokens,\n maxGapTokens:\n cli.duplicationMaxGapTokens ?? config.duplication?.maxGapTokens ?? defaultDuplicationOptions.maxGapTokens,\n minSimilarityPercent:\n cli.duplicationMinSimilarityPercent ??\n config.duplication?.minSimilarityPercent ??\n defaultDuplicationOptions.minSimilarityPercent,\n },\n top: cli.top ?? config.rank?.top ?? defaultTopFileCount,\n includeTests: cli.includeTests ?? config.includeTests ?? false,\n failOnError: cli.failOnError ?? config.failOnError ?? false,\n json: cli.json ?? false,\n };\n}\n\n/** Resolves the regression-gate settings with precedence configuration file > built-in defaults. */\nexport function resolveGateOptions(config: CodeGaugeConfig): GateOptions {\n return {\n newFunction: { ...defaultGateOptions.newFunction, ...config.gate?.newFunction },\n tolerance: { ...defaultGateOptions.tolerance, ...config.gate?.tolerance },\n matchSimilarityPercent: config.gate?.matchSimilarityPercent ?? defaultGateOptions.matchSimilarityPercent,\n };\n}\n\n/**\n * Loads the configuration file. An explicit path must exist; otherwise the nearest\n * `code-gauge.config.json` is searched by walking up from the target directory.\n */\nexport async function loadConfig(explicitPath: string | undefined, targetDirectory: string): Promise<CodeGaugeConfig> {\n const configFile = explicitPath ?? (await findNearestConfig(targetDirectory));\n if (!configFile) {\n return {};\n }\n\n let content;\n try {\n content = await readFile(configFile, 'utf8');\n } catch (error) {\n if (explicitPath) {\n throw new Error(`Cannot read config file \"${configFile}\": ${formatError(error)}`);\n }\n return {};\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch (error) {\n throw new Error(`Invalid JSON in config file \"${configFile}\": ${formatError(error)}`);\n }\n\n return validateConfig(parsed, configFile);\n}\n\nasync function findNearestConfig(targetDirectory: string): Promise<string | undefined> {\n let currentDirectory = targetDirectory;\n while (true) {\n const configFile = path.join(currentDirectory, configFileName);\n if (await fileExists(configFile)) {\n return configFile;\n }\n\n const parentDirectory = path.dirname(currentDirectory);\n if (parentDirectory === currentDirectory) {\n return undefined;\n }\n currentDirectory = parentDirectory;\n }\n}\n\nasync function fileExists(file: string): Promise<boolean> {\n try {\n const fileStat = await stat(file);\n return fileStat.isFile();\n } catch {\n return false;\n }\n}\n\nfunction validateConfig(value: unknown, configFile: string): CodeGaugeConfig {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\" must contain a JSON object.`);\n }\n\n const raw = value as Record<string, unknown>;\n const knownKeys = new Set(['duplication', 'rank', 'gate', 'includeTests', 'failOnError']);\n for (const key of Object.keys(raw)) {\n if (!knownKeys.has(key)) {\n throw new Error(`Config file \"${configFile}\": unknown setting \"${key}\" (expected ${[...knownKeys].join(', ')}).`);\n }\n }\n const config: CodeGaugeConfig = {};\n\n if (raw.duplication !== undefined) {\n config.duplication = validateDuplicationObject(raw.duplication, configFile);\n }\n\n if (raw.rank !== undefined) {\n config.rank = validateRankObject(raw.rank, configFile);\n }\n\n if (raw.gate !== undefined) {\n config.gate = validateGateObject(raw.gate, configFile);\n }\n\n for (const key of ['includeTests', 'failOnError'] as const) {\n if (raw[key] !== undefined) {\n config[key] = requireBoolean(raw[key], key, configFile);\n }\n }\n\n return config;\n}\n\nfunction validateRankObject(value: unknown, configFile: string): { top?: number } {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"rank\" must be an object.`);\n }\n const rank: { top?: number } = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key !== 'top') {\n throw new Error(`Config file \"${configFile}\": unknown setting \"${key}\" in \"rank\" (expected top).`);\n }\n rank.top = requirePositiveInteger(setting, 'rank.top', configFile);\n }\n return rank;\n}\n\nfunction validateGateObject(value: unknown, configFile: string): GateConfig {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"gate\" must be an object.`);\n }\n const gate: GateConfig = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key === 'newFunction') {\n // Zero is a meaningful upper bound (branch-free or unnested new functions), so the limits\n // are validated as non-negative rather than positive.\n gate.newFunction = validateGateNumberObject(\n setting,\n 'gate.newFunction',\n Object.keys(defaultGateOptions.newFunction),\n configFile,\n requireNonNegativeInteger\n ) as Partial<NewFunctionThresholds>;\n } else if (key === 'tolerance') {\n gate.tolerance = validateGateNumberObject(\n setting,\n 'gate.tolerance',\n Object.keys(defaultGateOptions.tolerance),\n configFile,\n requireNonNegativeNumber\n ) as Partial<GateTolerances>;\n } else if (key === 'matchSimilarityPercent') {\n const parsed = requirePositiveInteger(setting, 'gate.matchSimilarityPercent', configFile);\n if (parsed > 100) {\n throw new Error(`Config file \"${configFile}\": \"gate.matchSimilarityPercent\" must be between 1 and 100.`);\n }\n gate.matchSimilarityPercent = parsed;\n } else {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"gate\" (expected newFunction, tolerance, or matchSimilarityPercent).`\n );\n }\n }\n return gate;\n}\n\nfunction validateGateNumberObject(\n value: unknown,\n settingName: string,\n knownKeys: string[],\n configFile: string,\n requireNumber: (value: unknown, key: string, configFile: string) => number\n): Record<string, number> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"${settingName}\" must be an object.`);\n }\n const validated: Record<string, number> = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (!knownKeys.includes(key)) {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"${settingName}\" (expected ${knownKeys.join(', ')}).`\n );\n }\n validated[key] = requireNumber(setting, `${settingName}.${key}`, configFile);\n }\n return validated;\n}\n\nfunction validateDuplicationObject(value: unknown, configFile: string): DuplicationOptions {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"duplication\" must be an object.`);\n }\n const duplication: DuplicationOptions = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key === 'minTokens') {\n duplication.minTokens = requirePositiveInteger(setting, 'duplication.minTokens', configFile);\n } else if (key === 'maxGapTokens') {\n // 0 is meaningful: it disables gapped-clone merging.\n duplication.maxGapTokens = requireNonNegativeInteger(setting, 'duplication.maxGapTokens', configFile);\n } else if (key === 'minSimilarityPercent') {\n const parsed = requirePositiveInteger(setting, 'duplication.minSimilarityPercent', configFile);\n if (parsed > 100) {\n throw new Error(`Config file \"${configFile}\": \"duplication.minSimilarityPercent\" must be between 1 and 100.`);\n }\n duplication.minSimilarityPercent = parsed;\n } else {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"duplication\" (expected minTokens, maxGapTokens, or minSimilarityPercent).`\n );\n }\n }\n return duplication;\n}\n\n/** Tolerances may be fractional (e.g. Halstead volume), so only finiteness and sign are checked. */\nfunction requireNonNegativeNumber(value: unknown, key: string, configFile: string): number {\n return requireNumber(value, key, configFile, Number.isFinite, 'a non-negative number');\n}\n\nfunction requireNonNegativeInteger(value: unknown, key: string, configFile: string): number {\n return requireNumber(value, key, configFile, Number.isSafeInteger, 'a non-negative integer');\n}\n\nfunction requirePositiveInteger(value: unknown, key: string, configFile: string): number {\n const parsed = requireNumber(value, key, configFile, Number.isSafeInteger, 'a positive integer');\n if (parsed < 1) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be a positive integer.`);\n }\n return parsed;\n}\n\n/** Shared core of the numeric validators: the right kind of number, and never negative. */\nfunction requireNumber(\n value: unknown,\n key: string,\n configFile: string,\n isValidKind: (value: unknown) => boolean,\n description: string\n): number {\n if (typeof value !== 'number' || !isValidKind(value) || value < 0) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be ${description}.`);\n }\n return value;\n}\n\nfunction requireBoolean(value: unknown, key: string, configFile: string): boolean {\n if (typeof value !== 'boolean') {\n throw new TypeError(`Config file \"${configFile}\": \"${key}\" must be a boolean.`);\n }\n return value;\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
|
package/dist/cliConfig.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import { type GateOptions, type GateTolerances, type NewFunctionThresholds } from './regressionGate.js';
|
|
1
2
|
import type { DuplicationOptions } from './types.js';
|
|
2
3
|
export declare const configFileName = "code-gauge.config.json";
|
|
3
4
|
export declare const defaultTopFileCount = 10;
|
|
5
|
+
/** Regression-gate settings for `code-gauge diff`; unset fields use the built-in defaults. */
|
|
6
|
+
export interface GateConfig {
|
|
7
|
+
newFunction?: Partial<NewFunctionThresholds>;
|
|
8
|
+
tolerance?: Partial<GateTolerances>;
|
|
9
|
+
matchSimilarityPercent?: number;
|
|
10
|
+
}
|
|
4
11
|
/** Shape of the JSON configuration file. All fields are optional and fall back to the built-in defaults. */
|
|
5
12
|
export interface CodeGaugeConfig {
|
|
6
13
|
/** Duplication detection settings applied to every measured file. */
|
|
@@ -9,6 +16,8 @@ export interface CodeGaugeConfig {
|
|
|
9
16
|
rank?: {
|
|
10
17
|
top?: number;
|
|
11
18
|
};
|
|
19
|
+
/** Regression-gate settings for `code-gauge diff`. */
|
|
20
|
+
gate?: GateConfig;
|
|
12
21
|
includeTests?: boolean;
|
|
13
22
|
failOnError?: boolean;
|
|
14
23
|
}
|
|
@@ -34,6 +43,8 @@ export interface ResolvedOptions {
|
|
|
34
43
|
}
|
|
35
44
|
/** Resolves options with precedence command-line flags > configuration file > built-in defaults. */
|
|
36
45
|
export declare function resolveOptions(cli: CliOptions, config: CodeGaugeConfig): ResolvedOptions;
|
|
46
|
+
/** Resolves the regression-gate settings with precedence configuration file > built-in defaults. */
|
|
47
|
+
export declare function resolveGateOptions(config: CodeGaugeConfig): GateOptions;
|
|
37
48
|
/**
|
|
38
49
|
* Loads the configuration file. An explicit path must exist; otherwise the nearest
|
|
39
50
|
* `code-gauge.config.json` is searched by walking up from the target directory.
|
package/dist/cliConfig.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{defaultDuplicationOptions as e}from"./duplication.js";import{readFile as
|
|
1
|
+
import{defaultDuplicationOptions as e}from"./duplication.js";import{defaultGateOptions as t}from"./regressionGate.js";import{readFile as n,stat as r}from"node:fs/promises";import i from"node:path";const a=`code-gauge.config.json`;function o(t,n){return{duplication:{minTokens:t.duplicationMinTokens??n.duplication?.minTokens??e.minTokens,maxGapTokens:t.duplicationMaxGapTokens??n.duplication?.maxGapTokens??e.maxGapTokens,minSimilarityPercent:t.duplicationMinSimilarityPercent??n.duplication?.minSimilarityPercent??e.minSimilarityPercent},top:t.top??n.rank?.top??10,includeTests:t.includeTests??n.includeTests??!1,failOnError:t.failOnError??n.failOnError??!1,json:t.json??!1}}function s(e){return{newFunction:{...t.newFunction,...e.gate?.newFunction},tolerance:{...t.tolerance,...e.gate?.tolerance},matchSimilarityPercent:e.gate?.matchSimilarityPercent??t.matchSimilarityPercent}}async function c(e,t){let r=e??await l(t);if(!r)return{};let i;try{i=await n(r,`utf8`)}catch(t){if(e)throw Error(`Cannot read config file "${r}": ${x(t)}`);return{}}let a;try{a=JSON.parse(i)}catch(e){throw Error(`Invalid JSON in config file "${r}": ${x(e)}`)}return d(a,r)}async function l(e){let t=e;for(;;){let e=i.join(t,a);if(await u(e))return e;let n=i.dirname(t);if(n===t)return;t=n}}async function u(e){try{return(await r(e)).isFile()}catch{return!1}}function d(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${t}" must contain a JSON object.`);let n=e,r=new Set([`duplication`,`rank`,`gate`,`includeTests`,`failOnError`]);for(let e of Object.keys(n))if(!r.has(e))throw Error(`Config file "${t}": unknown setting "${e}" (expected ${[...r].join(`, `)}).`);let i={};n.duplication!==void 0&&(i.duplication=h(n.duplication,t)),n.rank!==void 0&&(i.rank=f(n.rank,t)),n.gate!==void 0&&(i.gate=p(n.gate,t));for(let e of[`includeTests`,`failOnError`])n[e]!==void 0&&(i[e]=b(n[e],e,t));return i}function f(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${t}": "rank" must be an object.`);let n={};for(let[r,i]of Object.entries(e)){if(r!==`top`)throw Error(`Config file "${t}": unknown setting "${r}" in "rank" (expected top).`);n.top=v(i,`rank.top`,t)}return n}function p(e,n){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${n}": "gate" must be an object.`);let r={};for(let[i,a]of Object.entries(e))if(i===`newFunction`)r.newFunction=m(a,`gate.newFunction`,Object.keys(t.newFunction),n,_);else if(i===`tolerance`)r.tolerance=m(a,`gate.tolerance`,Object.keys(t.tolerance),n,g);else if(i===`matchSimilarityPercent`){let e=v(a,`gate.matchSimilarityPercent`,n);if(e>100)throw Error(`Config file "${n}": "gate.matchSimilarityPercent" must be between 1 and 100.`);r.matchSimilarityPercent=e}else throw Error(`Config file "${n}": unknown setting "${i}" in "gate" (expected newFunction, tolerance, or matchSimilarityPercent).`);return r}function m(e,t,n,r,i){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${r}": "${t}" must be an object.`);let a={};for(let[o,s]of Object.entries(e)){if(!n.includes(o))throw Error(`Config file "${r}": unknown setting "${o}" in "${t}" (expected ${n.join(`, `)}).`);a[o]=i(s,`${t}.${o}`,r)}return a}function h(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${t}": "duplication" must be an object.`);let n={};for(let[r,i]of Object.entries(e))if(r===`minTokens`)n.minTokens=v(i,`duplication.minTokens`,t);else if(r===`maxGapTokens`)n.maxGapTokens=_(i,`duplication.maxGapTokens`,t);else if(r===`minSimilarityPercent`){let e=v(i,`duplication.minSimilarityPercent`,t);if(e>100)throw Error(`Config file "${t}": "duplication.minSimilarityPercent" must be between 1 and 100.`);n.minSimilarityPercent=e}else throw Error(`Config file "${t}": unknown setting "${r}" in "duplication" (expected minTokens, maxGapTokens, or minSimilarityPercent).`);return n}function g(e,t,n){return y(e,t,n,Number.isFinite,`a non-negative number`)}function _(e,t,n){return y(e,t,n,Number.isSafeInteger,`a non-negative integer`)}function v(e,t,n){let r=y(e,t,n,Number.isSafeInteger,`a positive integer`);if(r<1)throw Error(`Config file "${n}": "${t}" must be a positive integer.`);return r}function y(e,t,n,r,i){if(typeof e!=`number`||!r(e)||e<0)throw Error(`Config file "${n}": "${t}" must be ${i}.`);return e}function b(e,t,n){if(typeof e!=`boolean`)throw TypeError(`Config file "${n}": "${t}" must be a boolean.`);return e}function x(e){return e instanceof Error?e.message:String(e)}export{a as configFileName,c as loadConfig,s as resolveGateOptions,o as resolveOptions};
|
|
2
2
|
//# sourceMappingURL=cliConfig.js.map
|
package/dist/cliConfig.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cliConfig.js","names":[],"sources":["../src/cliConfig.ts"],"sourcesContent":["import { readFile, stat } from 'node:fs/promises';\nimport path from 'node:path';\nimport { defaultDuplicationOptions } from './duplication.js';\nimport type { DuplicationOptions } from './types.js';\n\nexport const configFileName = 'code-gauge.config.json';\nexport const defaultTopFileCount = 10;\n\n/** Shape of the JSON configuration file. All fields are optional and fall back to the built-in defaults. */\nexport interface CodeGaugeConfig {\n /** Duplication detection settings applied to every measured file. */\n duplication?: DuplicationOptions;\n /** Refactoring-candidate ranking settings. */\n rank?: { top?: number };\n includeTests?: boolean;\n failOnError?: boolean;\n}\n\n/** Raw command-line options; every field is undefined unless the user passed the flag. */\nexport interface CliOptions {\n config?: string;\n top?: number;\n duplicationMinTokens?: number;\n duplicationMaxGapTokens?: number;\n duplicationMinSimilarityPercent?: number;\n includeTests?: boolean;\n failOnError?: boolean;\n json?: boolean;\n}\n\n/** Options after merging command-line flags, the configuration file, and the built-in defaults. */\nexport interface ResolvedOptions {\n duplication: Required<DuplicationOptions>;\n /** Number of top-ranked files to report. */\n top: number;\n includeTests: boolean;\n failOnError: boolean;\n json: boolean;\n}\n\n/** Resolves options with precedence command-line flags > configuration file > built-in defaults. */\nexport function resolveOptions(cli: CliOptions, config: CodeGaugeConfig): ResolvedOptions {\n return {\n duplication: {\n minTokens: cli.duplicationMinTokens ?? config.duplication?.minTokens ?? defaultDuplicationOptions.minTokens,\n maxGapTokens:\n cli.duplicationMaxGapTokens ?? config.duplication?.maxGapTokens ?? defaultDuplicationOptions.maxGapTokens,\n minSimilarityPercent:\n cli.duplicationMinSimilarityPercent ??\n config.duplication?.minSimilarityPercent ??\n defaultDuplicationOptions.minSimilarityPercent,\n },\n top: cli.top ?? config.rank?.top ?? defaultTopFileCount,\n includeTests: cli.includeTests ?? config.includeTests ?? false,\n failOnError: cli.failOnError ?? config.failOnError ?? false,\n json: cli.json ?? false,\n };\n}\n\n/**\n * Loads the configuration file. An explicit path must exist; otherwise the nearest\n * `code-gauge.config.json` is searched by walking up from the target directory.\n */\nexport async function loadConfig(explicitPath: string | undefined, targetDirectory: string): Promise<CodeGaugeConfig> {\n const configFile = explicitPath ?? (await findNearestConfig(targetDirectory));\n if (!configFile) {\n return {};\n }\n\n let content;\n try {\n content = await readFile(configFile, 'utf8');\n } catch (error) {\n if (explicitPath) {\n throw new Error(`Cannot read config file \"${configFile}\": ${formatError(error)}`);\n }\n return {};\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch (error) {\n throw new Error(`Invalid JSON in config file \"${configFile}\": ${formatError(error)}`);\n }\n\n return validateConfig(parsed, configFile);\n}\n\nasync function findNearestConfig(targetDirectory: string): Promise<string | undefined> {\n let currentDirectory = targetDirectory;\n while (true) {\n const configFile = path.join(currentDirectory, configFileName);\n if (await fileExists(configFile)) {\n return configFile;\n }\n\n const parentDirectory = path.dirname(currentDirectory);\n if (parentDirectory === currentDirectory) {\n return undefined;\n }\n currentDirectory = parentDirectory;\n }\n}\n\nasync function fileExists(file: string): Promise<boolean> {\n try {\n const fileStat = await stat(file);\n return fileStat.isFile();\n } catch {\n return false;\n }\n}\n\nfunction validateConfig(value: unknown, configFile: string): CodeGaugeConfig {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\" must contain a JSON object.`);\n }\n\n const raw = value as Record<string, unknown>;\n const knownKeys = new Set(['duplication', 'rank', 'includeTests', 'failOnError']);\n for (const key of Object.keys(raw)) {\n if (!knownKeys.has(key)) {\n throw new Error(`Config file \"${configFile}\": unknown setting \"${key}\" (expected ${[...knownKeys].join(', ')}).`);\n }\n }\n const config: CodeGaugeConfig = {};\n\n if (raw.duplication !== undefined) {\n config.duplication = validateDuplicationObject(raw.duplication, configFile);\n }\n\n if (raw.rank !== undefined) {\n config.rank = validateRankObject(raw.rank, configFile);\n }\n\n for (const key of ['includeTests', 'failOnError'] as const) {\n if (raw[key] !== undefined) {\n config[key] = requireBoolean(raw[key], key, configFile);\n }\n }\n\n return config;\n}\n\nfunction validateRankObject(value: unknown, configFile: string): { top?: number } {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"rank\" must be an object.`);\n }\n const rank: { top?: number } = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key !== 'top') {\n throw new Error(`Config file \"${configFile}\": unknown setting \"${key}\" in \"rank\" (expected top).`);\n }\n rank.top = requirePositiveInteger(setting, 'rank.top', configFile);\n }\n return rank;\n}\n\nfunction validateDuplicationObject(value: unknown, configFile: string): DuplicationOptions {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"duplication\" must be an object.`);\n }\n const duplication: DuplicationOptions = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key === 'minTokens') {\n duplication.minTokens = requirePositiveInteger(setting, 'duplication.minTokens', configFile);\n } else if (key === 'maxGapTokens') {\n // 0 is meaningful: it disables gapped-clone merging.\n duplication.maxGapTokens = requireNonNegativeInteger(setting, 'duplication.maxGapTokens', configFile);\n } else if (key === 'minSimilarityPercent') {\n const parsed = requirePositiveInteger(setting, 'duplication.minSimilarityPercent', configFile);\n if (parsed > 100) {\n throw new Error(`Config file \"${configFile}\": \"duplication.minSimilarityPercent\" must be between 1 and 100.`);\n }\n duplication.minSimilarityPercent = parsed;\n } else {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"duplication\" (expected minTokens, maxGapTokens, or minSimilarityPercent).`\n );\n }\n }\n return duplication;\n}\n\nfunction requireNonNegativeInteger(value: unknown, key: string, configFile: string): number {\n if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be a non-negative integer.`);\n }\n return value;\n}\n\nfunction requirePositiveInteger(value: unknown, key: string, configFile: string): number {\n if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be a positive integer.`);\n }\n return value;\n}\n\nfunction requireBoolean(value: unknown, key: string, configFile: string): boolean {\n if (typeof value !== 'boolean') {\n throw new TypeError(`Config file \"${configFile}\": \"${key}\" must be a boolean.`);\n }\n return value;\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cliConfig.js","names":[],"sources":["../src/cliConfig.ts"],"sourcesContent":["import { readFile, stat } from 'node:fs/promises';\nimport path from 'node:path';\nimport { defaultDuplicationOptions } from './duplication.js';\nimport {\n defaultGateOptions,\n type GateOptions,\n type GateTolerances,\n type NewFunctionThresholds,\n} from './regressionGate.js';\nimport type { DuplicationOptions } from './types.js';\n\nexport const configFileName = 'code-gauge.config.json';\nexport const defaultTopFileCount = 10;\n\n/** Regression-gate settings for `code-gauge diff`; unset fields use the built-in defaults. */\nexport interface GateConfig {\n newFunction?: Partial<NewFunctionThresholds>;\n tolerance?: Partial<GateTolerances>;\n matchSimilarityPercent?: number;\n}\n\n/** Shape of the JSON configuration file. All fields are optional and fall back to the built-in defaults. */\nexport interface CodeGaugeConfig {\n /** Duplication detection settings applied to every measured file. */\n duplication?: DuplicationOptions;\n /** Refactoring-candidate ranking settings. */\n rank?: { top?: number };\n /** Regression-gate settings for `code-gauge diff`. */\n gate?: GateConfig;\n includeTests?: boolean;\n failOnError?: boolean;\n}\n\n/** Raw command-line options; every field is undefined unless the user passed the flag. */\nexport interface CliOptions {\n config?: string;\n top?: number;\n duplicationMinTokens?: number;\n duplicationMaxGapTokens?: number;\n duplicationMinSimilarityPercent?: number;\n includeTests?: boolean;\n failOnError?: boolean;\n json?: boolean;\n}\n\n/** Options after merging command-line flags, the configuration file, and the built-in defaults. */\nexport interface ResolvedOptions {\n duplication: Required<DuplicationOptions>;\n /** Number of top-ranked files to report. */\n top: number;\n includeTests: boolean;\n failOnError: boolean;\n json: boolean;\n}\n\n/** Resolves options with precedence command-line flags > configuration file > built-in defaults. */\nexport function resolveOptions(cli: CliOptions, config: CodeGaugeConfig): ResolvedOptions {\n return {\n duplication: {\n minTokens: cli.duplicationMinTokens ?? config.duplication?.minTokens ?? defaultDuplicationOptions.minTokens,\n maxGapTokens:\n cli.duplicationMaxGapTokens ?? config.duplication?.maxGapTokens ?? defaultDuplicationOptions.maxGapTokens,\n minSimilarityPercent:\n cli.duplicationMinSimilarityPercent ??\n config.duplication?.minSimilarityPercent ??\n defaultDuplicationOptions.minSimilarityPercent,\n },\n top: cli.top ?? config.rank?.top ?? defaultTopFileCount,\n includeTests: cli.includeTests ?? config.includeTests ?? false,\n failOnError: cli.failOnError ?? config.failOnError ?? false,\n json: cli.json ?? false,\n };\n}\n\n/** Resolves the regression-gate settings with precedence configuration file > built-in defaults. */\nexport function resolveGateOptions(config: CodeGaugeConfig): GateOptions {\n return {\n newFunction: { ...defaultGateOptions.newFunction, ...config.gate?.newFunction },\n tolerance: { ...defaultGateOptions.tolerance, ...config.gate?.tolerance },\n matchSimilarityPercent: config.gate?.matchSimilarityPercent ?? defaultGateOptions.matchSimilarityPercent,\n };\n}\n\n/**\n * Loads the configuration file. An explicit path must exist; otherwise the nearest\n * `code-gauge.config.json` is searched by walking up from the target directory.\n */\nexport async function loadConfig(explicitPath: string | undefined, targetDirectory: string): Promise<CodeGaugeConfig> {\n const configFile = explicitPath ?? (await findNearestConfig(targetDirectory));\n if (!configFile) {\n return {};\n }\n\n let content;\n try {\n content = await readFile(configFile, 'utf8');\n } catch (error) {\n if (explicitPath) {\n throw new Error(`Cannot read config file \"${configFile}\": ${formatError(error)}`);\n }\n return {};\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch (error) {\n throw new Error(`Invalid JSON in config file \"${configFile}\": ${formatError(error)}`);\n }\n\n return validateConfig(parsed, configFile);\n}\n\nasync function findNearestConfig(targetDirectory: string): Promise<string | undefined> {\n let currentDirectory = targetDirectory;\n while (true) {\n const configFile = path.join(currentDirectory, configFileName);\n if (await fileExists(configFile)) {\n return configFile;\n }\n\n const parentDirectory = path.dirname(currentDirectory);\n if (parentDirectory === currentDirectory) {\n return undefined;\n }\n currentDirectory = parentDirectory;\n }\n}\n\nasync function fileExists(file: string): Promise<boolean> {\n try {\n const fileStat = await stat(file);\n return fileStat.isFile();\n } catch {\n return false;\n }\n}\n\nfunction validateConfig(value: unknown, configFile: string): CodeGaugeConfig {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\" must contain a JSON object.`);\n }\n\n const raw = value as Record<string, unknown>;\n const knownKeys = new Set(['duplication', 'rank', 'gate', 'includeTests', 'failOnError']);\n for (const key of Object.keys(raw)) {\n if (!knownKeys.has(key)) {\n throw new Error(`Config file \"${configFile}\": unknown setting \"${key}\" (expected ${[...knownKeys].join(', ')}).`);\n }\n }\n const config: CodeGaugeConfig = {};\n\n if (raw.duplication !== undefined) {\n config.duplication = validateDuplicationObject(raw.duplication, configFile);\n }\n\n if (raw.rank !== undefined) {\n config.rank = validateRankObject(raw.rank, configFile);\n }\n\n if (raw.gate !== undefined) {\n config.gate = validateGateObject(raw.gate, configFile);\n }\n\n for (const key of ['includeTests', 'failOnError'] as const) {\n if (raw[key] !== undefined) {\n config[key] = requireBoolean(raw[key], key, configFile);\n }\n }\n\n return config;\n}\n\nfunction validateRankObject(value: unknown, configFile: string): { top?: number } {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"rank\" must be an object.`);\n }\n const rank: { top?: number } = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key !== 'top') {\n throw new Error(`Config file \"${configFile}\": unknown setting \"${key}\" in \"rank\" (expected top).`);\n }\n rank.top = requirePositiveInteger(setting, 'rank.top', configFile);\n }\n return rank;\n}\n\nfunction validateGateObject(value: unknown, configFile: string): GateConfig {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"gate\" must be an object.`);\n }\n const gate: GateConfig = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key === 'newFunction') {\n // Zero is a meaningful upper bound (branch-free or unnested new functions), so the limits\n // are validated as non-negative rather than positive.\n gate.newFunction = validateGateNumberObject(\n setting,\n 'gate.newFunction',\n Object.keys(defaultGateOptions.newFunction),\n configFile,\n requireNonNegativeInteger\n ) as Partial<NewFunctionThresholds>;\n } else if (key === 'tolerance') {\n gate.tolerance = validateGateNumberObject(\n setting,\n 'gate.tolerance',\n Object.keys(defaultGateOptions.tolerance),\n configFile,\n requireNonNegativeNumber\n ) as Partial<GateTolerances>;\n } else if (key === 'matchSimilarityPercent') {\n const parsed = requirePositiveInteger(setting, 'gate.matchSimilarityPercent', configFile);\n if (parsed > 100) {\n throw new Error(`Config file \"${configFile}\": \"gate.matchSimilarityPercent\" must be between 1 and 100.`);\n }\n gate.matchSimilarityPercent = parsed;\n } else {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"gate\" (expected newFunction, tolerance, or matchSimilarityPercent).`\n );\n }\n }\n return gate;\n}\n\nfunction validateGateNumberObject(\n value: unknown,\n settingName: string,\n knownKeys: string[],\n configFile: string,\n requireNumber: (value: unknown, key: string, configFile: string) => number\n): Record<string, number> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"${settingName}\" must be an object.`);\n }\n const validated: Record<string, number> = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (!knownKeys.includes(key)) {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"${settingName}\" (expected ${knownKeys.join(', ')}).`\n );\n }\n validated[key] = requireNumber(setting, `${settingName}.${key}`, configFile);\n }\n return validated;\n}\n\nfunction validateDuplicationObject(value: unknown, configFile: string): DuplicationOptions {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"duplication\" must be an object.`);\n }\n const duplication: DuplicationOptions = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key === 'minTokens') {\n duplication.minTokens = requirePositiveInteger(setting, 'duplication.minTokens', configFile);\n } else if (key === 'maxGapTokens') {\n // 0 is meaningful: it disables gapped-clone merging.\n duplication.maxGapTokens = requireNonNegativeInteger(setting, 'duplication.maxGapTokens', configFile);\n } else if (key === 'minSimilarityPercent') {\n const parsed = requirePositiveInteger(setting, 'duplication.minSimilarityPercent', configFile);\n if (parsed > 100) {\n throw new Error(`Config file \"${configFile}\": \"duplication.minSimilarityPercent\" must be between 1 and 100.`);\n }\n duplication.minSimilarityPercent = parsed;\n } else {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"duplication\" (expected minTokens, maxGapTokens, or minSimilarityPercent).`\n );\n }\n }\n return duplication;\n}\n\n/** Tolerances may be fractional (e.g. Halstead volume), so only finiteness and sign are checked. */\nfunction requireNonNegativeNumber(value: unknown, key: string, configFile: string): number {\n return requireNumber(value, key, configFile, Number.isFinite, 'a non-negative number');\n}\n\nfunction requireNonNegativeInteger(value: unknown, key: string, configFile: string): number {\n return requireNumber(value, key, configFile, Number.isSafeInteger, 'a non-negative integer');\n}\n\nfunction requirePositiveInteger(value: unknown, key: string, configFile: string): number {\n const parsed = requireNumber(value, key, configFile, Number.isSafeInteger, 'a positive integer');\n if (parsed < 1) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be a positive integer.`);\n }\n return parsed;\n}\n\n/** Shared core of the numeric validators: the right kind of number, and never negative. */\nfunction requireNumber(\n value: unknown,\n key: string,\n configFile: string,\n isValidKind: (value: unknown) => boolean,\n description: string\n): number {\n if (typeof value !== 'number' || !isValidKind(value) || value < 0) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be ${description}.`);\n }\n return value;\n}\n\nfunction requireBoolean(value: unknown, key: string, configFile: string): boolean {\n if (typeof value !== 'boolean') {\n throw new TypeError(`Config file \"${configFile}\": \"${key}\" must be a boolean.`);\n }\n return value;\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const e=require("./duplicateSelection.cjs"),t=require("./duplication.cjs");function n(n,c){let
|
|
1
|
+
"use strict";const e=require("./duplicateSelection.cjs"),t=require("./duplication.cjs");function n(n,c){let{minTokens:l,maxGapTokens:u}=t.resolveDuplicationOptions(c),d=n.flatMap(({file:e,candidates:t},n)=>t.map(t=>({...t,regionBucket:n,file:e})));for(let e of r(n,l))d.push(e);let f=e.selectMaximalGroups(d,i,(e,t)=>e.regionBucket-t.regionBucket||e.startIndex-t.startIndex),p=a(n,u);return s(o([...f.values()],p,u),n,p)}function r(e,n){let r=[],i=[];for(let[n,{tokens:a,containerStatements:o}]of e.entries())a&&o&&(r.push(n),i.push({tokens:a,literalCountPrefix:t.buildLiteralCountPrefix(a),containers:o}));return i.length<2?[]:t.collectSequenceWindowCandidates(i,n,!0).flatMap(({candidate:t,contextIndex:n})=>{let i=r[n],a=i===void 0?void 0:e[i];return i===void 0||a===void 0?[]:[{...t,regionBucket:i,file:a.file}]})}function i(e){return e.length>=2&&new Set(e.map(e=>e.regionBucket)).size>=2}function a(e,t){let n=[],r=0;for(let{tokens:i,candidates:a}of e){n.push(r);let e=i?.length??0;if(!i)for(let t of a)e=Math.max(e,t.endTokenIndex);r+=e+t+1}return n}function o(e,n,r){let i=e.map(e=>e.map(e=>{let t=e.startTokenIndex+(n[e.regionBucket]??0),r=e.endTokenIndex+(n[e.regionBucket]??0);return{file:e.file,segments:[{startTokenIndex:t,endTokenIndex:r}],tokenCount:e.tokenCount,startTokenIndex:t,endTokenIndex:r,startIndex:e.startIndex,endIndex:e.endIndex,startLine:e.startLine,endLine:e.endLine}}).toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex));return t.mergeAdjacentGroups(i,r)}function s(e,n,r){let i=[],a=new Map,o=new Map(n.map((e,t)=>[e.file,{tokens:e.tokens,codeLineNumbers:e.codeLineNumbers,offset:r[t]??0}])),s=new Map,l=0;for(let n of e){l+=t.countRedundantFragments(n);for(let e of n)c(e,o,s);let e=n.map(({file:e,startLine:t,endLine:n})=>({file:e,startLine:t,endLine:n})).toSorted((e,t)=>e.file.localeCompare(t.file)||e.startLine-t.startLine),r=[...new Set(e.map(({file:e})=>e))];for(let e of r)a.set(e,(a.get(e)??0)+1);i.push({files:r,occurrences:e,tokenCount:n[0]?.tokenCount??0})}return i.sort((e,t)=>t.tokenCount-e.tokenCount||(e.occurrences[0]?.file??``).localeCompare(t.occurrences[0]?.file??``)||(e.occurrences[0]?.startLine??0)-(t.occurrences[0]?.startLine??0)),{duplicateBlockCount:l,duplicateBlockGroupCountByFile:Object.fromEntries(a),duplicateLineNumbersByFile:Object.fromEntries([...s].map(([e,t])=>[e,[...t].toSorted((e,t)=>e-t)])),groups:i}}function c(e,n,r){let i=n.get(e.file);if(!i?.tokens)return;let a=r.get(e.file);a||(a=new Set,r.set(e.file,a));for(let n of e.segments)t.collectSegmentLines({startTokenIndex:n.startTokenIndex-i.offset,endTokenIndex:n.endTokenIndex-i.offset},i.tokens,i.codeLineNumbers,a)}exports.measureCrossFileDuplication=n;
|
|
2
2
|
//# sourceMappingURL=crossFileDuplication.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crossFileDuplication.cjs","names":["defaultDuplicationOptions","selectMaximalGroups","buildLiteralCountPrefix","collectSequenceWindowCandidates","mergeAdjacentGroups","countRedundantFragments"],"sources":["../src/crossFileDuplication.ts"],"sourcesContent":["import { selectMaximalGroups } from './duplicateSelection.js';\nimport {\n buildLiteralCountPrefix,\n collectSegmentLines,\n collectSequenceWindowCandidates,\n countRedundantFragments,\n defaultDuplicationOptions,\n mergeAdjacentGroups,\n type CountedOccurrence,\n type CrossFileDuplicateCandidate,\n type CrossFileDuplicationFileData,\n type SequenceWindowContext,\n} from './duplication.js';\nimport type { DuplicationOptions } from './types.js';\n\nexport interface CrossFileDuplicationSourceFile extends Partial<CrossFileDuplicationFileData> {\n file: string;\n candidates: CrossFileDuplicateCandidate[];\n}\n\nexport interface CrossFileDuplicateOccurrence {\n endLine: number;\n file: string;\n startLine: number;\n}\n\nexport interface CrossFileDuplicateBlockGroup {\n files: string[];\n occurrences: CrossFileDuplicateOccurrence[];\n /** Matched token count of one occurrence (all occurrences share it; gaps are not counted). */\n tokenCount: number;\n}\n\nexport interface CrossFileDuplicationMetrics {\n /** Number of redundant copies across all groups, counted per matched fragment like within-file. */\n duplicateBlockCount: number;\n /** Groups the file participates in, keyed by the file name passed in. */\n duplicateBlockGroupCountByFile: Record<string, number>;\n /**\n * Per file, the 1-based code lines covered by matched tokens of its cross-file occurrences,\n * sorted ascending. Exact like within-file duplicateLineNumbers: the unmatched gap of a merged\n * clone and comment/blank lines inside an occurrence's bounding range are excluded (blank rows\n * inside multi-row tokens only when the file supplied codeLineNumbers). A file that supplied\n * only candidates (no `tokens`) has no entry — without its token stream the matched lines are\n * unknowable, and an approximate bounding range would break this field's exactness.\n */\n duplicateLineNumbersByFile: Record<string, number[]>;\n groups: CrossFileDuplicateBlockGroup[];\n}\n\ninterface SelectableCandidate extends CrossFileDuplicateCandidate {\n regionBucket: number;\n file: string;\n}\n\n/** A cross-file occurrence: a within-file occurrence in the project-wide token index space. */\ninterface CrossFileOccurrence extends CountedOccurrence {\n file: string;\n}\n\n/**\n * Detects code regions duplicated across files. Per-file candidates (whole block subtrees and full\n * container runs, fingerprinted with the same normalization as within-file duplication) are joined\n * by a project-level window index over per-statement fingerprint sequences (CPD-style), so a\n * copy-pasted partial statement run embedded in different surrounding code is matched even though\n * no single file can know it repeats elsewhere. Candidates are grouped by fingerprint, and only\n * maximal, non-overlapping regions whose group spans at least two files are counted. Groups that\n * shrink to a single file during selection are shed — a within-file repeat is already reported by\n * that file's own duplication metrics. Groups separated by a small token gap within each file then\n * merge into gapped (Type-3) clone groups under `maxGapTokens`, exactly like within-file merging.\n */\nexport function measureCrossFileDuplication(\n files: CrossFileDuplicationSourceFile[],\n options?: DuplicationOptions\n): CrossFileDuplicationMetrics {\n const minTokens = options?.minTokens ?? defaultDuplicationOptions.minTokens;\n const maxGapTokens = options?.maxGapTokens ?? defaultDuplicationOptions.maxGapTokens;\n const candidates: SelectableCandidate[] = files.flatMap(({ file, candidates }, fileIndex) =>\n candidates.map((candidate) => ({ ...candidate, regionBucket: fileIndex, file }))\n );\n // Pushed one by one: spreading the project-scale window-candidate array as call arguments\n // overflows V8's argument limit (~124k) and crashes on Node, though Bun/JSC tolerates it.\n for (const candidate of collectWindowCandidates(files, minTokens)) {\n candidates.push(candidate);\n }\n const counted = selectMaximalGroups(\n candidates,\n spansMultipleFiles,\n // File index and position break coverage ties deterministically.\n (left, right) => left.regionBucket - right.regionBucket || left.startIndex - right.startIndex\n );\n const tokenOffsets = computeTokenOffsets(files, maxGapTokens);\n return summarize(mergeGapAdjacentGroups([...counted.values()], tokenOffsets, maxGapTokens), files, tokenOffsets);\n}\n\n/** Repeated sub-windows of sibling statements matched across the whole project's files. */\nfunction collectWindowCandidates(files: CrossFileDuplicationSourceFile[], minTokens: number): SelectableCandidate[] {\n const fileIndexByContext: number[] = [];\n const contexts: SequenceWindowContext[] = [];\n for (const [fileIndex, { tokens, containerStatements }] of files.entries()) {\n if (tokens && containerStatements) {\n fileIndexByContext.push(fileIndex);\n contexts.push({ tokens, literalCountPrefix: buildLiteralCountPrefix(tokens), containers: containerStatements });\n }\n }\n if (contexts.length < 2) {\n return [];\n }\n return collectSequenceWindowCandidates(contexts, minTokens, true).flatMap(({ candidate, contextIndex }) => {\n const fileIndex = fileIndexByContext[contextIndex];\n const file = fileIndex === undefined ? undefined : files[fileIndex];\n return fileIndex === undefined || file === undefined\n ? []\n : [{ ...candidate, regionBucket: fileIndex, file: file.file }];\n });\n}\n\nfunction spansMultipleFiles(group: SelectableCandidate[]): boolean {\n return group.length >= 2 && new Set(group.map((candidate) => candidate.regionBucket)).size >= 2;\n}\n\n/**\n * Per-file token offsets that map every file into one project-wide token index space: each file's\n * tokens are offset by more than `maxGapTokens` past the previous file's, so occurrences in\n * different files are never gap-adjacent and merged pairs always stay within one file.\n */\nfunction computeTokenOffsets(files: CrossFileDuplicationSourceFile[], maxGapTokens: number): number[] {\n const tokenOffsets: number[] = [];\n let offset = 0;\n for (const { tokens, candidates } of files) {\n tokenOffsets.push(offset);\n // Accumulated in a loop: spreading a project-scale candidate array as call arguments would\n // overflow V8's argument limit (~124k) and crash on Node.\n let tokenCount = tokens?.length ?? 0;\n if (!tokens) {\n for (const candidate of candidates) {\n tokenCount = Math.max(tokenCount, candidate.endTokenIndex);\n }\n }\n offset += tokenCount + maxGapTokens + 1;\n }\n return tokenOffsets;\n}\n\n/** Reuses the within-file gapped (Type-3) merging in the project-wide token index space. */\nfunction mergeGapAdjacentGroups(\n groups: SelectableCandidate[][],\n tokenOffsets: number[],\n maxGapTokens: number\n): CrossFileOccurrence[][] {\n const occurrenceGroups = groups.map((group) =>\n group\n .map((candidate): CrossFileOccurrence => {\n const start = candidate.startTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n const end = candidate.endTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n return {\n file: candidate.file,\n segments: [{ startTokenIndex: start, endTokenIndex: end }],\n tokenCount: candidate.tokenCount,\n startTokenIndex: start,\n endTokenIndex: end,\n startIndex: candidate.startIndex,\n endIndex: candidate.endIndex,\n startLine: candidate.startLine,\n endLine: candidate.endLine,\n };\n })\n .toSorted((left, right) => left.startTokenIndex - right.startTokenIndex)\n );\n return mergeAdjacentGroups(occurrenceGroups, maxGapTokens);\n}\n\nfunction summarize(\n groups: CrossFileOccurrence[][],\n files: CrossFileDuplicationSourceFile[],\n tokenOffsets: number[]\n): CrossFileDuplicationMetrics {\n const reported: CrossFileDuplicateBlockGroup[] = [];\n // Accumulated in Maps: file names are arbitrary strings, and a plain object would read\n // inherited properties for names like \"constructor\".\n const groupCountByFile = new Map<string, number>();\n const fileDataByName = new Map(\n files.map((file, index) => [\n file.file,\n { tokens: file.tokens, codeLineNumbers: file.codeLineNumbers, offset: tokenOffsets[index] ?? 0 },\n ])\n );\n const lineNumbersByFile = new Map<string, Set<number>>();\n let duplicateBlockCount = 0;\n for (const group of groups) {\n // Mirrors within-file counting: each redundant occurrence contributes one count per matched\n // fragment, gapped merging consolidates the grouping without halving the count, and spans a\n // partial merge shares between a retained group and the merged group count once.\n duplicateBlockCount += countRedundantFragments(group);\n for (const occurrence of group) {\n collectOccurrenceLines(occurrence, fileDataByName, lineNumbersByFile);\n }\n const occurrences = group\n .map(({ file, startLine, endLine }) => ({ file, startLine, endLine }))\n .toSorted((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n const files = [...new Set(occurrences.map(({ file }) => file))];\n for (const file of files) {\n groupCountByFile.set(file, (groupCountByFile.get(file) ?? 0) + 1);\n }\n reported.push({ files, occurrences, tokenCount: group[0]?.tokenCount ?? 0 });\n }\n reported.sort(\n (left, right) =>\n right.tokenCount - left.tokenCount ||\n (left.occurrences[0]?.file ?? '').localeCompare(right.occurrences[0]?.file ?? '') ||\n (left.occurrences[0]?.startLine ?? 0) - (right.occurrences[0]?.startLine ?? 0)\n );\n return {\n duplicateBlockCount,\n duplicateBlockGroupCountByFile: Object.fromEntries(groupCountByFile),\n duplicateLineNumbersByFile: Object.fromEntries(\n [...lineNumbersByFile].map(([file, lines]) => [file, [...lines].toSorted((left, right) => left - right)])\n ),\n groups: reported,\n };\n}\n\n/**\n * Adds the code lines an occurrence's matched tokens cover to its file's line set, mapping the\n * project-wide token segments back into the file's own token stream. A file that supplied only\n * candidates (no token stream) is skipped rather than approximated from the bounding line range,\n * which would include gap and comment/blank lines and break the field's exactness contract.\n */\nfunction collectOccurrenceLines(\n occurrence: CrossFileOccurrence,\n fileDataByName: Map<\n string,\n { tokens?: CrossFileDuplicationSourceFile['tokens']; codeLineNumbers?: Set<number>; offset: number }\n >,\n lineNumbersByFile: Map<string, Set<number>>\n): void {\n const fileData = fileDataByName.get(occurrence.file);\n if (!fileData?.tokens) {\n return;\n }\n let lines = lineNumbersByFile.get(occurrence.file);\n if (!lines) {\n lines = new Set();\n lineNumbersByFile.set(occurrence.file, lines);\n }\n for (const segment of occurrence.segments) {\n collectSegmentLines(\n {\n startTokenIndex: segment.startTokenIndex - fileData.offset,\n endTokenIndex: segment.endTokenIndex - fileData.offset,\n },\n fileData.tokens,\n fileData.codeLineNumbers,\n lines\n );\n }\n}\n"],"mappings":"wFAuEA,SAAgB,EACd,EACA,EAC6B,CAC7B,IAAM,EAAY,GAAS,WAAaA,EAAAA,0BAA0B,UAC5D,EAAe,GAAS,cAAgBA,EAAAA,0BAA0B,aAClE,EAAoC,EAAM,SAAS,CAAE,OAAM,cAAc,IAC7E,EAAW,IAAK,IAAe,CAAE,GAAG,EAAW,aAAc,EAAW,MAAK,EAAE,CACjF,EAGA,IAAK,IAAM,KAAa,EAAwB,EAAO,CAAS,EAC9D,EAAW,KAAK,CAAS,EAE3B,IAAM,EAAUC,EAAAA,oBACd,EACA,GAEC,EAAM,IAAU,EAAK,aAAe,EAAM,cAAgB,EAAK,WAAa,EAAM,UACrF,EACM,EAAe,EAAoB,EAAO,CAAY,EAC5D,OAAO,EAAU,EAAuB,CAAC,GAAG,EAAQ,OAAO,CAAC,EAAG,EAAc,CAAY,EAAG,EAAO,CAAY,CACjH,CAGA,SAAS,EAAwB,EAAyC,EAA0C,CAClH,IAAM,EAA+B,CAAC,EAChC,EAAoC,CAAC,EAC3C,IAAK,GAAM,CAAC,EAAW,CAAE,SAAQ,0BAA0B,EAAM,QAAQ,EACnE,GAAU,IACZ,EAAmB,KAAK,CAAS,EACjC,EAAS,KAAK,CAAE,SAAQ,mBAAoBC,EAAAA,wBAAwB,CAAM,EAAG,WAAY,CAAoB,CAAC,GAMlH,OAHI,EAAS,OAAS,EACb,CAAC,EAEHC,EAAAA,gCAAgC,EAAU,EAAW,EAAI,CAAC,CAAC,SAAS,CAAE,YAAW,kBAAmB,CACzG,IAAM,EAAY,EAAmB,GAC/B,EAAO,IAAc,IAAA,GAAY,IAAA,GAAY,EAAM,GACzD,OAAO,IAAc,IAAA,IAAa,IAAS,IAAA,GACvC,CAAC,EACD,CAAC,CAAE,GAAG,EAAW,aAAc,EAAW,KAAM,EAAK,IAAK,CAAC,CACjE,CAAC,CACH,CAEA,SAAS,EAAmB,EAAuC,CACjE,OAAO,EAAM,QAAU,GAAK,IAAI,IAAI,EAAM,IAAK,GAAc,EAAU,YAAY,CAAC,CAAC,CAAC,MAAQ,CAChG,CAOA,SAAS,EAAoB,EAAyC,EAAgC,CACpG,IAAM,EAAyB,CAAC,EAC5B,EAAS,EACb,IAAK,GAAM,CAAE,SAAQ,gBAAgB,EAAO,CAC1C,EAAa,KAAK,CAAM,EAGxB,IAAI,EAAa,GAAQ,QAAU,EACnC,GAAI,CAAC,EACH,IAAK,IAAM,KAAa,EACtB,EAAa,KAAK,IAAI,EAAY,EAAU,aAAa,EAG7D,GAAU,EAAa,EAAe,CACxC,CACA,OAAO,CACT,CAGA,SAAS,EACP,EACA,EACA,EACyB,CACzB,IAAM,EAAmB,EAAO,IAAK,GACnC,EACG,IAAK,GAAmC,CACvC,IAAM,EAAQ,EAAU,iBAAmB,EAAa,EAAU,eAAiB,GAC7E,EAAM,EAAU,eAAiB,EAAa,EAAU,eAAiB,GAC/E,MAAO,CACL,KAAM,EAAU,KAChB,SAAU,CAAC,CAAE,gBAAiB,EAAO,cAAe,CAAI,CAAC,EACzD,WAAY,EAAU,WACtB,gBAAiB,EACjB,cAAe,EACf,WAAY,EAAU,WACtB,SAAU,EAAU,SACpB,UAAW,EAAU,UACrB,QAAS,EAAU,OACrB,CACF,CAAC,CAAC,CACD,UAAU,EAAM,IAAU,EAAK,gBAAkB,EAAM,eAAe,CAC3E,EACA,OAAOC,EAAAA,oBAAoB,EAAkB,CAAY,CAC3D,CAEA,SAAS,EACP,EACA,EACA,EAC6B,CAC7B,IAAM,EAA2C,CAAC,EAG5C,EAAmB,IAAI,IACvB,EAAiB,IAAI,IACzB,EAAM,KAAK,EAAM,IAAU,CACzB,EAAK,KACL,CAAE,OAAQ,EAAK,OAAQ,gBAAiB,EAAK,gBAAiB,OAAQ,EAAa,IAAU,CAAE,CACjG,CAAC,CACH,EACM,EAAoB,IAAI,IAC1B,EAAsB,EAC1B,IAAK,IAAM,KAAS,EAAQ,CAI1B,GAAuBC,EAAAA,wBAAwB,CAAK,EACpD,IAAK,IAAM,KAAc,EACvB,EAAuB,EAAY,EAAgB,CAAiB,EAEtE,IAAM,EAAc,EACjB,KAAK,CAAE,OAAM,YAAW,cAAe,CAAE,OAAM,YAAW,SAAQ,EAAE,CAAC,CACrE,UAAU,EAAM,IAAU,EAAK,KAAK,cAAc,EAAM,IAAI,GAAK,EAAK,UAAY,EAAM,SAAS,EAC9F,EAAQ,CAAC,GAAG,IAAI,IAAI,EAAY,KAAK,CAAE,UAAW,CAAI,CAAC,CAAC,EAC9D,IAAK,IAAM,KAAQ,EACjB,EAAiB,IAAI,GAAO,EAAiB,IAAI,CAAI,GAAK,GAAK,CAAC,EAElE,EAAS,KAAK,CAAE,QAAO,cAAa,WAAY,EAAM,EAAE,EAAE,YAAc,CAAE,CAAC,CAC7E,CAOA,OANA,EAAS,MACN,EAAM,IACL,EAAM,WAAa,EAAK,aACvB,EAAK,YAAY,EAAE,EAAE,MAAQ,GAAA,CAAI,cAAc,EAAM,YAAY,EAAE,EAAE,MAAQ,EAAE,IAC/E,EAAK,YAAY,EAAE,EAAE,WAAa,IAAM,EAAM,YAAY,EAAE,EAAE,WAAa,EAChF,EACO,CACL,sBACA,+BAAgC,OAAO,YAAY,CAAgB,EACnE,2BAA4B,OAAO,YACjC,CAAC,GAAG,CAAiB,CAAC,CAAC,KAAK,CAAC,EAAM,KAAW,CAAC,EAAM,CAAC,GAAG,CAAK,CAAC,CAAC,UAAU,EAAM,IAAU,EAAO,CAAK,CAAC,CAAC,CAC1G,EACA,OAAQ,CACV,CACF,CAQA,SAAS,EACP,EACA,EAIA,EACM,CACN,IAAM,EAAW,EAAe,IAAI,EAAW,IAAI,EACnD,GAAI,CAAC,GAAU,OACb,OAEF,IAAI,EAAQ,EAAkB,IAAI,EAAW,IAAI,EAC5C,IACH,EAAQ,IAAI,IACZ,EAAkB,IAAI,EAAW,KAAM,CAAK,GAE9C,IAAK,IAAM,KAAW,EAAW,SAC/B,EAAA,oBACE,CACE,gBAAiB,EAAQ,gBAAkB,EAAS,OACpD,cAAe,EAAQ,cAAgB,EAAS,MAClD,EACA,EAAS,OACT,EAAS,gBACT,CACF,CAEJ"}
|
|
1
|
+
{"version":3,"file":"crossFileDuplication.cjs","names":["resolveDuplicationOptions","selectMaximalGroups","buildLiteralCountPrefix","collectSequenceWindowCandidates","mergeAdjacentGroups","countRedundantFragments"],"sources":["../src/crossFileDuplication.ts"],"sourcesContent":["import { selectMaximalGroups } from './duplicateSelection.js';\nimport {\n buildLiteralCountPrefix,\n collectSegmentLines,\n collectSequenceWindowCandidates,\n countRedundantFragments,\n mergeAdjacentGroups,\n resolveDuplicationOptions,\n type CountedOccurrence,\n type CrossFileDuplicateCandidate,\n type CrossFileDuplicationFileData,\n type SequenceWindowContext,\n} from './duplication.js';\nimport type { DuplicationOptions } from './types.js';\n\nexport interface CrossFileDuplicationSourceFile extends Partial<CrossFileDuplicationFileData> {\n file: string;\n candidates: CrossFileDuplicateCandidate[];\n}\n\nexport interface CrossFileDuplicateOccurrence {\n endLine: number;\n file: string;\n startLine: number;\n}\n\nexport interface CrossFileDuplicateBlockGroup {\n files: string[];\n occurrences: CrossFileDuplicateOccurrence[];\n /** Matched token count of one occurrence (all occurrences share it; gaps are not counted). */\n tokenCount: number;\n}\n\nexport interface CrossFileDuplicationMetrics {\n /** Number of redundant copies across all groups, counted per matched fragment like within-file. */\n duplicateBlockCount: number;\n /** Groups the file participates in, keyed by the file name passed in. */\n duplicateBlockGroupCountByFile: Record<string, number>;\n /**\n * Per file, the 1-based code lines covered by matched tokens of its cross-file occurrences,\n * sorted ascending. Exact like within-file duplicateLineNumbers: the unmatched gap of a merged\n * clone and comment/blank lines inside an occurrence's bounding range are excluded (blank rows\n * inside multi-row tokens only when the file supplied codeLineNumbers). A file that supplied\n * only candidates (no `tokens`) has no entry — without its token stream the matched lines are\n * unknowable, and an approximate bounding range would break this field's exactness.\n */\n duplicateLineNumbersByFile: Record<string, number[]>;\n groups: CrossFileDuplicateBlockGroup[];\n}\n\ninterface SelectableCandidate extends CrossFileDuplicateCandidate {\n regionBucket: number;\n file: string;\n}\n\n/** A cross-file occurrence: a within-file occurrence in the project-wide token index space. */\ninterface CrossFileOccurrence extends CountedOccurrence {\n file: string;\n}\n\n/**\n * Detects code regions duplicated across files. Per-file candidates (whole block subtrees and full\n * container runs, fingerprinted with the same normalization as within-file duplication) are joined\n * by a project-level window index over per-statement fingerprint sequences (CPD-style), so a\n * copy-pasted partial statement run embedded in different surrounding code is matched even though\n * no single file can know it repeats elsewhere. Candidates are grouped by fingerprint, and only\n * maximal, non-overlapping regions whose group spans at least two files are counted. Groups that\n * shrink to a single file during selection are shed — a within-file repeat is already reported by\n * that file's own duplication metrics. Groups separated by a small token gap within each file then\n * merge into gapped (Type-3) clone groups under `maxGapTokens`, exactly like within-file merging.\n */\nexport function measureCrossFileDuplication(\n files: CrossFileDuplicationSourceFile[],\n options?: DuplicationOptions\n): CrossFileDuplicationMetrics {\n const { minTokens, maxGapTokens } = resolveDuplicationOptions(options);\n const candidates: SelectableCandidate[] = files.flatMap(({ file, candidates }, fileIndex) =>\n candidates.map((candidate) => ({ ...candidate, regionBucket: fileIndex, file }))\n );\n // Pushed one by one: spreading the project-scale window-candidate array as call arguments\n // overflows V8's argument limit (~124k) and crashes on Node, though Bun/JSC tolerates it.\n for (const candidate of collectWindowCandidates(files, minTokens)) {\n candidates.push(candidate);\n }\n const counted = selectMaximalGroups(\n candidates,\n spansMultipleFiles,\n // File index and position break coverage ties deterministically.\n (left, right) => left.regionBucket - right.regionBucket || left.startIndex - right.startIndex\n );\n const tokenOffsets = computeTokenOffsets(files, maxGapTokens);\n return summarize(mergeGapAdjacentGroups([...counted.values()], tokenOffsets, maxGapTokens), files, tokenOffsets);\n}\n\n/** Repeated sub-windows of sibling statements matched across the whole project's files. */\nfunction collectWindowCandidates(files: CrossFileDuplicationSourceFile[], minTokens: number): SelectableCandidate[] {\n const fileIndexByContext: number[] = [];\n const contexts: SequenceWindowContext[] = [];\n for (const [fileIndex, { tokens, containerStatements }] of files.entries()) {\n if (tokens && containerStatements) {\n fileIndexByContext.push(fileIndex);\n contexts.push({ tokens, literalCountPrefix: buildLiteralCountPrefix(tokens), containers: containerStatements });\n }\n }\n if (contexts.length < 2) {\n return [];\n }\n return collectSequenceWindowCandidates(contexts, minTokens, true).flatMap(({ candidate, contextIndex }) => {\n const fileIndex = fileIndexByContext[contextIndex];\n const file = fileIndex === undefined ? undefined : files[fileIndex];\n return fileIndex === undefined || file === undefined\n ? []\n : [{ ...candidate, regionBucket: fileIndex, file: file.file }];\n });\n}\n\nfunction spansMultipleFiles(group: SelectableCandidate[]): boolean {\n return group.length >= 2 && new Set(group.map((candidate) => candidate.regionBucket)).size >= 2;\n}\n\n/**\n * Per-file token offsets that map every file into one project-wide token index space: each file's\n * tokens are offset by more than `maxGapTokens` past the previous file's, so occurrences in\n * different files are never gap-adjacent and merged pairs always stay within one file.\n */\nfunction computeTokenOffsets(files: CrossFileDuplicationSourceFile[], maxGapTokens: number): number[] {\n const tokenOffsets: number[] = [];\n let offset = 0;\n for (const { tokens, candidates } of files) {\n tokenOffsets.push(offset);\n // Accumulated in a loop: spreading a project-scale candidate array as call arguments would\n // overflow V8's argument limit (~124k) and crash on Node.\n let tokenCount = tokens?.length ?? 0;\n if (!tokens) {\n for (const candidate of candidates) {\n tokenCount = Math.max(tokenCount, candidate.endTokenIndex);\n }\n }\n offset += tokenCount + maxGapTokens + 1;\n }\n return tokenOffsets;\n}\n\n/** Reuses the within-file gapped (Type-3) merging in the project-wide token index space. */\nfunction mergeGapAdjacentGroups(\n groups: SelectableCandidate[][],\n tokenOffsets: number[],\n maxGapTokens: number\n): CrossFileOccurrence[][] {\n const occurrenceGroups = groups.map((group) =>\n group\n .map((candidate): CrossFileOccurrence => {\n const start = candidate.startTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n const end = candidate.endTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n return {\n file: candidate.file,\n segments: [{ startTokenIndex: start, endTokenIndex: end }],\n tokenCount: candidate.tokenCount,\n startTokenIndex: start,\n endTokenIndex: end,\n startIndex: candidate.startIndex,\n endIndex: candidate.endIndex,\n startLine: candidate.startLine,\n endLine: candidate.endLine,\n };\n })\n .toSorted((left, right) => left.startTokenIndex - right.startTokenIndex)\n );\n return mergeAdjacentGroups(occurrenceGroups, maxGapTokens);\n}\n\nfunction summarize(\n groups: CrossFileOccurrence[][],\n files: CrossFileDuplicationSourceFile[],\n tokenOffsets: number[]\n): CrossFileDuplicationMetrics {\n const reported: CrossFileDuplicateBlockGroup[] = [];\n // Accumulated in Maps: file names are arbitrary strings, and a plain object would read\n // inherited properties for names like \"constructor\".\n const groupCountByFile = new Map<string, number>();\n const fileDataByName = new Map(\n files.map((file, index) => [\n file.file,\n { tokens: file.tokens, codeLineNumbers: file.codeLineNumbers, offset: tokenOffsets[index] ?? 0 },\n ])\n );\n const lineNumbersByFile = new Map<string, Set<number>>();\n let duplicateBlockCount = 0;\n for (const group of groups) {\n // Mirrors within-file counting: each redundant occurrence contributes one count per matched\n // fragment, gapped merging consolidates the grouping without halving the count, and spans a\n // partial merge shares between a retained group and the merged group count once.\n duplicateBlockCount += countRedundantFragments(group);\n for (const occurrence of group) {\n collectOccurrenceLines(occurrence, fileDataByName, lineNumbersByFile);\n }\n const occurrences = group\n .map(({ file, startLine, endLine }) => ({ file, startLine, endLine }))\n .toSorted((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n const files = [...new Set(occurrences.map(({ file }) => file))];\n for (const file of files) {\n groupCountByFile.set(file, (groupCountByFile.get(file) ?? 0) + 1);\n }\n reported.push({ files, occurrences, tokenCount: group[0]?.tokenCount ?? 0 });\n }\n reported.sort(\n (left, right) =>\n right.tokenCount - left.tokenCount ||\n (left.occurrences[0]?.file ?? '').localeCompare(right.occurrences[0]?.file ?? '') ||\n (left.occurrences[0]?.startLine ?? 0) - (right.occurrences[0]?.startLine ?? 0)\n );\n return {\n duplicateBlockCount,\n duplicateBlockGroupCountByFile: Object.fromEntries(groupCountByFile),\n duplicateLineNumbersByFile: Object.fromEntries(\n [...lineNumbersByFile].map(([file, lines]) => [file, [...lines].toSorted((left, right) => left - right)])\n ),\n groups: reported,\n };\n}\n\n/**\n * Adds the code lines an occurrence's matched tokens cover to its file's line set, mapping the\n * project-wide token segments back into the file's own token stream. A file that supplied only\n * candidates (no token stream) is skipped rather than approximated from the bounding line range,\n * which would include gap and comment/blank lines and break the field's exactness contract.\n */\nfunction collectOccurrenceLines(\n occurrence: CrossFileOccurrence,\n fileDataByName: Map<\n string,\n { tokens?: CrossFileDuplicationSourceFile['tokens']; codeLineNumbers?: Set<number>; offset: number }\n >,\n lineNumbersByFile: Map<string, Set<number>>\n): void {\n const fileData = fileDataByName.get(occurrence.file);\n if (!fileData?.tokens) {\n return;\n }\n let lines = lineNumbersByFile.get(occurrence.file);\n if (!lines) {\n lines = new Set();\n lineNumbersByFile.set(occurrence.file, lines);\n }\n for (const segment of occurrence.segments) {\n collectSegmentLines(\n {\n startTokenIndex: segment.startTokenIndex - fileData.offset,\n endTokenIndex: segment.endTokenIndex - fileData.offset,\n },\n fileData.tokens,\n fileData.codeLineNumbers,\n lines\n );\n }\n}\n"],"mappings":"wFAuEA,SAAgB,EACd,EACA,EAC6B,CAC7B,GAAM,CAAE,YAAW,gBAAiBA,EAAAA,0BAA0B,CAAO,EAC/D,EAAoC,EAAM,SAAS,CAAE,OAAM,cAAc,IAC7E,EAAW,IAAK,IAAe,CAAE,GAAG,EAAW,aAAc,EAAW,MAAK,EAAE,CACjF,EAGA,IAAK,IAAM,KAAa,EAAwB,EAAO,CAAS,EAC9D,EAAW,KAAK,CAAS,EAE3B,IAAM,EAAUC,EAAAA,oBACd,EACA,GAEC,EAAM,IAAU,EAAK,aAAe,EAAM,cAAgB,EAAK,WAAa,EAAM,UACrF,EACM,EAAe,EAAoB,EAAO,CAAY,EAC5D,OAAO,EAAU,EAAuB,CAAC,GAAG,EAAQ,OAAO,CAAC,EAAG,EAAc,CAAY,EAAG,EAAO,CAAY,CACjH,CAGA,SAAS,EAAwB,EAAyC,EAA0C,CAClH,IAAM,EAA+B,CAAC,EAChC,EAAoC,CAAC,EAC3C,IAAK,GAAM,CAAC,EAAW,CAAE,SAAQ,0BAA0B,EAAM,QAAQ,EACnE,GAAU,IACZ,EAAmB,KAAK,CAAS,EACjC,EAAS,KAAK,CAAE,SAAQ,mBAAoBC,EAAAA,wBAAwB,CAAM,EAAG,WAAY,CAAoB,CAAC,GAMlH,OAHI,EAAS,OAAS,EACb,CAAC,EAEHC,EAAAA,gCAAgC,EAAU,EAAW,EAAI,CAAC,CAAC,SAAS,CAAE,YAAW,kBAAmB,CACzG,IAAM,EAAY,EAAmB,GAC/B,EAAO,IAAc,IAAA,GAAY,IAAA,GAAY,EAAM,GACzD,OAAO,IAAc,IAAA,IAAa,IAAS,IAAA,GACvC,CAAC,EACD,CAAC,CAAE,GAAG,EAAW,aAAc,EAAW,KAAM,EAAK,IAAK,CAAC,CACjE,CAAC,CACH,CAEA,SAAS,EAAmB,EAAuC,CACjE,OAAO,EAAM,QAAU,GAAK,IAAI,IAAI,EAAM,IAAK,GAAc,EAAU,YAAY,CAAC,CAAC,CAAC,MAAQ,CAChG,CAOA,SAAS,EAAoB,EAAyC,EAAgC,CACpG,IAAM,EAAyB,CAAC,EAC5B,EAAS,EACb,IAAK,GAAM,CAAE,SAAQ,gBAAgB,EAAO,CAC1C,EAAa,KAAK,CAAM,EAGxB,IAAI,EAAa,GAAQ,QAAU,EACnC,GAAI,CAAC,EACH,IAAK,IAAM,KAAa,EACtB,EAAa,KAAK,IAAI,EAAY,EAAU,aAAa,EAG7D,GAAU,EAAa,EAAe,CACxC,CACA,OAAO,CACT,CAGA,SAAS,EACP,EACA,EACA,EACyB,CACzB,IAAM,EAAmB,EAAO,IAAK,GACnC,EACG,IAAK,GAAmC,CACvC,IAAM,EAAQ,EAAU,iBAAmB,EAAa,EAAU,eAAiB,GAC7E,EAAM,EAAU,eAAiB,EAAa,EAAU,eAAiB,GAC/E,MAAO,CACL,KAAM,EAAU,KAChB,SAAU,CAAC,CAAE,gBAAiB,EAAO,cAAe,CAAI,CAAC,EACzD,WAAY,EAAU,WACtB,gBAAiB,EACjB,cAAe,EACf,WAAY,EAAU,WACtB,SAAU,EAAU,SACpB,UAAW,EAAU,UACrB,QAAS,EAAU,OACrB,CACF,CAAC,CAAC,CACD,UAAU,EAAM,IAAU,EAAK,gBAAkB,EAAM,eAAe,CAC3E,EACA,OAAOC,EAAAA,oBAAoB,EAAkB,CAAY,CAC3D,CAEA,SAAS,EACP,EACA,EACA,EAC6B,CAC7B,IAAM,EAA2C,CAAC,EAG5C,EAAmB,IAAI,IACvB,EAAiB,IAAI,IACzB,EAAM,KAAK,EAAM,IAAU,CACzB,EAAK,KACL,CAAE,OAAQ,EAAK,OAAQ,gBAAiB,EAAK,gBAAiB,OAAQ,EAAa,IAAU,CAAE,CACjG,CAAC,CACH,EACM,EAAoB,IAAI,IAC1B,EAAsB,EAC1B,IAAK,IAAM,KAAS,EAAQ,CAI1B,GAAuBC,EAAAA,wBAAwB,CAAK,EACpD,IAAK,IAAM,KAAc,EACvB,EAAuB,EAAY,EAAgB,CAAiB,EAEtE,IAAM,EAAc,EACjB,KAAK,CAAE,OAAM,YAAW,cAAe,CAAE,OAAM,YAAW,SAAQ,EAAE,CAAC,CACrE,UAAU,EAAM,IAAU,EAAK,KAAK,cAAc,EAAM,IAAI,GAAK,EAAK,UAAY,EAAM,SAAS,EAC9F,EAAQ,CAAC,GAAG,IAAI,IAAI,EAAY,KAAK,CAAE,UAAW,CAAI,CAAC,CAAC,EAC9D,IAAK,IAAM,KAAQ,EACjB,EAAiB,IAAI,GAAO,EAAiB,IAAI,CAAI,GAAK,GAAK,CAAC,EAElE,EAAS,KAAK,CAAE,QAAO,cAAa,WAAY,EAAM,EAAE,EAAE,YAAc,CAAE,CAAC,CAC7E,CAOA,OANA,EAAS,MACN,EAAM,IACL,EAAM,WAAa,EAAK,aACvB,EAAK,YAAY,EAAE,EAAE,MAAQ,GAAA,CAAI,cAAc,EAAM,YAAY,EAAE,EAAE,MAAQ,EAAE,IAC/E,EAAK,YAAY,EAAE,EAAE,WAAa,IAAM,EAAM,YAAY,EAAE,EAAE,WAAa,EAChF,EACO,CACL,sBACA,+BAAgC,OAAO,YAAY,CAAgB,EACnE,2BAA4B,OAAO,YACjC,CAAC,GAAG,CAAiB,CAAC,CAAC,KAAK,CAAC,EAAM,KAAW,CAAC,EAAM,CAAC,GAAG,CAAK,CAAC,CAAC,UAAU,EAAM,IAAU,EAAO,CAAK,CAAC,CAAC,CAC1G,EACA,OAAQ,CACV,CACF,CAQA,SAAS,EACP,EACA,EAIA,EACM,CACN,IAAM,EAAW,EAAe,IAAI,EAAW,IAAI,EACnD,GAAI,CAAC,GAAU,OACb,OAEF,IAAI,EAAQ,EAAkB,IAAI,EAAW,IAAI,EAC5C,IACH,EAAQ,IAAI,IACZ,EAAkB,IAAI,EAAW,KAAM,CAAK,GAE9C,IAAK,IAAM,KAAW,EAAW,SAC/B,EAAA,oBACE,CACE,gBAAiB,EAAQ,gBAAkB,EAAS,OACpD,cAAe,EAAQ,cAAgB,EAAS,MAClD,EACA,EAAS,OACT,EAAS,gBACT,CACF,CAEJ"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{selectMaximalGroups as e}from"./duplicateSelection.js";import{buildLiteralCountPrefix as t,collectSegmentLines as n,collectSequenceWindowCandidates as r,countRedundantFragments as i,
|
|
1
|
+
import{selectMaximalGroups as e}from"./duplicateSelection.js";import{buildLiteralCountPrefix as t,collectSegmentLines as n,collectSequenceWindowCandidates as r,countRedundantFragments as i,mergeAdjacentGroups as a,resolveDuplicationOptions as o}from"./duplication.js";function s(t,n){let{minTokens:r,maxGapTokens:i}=o(n),a=t.flatMap(({file:e,candidates:t},n)=>t.map(t=>({...t,regionBucket:n,file:e})));for(let e of c(t,r))a.push(e);let s=e(a,l,(e,t)=>e.regionBucket-t.regionBucket||e.startIndex-t.startIndex),p=u(t,i);return f(d([...s.values()],p,i),t,p)}function c(e,n){let i=[],a=[];for(let[n,{tokens:r,containerStatements:o}]of e.entries())r&&o&&(i.push(n),a.push({tokens:r,literalCountPrefix:t(r),containers:o}));return a.length<2?[]:r(a,n,!0).flatMap(({candidate:t,contextIndex:n})=>{let r=i[n],a=r===void 0?void 0:e[r];return r===void 0||a===void 0?[]:[{...t,regionBucket:r,file:a.file}]})}function l(e){return e.length>=2&&new Set(e.map(e=>e.regionBucket)).size>=2}function u(e,t){let n=[],r=0;for(let{tokens:i,candidates:a}of e){n.push(r);let e=i?.length??0;if(!i)for(let t of a)e=Math.max(e,t.endTokenIndex);r+=e+t+1}return n}function d(e,t,n){let r=e.map(e=>e.map(e=>{let n=e.startTokenIndex+(t[e.regionBucket]??0),r=e.endTokenIndex+(t[e.regionBucket]??0);return{file:e.file,segments:[{startTokenIndex:n,endTokenIndex:r}],tokenCount:e.tokenCount,startTokenIndex:n,endTokenIndex:r,startIndex:e.startIndex,endIndex:e.endIndex,startLine:e.startLine,endLine:e.endLine}}).toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex));return a(r,n)}function f(e,t,n){let r=[],a=new Map,o=new Map(t.map((e,t)=>[e.file,{tokens:e.tokens,codeLineNumbers:e.codeLineNumbers,offset:n[t]??0}])),s=new Map,c=0;for(let t of e){c+=i(t);for(let e of t)p(e,o,s);let e=t.map(({file:e,startLine:t,endLine:n})=>({file:e,startLine:t,endLine:n})).toSorted((e,t)=>e.file.localeCompare(t.file)||e.startLine-t.startLine),n=[...new Set(e.map(({file:e})=>e))];for(let e of n)a.set(e,(a.get(e)??0)+1);r.push({files:n,occurrences:e,tokenCount:t[0]?.tokenCount??0})}return r.sort((e,t)=>t.tokenCount-e.tokenCount||(e.occurrences[0]?.file??``).localeCompare(t.occurrences[0]?.file??``)||(e.occurrences[0]?.startLine??0)-(t.occurrences[0]?.startLine??0)),{duplicateBlockCount:c,duplicateBlockGroupCountByFile:Object.fromEntries(a),duplicateLineNumbersByFile:Object.fromEntries([...s].map(([e,t])=>[e,[...t].toSorted((e,t)=>e-t)])),groups:r}}function p(e,t,r){let i=t.get(e.file);if(!i?.tokens)return;let a=r.get(e.file);a||(a=new Set,r.set(e.file,a));for(let t of e.segments)n({startTokenIndex:t.startTokenIndex-i.offset,endTokenIndex:t.endTokenIndex-i.offset},i.tokens,i.codeLineNumbers,a)}export{s as measureCrossFileDuplication};
|
|
2
2
|
//# sourceMappingURL=crossFileDuplication.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crossFileDuplication.js","names":[],"sources":["../src/crossFileDuplication.ts"],"sourcesContent":["import { selectMaximalGroups } from './duplicateSelection.js';\nimport {\n buildLiteralCountPrefix,\n collectSegmentLines,\n collectSequenceWindowCandidates,\n countRedundantFragments,\n defaultDuplicationOptions,\n mergeAdjacentGroups,\n type CountedOccurrence,\n type CrossFileDuplicateCandidate,\n type CrossFileDuplicationFileData,\n type SequenceWindowContext,\n} from './duplication.js';\nimport type { DuplicationOptions } from './types.js';\n\nexport interface CrossFileDuplicationSourceFile extends Partial<CrossFileDuplicationFileData> {\n file: string;\n candidates: CrossFileDuplicateCandidate[];\n}\n\nexport interface CrossFileDuplicateOccurrence {\n endLine: number;\n file: string;\n startLine: number;\n}\n\nexport interface CrossFileDuplicateBlockGroup {\n files: string[];\n occurrences: CrossFileDuplicateOccurrence[];\n /** Matched token count of one occurrence (all occurrences share it; gaps are not counted). */\n tokenCount: number;\n}\n\nexport interface CrossFileDuplicationMetrics {\n /** Number of redundant copies across all groups, counted per matched fragment like within-file. */\n duplicateBlockCount: number;\n /** Groups the file participates in, keyed by the file name passed in. */\n duplicateBlockGroupCountByFile: Record<string, number>;\n /**\n * Per file, the 1-based code lines covered by matched tokens of its cross-file occurrences,\n * sorted ascending. Exact like within-file duplicateLineNumbers: the unmatched gap of a merged\n * clone and comment/blank lines inside an occurrence's bounding range are excluded (blank rows\n * inside multi-row tokens only when the file supplied codeLineNumbers). A file that supplied\n * only candidates (no `tokens`) has no entry — without its token stream the matched lines are\n * unknowable, and an approximate bounding range would break this field's exactness.\n */\n duplicateLineNumbersByFile: Record<string, number[]>;\n groups: CrossFileDuplicateBlockGroup[];\n}\n\ninterface SelectableCandidate extends CrossFileDuplicateCandidate {\n regionBucket: number;\n file: string;\n}\n\n/** A cross-file occurrence: a within-file occurrence in the project-wide token index space. */\ninterface CrossFileOccurrence extends CountedOccurrence {\n file: string;\n}\n\n/**\n * Detects code regions duplicated across files. Per-file candidates (whole block subtrees and full\n * container runs, fingerprinted with the same normalization as within-file duplication) are joined\n * by a project-level window index over per-statement fingerprint sequences (CPD-style), so a\n * copy-pasted partial statement run embedded in different surrounding code is matched even though\n * no single file can know it repeats elsewhere. Candidates are grouped by fingerprint, and only\n * maximal, non-overlapping regions whose group spans at least two files are counted. Groups that\n * shrink to a single file during selection are shed — a within-file repeat is already reported by\n * that file's own duplication metrics. Groups separated by a small token gap within each file then\n * merge into gapped (Type-3) clone groups under `maxGapTokens`, exactly like within-file merging.\n */\nexport function measureCrossFileDuplication(\n files: CrossFileDuplicationSourceFile[],\n options?: DuplicationOptions\n): CrossFileDuplicationMetrics {\n const minTokens = options?.minTokens ?? defaultDuplicationOptions.minTokens;\n const maxGapTokens = options?.maxGapTokens ?? defaultDuplicationOptions.maxGapTokens;\n const candidates: SelectableCandidate[] = files.flatMap(({ file, candidates }, fileIndex) =>\n candidates.map((candidate) => ({ ...candidate, regionBucket: fileIndex, file }))\n );\n // Pushed one by one: spreading the project-scale window-candidate array as call arguments\n // overflows V8's argument limit (~124k) and crashes on Node, though Bun/JSC tolerates it.\n for (const candidate of collectWindowCandidates(files, minTokens)) {\n candidates.push(candidate);\n }\n const counted = selectMaximalGroups(\n candidates,\n spansMultipleFiles,\n // File index and position break coverage ties deterministically.\n (left, right) => left.regionBucket - right.regionBucket || left.startIndex - right.startIndex\n );\n const tokenOffsets = computeTokenOffsets(files, maxGapTokens);\n return summarize(mergeGapAdjacentGroups([...counted.values()], tokenOffsets, maxGapTokens), files, tokenOffsets);\n}\n\n/** Repeated sub-windows of sibling statements matched across the whole project's files. */\nfunction collectWindowCandidates(files: CrossFileDuplicationSourceFile[], minTokens: number): SelectableCandidate[] {\n const fileIndexByContext: number[] = [];\n const contexts: SequenceWindowContext[] = [];\n for (const [fileIndex, { tokens, containerStatements }] of files.entries()) {\n if (tokens && containerStatements) {\n fileIndexByContext.push(fileIndex);\n contexts.push({ tokens, literalCountPrefix: buildLiteralCountPrefix(tokens), containers: containerStatements });\n }\n }\n if (contexts.length < 2) {\n return [];\n }\n return collectSequenceWindowCandidates(contexts, minTokens, true).flatMap(({ candidate, contextIndex }) => {\n const fileIndex = fileIndexByContext[contextIndex];\n const file = fileIndex === undefined ? undefined : files[fileIndex];\n return fileIndex === undefined || file === undefined\n ? []\n : [{ ...candidate, regionBucket: fileIndex, file: file.file }];\n });\n}\n\nfunction spansMultipleFiles(group: SelectableCandidate[]): boolean {\n return group.length >= 2 && new Set(group.map((candidate) => candidate.regionBucket)).size >= 2;\n}\n\n/**\n * Per-file token offsets that map every file into one project-wide token index space: each file's\n * tokens are offset by more than `maxGapTokens` past the previous file's, so occurrences in\n * different files are never gap-adjacent and merged pairs always stay within one file.\n */\nfunction computeTokenOffsets(files: CrossFileDuplicationSourceFile[], maxGapTokens: number): number[] {\n const tokenOffsets: number[] = [];\n let offset = 0;\n for (const { tokens, candidates } of files) {\n tokenOffsets.push(offset);\n // Accumulated in a loop: spreading a project-scale candidate array as call arguments would\n // overflow V8's argument limit (~124k) and crash on Node.\n let tokenCount = tokens?.length ?? 0;\n if (!tokens) {\n for (const candidate of candidates) {\n tokenCount = Math.max(tokenCount, candidate.endTokenIndex);\n }\n }\n offset += tokenCount + maxGapTokens + 1;\n }\n return tokenOffsets;\n}\n\n/** Reuses the within-file gapped (Type-3) merging in the project-wide token index space. */\nfunction mergeGapAdjacentGroups(\n groups: SelectableCandidate[][],\n tokenOffsets: number[],\n maxGapTokens: number\n): CrossFileOccurrence[][] {\n const occurrenceGroups = groups.map((group) =>\n group\n .map((candidate): CrossFileOccurrence => {\n const start = candidate.startTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n const end = candidate.endTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n return {\n file: candidate.file,\n segments: [{ startTokenIndex: start, endTokenIndex: end }],\n tokenCount: candidate.tokenCount,\n startTokenIndex: start,\n endTokenIndex: end,\n startIndex: candidate.startIndex,\n endIndex: candidate.endIndex,\n startLine: candidate.startLine,\n endLine: candidate.endLine,\n };\n })\n .toSorted((left, right) => left.startTokenIndex - right.startTokenIndex)\n );\n return mergeAdjacentGroups(occurrenceGroups, maxGapTokens);\n}\n\nfunction summarize(\n groups: CrossFileOccurrence[][],\n files: CrossFileDuplicationSourceFile[],\n tokenOffsets: number[]\n): CrossFileDuplicationMetrics {\n const reported: CrossFileDuplicateBlockGroup[] = [];\n // Accumulated in Maps: file names are arbitrary strings, and a plain object would read\n // inherited properties for names like \"constructor\".\n const groupCountByFile = new Map<string, number>();\n const fileDataByName = new Map(\n files.map((file, index) => [\n file.file,\n { tokens: file.tokens, codeLineNumbers: file.codeLineNumbers, offset: tokenOffsets[index] ?? 0 },\n ])\n );\n const lineNumbersByFile = new Map<string, Set<number>>();\n let duplicateBlockCount = 0;\n for (const group of groups) {\n // Mirrors within-file counting: each redundant occurrence contributes one count per matched\n // fragment, gapped merging consolidates the grouping without halving the count, and spans a\n // partial merge shares between a retained group and the merged group count once.\n duplicateBlockCount += countRedundantFragments(group);\n for (const occurrence of group) {\n collectOccurrenceLines(occurrence, fileDataByName, lineNumbersByFile);\n }\n const occurrences = group\n .map(({ file, startLine, endLine }) => ({ file, startLine, endLine }))\n .toSorted((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n const files = [...new Set(occurrences.map(({ file }) => file))];\n for (const file of files) {\n groupCountByFile.set(file, (groupCountByFile.get(file) ?? 0) + 1);\n }\n reported.push({ files, occurrences, tokenCount: group[0]?.tokenCount ?? 0 });\n }\n reported.sort(\n (left, right) =>\n right.tokenCount - left.tokenCount ||\n (left.occurrences[0]?.file ?? '').localeCompare(right.occurrences[0]?.file ?? '') ||\n (left.occurrences[0]?.startLine ?? 0) - (right.occurrences[0]?.startLine ?? 0)\n );\n return {\n duplicateBlockCount,\n duplicateBlockGroupCountByFile: Object.fromEntries(groupCountByFile),\n duplicateLineNumbersByFile: Object.fromEntries(\n [...lineNumbersByFile].map(([file, lines]) => [file, [...lines].toSorted((left, right) => left - right)])\n ),\n groups: reported,\n };\n}\n\n/**\n * Adds the code lines an occurrence's matched tokens cover to its file's line set, mapping the\n * project-wide token segments back into the file's own token stream. A file that supplied only\n * candidates (no token stream) is skipped rather than approximated from the bounding line range,\n * which would include gap and comment/blank lines and break the field's exactness contract.\n */\nfunction collectOccurrenceLines(\n occurrence: CrossFileOccurrence,\n fileDataByName: Map<\n string,\n { tokens?: CrossFileDuplicationSourceFile['tokens']; codeLineNumbers?: Set<number>; offset: number }\n >,\n lineNumbersByFile: Map<string, Set<number>>\n): void {\n const fileData = fileDataByName.get(occurrence.file);\n if (!fileData?.tokens) {\n return;\n }\n let lines = lineNumbersByFile.get(occurrence.file);\n if (!lines) {\n lines = new Set();\n lineNumbersByFile.set(occurrence.file, lines);\n }\n for (const segment of occurrence.segments) {\n collectSegmentLines(\n {\n startTokenIndex: segment.startTokenIndex - fileData.offset,\n endTokenIndex: segment.endTokenIndex - fileData.offset,\n },\n fileData.tokens,\n fileData.codeLineNumbers,\n lines\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"crossFileDuplication.js","names":[],"sources":["../src/crossFileDuplication.ts"],"sourcesContent":["import { selectMaximalGroups } from './duplicateSelection.js';\nimport {\n buildLiteralCountPrefix,\n collectSegmentLines,\n collectSequenceWindowCandidates,\n countRedundantFragments,\n mergeAdjacentGroups,\n resolveDuplicationOptions,\n type CountedOccurrence,\n type CrossFileDuplicateCandidate,\n type CrossFileDuplicationFileData,\n type SequenceWindowContext,\n} from './duplication.js';\nimport type { DuplicationOptions } from './types.js';\n\nexport interface CrossFileDuplicationSourceFile extends Partial<CrossFileDuplicationFileData> {\n file: string;\n candidates: CrossFileDuplicateCandidate[];\n}\n\nexport interface CrossFileDuplicateOccurrence {\n endLine: number;\n file: string;\n startLine: number;\n}\n\nexport interface CrossFileDuplicateBlockGroup {\n files: string[];\n occurrences: CrossFileDuplicateOccurrence[];\n /** Matched token count of one occurrence (all occurrences share it; gaps are not counted). */\n tokenCount: number;\n}\n\nexport interface CrossFileDuplicationMetrics {\n /** Number of redundant copies across all groups, counted per matched fragment like within-file. */\n duplicateBlockCount: number;\n /** Groups the file participates in, keyed by the file name passed in. */\n duplicateBlockGroupCountByFile: Record<string, number>;\n /**\n * Per file, the 1-based code lines covered by matched tokens of its cross-file occurrences,\n * sorted ascending. Exact like within-file duplicateLineNumbers: the unmatched gap of a merged\n * clone and comment/blank lines inside an occurrence's bounding range are excluded (blank rows\n * inside multi-row tokens only when the file supplied codeLineNumbers). A file that supplied\n * only candidates (no `tokens`) has no entry — without its token stream the matched lines are\n * unknowable, and an approximate bounding range would break this field's exactness.\n */\n duplicateLineNumbersByFile: Record<string, number[]>;\n groups: CrossFileDuplicateBlockGroup[];\n}\n\ninterface SelectableCandidate extends CrossFileDuplicateCandidate {\n regionBucket: number;\n file: string;\n}\n\n/** A cross-file occurrence: a within-file occurrence in the project-wide token index space. */\ninterface CrossFileOccurrence extends CountedOccurrence {\n file: string;\n}\n\n/**\n * Detects code regions duplicated across files. Per-file candidates (whole block subtrees and full\n * container runs, fingerprinted with the same normalization as within-file duplication) are joined\n * by a project-level window index over per-statement fingerprint sequences (CPD-style), so a\n * copy-pasted partial statement run embedded in different surrounding code is matched even though\n * no single file can know it repeats elsewhere. Candidates are grouped by fingerprint, and only\n * maximal, non-overlapping regions whose group spans at least two files are counted. Groups that\n * shrink to a single file during selection are shed — a within-file repeat is already reported by\n * that file's own duplication metrics. Groups separated by a small token gap within each file then\n * merge into gapped (Type-3) clone groups under `maxGapTokens`, exactly like within-file merging.\n */\nexport function measureCrossFileDuplication(\n files: CrossFileDuplicationSourceFile[],\n options?: DuplicationOptions\n): CrossFileDuplicationMetrics {\n const { minTokens, maxGapTokens } = resolveDuplicationOptions(options);\n const candidates: SelectableCandidate[] = files.flatMap(({ file, candidates }, fileIndex) =>\n candidates.map((candidate) => ({ ...candidate, regionBucket: fileIndex, file }))\n );\n // Pushed one by one: spreading the project-scale window-candidate array as call arguments\n // overflows V8's argument limit (~124k) and crashes on Node, though Bun/JSC tolerates it.\n for (const candidate of collectWindowCandidates(files, minTokens)) {\n candidates.push(candidate);\n }\n const counted = selectMaximalGroups(\n candidates,\n spansMultipleFiles,\n // File index and position break coverage ties deterministically.\n (left, right) => left.regionBucket - right.regionBucket || left.startIndex - right.startIndex\n );\n const tokenOffsets = computeTokenOffsets(files, maxGapTokens);\n return summarize(mergeGapAdjacentGroups([...counted.values()], tokenOffsets, maxGapTokens), files, tokenOffsets);\n}\n\n/** Repeated sub-windows of sibling statements matched across the whole project's files. */\nfunction collectWindowCandidates(files: CrossFileDuplicationSourceFile[], minTokens: number): SelectableCandidate[] {\n const fileIndexByContext: number[] = [];\n const contexts: SequenceWindowContext[] = [];\n for (const [fileIndex, { tokens, containerStatements }] of files.entries()) {\n if (tokens && containerStatements) {\n fileIndexByContext.push(fileIndex);\n contexts.push({ tokens, literalCountPrefix: buildLiteralCountPrefix(tokens), containers: containerStatements });\n }\n }\n if (contexts.length < 2) {\n return [];\n }\n return collectSequenceWindowCandidates(contexts, minTokens, true).flatMap(({ candidate, contextIndex }) => {\n const fileIndex = fileIndexByContext[contextIndex];\n const file = fileIndex === undefined ? undefined : files[fileIndex];\n return fileIndex === undefined || file === undefined\n ? []\n : [{ ...candidate, regionBucket: fileIndex, file: file.file }];\n });\n}\n\nfunction spansMultipleFiles(group: SelectableCandidate[]): boolean {\n return group.length >= 2 && new Set(group.map((candidate) => candidate.regionBucket)).size >= 2;\n}\n\n/**\n * Per-file token offsets that map every file into one project-wide token index space: each file's\n * tokens are offset by more than `maxGapTokens` past the previous file's, so occurrences in\n * different files are never gap-adjacent and merged pairs always stay within one file.\n */\nfunction computeTokenOffsets(files: CrossFileDuplicationSourceFile[], maxGapTokens: number): number[] {\n const tokenOffsets: number[] = [];\n let offset = 0;\n for (const { tokens, candidates } of files) {\n tokenOffsets.push(offset);\n // Accumulated in a loop: spreading a project-scale candidate array as call arguments would\n // overflow V8's argument limit (~124k) and crash on Node.\n let tokenCount = tokens?.length ?? 0;\n if (!tokens) {\n for (const candidate of candidates) {\n tokenCount = Math.max(tokenCount, candidate.endTokenIndex);\n }\n }\n offset += tokenCount + maxGapTokens + 1;\n }\n return tokenOffsets;\n}\n\n/** Reuses the within-file gapped (Type-3) merging in the project-wide token index space. */\nfunction mergeGapAdjacentGroups(\n groups: SelectableCandidate[][],\n tokenOffsets: number[],\n maxGapTokens: number\n): CrossFileOccurrence[][] {\n const occurrenceGroups = groups.map((group) =>\n group\n .map((candidate): CrossFileOccurrence => {\n const start = candidate.startTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n const end = candidate.endTokenIndex + (tokenOffsets[candidate.regionBucket] ?? 0);\n return {\n file: candidate.file,\n segments: [{ startTokenIndex: start, endTokenIndex: end }],\n tokenCount: candidate.tokenCount,\n startTokenIndex: start,\n endTokenIndex: end,\n startIndex: candidate.startIndex,\n endIndex: candidate.endIndex,\n startLine: candidate.startLine,\n endLine: candidate.endLine,\n };\n })\n .toSorted((left, right) => left.startTokenIndex - right.startTokenIndex)\n );\n return mergeAdjacentGroups(occurrenceGroups, maxGapTokens);\n}\n\nfunction summarize(\n groups: CrossFileOccurrence[][],\n files: CrossFileDuplicationSourceFile[],\n tokenOffsets: number[]\n): CrossFileDuplicationMetrics {\n const reported: CrossFileDuplicateBlockGroup[] = [];\n // Accumulated in Maps: file names are arbitrary strings, and a plain object would read\n // inherited properties for names like \"constructor\".\n const groupCountByFile = new Map<string, number>();\n const fileDataByName = new Map(\n files.map((file, index) => [\n file.file,\n { tokens: file.tokens, codeLineNumbers: file.codeLineNumbers, offset: tokenOffsets[index] ?? 0 },\n ])\n );\n const lineNumbersByFile = new Map<string, Set<number>>();\n let duplicateBlockCount = 0;\n for (const group of groups) {\n // Mirrors within-file counting: each redundant occurrence contributes one count per matched\n // fragment, gapped merging consolidates the grouping without halving the count, and spans a\n // partial merge shares between a retained group and the merged group count once.\n duplicateBlockCount += countRedundantFragments(group);\n for (const occurrence of group) {\n collectOccurrenceLines(occurrence, fileDataByName, lineNumbersByFile);\n }\n const occurrences = group\n .map(({ file, startLine, endLine }) => ({ file, startLine, endLine }))\n .toSorted((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n const files = [...new Set(occurrences.map(({ file }) => file))];\n for (const file of files) {\n groupCountByFile.set(file, (groupCountByFile.get(file) ?? 0) + 1);\n }\n reported.push({ files, occurrences, tokenCount: group[0]?.tokenCount ?? 0 });\n }\n reported.sort(\n (left, right) =>\n right.tokenCount - left.tokenCount ||\n (left.occurrences[0]?.file ?? '').localeCompare(right.occurrences[0]?.file ?? '') ||\n (left.occurrences[0]?.startLine ?? 0) - (right.occurrences[0]?.startLine ?? 0)\n );\n return {\n duplicateBlockCount,\n duplicateBlockGroupCountByFile: Object.fromEntries(groupCountByFile),\n duplicateLineNumbersByFile: Object.fromEntries(\n [...lineNumbersByFile].map(([file, lines]) => [file, [...lines].toSorted((left, right) => left - right)])\n ),\n groups: reported,\n };\n}\n\n/**\n * Adds the code lines an occurrence's matched tokens cover to its file's line set, mapping the\n * project-wide token segments back into the file's own token stream. A file that supplied only\n * candidates (no token stream) is skipped rather than approximated from the bounding line range,\n * which would include gap and comment/blank lines and break the field's exactness contract.\n */\nfunction collectOccurrenceLines(\n occurrence: CrossFileOccurrence,\n fileDataByName: Map<\n string,\n { tokens?: CrossFileDuplicationSourceFile['tokens']; codeLineNumbers?: Set<number>; offset: number }\n >,\n lineNumbersByFile: Map<string, Set<number>>\n): void {\n const fileData = fileDataByName.get(occurrence.file);\n if (!fileData?.tokens) {\n return;\n }\n let lines = lineNumbersByFile.get(occurrence.file);\n if (!lines) {\n lines = new Set();\n lineNumbersByFile.set(occurrence.file, lines);\n }\n for (const segment of occurrence.segments) {\n collectSegmentLines(\n {\n startTokenIndex: segment.startTokenIndex - fileData.offset,\n endTokenIndex: segment.endTokenIndex - fileData.offset,\n },\n fileData.tokens,\n fileData.codeLineNumbers,\n lines\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./crossFileDuplication.cjs"),n=require("./metrics.cjs"),r=require("./regressionGate.cjs"),i=require("./cliConfig.cjs"),a=require("./git.cjs"),o=require("./scan.cjs");let s=require("node:fs/promises"),c=require("node:path");c=e.__toESM(c,1);async function l(e,t){try{await u(e,t)}catch(e){o.writeStderr(`Error: ${o.formatError(e)}\n`),process.exitCode=2}}async function u(e,t){let n=o.resolveTarget(e),c=await i.loadConfig(t.config,await o.configSearchDirectory(n)),l=i.resolveOptions(t,c),u=i.resolveGateOptions(c),p=await(0,s.realpath)(await a.resolveRepoRoot(await v(await o.configSearchDirectory(n)))),m=await a.resolveMergeBase(p,t.base),h=await a.listChangedFiles(p,m),g=await a.listRepositoryFiles(p),_=await a.listSymlinkPathsAtRevision(p,m),y=await o.scanListedFiles(p,g,l);if(y.fatalError)throw Error(y.fatalError);let S=y.files.map(e=>({relativePath:o.formatPath(e.file,y.displayRoot),file:e})),C=new Set(h.flatMap(e=>[e.headPath,...e.basePath===void 0?[]:[e.basePath]]).filter(e=>o.isScannedPath(e,l))),T=[],E=[...y.warnings];for(let e of y.errors)[...C].some(t=>e.startsWith(`${t}:`))?T.push(e):E.push(e);let{canonicalTarget:D,targetExists:k}=await d(n),A=await f(h,{repoRoot:p,mergeBase:m,canonicalTarget:D,options:l,scannedFiles:S,baseSymlinkPaths:_},T,E);if(!k&&!A.some(e=>e.gated))throw Error(`target "${e}" does not exist and matches no changed file`);let{baseCross:j,headCross:M}=b(A,S,l),N=A.map(e=>x(e,j,M)),P=r.evaluateRegressionGate(N,u);t.json?O(t,m,P,N,T,E):w(t,m,P,T,E),T.length>0?process.exitCode=2:P.violations.length>0&&(process.exitCode=1)}async function d(e){try{return{canonicalTarget:await(0,s.realpath)(e),targetExists:!0}}catch{return{canonicalTarget:e,targetExists:!1}}}async function f(e,t,n,r){let i=new Map(t.scannedFiles.map(({relativePath:e,file:t})=>[e,t])),a=[];for(let o of e){let e=await p(o,t,i,n,r);e&&a.push(e)}return a}async function p(e,t,n,r,i){let a=e.status!==`deleted`&&o.isScannedPath(e.headPath,t.options)&&!await _(c.default.join(t.repoRoot,e.headPath)),s=e.basePath!==void 0&&o.isScannedPath(e.basePath,t.options)&&!t.baseSymlinkPaths.has(e.basePath);if(!a&&!s)return;let l=e.status===`deleted`?e.basePath:e.headPath,u=a?n.get(e.headPath):void 0;if(a&&!u){m(e.headPath,r);return}let d={changed:e,displayFile:l,gated:a||e.status===`deleted`?y(c.default.join(t.repoRoot,l),t.canonicalTarget):!1,headFile:u};if(!(s&&!await g(d,e.basePath,t,r,i)))return u&&await h(d,u,t,i),d}function m(e,t){t.some(t=>t.startsWith(`${e}:`))||t.push(`${e}: changed file was not measured`)}async function h(e,t,r,i){try{let i=await(0,s.readFile)(t.file,`utf8`);e.headFunctionTokens=n.collectFunctionTokenSequences(i,{language:o.getLanguage(e.changed.headPath,r.options),duplication:r.options.duplication})}catch(t){i.push(`${e.displayFile}: function token sequences unavailable: ${o.formatError(t)}`)}}async function g(e,t,r,i,s){let c={language:o.getLanguage(t,r.options),duplication:r.options.duplication},l;try{l=await a.readFileAtRevision(r.repoRoot,r.mergeBase,t),e.baseMetrics=n.measureCode(l,c)}catch(e){return i.push(`${t} (at merge-base): ${o.formatError(e)}`),!1}try{e.baseCandidates=n.collectCrossFileDuplicationFileData(l,c),e.baseFunctionTokens=n.collectFunctionTokenSequences(l,c)}catch(e){s.push(`${t} (at merge-base): duplication candidates and token sequences unavailable: ${o.formatError(e)}`)}return!0}async function _(e){return(await(0,s.lstat)(e).catch(()=>{}))?.isSymbolicLink()??!1}async function v(e){let t=e;for(;;){if((await(0,s.stat)(t).catch(()=>{}))?.isDirectory())return t;let e=c.default.dirname(t);if(e===t)return t;t=e}}function y(e,t){let n=c.default.relative(t,e);return n===``||!n.startsWith(`..${c.default.sep}`)&&n!==`..`&&!c.default.isAbsolute(n)}function b(e,n,r){let i=n.flatMap(({relativePath:e,file:t})=>t.duplicationCandidates?[{file:e,...t.duplicationCandidates}]:[]),a=new Set(e.flatMap(e=>e.changed.status===`deleted`?[]:[e.changed.headPath])),o=i.filter(e=>!a.has(e.file));for(let t of e)t.baseCandidates&&t.changed.basePath!==void 0&&o.push({file:t.changed.basePath,...t.baseCandidates});return{baseCross:o.length>=2?t.measureCrossFileDuplication(o,r.duplication):void 0,headCross:i.length>=2?t.measureCrossFileDuplication(i,r.duplication):void 0}}function x(e,t,n){return{file:e.displayFile,baseMetrics:e.baseMetrics,headMetrics:e.headFile?.metrics,baseFunctionTokens:e.baseFunctionTokens,headFunctionTokens:e.headFunctionTokens,baseDuplicatedLineCount:e.baseMetrics===void 0||e.changed.basePath===void 0?0:S(e.baseMetrics,t,e.changed.basePath),headDuplicatedLineCount:e.changed.status===`deleted`?0:S(e.headFile?.metrics,n,e.changed.headPath),duplicationPartners:C(n,e.changed.headPath),gated:e.gated}}function S(e,t,n){return o.collectDuplicatedLineNumbers(e,t,n).size}function C(e,t){if(!e)return[];let n=new Set;for(let r of e.groups)if(r.files.includes(t))for(let e of r.files)e!==t&&n.add(e);return[...n].toSorted()}function w(e,t,n,r,i){let a=t.slice(0,12);r.length>0?(o.writeStdout(`Regression gate could not complete: ${r.length} measurement failures (details on stderr)${n.violations.length>0?`; ${n.violations.length} violations in the measured files`:``} (base ${e.base}, merge-base ${a}).\n`),T(n)):n.violations.length===0?o.writeStdout(`Regression gate passed: ${n.checkedFileCount} changed files, ${n.checkedFunctionCount} functions checked (base ${e.base}, merge-base ${a}).\n`):(o.writeStdout(`Regression gate vs ${e.base} (merge-base ${a}): ${n.violations.length} violations\n`),T(n)),e.full&&E(n);for(let e of i)o.writeStderr(`Warning: ${e}\n`);for(let e of r)o.writeStderr(`Error: ${e}\n`)}function T(e){for(let[t,n]of e.violations.entries())o.writeStdout(`${t+1}. ${n.message}\n`)}function E(e){if(e.checkedFunctions.length!==0){o.writeStdout(`
|
|
2
|
+
Checked functions (base -> head):
|
|
3
|
+
`);for(let t of e.checkedFunctions)o.writeStdout(`- ${D(t)}\n`)}}function D(e){let t=(t,n=String)=>{let r=n(t(e.head));return e.base?`${n(t(e.base))} -> ${r}`:r},n=[`cognitive ${t(e=>e.cognitiveComplexity)}`,`NCSS ${t(e=>e.ncss)}`,`nesting ${t(e=>e.nestingDepth)}`,`DepDegree ${t(e=>e.depDegree)}`,`volume ${t(e=>e.halsteadVolume,e=>e.toFixed(1))}`];return`${e.file}:${e.startLine}-${e.endLine} ${e.name}${e.base?``:` (new)`}: ${n.join(`, `)}`}function O(e,t,n,r,i,a){let s={base:e.base,mergeBase:t,passed:n.violations.length===0&&i.length===0,violations:n.violations,checkedFileCount:n.checkedFileCount,checkedFunctionCount:n.checkedFunctionCount,newFunctionCount:n.newFunctionCount,errors:i,warnings:a};e.full&&(s.files=r.filter(e=>e.gated!==!1).map(e=>({file:e.file,baseFunctionCount:e.baseMetrics?.functions.length??0,headFunctionCount:e.headMetrics?.functions.length??0,baseNcss:e.baseMetrics?.ncssCount??0,headNcss:e.headMetrics?.ncssCount??0,baseMaxCognitiveComplexity:e.baseMetrics?.maxCognitiveComplexity??0,headMaxCognitiveComplexity:e.headMetrics?.maxCognitiveComplexity??0,baseDuplicatedLineCount:e.baseDuplicatedLineCount,headDuplicatedLineCount:e.headDuplicatedLineCount,duplicationPartners:e.duplicationPartners,functions:n.checkedFunctions.filter(t=>t.file===e.file)}))),o.writeStdout(JSON.stringify(s,void 0,2)+`
|
|
4
|
+
`)}exports.runDiffCommand=l;
|
|
5
|
+
//# sourceMappingURL=diffCommand.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diffCommand.cjs","names":["formatError","resolveTarget","loadConfig","configSearchDirectory","resolveOptions","resolveGateOptions","realpath","resolveRepoRoot","resolveMergeBase","listChangedFiles","listRepositoryFiles","listSymlinkPathsAtRevision","scanListedFiles","formatPath","isScannedPath","evaluateRegressionGate","path","readFile","collectFunctionTokenSequences","getLanguage","readFileAtRevision","measureCode","collectCrossFileDuplicationFileData","lstat","stat","measureCrossFileDuplication","collectDuplicatedLineNumbers"],"sources":["../src/diffCommand.ts"],"sourcesContent":["import { lstat, readFile, realpath, stat } from 'node:fs/promises';\nimport path from 'node:path';\nimport { loadConfig, resolveGateOptions, resolveOptions, type ResolvedOptions } from './cliConfig.js';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData } from './duplication.js';\nimport {\n listChangedFiles,\n listRepositoryFiles,\n listSymlinkPathsAtRevision,\n readFileAtRevision,\n resolveMergeBase,\n resolveRepoRoot,\n type ChangedFile,\n} from './git.js';\nimport { collectCrossFileDuplicationFileData, collectFunctionTokenSequences, measureCode } from './metrics.js';\nimport {\n evaluateRegressionGate,\n type CheckedFunctionReport,\n type GateFileInput,\n type GateFunctionValues,\n type GateResult,\n} from './regressionGate.js';\nimport {\n collectDuplicatedLineNumbers,\n configSearchDirectory,\n formatError,\n formatPath,\n getLanguage,\n isScannedPath,\n resolveTarget,\n scanListedFiles,\n writeStderr,\n writeStdout,\n type FileMetrics,\n} from './scan.js';\nimport type { CodeMetrics, LanguageName } from './types.js';\n\n/** Raw options of the `diff` subcommand; every field but base is undefined unless the flag was passed. */\nexport interface DiffCliOptions {\n base: string;\n config?: string;\n duplicationMinTokens?: number;\n duplicationMaxGapTokens?: number;\n duplicationMinSimilarityPercent?: number;\n includeTests?: boolean;\n json?: boolean;\n full?: boolean;\n}\n\n/** One changed file measured at both revisions, plus its duplication-universe contribution. */\ninterface PreparedFile {\n changed: ChangedFile;\n /** Repository-relative display path: the head path, or the base path for deleted files. */\n displayFile: string;\n /** Whether the file is gated (under the target directory); others only feed the base universe. */\n gated: boolean;\n headFile?: FileMetrics;\n baseMetrics?: CodeMetrics;\n baseCandidates?: CrossFileDuplicationFileData;\n baseFunctionTokens?: Int32Array[];\n headFunctionTokens?: Int32Array[];\n}\n\n/** A scanned file that git considers part of the project, keyed by its repository-relative path. */\ninterface ScannedFile {\n relativePath: string;\n file: FileMetrics;\n}\n\n/**\n * Runs the regression gate: measures the files changed relative to the merge-base with the base\n * ref, at both revisions (`git cat-file`; no checkout, no persisted baseline), and reports only\n * violations. Exit codes: 0 all gates passed, 1 violations, 2 changed files could not be measured.\n */\nexport async function runDiffCommand(target: string, cliOptions: DiffCliOptions): Promise<void> {\n try {\n await runGate(target, cliOptions);\n } catch (error) {\n writeStderr(`Error: ${formatError(error)}\\n`);\n process.exitCode = 2;\n }\n}\n\nasync function runGate(target: string, cliOptions: DiffCliOptions): Promise<void> {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const gateOptions = resolveGateOptions(config);\n\n // The target may be a typo'd path whose ancestors don't exist either; repository discovery must\n // still run so the mistyped target gets its own diagnostic instead of a git spawn failure.\n const repoRoot = await realpath(\n await resolveRepoRoot(await firstExistingDirectory(await configSearchDirectory(resolvedTarget)))\n );\n const mergeBase = await resolveMergeBase(repoRoot, cliOptions.base);\n const changedFiles = await listChangedFiles(repoRoot, mergeBase);\n\n // Every git-visible file (tracked or untracked non-ignored) is measured at head: that provides\n // the head metrics of changed files and the project-wide duplication universe, so copy-paste\n // from unchanged code into changed files is caught. Scanning the explicit git list (instead of\n // walking the tree) keeps ignored artifact directories from ever being parsed: they exist in\n // neither the base commit nor CI, so they would only cost time and skew duplication counts.\n // Unchanged files are byte-identical at both revisions, so the base universe is the same scan\n // with the changed files' contents swapped for their merge-base blobs.\n const repositoryFiles = await listRepositoryFiles(repoRoot);\n const baseSymlinkPaths = await listSymlinkPathsAtRevision(repoRoot, mergeBase);\n const scan = await scanListedFiles(repoRoot, repositoryFiles, options);\n // A run-wide failure (a missing native addon) invalidates the whole gate: surface it once as\n // the fatal error (exit 2) instead of diagnosing every changed file as unmeasured.\n if (scan.fatalError) {\n throw new Error(scan.fatalError);\n }\n const scannedFiles: ScannedFile[] = scan.files.map((file) => ({\n relativePath: formatPath(file.file, scan.displayRoot),\n file,\n }));\n\n // A measurement failure on ANY scannable changed file forces exit 2 — deliberately including\n // files outside a scoped target, because cross-file function matching and the base duplication\n // universe for the gated files depend on them. Failures elsewhere (unchanged files) and\n // unsupported changed paths degrade to warnings.\n const changedPaths = new Set(\n changedFiles\n .flatMap((changed) => [changed.headPath, ...(changed.basePath === undefined ? [] : [changed.basePath])])\n .filter((changedPath) => isScannedPath(changedPath, options))\n );\n const errors: string[] = [];\n const warnings = [...scan.warnings];\n for (const error of scan.errors) {\n if ([...changedPaths].some((changedPath) => error.startsWith(`${changedPath}:`))) {\n errors.push(error);\n } else {\n warnings.push(error);\n }\n }\n\n const { canonicalTarget, targetExists } = await canonicalizeTarget(resolvedTarget);\n const prepared = await prepareChangedFiles(\n changedFiles,\n { repoRoot, mergeBase, canonicalTarget, options, scannedFiles, baseSymlinkPaths },\n errors,\n warnings\n );\n // A gate must not fail open on a mistyped target: a nonexistent path is only acceptable when it\n // still matches changed files (e.g. a fully deleted directory).\n if (!targetExists && !prepared.some((file) => file.gated)) {\n throw new Error(`target \"${target}\" does not exist and matches no changed file`);\n }\n\n // Non-gated files (outside the target, or renamed out of scan scope) still feed function\n // matching and the duplication universes; the evaluator reports nothing for them.\n const { baseCross, headCross } = measureDuplicationUniverses(prepared, scannedFiles, options);\n const inputs = prepared.map((file) => toGateInput(file, baseCross, headCross));\n const result = evaluateRegressionGate(inputs, gateOptions);\n\n if (cliOptions.json) {\n printJsonReport(cliOptions, mergeBase, result, inputs, errors, warnings);\n } else {\n printTextReport(cliOptions, mergeBase, result, errors, warnings);\n }\n\n if (errors.length > 0) {\n process.exitCode = 2;\n } else if (result.violations.length > 0) {\n process.exitCode = 1;\n }\n}\n\n/** The target may not exist (e.g. only deleted files under it); fall back to the resolved path. */\nasync function canonicalizeTarget(resolvedTarget: string): Promise<{ canonicalTarget: string; targetExists: boolean }> {\n try {\n return { canonicalTarget: await realpath(resolvedTarget), targetExists: true };\n } catch {\n return { canonicalTarget: resolvedTarget, targetExists: false };\n }\n}\n\ninterface GateContext {\n repoRoot: string;\n mergeBase: string;\n canonicalTarget: string;\n options: ResolvedOptions;\n scannedFiles: ScannedFile[];\n /** Paths that are symbolic links at the merge-base; like head symlinks, they are not gated. */\n baseSymlinkPaths: Set<string>;\n}\n\nasync function prepareChangedFiles(\n changedFiles: ChangedFile[],\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile[]> {\n const headByPath = new Map(context.scannedFiles.map(({ relativePath, file }) => [relativePath, file]));\n const prepared: PreparedFile[] = [];\n for (const changed of changedFiles) {\n const file = await prepareChangedFile(changed, context, headByPath, errors, warnings);\n if (file) {\n prepared.push(file);\n }\n }\n return prepared;\n}\n\nasync function prepareChangedFile(\n changed: ChangedFile,\n context: GateContext,\n headByPath: Map<string, FileMetrics>,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile | undefined> {\n // Symbolic links are skipped on both sides, mirroring scanListedFiles: git stores only the\n // target string, so a symlink blob is not measurable source.\n const headScannable =\n changed.status !== 'deleted' &&\n isScannedPath(changed.headPath, context.options) &&\n !(await isSymbolicLink(path.join(context.repoRoot, changed.headPath)));\n // A base path outside the scan scope (renamed from a test/ignored directory, or an unsupported\n // extension) was never measurable code: its content gates as new code instead of ratcheting\n // against a blob the scanner would not have measured.\n const baseScannable =\n changed.basePath !== undefined &&\n isScannedPath(changed.basePath, context.options) &&\n !context.baseSymlinkPaths.has(changed.basePath);\n if (!headScannable && !baseScannable) {\n return undefined;\n }\n\n const displayFile = changed.status === 'deleted' ? (changed.basePath as string) : changed.headPath;\n const headFile = headScannable ? headByPath.get(changed.headPath) : undefined;\n if (headScannable && !headFile) {\n reportUnmeasuredChangedFile(changed.headPath, errors);\n return undefined;\n }\n\n const file: PreparedFile = {\n changed,\n displayFile,\n // A file whose head left the scan scope still contributes its base functions to matching and\n // its base blob to the base universe, but nothing about it is gated or reported.\n gated:\n headScannable || changed.status === 'deleted'\n ? isWithinTarget(path.join(context.repoRoot, displayFile), context.canonicalTarget)\n : false,\n headFile,\n };\n\n if (baseScannable && !(await measureBaseRevision(file, changed.basePath as string, context, errors, warnings))) {\n return undefined;\n }\n\n if (headFile) {\n await collectHeadFunctionTokens(file, headFile, context, warnings);\n }\n\n return file;\n}\n\n/**\n * The scan covers exactly the git-visible list, so a scannable changed path can only be missing\n * after a measurement failure (already recorded as an error) or a silent exclusion (an alias of\n * an already-visited file, or absence from the git list). Failing loudly keeps the gate from\n * passing with the file unchecked.\n */\nfunction reportUnmeasuredChangedFile(headPath: string, errors: string[]): void {\n if (!errors.some((error) => error.startsWith(`${headPath}:`))) {\n errors.push(`${headPath}: changed file was not measured`);\n }\n}\n\nasync function collectHeadFunctionTokens(\n file: PreparedFile,\n headFile: FileMetrics,\n context: GateContext,\n warnings: string[]\n): Promise<void> {\n try {\n const headContent = await readFile(headFile.file, 'utf8');\n file.headFunctionTokens = collectFunctionTokenSequences(headContent, {\n language: getLanguage(file.changed.headPath, context.options) as LanguageName,\n duplication: context.options.duplication,\n });\n } catch (error) {\n // Only rename re-matching degrades without token sequences; the head metrics still gate.\n warnings.push(`${file.displayFile}: function token sequences unavailable: ${formatError(error)}`);\n }\n}\n\n/**\n * Measures the merge-base blob into `file`; false (with an error recorded) only when the metrics\n * themselves cannot be measured. The auxiliary collections (duplication candidates, token\n * sequences) may fail independently of the metrics, so their failure only degrades duplication\n * data and rename re-matching — the function-level ratchets still run.\n */\nasync function measureBaseRevision(\n file: PreparedFile,\n basePath: string,\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<boolean> {\n const measureOptions = {\n language: getLanguage(basePath, context.options) as LanguageName,\n duplication: context.options.duplication,\n };\n let baseContent;\n try {\n baseContent = await readFileAtRevision(context.repoRoot, context.mergeBase, basePath);\n file.baseMetrics = measureCode(baseContent, measureOptions);\n } catch (error) {\n errors.push(`${basePath} (at merge-base): ${formatError(error)}`);\n return false;\n }\n try {\n file.baseCandidates = collectCrossFileDuplicationFileData(baseContent, measureOptions);\n file.baseFunctionTokens = collectFunctionTokenSequences(baseContent, measureOptions);\n } catch (error) {\n warnings.push(\n `${basePath} (at merge-base): duplication candidates and token sequences unavailable: ${formatError(error)}`\n );\n }\n return true;\n}\n\nasync function isSymbolicLink(absolutePath: string): Promise<boolean> {\n const stats = await lstat(absolutePath).catch(() => {});\n return stats?.isSymbolicLink() ?? false;\n}\n\n/** Walks up to the nearest existing DIRECTORY, so git commands never spawn in a missing or non-directory cwd. */\nasync function firstExistingDirectory(directory: string): Promise<string> {\n let current = directory;\n while (true) {\n const stats = await stat(current).catch(() => {});\n if (stats?.isDirectory()) {\n return current;\n }\n const parent = path.dirname(current);\n if (parent === current) {\n return current;\n }\n current = parent;\n }\n}\n\nfunction isWithinTarget(candidate: string, targetDirectory: string): boolean {\n const relative = path.relative(targetDirectory, candidate);\n return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));\n}\n\nfunction measureDuplicationUniverses(\n prepared: PreparedFile[],\n scannedFiles: ScannedFile[],\n options: ResolvedOptions\n): { baseCross?: CrossFileDuplicationMetrics; headCross?: CrossFileDuplicationMetrics } {\n const headSources = scannedFiles.flatMap(({ relativePath, file }) =>\n file.duplicationCandidates ? [{ file: relativePath, ...file.duplicationCandidates }] : []\n );\n\n const changedHeadPaths = new Set(\n prepared.flatMap((file) => (file.changed.status === 'deleted' ? [] : [file.changed.headPath]))\n );\n const baseSources = headSources.filter((source) => !changedHeadPaths.has(source.file));\n for (const file of prepared) {\n if (file.baseCandidates && file.changed.basePath !== undefined) {\n baseSources.push({ file: file.changed.basePath, ...file.baseCandidates });\n }\n }\n\n return {\n baseCross: baseSources.length >= 2 ? measureCrossFileDuplication(baseSources, options.duplication) : undefined,\n headCross: headSources.length >= 2 ? measureCrossFileDuplication(headSources, options.duplication) : undefined,\n };\n}\n\nfunction toGateInput(\n file: PreparedFile,\n baseCross: CrossFileDuplicationMetrics | undefined,\n headCross: CrossFileDuplicationMetrics | undefined\n): GateFileInput {\n return {\n file: file.displayFile,\n baseMetrics: file.baseMetrics,\n headMetrics: file.headFile?.metrics,\n baseFunctionTokens: file.baseFunctionTokens,\n headFunctionTokens: file.headFunctionTokens,\n baseDuplicatedLineCount:\n file.baseMetrics === undefined || file.changed.basePath === undefined\n ? 0\n : countDuplicatedLines(file.baseMetrics, baseCross, file.changed.basePath),\n headDuplicatedLineCount:\n file.changed.status === 'deleted'\n ? 0\n : countDuplicatedLines(file.headFile?.metrics, headCross, file.changed.headPath),\n duplicationPartners: collectPartners(headCross, file.changed.headPath),\n gated: file.gated,\n };\n}\n\nfunction countDuplicatedLines(\n metrics: CodeMetrics | undefined,\n cross: CrossFileDuplicationMetrics | undefined,\n file: string\n): number {\n return collectDuplicatedLineNumbers(metrics, cross, file).size;\n}\n\nfunction collectPartners(cross: CrossFileDuplicationMetrics | undefined, file: string): string[] {\n if (!cross) {\n return [];\n }\n const partners = new Set<string>();\n for (const group of cross.groups) {\n if (group.files.includes(file)) {\n for (const partner of group.files) {\n if (partner !== file) {\n partners.add(partner);\n }\n }\n }\n }\n return [...partners].toSorted();\n}\n\nfunction printTextReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n errors: string[],\n warnings: string[]\n): void {\n const shortBase = mergeBase.slice(0, 12);\n if (errors.length > 0) {\n // Unmeasured files were not gated, so \"0 violations\" would be vacuous; never claim a pass.\n writeStdout(\n `Regression gate could not complete: ${errors.length} measurement failures (details on stderr)` +\n `${result.violations.length > 0 ? `; ${result.violations.length} violations in the measured files` : ''} (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n printViolations(result);\n } else if (result.violations.length === 0) {\n writeStdout(\n `Regression gate passed: ${result.checkedFileCount} changed files, ${result.checkedFunctionCount} functions checked (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n } else {\n writeStdout(\n `Regression gate vs ${cliOptions.base} (merge-base ${shortBase}): ${result.violations.length} violations\\n`\n );\n printViolations(result);\n }\n\n if (cliOptions.full) {\n printFullDetails(result);\n }\n\n for (const warning of warnings) {\n writeStderr(`Warning: ${warning}\\n`);\n }\n for (const error of errors) {\n writeStderr(`Error: ${error}\\n`);\n }\n}\n\nfunction printViolations(result: GateResult): void {\n for (const [index, violation] of result.violations.entries()) {\n writeStdout(`${index + 1}. ${violation.message}\\n`);\n }\n}\n\n/** Base -> head values of every checked function; kept behind --full for humans and trending. */\nfunction printFullDetails(result: GateResult): void {\n if (result.checkedFunctions.length === 0) {\n return;\n }\n writeStdout('\\nChecked functions (base -> head):\\n');\n for (const report of result.checkedFunctions) {\n writeStdout(`- ${formatFunctionReport(report)}\\n`);\n }\n}\n\nfunction formatFunctionReport(report: CheckedFunctionReport): string {\n const range = (\n select: (values: GateFunctionValues) => number,\n format: (value: number) => string = String\n ): string => {\n const head = format(select(report.head));\n return report.base ? `${format(select(report.base))} -> ${head}` : head;\n };\n const values = [\n `cognitive ${range((fn) => fn.cognitiveComplexity)}`,\n `NCSS ${range((fn) => fn.ncss)}`,\n `nesting ${range((fn) => fn.nestingDepth)}`,\n `DepDegree ${range((fn) => fn.depDegree)}`,\n `volume ${range(\n (fn) => fn.halsteadVolume,\n (value) => value.toFixed(1)\n )}`,\n ];\n return `${report.file}:${report.startLine}-${report.endLine} ${report.name}${report.base ? '' : ' (new)'}: ${values.join(', ')}`;\n}\n\nfunction printJsonReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n inputs: GateFileInput[],\n errors: string[],\n warnings: string[]\n): void {\n const report: Record<string, unknown> = {\n base: cliOptions.base,\n mergeBase,\n passed: result.violations.length === 0 && errors.length === 0,\n violations: result.violations,\n checkedFileCount: result.checkedFileCount,\n checkedFunctionCount: result.checkedFunctionCount,\n newFunctionCount: result.newFunctionCount,\n errors,\n warnings,\n };\n if (cliOptions.full) {\n report.files = inputs\n .filter((input) => input.gated !== false)\n .map((input) => ({\n file: input.file,\n baseFunctionCount: input.baseMetrics?.functions.length ?? 0,\n headFunctionCount: input.headMetrics?.functions.length ?? 0,\n baseNcss: input.baseMetrics?.ncssCount ?? 0,\n headNcss: input.headMetrics?.ncssCount ?? 0,\n baseMaxCognitiveComplexity: input.baseMetrics?.maxCognitiveComplexity ?? 0,\n headMaxCognitiveComplexity: input.headMetrics?.maxCognitiveComplexity ?? 0,\n baseDuplicatedLineCount: input.baseDuplicatedLineCount,\n headDuplicatedLineCount: input.headDuplicatedLineCount,\n duplicationPartners: input.duplicationPartners,\n functions: result.checkedFunctions.filter((fn) => fn.file === input.file),\n }));\n }\n writeStdout(JSON.stringify(report, undefined, 2) + '\\n');\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Raw options of the `diff` subcommand; every field but base is undefined unless the flag was passed. */
|
|
2
|
+
export interface DiffCliOptions {
|
|
3
|
+
base: string;
|
|
4
|
+
config?: string;
|
|
5
|
+
duplicationMinTokens?: number;
|
|
6
|
+
duplicationMaxGapTokens?: number;
|
|
7
|
+
duplicationMinSimilarityPercent?: number;
|
|
8
|
+
includeTests?: boolean;
|
|
9
|
+
json?: boolean;
|
|
10
|
+
full?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Runs the regression gate: measures the files changed relative to the merge-base with the base
|
|
14
|
+
* ref, at both revisions (`git cat-file`; no checkout, no persisted baseline), and reports only
|
|
15
|
+
* violations. Exit codes: 0 all gates passed, 1 violations, 2 changed files could not be measured.
|
|
16
|
+
*/
|
|
17
|
+
export declare function runDiffCommand(target: string, cliOptions: DiffCliOptions): Promise<void>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{collectCrossFileDuplicationFileData as t,collectFunctionTokenSequences as n,measureCode as r}from"./metrics.js";import{evaluateRegressionGate as i}from"./regressionGate.js";import{loadConfig as a,resolveGateOptions as o,resolveOptions as s}from"./cliConfig.js";import{listChangedFiles as c,listRepositoryFiles as l,listSymlinkPathsAtRevision as u,readFileAtRevision as d,resolveMergeBase as f,resolveRepoRoot as p}from"./git.js";import{collectDuplicatedLineNumbers as m,configSearchDirectory as h,formatError as g,formatPath as _,getLanguage as v,isScannedPath as y,resolveTarget as b,scanListedFiles as x,writeStderr as S,writeStdout as C}from"./scan.js";import{lstat as w,readFile as T,realpath as E,stat as D}from"node:fs/promises";import O from"node:path";async function k(e,t){try{await A(e,t)}catch(e){S(`Error: ${g(e)}\n`),process.exitCode=2}}async function A(e,t){let n=b(e),r=await a(t.config,await h(n)),d=s(t,r),m=o(r),g=await E(await p(await R(await h(n)))),v=await f(g,t.base),S=await c(g,v),C=await l(g),w=await u(g,v),T=await x(g,C,d);if(T.fatalError)throw Error(T.fatalError);let D=T.files.map(e=>({relativePath:_(e.file,T.displayRoot),file:e})),O=new Set(S.flatMap(e=>[e.headPath,...e.basePath===void 0?[]:[e.basePath]]).filter(e=>y(e,d))),k=[],A=[...T.warnings];for(let e of T.errors)[...O].some(t=>e.startsWith(`${t}:`))?k.push(e):A.push(e);let{canonicalTarget:N,targetExists:P}=await j(n),F=await M(S,{repoRoot:g,mergeBase:v,canonicalTarget:N,options:d,scannedFiles:D,baseSymlinkPaths:w},k,A);if(!P&&!F.some(e=>e.gated))throw Error(`target "${e}" does not exist and matches no changed file`);let{baseCross:I,headCross:L}=B(F,D,d),z=F.map(e=>V(e,I,L)),H=i(z,m);t.json?J(t,v,H,z,k,A):W(t,v,H,k,A),k.length>0?process.exitCode=2:H.violations.length>0&&(process.exitCode=1)}async function j(e){try{return{canonicalTarget:await E(e),targetExists:!0}}catch{return{canonicalTarget:e,targetExists:!1}}}async function M(e,t,n,r){let i=new Map(t.scannedFiles.map(({relativePath:e,file:t})=>[e,t])),a=[];for(let o of e){let e=await N(o,t,i,n,r);e&&a.push(e)}return a}async function N(e,t,n,r,i){let a=e.status!==`deleted`&&y(e.headPath,t.options)&&!await L(O.join(t.repoRoot,e.headPath)),o=e.basePath!==void 0&&y(e.basePath,t.options)&&!t.baseSymlinkPaths.has(e.basePath);if(!a&&!o)return;let s=e.status===`deleted`?e.basePath:e.headPath,c=a?n.get(e.headPath):void 0;if(a&&!c){P(e.headPath,r);return}let l={changed:e,displayFile:s,gated:a||e.status===`deleted`?z(O.join(t.repoRoot,s),t.canonicalTarget):!1,headFile:c};if(!(o&&!await I(l,e.basePath,t,r,i)))return c&&await F(l,c,t,i),l}function P(e,t){t.some(t=>t.startsWith(`${e}:`))||t.push(`${e}: changed file was not measured`)}async function F(e,t,r,i){try{let i=await T(t.file,`utf8`);e.headFunctionTokens=n(i,{language:v(e.changed.headPath,r.options),duplication:r.options.duplication})}catch(t){i.push(`${e.displayFile}: function token sequences unavailable: ${g(t)}`)}}async function I(e,i,a,o,s){let c={language:v(i,a.options),duplication:a.options.duplication},l;try{l=await d(a.repoRoot,a.mergeBase,i),e.baseMetrics=r(l,c)}catch(e){return o.push(`${i} (at merge-base): ${g(e)}`),!1}try{e.baseCandidates=t(l,c),e.baseFunctionTokens=n(l,c)}catch(e){s.push(`${i} (at merge-base): duplication candidates and token sequences unavailable: ${g(e)}`)}return!0}async function L(e){return(await w(e).catch(()=>{}))?.isSymbolicLink()??!1}async function R(e){let t=e;for(;;){if((await D(t).catch(()=>{}))?.isDirectory())return t;let e=O.dirname(t);if(e===t)return t;t=e}}function z(e,t){let n=O.relative(t,e);return n===``||!n.startsWith(`..${O.sep}`)&&n!==`..`&&!O.isAbsolute(n)}function B(t,n,r){let i=n.flatMap(({relativePath:e,file:t})=>t.duplicationCandidates?[{file:e,...t.duplicationCandidates}]:[]),a=new Set(t.flatMap(e=>e.changed.status===`deleted`?[]:[e.changed.headPath])),o=i.filter(e=>!a.has(e.file));for(let e of t)e.baseCandidates&&e.changed.basePath!==void 0&&o.push({file:e.changed.basePath,...e.baseCandidates});return{baseCross:o.length>=2?e(o,r.duplication):void 0,headCross:i.length>=2?e(i,r.duplication):void 0}}function V(e,t,n){return{file:e.displayFile,baseMetrics:e.baseMetrics,headMetrics:e.headFile?.metrics,baseFunctionTokens:e.baseFunctionTokens,headFunctionTokens:e.headFunctionTokens,baseDuplicatedLineCount:e.baseMetrics===void 0||e.changed.basePath===void 0?0:H(e.baseMetrics,t,e.changed.basePath),headDuplicatedLineCount:e.changed.status===`deleted`?0:H(e.headFile?.metrics,n,e.changed.headPath),duplicationPartners:U(n,e.changed.headPath),gated:e.gated}}function H(e,t,n){return m(e,t,n).size}function U(e,t){if(!e)return[];let n=new Set;for(let r of e.groups)if(r.files.includes(t))for(let e of r.files)e!==t&&n.add(e);return[...n].toSorted()}function W(e,t,n,r,i){let a=t.slice(0,12);r.length>0?(C(`Regression gate could not complete: ${r.length} measurement failures (details on stderr)${n.violations.length>0?`; ${n.violations.length} violations in the measured files`:``} (base ${e.base}, merge-base ${a}).\n`),G(n)):n.violations.length===0?C(`Regression gate passed: ${n.checkedFileCount} changed files, ${n.checkedFunctionCount} functions checked (base ${e.base}, merge-base ${a}).\n`):(C(`Regression gate vs ${e.base} (merge-base ${a}): ${n.violations.length} violations\n`),G(n)),e.full&&K(n);for(let e of i)S(`Warning: ${e}\n`);for(let e of r)S(`Error: ${e}\n`)}function G(e){for(let[t,n]of e.violations.entries())C(`${t+1}. ${n.message}\n`)}function K(e){if(e.checkedFunctions.length!==0){C(`
|
|
2
|
+
Checked functions (base -> head):
|
|
3
|
+
`);for(let t of e.checkedFunctions)C(`- ${q(t)}\n`)}}function q(e){let t=(t,n=String)=>{let r=n(t(e.head));return e.base?`${n(t(e.base))} -> ${r}`:r},n=[`cognitive ${t(e=>e.cognitiveComplexity)}`,`NCSS ${t(e=>e.ncss)}`,`nesting ${t(e=>e.nestingDepth)}`,`DepDegree ${t(e=>e.depDegree)}`,`volume ${t(e=>e.halsteadVolume,e=>e.toFixed(1))}`];return`${e.file}:${e.startLine}-${e.endLine} ${e.name}${e.base?``:` (new)`}: ${n.join(`, `)}`}function J(e,t,n,r,i,a){let o={base:e.base,mergeBase:t,passed:n.violations.length===0&&i.length===0,violations:n.violations,checkedFileCount:n.checkedFileCount,checkedFunctionCount:n.checkedFunctionCount,newFunctionCount:n.newFunctionCount,errors:i,warnings:a};e.full&&(o.files=r.filter(e=>e.gated!==!1).map(e=>({file:e.file,baseFunctionCount:e.baseMetrics?.functions.length??0,headFunctionCount:e.headMetrics?.functions.length??0,baseNcss:e.baseMetrics?.ncssCount??0,headNcss:e.headMetrics?.ncssCount??0,baseMaxCognitiveComplexity:e.baseMetrics?.maxCognitiveComplexity??0,headMaxCognitiveComplexity:e.headMetrics?.maxCognitiveComplexity??0,baseDuplicatedLineCount:e.baseDuplicatedLineCount,headDuplicatedLineCount:e.headDuplicatedLineCount,duplicationPartners:e.duplicationPartners,functions:n.checkedFunctions.filter(t=>t.file===e.file)}))),C(JSON.stringify(o,void 0,2)+`
|
|
4
|
+
`)}export{k as runDiffCommand};
|
|
5
|
+
//# sourceMappingURL=diffCommand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diffCommand.js","names":[],"sources":["../src/diffCommand.ts"],"sourcesContent":["import { lstat, readFile, realpath, stat } from 'node:fs/promises';\nimport path from 'node:path';\nimport { loadConfig, resolveGateOptions, resolveOptions, type ResolvedOptions } from './cliConfig.js';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData } from './duplication.js';\nimport {\n listChangedFiles,\n listRepositoryFiles,\n listSymlinkPathsAtRevision,\n readFileAtRevision,\n resolveMergeBase,\n resolveRepoRoot,\n type ChangedFile,\n} from './git.js';\nimport { collectCrossFileDuplicationFileData, collectFunctionTokenSequences, measureCode } from './metrics.js';\nimport {\n evaluateRegressionGate,\n type CheckedFunctionReport,\n type GateFileInput,\n type GateFunctionValues,\n type GateResult,\n} from './regressionGate.js';\nimport {\n collectDuplicatedLineNumbers,\n configSearchDirectory,\n formatError,\n formatPath,\n getLanguage,\n isScannedPath,\n resolveTarget,\n scanListedFiles,\n writeStderr,\n writeStdout,\n type FileMetrics,\n} from './scan.js';\nimport type { CodeMetrics, LanguageName } from './types.js';\n\n/** Raw options of the `diff` subcommand; every field but base is undefined unless the flag was passed. */\nexport interface DiffCliOptions {\n base: string;\n config?: string;\n duplicationMinTokens?: number;\n duplicationMaxGapTokens?: number;\n duplicationMinSimilarityPercent?: number;\n includeTests?: boolean;\n json?: boolean;\n full?: boolean;\n}\n\n/** One changed file measured at both revisions, plus its duplication-universe contribution. */\ninterface PreparedFile {\n changed: ChangedFile;\n /** Repository-relative display path: the head path, or the base path for deleted files. */\n displayFile: string;\n /** Whether the file is gated (under the target directory); others only feed the base universe. */\n gated: boolean;\n headFile?: FileMetrics;\n baseMetrics?: CodeMetrics;\n baseCandidates?: CrossFileDuplicationFileData;\n baseFunctionTokens?: Int32Array[];\n headFunctionTokens?: Int32Array[];\n}\n\n/** A scanned file that git considers part of the project, keyed by its repository-relative path. */\ninterface ScannedFile {\n relativePath: string;\n file: FileMetrics;\n}\n\n/**\n * Runs the regression gate: measures the files changed relative to the merge-base with the base\n * ref, at both revisions (`git cat-file`; no checkout, no persisted baseline), and reports only\n * violations. Exit codes: 0 all gates passed, 1 violations, 2 changed files could not be measured.\n */\nexport async function runDiffCommand(target: string, cliOptions: DiffCliOptions): Promise<void> {\n try {\n await runGate(target, cliOptions);\n } catch (error) {\n writeStderr(`Error: ${formatError(error)}\\n`);\n process.exitCode = 2;\n }\n}\n\nasync function runGate(target: string, cliOptions: DiffCliOptions): Promise<void> {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const gateOptions = resolveGateOptions(config);\n\n // The target may be a typo'd path whose ancestors don't exist either; repository discovery must\n // still run so the mistyped target gets its own diagnostic instead of a git spawn failure.\n const repoRoot = await realpath(\n await resolveRepoRoot(await firstExistingDirectory(await configSearchDirectory(resolvedTarget)))\n );\n const mergeBase = await resolveMergeBase(repoRoot, cliOptions.base);\n const changedFiles = await listChangedFiles(repoRoot, mergeBase);\n\n // Every git-visible file (tracked or untracked non-ignored) is measured at head: that provides\n // the head metrics of changed files and the project-wide duplication universe, so copy-paste\n // from unchanged code into changed files is caught. Scanning the explicit git list (instead of\n // walking the tree) keeps ignored artifact directories from ever being parsed: they exist in\n // neither the base commit nor CI, so they would only cost time and skew duplication counts.\n // Unchanged files are byte-identical at both revisions, so the base universe is the same scan\n // with the changed files' contents swapped for their merge-base blobs.\n const repositoryFiles = await listRepositoryFiles(repoRoot);\n const baseSymlinkPaths = await listSymlinkPathsAtRevision(repoRoot, mergeBase);\n const scan = await scanListedFiles(repoRoot, repositoryFiles, options);\n // A run-wide failure (a missing native addon) invalidates the whole gate: surface it once as\n // the fatal error (exit 2) instead of diagnosing every changed file as unmeasured.\n if (scan.fatalError) {\n throw new Error(scan.fatalError);\n }\n const scannedFiles: ScannedFile[] = scan.files.map((file) => ({\n relativePath: formatPath(file.file, scan.displayRoot),\n file,\n }));\n\n // A measurement failure on ANY scannable changed file forces exit 2 — deliberately including\n // files outside a scoped target, because cross-file function matching and the base duplication\n // universe for the gated files depend on them. Failures elsewhere (unchanged files) and\n // unsupported changed paths degrade to warnings.\n const changedPaths = new Set(\n changedFiles\n .flatMap((changed) => [changed.headPath, ...(changed.basePath === undefined ? [] : [changed.basePath])])\n .filter((changedPath) => isScannedPath(changedPath, options))\n );\n const errors: string[] = [];\n const warnings = [...scan.warnings];\n for (const error of scan.errors) {\n if ([...changedPaths].some((changedPath) => error.startsWith(`${changedPath}:`))) {\n errors.push(error);\n } else {\n warnings.push(error);\n }\n }\n\n const { canonicalTarget, targetExists } = await canonicalizeTarget(resolvedTarget);\n const prepared = await prepareChangedFiles(\n changedFiles,\n { repoRoot, mergeBase, canonicalTarget, options, scannedFiles, baseSymlinkPaths },\n errors,\n warnings\n );\n // A gate must not fail open on a mistyped target: a nonexistent path is only acceptable when it\n // still matches changed files (e.g. a fully deleted directory).\n if (!targetExists && !prepared.some((file) => file.gated)) {\n throw new Error(`target \"${target}\" does not exist and matches no changed file`);\n }\n\n // Non-gated files (outside the target, or renamed out of scan scope) still feed function\n // matching and the duplication universes; the evaluator reports nothing for them.\n const { baseCross, headCross } = measureDuplicationUniverses(prepared, scannedFiles, options);\n const inputs = prepared.map((file) => toGateInput(file, baseCross, headCross));\n const result = evaluateRegressionGate(inputs, gateOptions);\n\n if (cliOptions.json) {\n printJsonReport(cliOptions, mergeBase, result, inputs, errors, warnings);\n } else {\n printTextReport(cliOptions, mergeBase, result, errors, warnings);\n }\n\n if (errors.length > 0) {\n process.exitCode = 2;\n } else if (result.violations.length > 0) {\n process.exitCode = 1;\n }\n}\n\n/** The target may not exist (e.g. only deleted files under it); fall back to the resolved path. */\nasync function canonicalizeTarget(resolvedTarget: string): Promise<{ canonicalTarget: string; targetExists: boolean }> {\n try {\n return { canonicalTarget: await realpath(resolvedTarget), targetExists: true };\n } catch {\n return { canonicalTarget: resolvedTarget, targetExists: false };\n }\n}\n\ninterface GateContext {\n repoRoot: string;\n mergeBase: string;\n canonicalTarget: string;\n options: ResolvedOptions;\n scannedFiles: ScannedFile[];\n /** Paths that are symbolic links at the merge-base; like head symlinks, they are not gated. */\n baseSymlinkPaths: Set<string>;\n}\n\nasync function prepareChangedFiles(\n changedFiles: ChangedFile[],\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile[]> {\n const headByPath = new Map(context.scannedFiles.map(({ relativePath, file }) => [relativePath, file]));\n const prepared: PreparedFile[] = [];\n for (const changed of changedFiles) {\n const file = await prepareChangedFile(changed, context, headByPath, errors, warnings);\n if (file) {\n prepared.push(file);\n }\n }\n return prepared;\n}\n\nasync function prepareChangedFile(\n changed: ChangedFile,\n context: GateContext,\n headByPath: Map<string, FileMetrics>,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile | undefined> {\n // Symbolic links are skipped on both sides, mirroring scanListedFiles: git stores only the\n // target string, so a symlink blob is not measurable source.\n const headScannable =\n changed.status !== 'deleted' &&\n isScannedPath(changed.headPath, context.options) &&\n !(await isSymbolicLink(path.join(context.repoRoot, changed.headPath)));\n // A base path outside the scan scope (renamed from a test/ignored directory, or an unsupported\n // extension) was never measurable code: its content gates as new code instead of ratcheting\n // against a blob the scanner would not have measured.\n const baseScannable =\n changed.basePath !== undefined &&\n isScannedPath(changed.basePath, context.options) &&\n !context.baseSymlinkPaths.has(changed.basePath);\n if (!headScannable && !baseScannable) {\n return undefined;\n }\n\n const displayFile = changed.status === 'deleted' ? (changed.basePath as string) : changed.headPath;\n const headFile = headScannable ? headByPath.get(changed.headPath) : undefined;\n if (headScannable && !headFile) {\n reportUnmeasuredChangedFile(changed.headPath, errors);\n return undefined;\n }\n\n const file: PreparedFile = {\n changed,\n displayFile,\n // A file whose head left the scan scope still contributes its base functions to matching and\n // its base blob to the base universe, but nothing about it is gated or reported.\n gated:\n headScannable || changed.status === 'deleted'\n ? isWithinTarget(path.join(context.repoRoot, displayFile), context.canonicalTarget)\n : false,\n headFile,\n };\n\n if (baseScannable && !(await measureBaseRevision(file, changed.basePath as string, context, errors, warnings))) {\n return undefined;\n }\n\n if (headFile) {\n await collectHeadFunctionTokens(file, headFile, context, warnings);\n }\n\n return file;\n}\n\n/**\n * The scan covers exactly the git-visible list, so a scannable changed path can only be missing\n * after a measurement failure (already recorded as an error) or a silent exclusion (an alias of\n * an already-visited file, or absence from the git list). Failing loudly keeps the gate from\n * passing with the file unchecked.\n */\nfunction reportUnmeasuredChangedFile(headPath: string, errors: string[]): void {\n if (!errors.some((error) => error.startsWith(`${headPath}:`))) {\n errors.push(`${headPath}: changed file was not measured`);\n }\n}\n\nasync function collectHeadFunctionTokens(\n file: PreparedFile,\n headFile: FileMetrics,\n context: GateContext,\n warnings: string[]\n): Promise<void> {\n try {\n const headContent = await readFile(headFile.file, 'utf8');\n file.headFunctionTokens = collectFunctionTokenSequences(headContent, {\n language: getLanguage(file.changed.headPath, context.options) as LanguageName,\n duplication: context.options.duplication,\n });\n } catch (error) {\n // Only rename re-matching degrades without token sequences; the head metrics still gate.\n warnings.push(`${file.displayFile}: function token sequences unavailable: ${formatError(error)}`);\n }\n}\n\n/**\n * Measures the merge-base blob into `file`; false (with an error recorded) only when the metrics\n * themselves cannot be measured. The auxiliary collections (duplication candidates, token\n * sequences) may fail independently of the metrics, so their failure only degrades duplication\n * data and rename re-matching — the function-level ratchets still run.\n */\nasync function measureBaseRevision(\n file: PreparedFile,\n basePath: string,\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<boolean> {\n const measureOptions = {\n language: getLanguage(basePath, context.options) as LanguageName,\n duplication: context.options.duplication,\n };\n let baseContent;\n try {\n baseContent = await readFileAtRevision(context.repoRoot, context.mergeBase, basePath);\n file.baseMetrics = measureCode(baseContent, measureOptions);\n } catch (error) {\n errors.push(`${basePath} (at merge-base): ${formatError(error)}`);\n return false;\n }\n try {\n file.baseCandidates = collectCrossFileDuplicationFileData(baseContent, measureOptions);\n file.baseFunctionTokens = collectFunctionTokenSequences(baseContent, measureOptions);\n } catch (error) {\n warnings.push(\n `${basePath} (at merge-base): duplication candidates and token sequences unavailable: ${formatError(error)}`\n );\n }\n return true;\n}\n\nasync function isSymbolicLink(absolutePath: string): Promise<boolean> {\n const stats = await lstat(absolutePath).catch(() => {});\n return stats?.isSymbolicLink() ?? false;\n}\n\n/** Walks up to the nearest existing DIRECTORY, so git commands never spawn in a missing or non-directory cwd. */\nasync function firstExistingDirectory(directory: string): Promise<string> {\n let current = directory;\n while (true) {\n const stats = await stat(current).catch(() => {});\n if (stats?.isDirectory()) {\n return current;\n }\n const parent = path.dirname(current);\n if (parent === current) {\n return current;\n }\n current = parent;\n }\n}\n\nfunction isWithinTarget(candidate: string, targetDirectory: string): boolean {\n const relative = path.relative(targetDirectory, candidate);\n return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));\n}\n\nfunction measureDuplicationUniverses(\n prepared: PreparedFile[],\n scannedFiles: ScannedFile[],\n options: ResolvedOptions\n): { baseCross?: CrossFileDuplicationMetrics; headCross?: CrossFileDuplicationMetrics } {\n const headSources = scannedFiles.flatMap(({ relativePath, file }) =>\n file.duplicationCandidates ? [{ file: relativePath, ...file.duplicationCandidates }] : []\n );\n\n const changedHeadPaths = new Set(\n prepared.flatMap((file) => (file.changed.status === 'deleted' ? [] : [file.changed.headPath]))\n );\n const baseSources = headSources.filter((source) => !changedHeadPaths.has(source.file));\n for (const file of prepared) {\n if (file.baseCandidates && file.changed.basePath !== undefined) {\n baseSources.push({ file: file.changed.basePath, ...file.baseCandidates });\n }\n }\n\n return {\n baseCross: baseSources.length >= 2 ? measureCrossFileDuplication(baseSources, options.duplication) : undefined,\n headCross: headSources.length >= 2 ? measureCrossFileDuplication(headSources, options.duplication) : undefined,\n };\n}\n\nfunction toGateInput(\n file: PreparedFile,\n baseCross: CrossFileDuplicationMetrics | undefined,\n headCross: CrossFileDuplicationMetrics | undefined\n): GateFileInput {\n return {\n file: file.displayFile,\n baseMetrics: file.baseMetrics,\n headMetrics: file.headFile?.metrics,\n baseFunctionTokens: file.baseFunctionTokens,\n headFunctionTokens: file.headFunctionTokens,\n baseDuplicatedLineCount:\n file.baseMetrics === undefined || file.changed.basePath === undefined\n ? 0\n : countDuplicatedLines(file.baseMetrics, baseCross, file.changed.basePath),\n headDuplicatedLineCount:\n file.changed.status === 'deleted'\n ? 0\n : countDuplicatedLines(file.headFile?.metrics, headCross, file.changed.headPath),\n duplicationPartners: collectPartners(headCross, file.changed.headPath),\n gated: file.gated,\n };\n}\n\nfunction countDuplicatedLines(\n metrics: CodeMetrics | undefined,\n cross: CrossFileDuplicationMetrics | undefined,\n file: string\n): number {\n return collectDuplicatedLineNumbers(metrics, cross, file).size;\n}\n\nfunction collectPartners(cross: CrossFileDuplicationMetrics | undefined, file: string): string[] {\n if (!cross) {\n return [];\n }\n const partners = new Set<string>();\n for (const group of cross.groups) {\n if (group.files.includes(file)) {\n for (const partner of group.files) {\n if (partner !== file) {\n partners.add(partner);\n }\n }\n }\n }\n return [...partners].toSorted();\n}\n\nfunction printTextReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n errors: string[],\n warnings: string[]\n): void {\n const shortBase = mergeBase.slice(0, 12);\n if (errors.length > 0) {\n // Unmeasured files were not gated, so \"0 violations\" would be vacuous; never claim a pass.\n writeStdout(\n `Regression gate could not complete: ${errors.length} measurement failures (details on stderr)` +\n `${result.violations.length > 0 ? `; ${result.violations.length} violations in the measured files` : ''} (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n printViolations(result);\n } else if (result.violations.length === 0) {\n writeStdout(\n `Regression gate passed: ${result.checkedFileCount} changed files, ${result.checkedFunctionCount} functions checked (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n } else {\n writeStdout(\n `Regression gate vs ${cliOptions.base} (merge-base ${shortBase}): ${result.violations.length} violations\\n`\n );\n printViolations(result);\n }\n\n if (cliOptions.full) {\n printFullDetails(result);\n }\n\n for (const warning of warnings) {\n writeStderr(`Warning: ${warning}\\n`);\n }\n for (const error of errors) {\n writeStderr(`Error: ${error}\\n`);\n }\n}\n\nfunction printViolations(result: GateResult): void {\n for (const [index, violation] of result.violations.entries()) {\n writeStdout(`${index + 1}. ${violation.message}\\n`);\n }\n}\n\n/** Base -> head values of every checked function; kept behind --full for humans and trending. */\nfunction printFullDetails(result: GateResult): void {\n if (result.checkedFunctions.length === 0) {\n return;\n }\n writeStdout('\\nChecked functions (base -> head):\\n');\n for (const report of result.checkedFunctions) {\n writeStdout(`- ${formatFunctionReport(report)}\\n`);\n }\n}\n\nfunction formatFunctionReport(report: CheckedFunctionReport): string {\n const range = (\n select: (values: GateFunctionValues) => number,\n format: (value: number) => string = String\n ): string => {\n const head = format(select(report.head));\n return report.base ? `${format(select(report.base))} -> ${head}` : head;\n };\n const values = [\n `cognitive ${range((fn) => fn.cognitiveComplexity)}`,\n `NCSS ${range((fn) => fn.ncss)}`,\n `nesting ${range((fn) => fn.nestingDepth)}`,\n `DepDegree ${range((fn) => fn.depDegree)}`,\n `volume ${range(\n (fn) => fn.halsteadVolume,\n (value) => value.toFixed(1)\n )}`,\n ];\n return `${report.file}:${report.startLine}-${report.endLine} ${report.name}${report.base ? '' : ' (new)'}: ${values.join(', ')}`;\n}\n\nfunction printJsonReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n inputs: GateFileInput[],\n errors: string[],\n warnings: string[]\n): void {\n const report: Record<string, unknown> = {\n base: cliOptions.base,\n mergeBase,\n passed: result.violations.length === 0 && errors.length === 0,\n violations: result.violations,\n checkedFileCount: result.checkedFileCount,\n checkedFunctionCount: result.checkedFunctionCount,\n newFunctionCount: result.newFunctionCount,\n errors,\n warnings,\n };\n if (cliOptions.full) {\n report.files = inputs\n .filter((input) => input.gated !== false)\n .map((input) => ({\n file: input.file,\n baseFunctionCount: input.baseMetrics?.functions.length ?? 0,\n headFunctionCount: input.headMetrics?.functions.length ?? 0,\n baseNcss: input.baseMetrics?.ncssCount ?? 0,\n headNcss: input.headMetrics?.ncssCount ?? 0,\n baseMaxCognitiveComplexity: input.baseMetrics?.maxCognitiveComplexity ?? 0,\n headMaxCognitiveComplexity: input.headMetrics?.maxCognitiveComplexity ?? 0,\n baseDuplicatedLineCount: input.baseDuplicatedLineCount,\n headDuplicatedLineCount: input.headDuplicatedLineCount,\n duplicationPartners: input.duplicationPartners,\n functions: result.checkedFunctions.filter((fn) => fn.file === input.file),\n }));\n }\n writeStdout(JSON.stringify(report, undefined, 2) + '\\n');\n}\n"],"mappings":"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"}
|