code-gauge 6.0.0 → 8.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.
@@ -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 { getErrorMessage, isRecord } from '@willbooster/shared-lib';\nimport { defaultDuplicationOptions } from './duplication.js';\nimport type { ExcludePatterns } from './exclusion.js';\nimport { supportedLanguages } from './languages.js';\nimport { thresholds, type Limits, type ThresholdsConfig } from './thresholds.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 /** Limits of `code-gauge check`. */\n thresholds?: ThresholdsConfig;\n /** Glob patterns, relative to the config file's directory, of files left out of every command. */\n exclude?: string[];\n includeTests?: boolean;\n failOnError?: boolean;\n}\n\n/** A parsed configuration file and the directory its relative settings resolve against. */\nexport interface LoadedConfig {\n config: CodeGaugeConfig;\n /** The config file's directory; the search start directory when no config file was found. */\n directory: string;\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 exclude: ExcludePatterns;\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, directory }: LoadedConfig): 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 exclude: { patterns: config.exclude ?? [], root: directory },\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<LoadedConfig> {\n const configFile = explicitPath ?? (await findNearestConfig(targetDirectory));\n if (!configFile) {\n return { config: {}, directory: targetDirectory };\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}\": ${getErrorMessage(error)}`);\n }\n return { config: {}, directory: targetDirectory };\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}\": ${getErrorMessage(error)}`);\n }\n\n return { config: validateConfig(parsed, configFile), directory: path.dirname(path.resolve(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 (!isRecord(value)) {\n throw new Error(`Config file \"${configFile}\" must contain a JSON object.`);\n }\n\n const knownKeys = new Set(['duplication', 'rank', 'thresholds', 'exclude', 'includeTests', 'failOnError']);\n for (const key of Object.keys(value)) {\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 (value.duplication !== undefined) {\n config.duplication = validateDuplicationObject(value.duplication, configFile);\n }\n\n if (value.rank !== undefined) {\n config.rank = validateRankObject(value.rank, configFile);\n }\n\n if (value.thresholds !== undefined) {\n config.thresholds = validateThresholdsObject(value.thresholds, configFile);\n }\n\n if (value.exclude !== undefined) {\n config.exclude = validateExcludePatterns(value.exclude, configFile);\n }\n\n for (const key of ['includeTests', 'failOnError'] as const) {\n if (value[key] !== undefined) {\n config[key] = requireBoolean(value[key], key, configFile);\n }\n }\n\n return config;\n}\n\nfunction validateExcludePatterns(value: unknown, configFile: string): string[] {\n if (!Array.isArray(value) || !value.every((pattern) => typeof pattern === 'string' && pattern !== '')) {\n throw new TypeError(`Config file \"${configFile}\": \"exclude\" must be an array of non-empty glob patterns.`);\n }\n return value as string[];\n}\n\nfunction validateRankObject(value: unknown, configFile: string): { top?: number } {\n if (!isRecord(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)) {\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 validateThresholdsObject(value: unknown, configFile: string): ThresholdsConfig {\n const { languages, ...limits } = requireRecord(value, 'thresholds', configFile);\n return {\n limits: validateLimits(limits, 'thresholds', ['languages'], configFile),\n languages: Object.fromEntries(\n Object.entries(languages === undefined ? {} : requireRecord(languages, 'thresholds.languages', configFile)).map(\n ([language, overrides]) => {\n if (!supportedLanguages.includes(language)) {\n throw new Error(\n `Config file \"${configFile}\": unknown language \"${language}\" in \"thresholds.languages\" (expected ${supportedLanguages.join(', ')}).`\n );\n }\n const settingName = `thresholds.languages.${language}`;\n return [\n language,\n validateLimits(requireRecord(overrides, settingName, configFile), settingName, [], configFile),\n ];\n }\n )\n ),\n };\n}\n\n/** Limits as written in the config file, where `null` disables one. */\nfunction validateLimits(\n value: Record<string, unknown>,\n settingName: string,\n otherKeys: string[],\n configFile: string\n): Limits {\n const knownKeys = thresholds.map(({ key }) => key);\n return Object.fromEntries(\n Object.entries(value).map(([key, limit]) => {\n if (!knownKeys.includes(key)) {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"${settingName}\" (expected ${[...knownKeys, ...otherKeys].join(', ')}).`\n );\n }\n if (limit !== null && (typeof limit !== 'number' || !Number.isFinite(limit) || limit < 0)) {\n throw new Error(`Config file \"${configFile}\": \"${settingName}.${key}\" must be a non-negative number or null.`);\n }\n return [key, limit ?? Infinity];\n })\n );\n}\n\nfunction requireRecord(value: unknown, settingName: string, configFile: string): Record<string, unknown> {\n if (!isRecord(value)) {\n throw new Error(`Config file \"${configFile}\": \"${settingName}\" must be an object.`);\n }\n return value;\n}\n\nfunction validateDuplicationObject(value: unknown, configFile: string): DuplicationOptions {\n if (!isRecord(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)) {\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 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"],"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 { getErrorMessage, isRecord } from '@willbooster/shared-lib';\nimport { defaultDuplicationOptions } from './duplication.js';\nimport type { ExcludePatterns } from './exclusion.js';\nimport { supportedLanguages } from './languages.js';\nimport {\n levels,\n thresholds,\n type Level,\n type LevelThresholdsConfig,\n type Limits,\n type ThresholdsConfig,\n} from './thresholds.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 /** Limits of `code-gauge check`. */\n thresholds?: ThresholdsConfig;\n /** Glob patterns, relative to the config file's directory, of files left out of every command. */\n exclude?: string[];\n includeTests?: boolean;\n failOnError?: boolean;\n}\n\n/** A parsed configuration file and the directory its relative settings resolve against. */\nexport interface LoadedConfig {\n config: CodeGaugeConfig;\n /** The config file's directory; the search start directory when no config file was found. */\n directory: string;\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 exclude: ExcludePatterns;\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, directory }: LoadedConfig): 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 exclude: { patterns: config.exclude ?? [], root: directory },\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<LoadedConfig> {\n const configFile = explicitPath ?? (await findNearestConfig(targetDirectory));\n if (!configFile) {\n return { config: {}, directory: targetDirectory };\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}\": ${getErrorMessage(error)}`);\n }\n return { config: {}, directory: targetDirectory };\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}\": ${getErrorMessage(error)}`);\n }\n\n return { config: validateConfig(parsed, configFile), directory: path.dirname(path.resolve(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 (!isRecord(value)) {\n throw new Error(`Config file \"${configFile}\" must contain a JSON object.`);\n }\n\n const knownKeys = new Set(['duplication', 'rank', 'thresholds', 'exclude', 'includeTests', 'failOnError']);\n for (const key of Object.keys(value)) {\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 (value.duplication !== undefined) {\n config.duplication = validateDuplicationObject(value.duplication, configFile);\n }\n\n if (value.rank !== undefined) {\n config.rank = validateRankObject(value.rank, configFile);\n }\n\n if (value.thresholds !== undefined) {\n config.thresholds = validateThresholdsObject(value.thresholds, configFile);\n }\n\n if (value.exclude !== undefined) {\n config.exclude = validateExcludePatterns(value.exclude, configFile);\n }\n\n for (const key of ['includeTests', 'failOnError'] as const) {\n if (value[key] !== undefined) {\n config[key] = requireBoolean(value[key], key, configFile);\n }\n }\n\n return config;\n}\n\nfunction validateExcludePatterns(value: unknown, configFile: string): string[] {\n if (!Array.isArray(value) || !value.every((pattern) => typeof pattern === 'string' && pattern !== '')) {\n throw new TypeError(`Config file \"${configFile}\": \"exclude\" must be an array of non-empty glob patterns.`);\n }\n return value as string[];\n}\n\nfunction validateRankObject(value: unknown, configFile: string): { top?: number } {\n if (!isRecord(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)) {\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 validateThresholdsObject(value: unknown, configFile: string): ThresholdsConfig {\n return Object.fromEntries(\n Object.entries(requireRecord(value, 'thresholds', configFile)).map(([level, levelThresholds]) => {\n if (!levels.includes(level as Level)) {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${level}\" in \"thresholds\" (expected ${levels.join(', ')}).`\n );\n }\n return [level, validateLevelThresholds(levelThresholds, `thresholds.${level}`, configFile)];\n })\n );\n}\n\nfunction validateLevelThresholds(value: unknown, settingName: string, configFile: string): LevelThresholdsConfig {\n const { languages, ...limits } = requireRecord(value, settingName, configFile);\n const languagesName = `${settingName}.languages`;\n return {\n limits: validateLimits(limits, settingName, ['languages'], configFile),\n languages: Object.fromEntries(\n Object.entries(languages === undefined ? {} : requireRecord(languages, languagesName, configFile)).map(\n ([language, overrides]) => {\n if (!supportedLanguages.includes(language)) {\n throw new Error(\n `Config file \"${configFile}\": unknown language \"${language}\" in \"${languagesName}\" (expected ${supportedLanguages.join(', ')}).`\n );\n }\n const languageName = `${languagesName}.${language}`;\n return [\n language,\n validateLimits(requireRecord(overrides, languageName, configFile), languageName, [], configFile),\n ];\n }\n )\n ),\n };\n}\n\n/** Limits as written in the config file, where `null` disables one. */\nfunction validateLimits(\n value: Record<string, unknown>,\n settingName: string,\n otherKeys: string[],\n configFile: string\n): Limits {\n const knownKeys = thresholds.map(({ key }) => key);\n return Object.fromEntries(\n Object.entries(value).map(([key, limit]) => {\n if (!knownKeys.includes(key)) {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"${settingName}\" (expected ${[...knownKeys, ...otherKeys].join(', ')}).`\n );\n }\n if (limit !== null && (typeof limit !== 'number' || !Number.isFinite(limit) || limit < 0)) {\n throw new Error(`Config file \"${configFile}\": \"${settingName}.${key}\" must be a non-negative number or null.`);\n }\n return [key, limit ?? Infinity];\n })\n );\n}\n\nfunction requireRecord(value: unknown, settingName: string, configFile: string): Record<string, unknown> {\n if (!isRecord(value)) {\n throw new Error(`Config file \"${configFile}\": \"${settingName}\" must be an object.`);\n }\n return value;\n}\n\nfunction validateDuplicationObject(value: unknown, configFile: string): DuplicationOptions {\n if (!isRecord(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)) {\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 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"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
1
  "use strict";let e=require("@willbooster/shared-lib");function t(e,t,n,i,a=!1){return JSON.parse(f().measureCodeNative(...r(e,t,n,i,a)))}function n(e,t,n,i,a=!1){return f().measureCodeNativeAsync(...r(e,t,n,i,a)).then(e=>JSON.parse(e))}function r(e,t,n,r,i){return[o(e),t,n,s(r?.minTokens),s(r?.maxGapTokens),s(r?.minSimilarityPercent),i]}function i(e,t,n){return JSON.parse(f().collectCrossFileDataNative(o(e),t,s(n)))}function a(e,t){return JSON.parse(f().collectFunctionTokenSequencesNative(o(e),t)).map(e=>Int32Array.from(e))}function o(e){return e.isWellFormed()?e:e.toWellFormed()}function s(t){return t===void 0||Number.isNaN(t)?void 0:(0,e.clamp)(Math.trunc(t),0,4294967295)}var c=class extends Error{};let l,u;function d(e){l=e}function f(){if(l)return l;if(u)throw u;let t=process.getBuiltinModule(`node:module`).createRequire(require("url").pathToFileURL(__filename).href),n=[`code-gauge-${p()}`,`../native/code-gauge.node`],r=[];for(let i of n){let n;try{n=t(i)}catch(t){r.push(` ${i}: ${(0,e.getErrorMessage)(t).split(`
2
- `)[0]}`);continue}let a=n.payloadVersion?.();if(a!==8){r.push(` ${i}: payload version ${a??`unknown`} does not match the expected 8; rebuild the addon with \`bun run build-native\``);continue}return l=n,n}throw u=new c(`The code-gauge native addon is not available for ${p()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain); when installing with npm, also allow install scripts for code-gauge so its postinstall build can run.\n${r.join(`
2
+ `)[0]}`);continue}let a=n.payloadVersion?.();if(a!==8){r.push(` ${i}: payload version ${a??`unknown`} does not match the expected 8; rebuild the addon with \`bun run build-native\``);continue}return l=n,n}throw u=new c(`The code-gauge native addon is not available for ${p()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain).\n${r.join(`
3
3
  `)}`),u}function p(){let e=`${process.platform}-${process.arch}`;return process.platform===`win32`?`${e}-msvc`:process.platform===`linux`?(process.report?.getReport())?.header?.glibcVersionRuntime?`${e}-gnu`:`${e}-musl`:e}exports.NativeAddonError=c,exports.collectCrossFileDataNative=i,exports.collectFunctionTokenSequencesNative=a,exports.measureCodeNative=t,exports.measureCodeNativeAsync=n,exports.setNativeBinding=d;
4
4
  //# sourceMappingURL=nativeMetrics.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"nativeMetrics.cjs","names":["clamp","getErrorMessage"],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { clamp, getErrorMessage } from '@willbooster/shared-lib';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ntype NativeMeasureArguments = [\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens: number | undefined,\n maxGapTokens: number | undefined,\n minSimilarityPercent: number | undefined,\n includeCrossFileData: boolean,\n];\n\nexport interface NativeBinding {\n measureCodeNative(...args: NativeMeasureArguments): string;\n measureCodeNativeAsync(...args: NativeMeasureArguments): Promise<string>;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields, or lack newer binding functions, instead of failing with a clear rebuild message.\n */\nexport const expectedPayloadVersion = 8;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n ) as NativeMetricsPayload;\n}\n\n/**\n * measureCodeNative on the addon's worker threads, so several files can be measured in parallel.\n * A missing addon still throws synchronously, like measureCodeNative.\n */\nexport function measureCodeNativeAsync(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): Promise<NativeMetricsPayload> {\n return loadBinding()\n .measureCodeNativeAsync(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n .then((json) => JSON.parse(json) as NativeMetricsPayload);\n}\n\n/** The binding arguments shared by the sync and async measurements, so both measure alike. */\nfunction toNativeMeasureArguments(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication: DuplicationOptions | undefined,\n includeCrossFileData: boolean\n): NativeMeasureArguments {\n return [\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData,\n ];\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value) ? undefined : clamp(Math.trunc(value), 0, 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\n/** Replaces the N-API addon, e.g. with the WebAssembly build on runtimes without N-API. */\nexport function setNativeBinding(binding: NativeBinding): void {\n cachedBinding = binding;\n}\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n // node:module is loaded lazily so that runtimes without it can bundle this module.\n const requireNative = process.getBuiltinModule('node:module').createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${getErrorMessage(error).split('\\n')[0]}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n 'toolchain); when installing with npm, also allow install scripts for code-gauge so its ' +\n `postinstall build can run.\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"sDAmEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CACF,CACF,CAMA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACQ,CAC/B,OAAO,EAAY,CAAC,CACjB,uBACC,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CAAC,CACA,KAAM,GAAS,KAAK,MAAM,CAAI,CAAyB,CAC5D,CAGA,SAAS,EACP,EACA,EACA,EACA,EACA,EACwB,CACxB,MAAO,CACL,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAAI,IAAA,IAAA,EAAYA,EAAAA,MAAAA,CAAM,KAAK,MAAM,CAAK,EAAG,EAAG,UAAa,CAC3G,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAGJ,SAAgB,EAAiB,EAA8B,CAC7D,EAAgB,CAClB,CAEA,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAIR,IAAM,EAAgB,QAAQ,iBAAiB,aAAa,CAAC,CAAC,cAAA,QAAA,KAAA,CAAA,CAAA,cAAA,UAAA,CAAA,CAAA,IAA6B,EACrF,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,KAAA,EAAIC,EAAAA,gBAAAA,CAAgB,CAAK,CAAC,CAAC,MAAM;CAAI,CAAC,CAAC,IAAI,EACxE,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAA,EAAoC,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAOA,KANA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,2NAGrC,EAAS,KAAK;CAAI,GACrD,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
1
+ {"version":3,"file":"nativeMetrics.cjs","names":["clamp","getErrorMessage"],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { clamp, getErrorMessage } from '@willbooster/shared-lib';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ntype NativeMeasureArguments = [\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens: number | undefined,\n maxGapTokens: number | undefined,\n minSimilarityPercent: number | undefined,\n includeCrossFileData: boolean,\n];\n\nexport interface NativeBinding {\n measureCodeNative(...args: NativeMeasureArguments): string;\n measureCodeNativeAsync(...args: NativeMeasureArguments): Promise<string>;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields, or lack newer binding functions, instead of failing with a clear rebuild message.\n */\nexport const expectedPayloadVersion = 8;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n ) as NativeMetricsPayload;\n}\n\n/**\n * measureCodeNative on the addon's worker threads, so several files can be measured in parallel.\n * A missing addon still throws synchronously, like measureCodeNative.\n */\nexport function measureCodeNativeAsync(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): Promise<NativeMetricsPayload> {\n return loadBinding()\n .measureCodeNativeAsync(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n .then((json) => JSON.parse(json) as NativeMetricsPayload);\n}\n\n/** The binding arguments shared by the sync and async measurements, so both measure alike. */\nfunction toNativeMeasureArguments(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication: DuplicationOptions | undefined,\n includeCrossFileData: boolean\n): NativeMeasureArguments {\n return [\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData,\n ];\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value) ? undefined : clamp(Math.trunc(value), 0, 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\n/** Replaces the N-API addon, e.g. with the WebAssembly build on runtimes without N-API. */\nexport function setNativeBinding(binding: NativeBinding): void {\n cachedBinding = binding;\n}\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n // node:module is loaded lazily so that runtimes without it can bundle this module.\n const requireNative = process.getBuiltinModule('node:module').createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${getErrorMessage(error).split('\\n')[0]}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n `toolchain).\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"sDAmEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CACF,CACF,CAMA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACQ,CAC/B,OAAO,EAAY,CAAC,CACjB,uBACC,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CAAC,CACA,KAAM,GAAS,KAAK,MAAM,CAAI,CAAyB,CAC5D,CAGA,SAAS,EACP,EACA,EACA,EACA,EACA,EACwB,CACxB,MAAO,CACL,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAAI,IAAA,IAAA,EAAYA,EAAAA,MAAAA,CAAM,KAAK,MAAM,CAAK,EAAG,EAAG,UAAa,CAC3G,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAGJ,SAAgB,EAAiB,EAA8B,CAC7D,EAAgB,CAClB,CAEA,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAIR,IAAM,EAAgB,QAAQ,iBAAiB,aAAa,CAAC,CAAC,cAAA,QAAA,KAAA,CAAA,CAAA,cAAA,UAAA,CAAA,CAAA,IAA6B,EACrF,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,KAAA,EAAIC,EAAAA,gBAAAA,CAAgB,CAAK,CAAC,CAAC,MAAM;CAAI,CAAC,CAAC,IAAI,EACxE,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAA,EAAoC,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAMA,KALA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,qHAEpD,EAAS,KAAK;CAAI,GACtC,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
@@ -1,4 +1,4 @@
1
1
  import{clamp as e,getErrorMessage as t}from"@willbooster/shared-lib";function n(e,t,n,r,a=!1){return JSON.parse(p().measureCodeNative(...i(e,t,n,r,a)))}function r(e,t,n,r,a=!1){return p().measureCodeNativeAsync(...i(e,t,n,r,a)).then(e=>JSON.parse(e))}function i(e,t,n,r,i){return[s(e),t,n,c(r?.minTokens),c(r?.maxGapTokens),c(r?.minSimilarityPercent),i]}function a(e,t,n){return JSON.parse(p().collectCrossFileDataNative(s(e),t,c(n)))}function o(e,t){return JSON.parse(p().collectFunctionTokenSequencesNative(s(e),t)).map(e=>Int32Array.from(e))}function s(e){return e.isWellFormed()?e:e.toWellFormed()}function c(t){return t===void 0||Number.isNaN(t)?void 0:e(Math.trunc(t),0,4294967295)}var l=class extends Error{};let u,d;function f(e){u=e}function p(){if(u)return u;if(d)throw d;let e=process.getBuiltinModule(`node:module`).createRequire(import.meta.url),n=[`code-gauge-${m()}`,`../native/code-gauge.node`],r=[];for(let i of n){let n;try{n=e(i)}catch(e){r.push(` ${i}: ${t(e).split(`
2
- `)[0]}`);continue}let a=n.payloadVersion?.();if(a!==8){r.push(` ${i}: payload version ${a??`unknown`} does not match the expected 8; rebuild the addon with \`bun run build-native\``);continue}return u=n,n}throw d=new l(`The code-gauge native addon is not available for ${m()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain); when installing with npm, also allow install scripts for code-gauge so its postinstall build can run.\n${r.join(`
2
+ `)[0]}`);continue}let a=n.payloadVersion?.();if(a!==8){r.push(` ${i}: payload version ${a??`unknown`} does not match the expected 8; rebuild the addon with \`bun run build-native\``);continue}return u=n,n}throw d=new l(`The code-gauge native addon is not available for ${m()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain).\n${r.join(`
3
3
  `)}`),d}function m(){let e=`${process.platform}-${process.arch}`;return process.platform===`win32`?`${e}-msvc`:process.platform===`linux`?(process.report?.getReport())?.header?.glibcVersionRuntime?`${e}-gnu`:`${e}-musl`:e}export{l as NativeAddonError,a as collectCrossFileDataNative,o as collectFunctionTokenSequencesNative,n as measureCodeNative,r as measureCodeNativeAsync,f as setNativeBinding};
4
4
  //# sourceMappingURL=nativeMetrics.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"nativeMetrics.js","names":[],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { clamp, getErrorMessage } from '@willbooster/shared-lib';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ntype NativeMeasureArguments = [\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens: number | undefined,\n maxGapTokens: number | undefined,\n minSimilarityPercent: number | undefined,\n includeCrossFileData: boolean,\n];\n\nexport interface NativeBinding {\n measureCodeNative(...args: NativeMeasureArguments): string;\n measureCodeNativeAsync(...args: NativeMeasureArguments): Promise<string>;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields, or lack newer binding functions, instead of failing with a clear rebuild message.\n */\nexport const expectedPayloadVersion = 8;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n ) as NativeMetricsPayload;\n}\n\n/**\n * measureCodeNative on the addon's worker threads, so several files can be measured in parallel.\n * A missing addon still throws synchronously, like measureCodeNative.\n */\nexport function measureCodeNativeAsync(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): Promise<NativeMetricsPayload> {\n return loadBinding()\n .measureCodeNativeAsync(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n .then((json) => JSON.parse(json) as NativeMetricsPayload);\n}\n\n/** The binding arguments shared by the sync and async measurements, so both measure alike. */\nfunction toNativeMeasureArguments(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication: DuplicationOptions | undefined,\n includeCrossFileData: boolean\n): NativeMeasureArguments {\n return [\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData,\n ];\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value) ? undefined : clamp(Math.trunc(value), 0, 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\n/** Replaces the N-API addon, e.g. with the WebAssembly build on runtimes without N-API. */\nexport function setNativeBinding(binding: NativeBinding): void {\n cachedBinding = binding;\n}\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n // node:module is loaded lazily so that runtimes without it can bundle this module.\n const requireNative = process.getBuiltinModule('node:module').createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${getErrorMessage(error).split('\\n')[0]}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n 'toolchain); when installing with npm, also allow install scripts for code-gauge so its ' +\n `postinstall build can run.\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"qEAmEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CACF,CACF,CAMA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACQ,CAC/B,OAAO,EAAY,CAAC,CACjB,uBACC,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CAAC,CACA,KAAM,GAAS,KAAK,MAAM,CAAI,CAAyB,CAC5D,CAGA,SAAS,EACP,EACA,EACA,EACA,EACA,EACwB,CACxB,MAAO,CACL,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAAI,IAAA,GAAY,EAAM,KAAK,MAAM,CAAK,EAAG,EAAG,UAAa,CAC3G,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAGJ,SAAgB,EAAiB,EAA8B,CAC7D,EAAgB,CAClB,CAEA,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAIR,IAAM,EAAgB,QAAQ,iBAAiB,aAAa,CAAC,CAAC,cAAc,YAAY,GAAG,EACrF,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,IAAI,EAAgB,CAAK,CAAC,CAAC,MAAM;CAAI,CAAC,CAAC,IAAI,EACxE,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAA,EAAoC,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAOA,KANA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,2NAGrC,EAAS,KAAK;CAAI,GACrD,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
1
+ {"version":3,"file":"nativeMetrics.js","names":[],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { clamp, getErrorMessage } from '@willbooster/shared-lib';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ntype NativeMeasureArguments = [\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens: number | undefined,\n maxGapTokens: number | undefined,\n minSimilarityPercent: number | undefined,\n includeCrossFileData: boolean,\n];\n\nexport interface NativeBinding {\n measureCodeNative(...args: NativeMeasureArguments): string;\n measureCodeNativeAsync(...args: NativeMeasureArguments): Promise<string>;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields, or lack newer binding functions, instead of failing with a clear rebuild message.\n */\nexport const expectedPayloadVersion = 8;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n ) as NativeMetricsPayload;\n}\n\n/**\n * measureCodeNative on the addon's worker threads, so several files can be measured in parallel.\n * A missing addon still throws synchronously, like measureCodeNative.\n */\nexport function measureCodeNativeAsync(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): Promise<NativeMetricsPayload> {\n return loadBinding()\n .measureCodeNativeAsync(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n .then((json) => JSON.parse(json) as NativeMetricsPayload);\n}\n\n/** The binding arguments shared by the sync and async measurements, so both measure alike. */\nfunction toNativeMeasureArguments(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication: DuplicationOptions | undefined,\n includeCrossFileData: boolean\n): NativeMeasureArguments {\n return [\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData,\n ];\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value) ? undefined : clamp(Math.trunc(value), 0, 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\n/** Replaces the N-API addon, e.g. with the WebAssembly build on runtimes without N-API. */\nexport function setNativeBinding(binding: NativeBinding): void {\n cachedBinding = binding;\n}\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n // node:module is loaded lazily so that runtimes without it can bundle this module.\n const requireNative = process.getBuiltinModule('node:module').createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${getErrorMessage(error).split('\\n')[0]}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n `toolchain).\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"qEAmEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CACF,CACF,CAMA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACQ,CAC/B,OAAO,EAAY,CAAC,CACjB,uBACC,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CAAC,CACA,KAAM,GAAS,KAAK,MAAM,CAAI,CAAyB,CAC5D,CAGA,SAAS,EACP,EACA,EACA,EACA,EACA,EACwB,CACxB,MAAO,CACL,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAAI,IAAA,GAAY,EAAM,KAAK,MAAM,CAAK,EAAG,EAAG,UAAa,CAC3G,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAGJ,SAAgB,EAAiB,EAA8B,CAC7D,EAAgB,CAClB,CAEA,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAIR,IAAM,EAAgB,QAAQ,iBAAiB,aAAa,CAAC,CAAC,cAAc,YAAY,GAAG,EACrF,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,IAAI,EAAgB,CAAK,CAAC,CAAC,MAAM;CAAI,CAAC,CAAC,IAAI,EACxE,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAA,EAAoC,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAMA,KALA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,qHAEpD,EAAS,KAAK;CAAI,GACtC,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
@@ -1,2 +1,2 @@
1
- "use strict";const e=require("./thresholds.cjs"),t=[`file`,`function`,`duplication`];function n(t,n,o){let l=s(n,new Set(t.map(({file:e})=>e))),u=[],d=0;for(let n of t){let{file:t,metrics:r,hunks:s}=n,f=o(r.language),p=s?.length===0?[]:a(e.fileThresholds,r,f);p.length>0&&u.push({kind:`file`,file:t,startLine:1,endLine:r.lines.total,exceeded:p});let m=i(n);d+=m.length;for(let n of m){let r=a(e.functionThresholds,n,f);if(r.length>0){let{startLine:e,endLine:i}=n;u.push({kind:`function`,file:t,startLine:e,endLine:i,name:n.name??`<anonymous>`,exceeded:r})}}u.push(...c(t,r,l.get(t)??[],s,f[e.duplicationThreshold.key]))}return{violations:u.toSorted(r),checkedFunctionCount:d}}function r(e,n){return d(e.file,n.file)||e.startLine-n.startLine||t.indexOf(e.kind)-t.indexOf(n.kind)||e.endLine-n.endLine}function i({metrics:e,hunks:t}){let n=e.language===`python`;return t?e.functions.filter(e=>t.some(t=>o(t,e.startLine,e.endLine,n))):e.functions}function a(t,n,r){return t.flatMap(t=>{let i=t.measure(n),a=r[t.key];return i>a?[{metric:e.metricNameOf(t),value:i,limit:a}]:[]})}function o(e,t,n,r=!1){return e.headCount===0?e.headStart>=t&&(e.headStart<n||r&&e.headStart===n):e.headStart<=n&&e.headStart+e.headCount>t}function s(e,t){let n=/* @__PURE__ */ new Map;for(let r of e?.groups??[])for(let e of r.files)if(t.has(e)){let t=n.get(e)??[];t.push(r),n.set(e,t)}return n}function c(t,n,r,i,a){let s=[...n.duplication.duplicateBlockGroups.map(e=>e.map(e=>({...e,file:t}))),...r.map(e=>e.occurrences)],c=n=>n.file===t&&e.duplicationThreshold.measure(n)>=a&&(!i||i.some(e=>e.headCount>0&&o(e,n.startLine,n.endLine))),d=/* @__PURE__ */ new Map;for(let e of s)for(let t of e.filter(e=>c(e)))d.set(t,e);return l([...d.keys()]).map(({merged:t,sources:n})=>({kind:`duplication`,...t,exceeded:[{metric:e.metricNameOf(e.duplicationThreshold),value:e.duplicationThreshold.measure(t),limit:a}],partners:l(n.flatMap(e=>d.get(e)??[])).map(e=>e.merged).filter(e=>!u(e,t))}))}function l(e){let t=[],n=e.toSorted((e,t)=>d(e.file,t.file)||e.startLine-t.startLine);for(let e of n){let n=t.at(-1);if(n&&u(n.merged,e))n.merged.endLine=Math.max(n.merged.endLine,e.endLine),n.sources.push(e);else{let{file:n,startLine:r,endLine:i}=e;t.push({merged:{file:n,startLine:r,endLine:i},sources:[e]})}}return t}function u(e,t){return e.file===t.file&&e.startLine<=t.endLine&&t.startLine<=e.endLine}function d(e,t){return e<t?-1:+(e>t)}exports.checkThresholds=n;
1
+ "use strict";const e=require("./thresholds.cjs"),t=[`file`,`function`,`duplication`],n=new Set(e.functionThresholds.filter(({includesNestedFunctions:e})=>e).map(t=>e.metricNameOf(t)));function r(t,n,r){let s=f(n,new Set(t.map(({file:e})=>e))),u=[],d=0;for(let n of t){let{file:t,metrics:i,hunks:f}=n,m=r(i.language),h=f?.length===0?[]:l(e.fileThresholds,i,m);h.length>0&&u.push(c({file:t,startLine:1,endLine:i.lines.total},`file`,h));let g=a(n);d+=g.length;let _=g.map(t=>({fn:t,exceeded:l(e.functionThresholds,t,m)})).filter(({exceeded:e})=>e.length>0);for(let{fn:e,exceeded:n}of _){let r=n.filter(t=>!o(t,e,_));if(r.length>0){let{startLine:n,endLine:i}=e;u.push({...c({file:t,startLine:n,endLine:i},`function`,r),name:e.name??`<anonymous>`})}}u.push(...p(t,i,s.get(t)??[],f,m))}return{violations:u.toSorted(i),checkedFunctionCount:d}}function i(n,r){return e.levels.indexOf(r.level)-e.levels.indexOf(n.level)||g(n.file,r.file)||n.startLine-r.startLine||t.indexOf(n.kind)-t.indexOf(r.kind)||n.endLine-r.endLine}function a({metrics:e,hunks:t}){let n=e.language===`python`;return t?e.functions.filter(e=>t.some(t=>d(t,e.startLine,e.endLine,n))):e.functions}function o(e,t,r){return n.has(e.metric)?r.some(n=>n.fn!==t&&s(n.fn,t)&&n.exceeded.some(({metric:t})=>t===e.metric)):!1}function s(e,t){let n=e.startLine<t.startLine||e.startLine===t.startLine&&e.startColumn<=t.startColumn,r=e.endLine>t.endLine||e.endLine===t.endLine&&e.endColumn>=t.endColumn;return n&&r}function c(e,t,n){return{kind:t,level:n.some(e=>e.level===`error`)?`error`:`warning`,...e,exceeded:n}}function l(e,t,n){return e.flatMap(e=>u(e,t,n,(e,t)=>e>t))}function u(t,n,r,i){let a=t.measure(n),o=e.levels.findLast(e=>i(a,r[e][t.key]));return o?[{metric:e.metricNameOf(t),value:a,level:o,limit:r[o][t.key]}]:[]}function d(e,t,n,r=!1){return e.headCount===0?e.headStart>=t&&(e.headStart<n||r&&e.headStart===n):e.headStart<=n&&e.headStart+e.headCount>t}function f(e,t){let n=/* @__PURE__ */ new Map;for(let r of e?.groups??[])for(let e of r.files)if(t.has(e)){let t=n.get(e)??[];t.push(r),n.set(e,t)}return n}function p(t,n,r,i,a){let o=t=>u(e.duplicationThreshold,t,a,(e,t)=>e>=t),s=[...n.duplication.duplicateBlockGroups.map(e=>e.map(e=>({...e,file:t}))),...r.map(e=>e.occurrences)],l=e=>e.file===t&&o(e).length>0&&(!i||i.some(t=>t.headCount>0&&d(t,e.startLine,e.endLine))),f=/* @__PURE__ */ new Map;for(let e of s)for(let t of e.filter(e=>l(e)))f.set(t,e);return m([...f.keys()]).map(({merged:e,sources:t})=>({...c(e,`duplication`,o(e)),partners:m(t.flatMap(e=>f.get(e)??[])).map(e=>e.merged).filter(t=>!h(t,e))}))}function m(e){let t=[],n=e.toSorted((e,t)=>g(e.file,t.file)||e.startLine-t.startLine);for(let e of n){let n=t.at(-1);if(n&&h(n.merged,e))n.merged.endLine=Math.max(n.merged.endLine,e.endLine),n.sources.push(e);else{let{file:n,startLine:r,endLine:i}=e;t.push({merged:{file:n,startLine:r,endLine:i},sources:[e]})}}return t}function h(e,t){return e.file===t.file&&e.startLine<=t.endLine&&t.startLine<=e.endLine}function g(e,t){return e<t?-1:+(e>t)}exports.checkThresholds=r;
2
2
  //# sourceMappingURL=thresholdCheck.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"thresholdCheck.cjs","names":["fileThresholds","functionThresholds","duplicationThreshold","metricNameOf"],"sources":["../src/thresholdCheck.ts"],"sourcesContent":["import type { CrossFileDuplicateBlockGroup, CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { LineHunk } from './git.js';\nimport {\n duplicationThreshold,\n fileThresholds,\n functionThresholds,\n metricNameOf,\n type Limits,\n type Threshold,\n} from './thresholds.js';\nimport type { CodeMetrics, FunctionMetrics } from './types.js';\n\n/** A measured file to check, under the path cross-file duplication knows it by. */\nexport interface CheckedFile {\n file: string;\n metrics: CodeMetrics;\n /** The change to the file; when set, only what the change touches is checked. */\n hunks?: readonly LineHunk[];\n}\n\nexport interface ExceededLimit {\n metric: string;\n value: number;\n /** The value is allowed up to this limit; a duplicated block is allowed below it. */\n limit: number;\n}\n\nexport interface BlockLocation {\n file: string;\n startLine: number;\n endLine: number;\n}\n\nexport interface Violation extends BlockLocation {\n kind: 'function' | 'file' | 'duplication';\n /** The function's name, for kind `function`. */\n name?: string;\n exceeded: ExceededLimit[];\n /** The other copies of the block, for kind `duplication`. */\n partners?: BlockLocation[];\n}\n\nexport interface CheckResult {\n violations: Violation[];\n checkedFunctionCount: number;\n}\n\nconst violationKinds: readonly Violation['kind'][] = ['file', 'function', 'duplication'];\n\n/** Every limit the files exceed, ordered by path, then line. */\nexport function checkThresholds(\n files: readonly CheckedFile[],\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n limitsOf: (language: string) => Limits\n): CheckResult {\n const crossFileGroupsByFile = indexGroupsByFile(crossFileDuplication, new Set(files.map(({ file }) => file)));\n const violations: Violation[] = [];\n let checkedFunctionCount = 0;\n for (const checkedFile of files) {\n const { file, metrics, hunks } = checkedFile;\n const limits = limitsOf(metrics.language);\n // A rename or a mode change lists the file as changed without touching any of its lines.\n const fileExceeded = hunks?.length === 0 ? [] : collectExceeded(fileThresholds, metrics, limits);\n if (fileExceeded.length > 0) {\n violations.push({ kind: 'file', file, startLine: 1, endLine: metrics.lines.total, exceeded: fileExceeded });\n }\n const checkedFunctions = listCheckedFunctions(checkedFile);\n checkedFunctionCount += checkedFunctions.length;\n for (const fn of checkedFunctions) {\n const exceeded = collectExceeded(functionThresholds, fn, limits);\n if (exceeded.length > 0) {\n const { startLine, endLine } = fn;\n violations.push({ kind: 'function', file, startLine, endLine, name: fn.name ?? '<anonymous>', exceeded });\n }\n }\n violations.push(\n ...collectDuplicationViolations(\n file,\n metrics,\n crossFileGroupsByFile.get(file) ?? [],\n hunks,\n limits[duplicationThreshold.key] as number\n )\n );\n }\n return { violations: violations.toSorted(compareViolations), checkedFunctionCount };\n}\n\n// The kind is compared before the end line because a file violation's end line is corrected after\n// this ordering (to the last line that exists), and the order must not depend on that correction.\nfunction compareViolations(left: Violation, right: Violation): number {\n return (\n compareStrings(left.file, right.file) ||\n left.startLine - right.startLine ||\n violationKinds.indexOf(left.kind) - violationKinds.indexOf(right.kind) ||\n left.endLine - right.endLine\n );\n}\n\n/** The file's functions or, for a changed file, those the change touches. */\nfunction listCheckedFunctions({ metrics, hunks }: CheckedFile): FunctionMetrics[] {\n const endsByIndentation = metrics.language === 'python';\n return hunks\n ? metrics.functions.filter((fn) =>\n hunks.some((hunk) => touchesSpan(hunk, fn.startLine, fn.endLine, endsByIndentation))\n )\n : metrics.functions;\n}\n\nfunction collectExceeded<Subject>(\n thresholds: readonly Threshold<Subject>[],\n subject: Subject,\n limits: Limits\n): ExceededLimit[] {\n return thresholds.flatMap((threshold) => {\n const value = threshold.measure(subject);\n const limit = limits[threshold.key] as number;\n return value > limit ? [{ metric: metricNameOf(threshold), value, limit }] : [];\n });\n}\n\n/**\n * Whether the hunk changes the head lines [startLine, endLine]. A hunk without head lines (a pure\n * deletion) sits between its headStart line and the next one. Where indentation ends a span, its\n * last line is a statement rather than a closing delimiter, so a deletion right after that line may\n * have removed the span's tail; `endsByIndentation` counts it as touching, since the head alone\n * cannot tell it from a deletion after the span.\n */\nfunction touchesSpan(hunk: LineHunk, startLine: number, endLine: number, endsByIndentation = false): boolean {\n return hunk.headCount === 0\n ? hunk.headStart >= startLine && (hunk.headStart < endLine || (endsByIndentation && hunk.headStart === endLine))\n : hunk.headStart <= endLine && hunk.headStart + hunk.headCount > startLine;\n}\n\n/** The cross-file groups with an occurrence in each of the given files. */\nfunction indexGroupsByFile(\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n files: ReadonlySet<string>\n): Map<string, CrossFileDuplicateBlockGroup[]> {\n const groupsByFile = new Map<string, CrossFileDuplicateBlockGroup[]>();\n for (const group of crossFileDuplication?.groups ?? []) {\n for (const file of group.files) {\n if (files.has(file)) {\n const groups = groupsByFile.get(file) ?? [];\n groups.push(group);\n groupsByFile.set(file, groups);\n }\n }\n }\n return groupsByFile;\n}\n\n/**\n * One violation per duplicated region of the file: its clone occurrences, within-file and\n * cross-file, that span at least `minLines` (with `hunks`, only those overlapping added lines),\n * with overlapping occurrences merged into one region.\n */\nfunction collectDuplicationViolations(\n file: string,\n metrics: CodeMetrics,\n crossFileGroups: readonly CrossFileDuplicateBlockGroup[],\n hunks: readonly LineHunk[] | undefined,\n minLines: number\n): Violation[] {\n const groups: BlockLocation[][] = [\n ...metrics.duplication.duplicateBlockGroups.map((group) => group.map((occurrence) => ({ ...occurrence, file }))),\n ...crossFileGroups.map((group) => group.occurrences),\n ];\n const isReported = (block: BlockLocation): boolean =>\n block.file === file &&\n duplicationThreshold.measure(block) >= minLines &&\n (!hunks || hunks.some((hunk) => hunk.headCount > 0 && touchesSpan(hunk, block.startLine, block.endLine)));\n const copiesByBlock = new Map<BlockLocation, BlockLocation[]>();\n for (const group of groups) {\n for (const block of group.filter((occurrence) => isReported(occurrence))) {\n copiesByBlock.set(block, group);\n }\n }\n return mergeOverlapping([...copiesByBlock.keys()]).map(({ merged, sources }) => ({\n kind: 'duplication',\n ...merged,\n exceeded: [\n { metric: metricNameOf(duplicationThreshold), value: duplicationThreshold.measure(merged), limit: minLines },\n ],\n // The region's own occurrences are among the copies of the groups it belongs to.\n partners: mergeOverlapping(sources.flatMap((source) => copiesByBlock.get(source) ?? []))\n .map((partner) => partner.merged)\n .filter((partner) => !overlaps(partner, merged)),\n }));\n}\n\n/** The locations with those overlapping in the same file merged, ordered by file, then line. */\nfunction mergeOverlapping(locations: readonly BlockLocation[]): { merged: BlockLocation; sources: BlockLocation[] }[] {\n const regions: { merged: BlockLocation; sources: BlockLocation[] }[] = [];\n const ordered = locations.toSorted(\n (left, right) => compareStrings(left.file, right.file) || left.startLine - right.startLine\n );\n for (const location of ordered) {\n const last = regions.at(-1);\n if (last && overlaps(last.merged, location)) {\n last.merged.endLine = Math.max(last.merged.endLine, location.endLine);\n last.sources.push(location);\n } else {\n const { file, startLine, endLine } = location;\n regions.push({ merged: { file, startLine, endLine }, sources: [location] });\n }\n }\n return regions;\n}\n\nfunction overlaps(left: BlockLocation, right: BlockLocation): boolean {\n return left.file === right.file && left.startLine <= right.endLine && right.startLine <= left.endLine;\n}\n\n/** Code-unit order, so reports do not depend on the locale. */\nfunction compareStrings(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"thresholdCheck.cjs","names":["functionThresholds","metricNameOf","fileThresholds","levels","duplicationThreshold"],"sources":["../src/thresholdCheck.ts"],"sourcesContent":["import type { CrossFileDuplicateBlockGroup, CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { LineHunk } from './git.js';\nimport {\n duplicationThreshold,\n fileThresholds,\n functionThresholds,\n levels,\n metricNameOf,\n type Level,\n type LimitsByLevel,\n type Threshold,\n} from './thresholds.js';\nimport type { CodeMetrics, FunctionMetrics } from './types.js';\n\n/** A measured file to check, under the path cross-file duplication knows it by. */\nexport interface CheckedFile {\n file: string;\n metrics: CodeMetrics;\n /** The change to the file; when set, only what the change touches is checked. */\n hunks?: readonly LineHunk[];\n}\n\nexport interface ExceededLimit {\n metric: string;\n value: number;\n /** The most severe level whose limit the value violates. */\n level: Level;\n /** That level's limit: the value is allowed up to it; a duplicated block is allowed below it. */\n limit: number;\n}\n\nexport interface BlockLocation {\n file: string;\n startLine: number;\n endLine: number;\n}\n\nexport interface Violation extends BlockLocation {\n kind: 'function' | 'file' | 'duplication';\n /** The most severe level among `exceeded`. */\n level: Level;\n /** The function's name, for kind `function`. */\n name?: string;\n exceeded: ExceededLimit[];\n /** The other copies of the block, for kind `duplication`. */\n partners?: BlockLocation[];\n}\n\nexport interface CheckResult {\n violations: Violation[];\n checkedFunctionCount: number;\n}\n\nconst violationKinds: readonly Violation['kind'][] = ['file', 'function', 'duplication'];\n\nconst nestedInclusiveMetrics = new Set(\n functionThresholds\n .filter(({ includesNestedFunctions }) => includesNestedFunctions)\n .map((threshold) => metricNameOf(threshold))\n);\n\n/** Every limit the files exceed, errors first, then ordered by path, then line. */\nexport function checkThresholds(\n files: readonly CheckedFile[],\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n limitsOf: (language: string) => LimitsByLevel\n): CheckResult {\n const crossFileGroupsByFile = indexGroupsByFile(crossFileDuplication, new Set(files.map(({ file }) => file)));\n const violations: Violation[] = [];\n let checkedFunctionCount = 0;\n for (const checkedFile of files) {\n const { file, metrics, hunks } = checkedFile;\n const limits = limitsOf(metrics.language);\n // A rename or a mode change lists the file as changed without touching any of its lines.\n const fileExceeded = hunks?.length === 0 ? [] : collectExceeded(fileThresholds, metrics, limits);\n if (fileExceeded.length > 0) {\n violations.push(toViolation({ file, startLine: 1, endLine: metrics.lines.total }, 'file', fileExceeded));\n }\n const checkedFunctions = listCheckedFunctions(checkedFile);\n checkedFunctionCount += checkedFunctions.length;\n const exceedingFunctions = checkedFunctions\n .map((fn) => ({ fn, exceeded: collectExceeded(functionThresholds, fn, limits) }))\n .filter(({ exceeded }) => exceeded.length > 0);\n for (const { fn, exceeded } of exceedingFunctions) {\n const ownExceeded = exceeded.filter((limit) => !isReportedByEnclosingFunction(limit, fn, exceedingFunctions));\n if (ownExceeded.length > 0) {\n const { startLine, endLine } = fn;\n violations.push({\n ...toViolation({ file, startLine, endLine }, 'function', ownExceeded),\n name: fn.name ?? '<anonymous>',\n });\n }\n }\n violations.push(\n ...collectDuplicationViolations(file, metrics, crossFileGroupsByFile.get(file) ?? [], hunks, limits)\n );\n }\n return { violations: violations.toSorted(compareViolations), checkedFunctionCount };\n}\n\n// The kind is compared before the end line because a file violation's end line is corrected after\n// this ordering (to the last line that exists), and the order must not depend on that correction.\nfunction compareViolations(left: Violation, right: Violation): number {\n return (\n levels.indexOf(right.level) - levels.indexOf(left.level) ||\n compareStrings(left.file, right.file) ||\n left.startLine - right.startLine ||\n violationKinds.indexOf(left.kind) - violationKinds.indexOf(right.kind) ||\n left.endLine - right.endLine\n );\n}\n\n/** The file's functions or, for a changed file, those the change touches. */\nfunction listCheckedFunctions({ metrics, hunks }: CheckedFile): FunctionMetrics[] {\n const endsByIndentation = metrics.language === 'python';\n return hunks\n ? metrics.functions.filter((fn) =>\n hunks.some((hunk) => touchesSpan(hunk, fn.startLine, fn.endLine, endsByIndentation))\n )\n : metrics.functions;\n}\n\n/**\n * Whether a function enclosing `fn` exceeds the same limit with a value that covers `fn`, so\n * reporting it for `fn` too would count the same code twice. The enclosing value is at least that\n * of `fn` and both share the file's limits, so its level is never milder.\n */\nfunction isReportedByEnclosingFunction(\n limit: ExceededLimit,\n fn: FunctionMetrics,\n exceedingFunctions: readonly { fn: FunctionMetrics; exceeded: ExceededLimit[] }[]\n): boolean {\n if (!nestedInclusiveMetrics.has(limit.metric)) {\n return false;\n }\n return exceedingFunctions.some(\n (outer) => outer.fn !== fn && encloses(outer.fn, fn) && outer.exceeded.some(({ metric }) => metric === limit.metric)\n );\n}\n\nfunction encloses(outer: FunctionMetrics, inner: FunctionMetrics): boolean {\n const startsBefore =\n outer.startLine < inner.startLine ||\n (outer.startLine === inner.startLine && outer.startColumn <= inner.startColumn);\n const endsAfter =\n outer.endLine > inner.endLine || (outer.endLine === inner.endLine && outer.endColumn >= inner.endColumn);\n return startsBefore && endsAfter;\n}\n\nfunction toViolation(location: BlockLocation, kind: Violation['kind'], exceeded: ExceededLimit[]): Violation {\n const level = exceeded.some((limit) => limit.level === 'error') ? 'error' : 'warning';\n return { kind, level, ...location, exceeded };\n}\n\nfunction collectExceeded<Subject>(\n thresholds: readonly Threshold<Subject>[],\n subject: Subject,\n limits: LimitsByLevel\n): ExceededLimit[] {\n return thresholds.flatMap((threshold) => findExceeded(threshold, subject, limits, (value, limit) => value > limit));\n}\n\n/** The most severe violated limit of the threshold, if any. */\nfunction findExceeded<Subject>(\n threshold: Threshold<Subject>,\n subject: Subject,\n limits: LimitsByLevel,\n violates: (value: number, limit: number) => boolean\n): ExceededLimit[] {\n const value = threshold.measure(subject);\n const level = levels.findLast((candidate) => violates(value, limits[candidate][threshold.key] as number));\n return level\n ? [{ metric: metricNameOf(threshold), value, level, limit: limits[level][threshold.key] as number }]\n : [];\n}\n\n/**\n * Whether the hunk changes the head lines [startLine, endLine]. A hunk without head lines (a pure\n * deletion) sits between its headStart line and the next one. Where indentation ends a span, its\n * last line is a statement rather than a closing delimiter, so a deletion right after that line may\n * have removed the span's tail; `endsByIndentation` counts it as touching, since the head alone\n * cannot tell it from a deletion after the span.\n */\nfunction touchesSpan(hunk: LineHunk, startLine: number, endLine: number, endsByIndentation = false): boolean {\n return hunk.headCount === 0\n ? hunk.headStart >= startLine && (hunk.headStart < endLine || (endsByIndentation && hunk.headStart === endLine))\n : hunk.headStart <= endLine && hunk.headStart + hunk.headCount > startLine;\n}\n\n/** The cross-file groups with an occurrence in each of the given files. */\nfunction indexGroupsByFile(\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n files: ReadonlySet<string>\n): Map<string, CrossFileDuplicateBlockGroup[]> {\n const groupsByFile = new Map<string, CrossFileDuplicateBlockGroup[]>();\n for (const group of crossFileDuplication?.groups ?? []) {\n for (const file of group.files) {\n if (files.has(file)) {\n const groups = groupsByFile.get(file) ?? [];\n groups.push(group);\n groupsByFile.set(file, groups);\n }\n }\n }\n return groupsByFile;\n}\n\n/**\n * One violation per duplicated region of the file: its clone occurrences, within-file and\n * cross-file, that span at least the line count of a level (with `hunks`, only those overlapping\n * added lines), with overlapping occurrences merged into one region.\n */\nfunction collectDuplicationViolations(\n file: string,\n metrics: CodeMetrics,\n crossFileGroups: readonly CrossFileDuplicateBlockGroup[],\n hunks: readonly LineHunk[] | undefined,\n limits: LimitsByLevel\n): Violation[] {\n const findExceededLines = (block: BlockLocation): ExceededLimit[] =>\n findExceeded(duplicationThreshold, block, limits, (value, limit) => value >= limit);\n const groups: BlockLocation[][] = [\n ...metrics.duplication.duplicateBlockGroups.map((group) => group.map((occurrence) => ({ ...occurrence, file }))),\n ...crossFileGroups.map((group) => group.occurrences),\n ];\n const isReported = (block: BlockLocation): boolean =>\n block.file === file &&\n findExceededLines(block).length > 0 &&\n (!hunks || hunks.some((hunk) => hunk.headCount > 0 && touchesSpan(hunk, block.startLine, block.endLine)));\n const copiesByBlock = new Map<BlockLocation, BlockLocation[]>();\n for (const group of groups) {\n for (const block of group.filter((occurrence) => isReported(occurrence))) {\n copiesByBlock.set(block, group);\n }\n }\n return mergeOverlapping([...copiesByBlock.keys()]).map(({ merged, sources }) => ({\n ...toViolation(merged, 'duplication', findExceededLines(merged)),\n // The region's own occurrences are among the copies of the groups it belongs to.\n partners: mergeOverlapping(sources.flatMap((source) => copiesByBlock.get(source) ?? []))\n .map((partner) => partner.merged)\n .filter((partner) => !overlaps(partner, merged)),\n }));\n}\n\n/** The locations with those overlapping in the same file merged, ordered by file, then line. */\nfunction mergeOverlapping(locations: readonly BlockLocation[]): { merged: BlockLocation; sources: BlockLocation[] }[] {\n const regions: { merged: BlockLocation; sources: BlockLocation[] }[] = [];\n const ordered = locations.toSorted(\n (left, right) => compareStrings(left.file, right.file) || left.startLine - right.startLine\n );\n for (const location of ordered) {\n const last = regions.at(-1);\n if (last && overlaps(last.merged, location)) {\n last.merged.endLine = Math.max(last.merged.endLine, location.endLine);\n last.sources.push(location);\n } else {\n const { file, startLine, endLine } = location;\n regions.push({ merged: { file, startLine, endLine }, sources: [location] });\n }\n }\n return regions;\n}\n\nfunction overlaps(left: BlockLocation, right: BlockLocation): boolean {\n return left.file === right.file && left.startLine <= right.endLine && right.startLine <= left.endLine;\n}\n\n/** Code-unit order, so reports do not depend on the locale. */\nfunction compareStrings(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import type { CrossFileDuplicationMetrics } from './crossFileDuplication.js';
2
2
  import type { LineHunk } from './git.js';
3
- import { type Limits } from './thresholds.js';
3
+ import { type Level, type LimitsByLevel } from './thresholds.js';
4
4
  import type { CodeMetrics } from './types.js';
5
5
  /** A measured file to check, under the path cross-file duplication knows it by. */
6
6
  export interface CheckedFile {
@@ -12,7 +12,9 @@ export interface CheckedFile {
12
12
  export interface ExceededLimit {
13
13
  metric: string;
14
14
  value: number;
15
- /** The value is allowed up to this limit; a duplicated block is allowed below it. */
15
+ /** The most severe level whose limit the value violates. */
16
+ level: Level;
17
+ /** That level's limit: the value is allowed up to it; a duplicated block is allowed below it. */
16
18
  limit: number;
17
19
  }
18
20
  export interface BlockLocation {
@@ -22,6 +24,8 @@ export interface BlockLocation {
22
24
  }
23
25
  export interface Violation extends BlockLocation {
24
26
  kind: 'function' | 'file' | 'duplication';
27
+ /** The most severe level among `exceeded`. */
28
+ level: Level;
25
29
  /** The function's name, for kind `function`. */
26
30
  name?: string;
27
31
  exceeded: ExceededLimit[];
@@ -32,5 +36,5 @@ export interface CheckResult {
32
36
  violations: Violation[];
33
37
  checkedFunctionCount: number;
34
38
  }
35
- /** Every limit the files exceed, ordered by path, then line. */
36
- export declare function checkThresholds(files: readonly CheckedFile[], crossFileDuplication: CrossFileDuplicationMetrics | undefined, limitsOf: (language: string) => Limits): CheckResult;
39
+ /** Every limit the files exceed, errors first, then ordered by path, then line. */
40
+ export declare function checkThresholds(files: readonly CheckedFile[], crossFileDuplication: CrossFileDuplicationMetrics | undefined, limitsOf: (language: string) => LimitsByLevel): CheckResult;
@@ -1,2 +1,2 @@
1
- import{duplicationThreshold as e,fileThresholds as t,functionThresholds as n,metricNameOf as r}from"./thresholds.js";const i=[`file`,`function`,`duplication`];function a(r,i,a){let l=u(i,new Set(r.map(({file:e})=>e))),f=[],p=0;for(let i of r){let{file:r,metrics:o,hunks:u}=i,m=a(o.language),h=u?.length===0?[]:c(t,o,m);h.length>0&&f.push({kind:`file`,file:r,startLine:1,endLine:o.lines.total,exceeded:h});let g=s(i);p+=g.length;for(let e of g){let t=c(n,e,m);if(t.length>0){let{startLine:n,endLine:i}=e;f.push({kind:`function`,file:r,startLine:n,endLine:i,name:e.name??`<anonymous>`,exceeded:t})}}f.push(...d(r,o,l.get(r)??[],u,m[e.key]))}return{violations:f.toSorted(o),checkedFunctionCount:p}}function o(e,t){return m(e.file,t.file)||e.startLine-t.startLine||i.indexOf(e.kind)-i.indexOf(t.kind)||e.endLine-t.endLine}function s({metrics:e,hunks:t}){let n=e.language===`python`;return t?e.functions.filter(e=>t.some(t=>l(t,e.startLine,e.endLine,n))):e.functions}function c(e,t,n){return e.flatMap(e=>{let i=e.measure(t),a=n[e.key];return i>a?[{metric:r(e),value:i,limit:a}]:[]})}function l(e,t,n,r=!1){return e.headCount===0?e.headStart>=t&&(e.headStart<n||r&&e.headStart===n):e.headStart<=n&&e.headStart+e.headCount>t}function u(e,t){let n=/* @__PURE__ */ new Map;for(let r of e?.groups??[])for(let e of r.files)if(t.has(e)){let t=n.get(e)??[];t.push(r),n.set(e,t)}return n}function d(t,n,i,a,o){let s=[...n.duplication.duplicateBlockGroups.map(e=>e.map(e=>({...e,file:t}))),...i.map(e=>e.occurrences)],c=n=>n.file===t&&e.measure(n)>=o&&(!a||a.some(e=>e.headCount>0&&l(e,n.startLine,n.endLine))),u=/* @__PURE__ */ new Map;for(let e of s)for(let t of e.filter(e=>c(e)))u.set(t,e);return f([...u.keys()]).map(({merged:t,sources:n})=>({kind:`duplication`,...t,exceeded:[{metric:r(e),value:e.measure(t),limit:o}],partners:f(n.flatMap(e=>u.get(e)??[])).map(e=>e.merged).filter(e=>!p(e,t))}))}function f(e){let t=[],n=e.toSorted((e,t)=>m(e.file,t.file)||e.startLine-t.startLine);for(let e of n){let n=t.at(-1);if(n&&p(n.merged,e))n.merged.endLine=Math.max(n.merged.endLine,e.endLine),n.sources.push(e);else{let{file:n,startLine:r,endLine:i}=e;t.push({merged:{file:n,startLine:r,endLine:i},sources:[e]})}}return t}function p(e,t){return e.file===t.file&&e.startLine<=t.endLine&&t.startLine<=e.endLine}function m(e,t){return e<t?-1:+(e>t)}export{a as checkThresholds};
1
+ import{duplicationThreshold as e,fileThresholds as t,functionThresholds as n,levels as r,metricNameOf as i}from"./thresholds.js";const a=[`file`,`function`,`duplication`],o=new Set(n.filter(({includesNestedFunctions:e})=>e).map(e=>i(e)));function s(e,r,i){let a=g(r,new Set(e.map(({file:e})=>e))),o=[],s=0;for(let r of e){let{file:e,metrics:c,hunks:d}=r,m=i(c.language),h=d?.length===0?[]:p(t,c,m);h.length>0&&o.push(f({file:e,startLine:1,endLine:c.lines.total},`file`,h));let g=l(r);s+=g.length;let v=g.map(e=>({fn:e,exceeded:p(n,e,m)})).filter(({exceeded:e})=>e.length>0);for(let{fn:t,exceeded:n}of v){let r=n.filter(e=>!u(e,t,v));if(r.length>0){let{startLine:n,endLine:i}=t;o.push({...f({file:e,startLine:n,endLine:i},`function`,r),name:t.name??`<anonymous>`})}}o.push(..._(e,c,a.get(e)??[],d,m))}return{violations:o.toSorted(c),checkedFunctionCount:s}}function c(e,t){return r.indexOf(t.level)-r.indexOf(e.level)||b(e.file,t.file)||e.startLine-t.startLine||a.indexOf(e.kind)-a.indexOf(t.kind)||e.endLine-t.endLine}function l({metrics:e,hunks:t}){let n=e.language===`python`;return t?e.functions.filter(e=>t.some(t=>h(t,e.startLine,e.endLine,n))):e.functions}function u(e,t,n){return o.has(e.metric)?n.some(n=>n.fn!==t&&d(n.fn,t)&&n.exceeded.some(({metric:t})=>t===e.metric)):!1}function d(e,t){let n=e.startLine<t.startLine||e.startLine===t.startLine&&e.startColumn<=t.startColumn,r=e.endLine>t.endLine||e.endLine===t.endLine&&e.endColumn>=t.endColumn;return n&&r}function f(e,t,n){return{kind:t,level:n.some(e=>e.level===`error`)?`error`:`warning`,...e,exceeded:n}}function p(e,t,n){return e.flatMap(e=>m(e,t,n,(e,t)=>e>t))}function m(e,t,n,a){let o=e.measure(t),s=r.findLast(t=>a(o,n[t][e.key]));return s?[{metric:i(e),value:o,level:s,limit:n[s][e.key]}]:[]}function h(e,t,n,r=!1){return e.headCount===0?e.headStart>=t&&(e.headStart<n||r&&e.headStart===n):e.headStart<=n&&e.headStart+e.headCount>t}function g(e,t){let n=/* @__PURE__ */ new Map;for(let r of e?.groups??[])for(let e of r.files)if(t.has(e)){let t=n.get(e)??[];t.push(r),n.set(e,t)}return n}function _(t,n,r,i,a){let o=t=>m(e,t,a,(e,t)=>e>=t),s=[...n.duplication.duplicateBlockGroups.map(e=>e.map(e=>({...e,file:t}))),...r.map(e=>e.occurrences)],c=e=>e.file===t&&o(e).length>0&&(!i||i.some(t=>t.headCount>0&&h(t,e.startLine,e.endLine))),l=/* @__PURE__ */ new Map;for(let e of s)for(let t of e.filter(e=>c(e)))l.set(t,e);return v([...l.keys()]).map(({merged:e,sources:t})=>({...f(e,`duplication`,o(e)),partners:v(t.flatMap(e=>l.get(e)??[])).map(e=>e.merged).filter(t=>!y(t,e))}))}function v(e){let t=[],n=e.toSorted((e,t)=>b(e.file,t.file)||e.startLine-t.startLine);for(let e of n){let n=t.at(-1);if(n&&y(n.merged,e))n.merged.endLine=Math.max(n.merged.endLine,e.endLine),n.sources.push(e);else{let{file:n,startLine:r,endLine:i}=e;t.push({merged:{file:n,startLine:r,endLine:i},sources:[e]})}}return t}function y(e,t){return e.file===t.file&&e.startLine<=t.endLine&&t.startLine<=e.endLine}function b(e,t){return e<t?-1:+(e>t)}export{s as checkThresholds};
2
2
  //# sourceMappingURL=thresholdCheck.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"thresholdCheck.js","names":[],"sources":["../src/thresholdCheck.ts"],"sourcesContent":["import type { CrossFileDuplicateBlockGroup, CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { LineHunk } from './git.js';\nimport {\n duplicationThreshold,\n fileThresholds,\n functionThresholds,\n metricNameOf,\n type Limits,\n type Threshold,\n} from './thresholds.js';\nimport type { CodeMetrics, FunctionMetrics } from './types.js';\n\n/** A measured file to check, under the path cross-file duplication knows it by. */\nexport interface CheckedFile {\n file: string;\n metrics: CodeMetrics;\n /** The change to the file; when set, only what the change touches is checked. */\n hunks?: readonly LineHunk[];\n}\n\nexport interface ExceededLimit {\n metric: string;\n value: number;\n /** The value is allowed up to this limit; a duplicated block is allowed below it. */\n limit: number;\n}\n\nexport interface BlockLocation {\n file: string;\n startLine: number;\n endLine: number;\n}\n\nexport interface Violation extends BlockLocation {\n kind: 'function' | 'file' | 'duplication';\n /** The function's name, for kind `function`. */\n name?: string;\n exceeded: ExceededLimit[];\n /** The other copies of the block, for kind `duplication`. */\n partners?: BlockLocation[];\n}\n\nexport interface CheckResult {\n violations: Violation[];\n checkedFunctionCount: number;\n}\n\nconst violationKinds: readonly Violation['kind'][] = ['file', 'function', 'duplication'];\n\n/** Every limit the files exceed, ordered by path, then line. */\nexport function checkThresholds(\n files: readonly CheckedFile[],\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n limitsOf: (language: string) => Limits\n): CheckResult {\n const crossFileGroupsByFile = indexGroupsByFile(crossFileDuplication, new Set(files.map(({ file }) => file)));\n const violations: Violation[] = [];\n let checkedFunctionCount = 0;\n for (const checkedFile of files) {\n const { file, metrics, hunks } = checkedFile;\n const limits = limitsOf(metrics.language);\n // A rename or a mode change lists the file as changed without touching any of its lines.\n const fileExceeded = hunks?.length === 0 ? [] : collectExceeded(fileThresholds, metrics, limits);\n if (fileExceeded.length > 0) {\n violations.push({ kind: 'file', file, startLine: 1, endLine: metrics.lines.total, exceeded: fileExceeded });\n }\n const checkedFunctions = listCheckedFunctions(checkedFile);\n checkedFunctionCount += checkedFunctions.length;\n for (const fn of checkedFunctions) {\n const exceeded = collectExceeded(functionThresholds, fn, limits);\n if (exceeded.length > 0) {\n const { startLine, endLine } = fn;\n violations.push({ kind: 'function', file, startLine, endLine, name: fn.name ?? '<anonymous>', exceeded });\n }\n }\n violations.push(\n ...collectDuplicationViolations(\n file,\n metrics,\n crossFileGroupsByFile.get(file) ?? [],\n hunks,\n limits[duplicationThreshold.key] as number\n )\n );\n }\n return { violations: violations.toSorted(compareViolations), checkedFunctionCount };\n}\n\n// The kind is compared before the end line because a file violation's end line is corrected after\n// this ordering (to the last line that exists), and the order must not depend on that correction.\nfunction compareViolations(left: Violation, right: Violation): number {\n return (\n compareStrings(left.file, right.file) ||\n left.startLine - right.startLine ||\n violationKinds.indexOf(left.kind) - violationKinds.indexOf(right.kind) ||\n left.endLine - right.endLine\n );\n}\n\n/** The file's functions or, for a changed file, those the change touches. */\nfunction listCheckedFunctions({ metrics, hunks }: CheckedFile): FunctionMetrics[] {\n const endsByIndentation = metrics.language === 'python';\n return hunks\n ? metrics.functions.filter((fn) =>\n hunks.some((hunk) => touchesSpan(hunk, fn.startLine, fn.endLine, endsByIndentation))\n )\n : metrics.functions;\n}\n\nfunction collectExceeded<Subject>(\n thresholds: readonly Threshold<Subject>[],\n subject: Subject,\n limits: Limits\n): ExceededLimit[] {\n return thresholds.flatMap((threshold) => {\n const value = threshold.measure(subject);\n const limit = limits[threshold.key] as number;\n return value > limit ? [{ metric: metricNameOf(threshold), value, limit }] : [];\n });\n}\n\n/**\n * Whether the hunk changes the head lines [startLine, endLine]. A hunk without head lines (a pure\n * deletion) sits between its headStart line and the next one. Where indentation ends a span, its\n * last line is a statement rather than a closing delimiter, so a deletion right after that line may\n * have removed the span's tail; `endsByIndentation` counts it as touching, since the head alone\n * cannot tell it from a deletion after the span.\n */\nfunction touchesSpan(hunk: LineHunk, startLine: number, endLine: number, endsByIndentation = false): boolean {\n return hunk.headCount === 0\n ? hunk.headStart >= startLine && (hunk.headStart < endLine || (endsByIndentation && hunk.headStart === endLine))\n : hunk.headStart <= endLine && hunk.headStart + hunk.headCount > startLine;\n}\n\n/** The cross-file groups with an occurrence in each of the given files. */\nfunction indexGroupsByFile(\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n files: ReadonlySet<string>\n): Map<string, CrossFileDuplicateBlockGroup[]> {\n const groupsByFile = new Map<string, CrossFileDuplicateBlockGroup[]>();\n for (const group of crossFileDuplication?.groups ?? []) {\n for (const file of group.files) {\n if (files.has(file)) {\n const groups = groupsByFile.get(file) ?? [];\n groups.push(group);\n groupsByFile.set(file, groups);\n }\n }\n }\n return groupsByFile;\n}\n\n/**\n * One violation per duplicated region of the file: its clone occurrences, within-file and\n * cross-file, that span at least `minLines` (with `hunks`, only those overlapping added lines),\n * with overlapping occurrences merged into one region.\n */\nfunction collectDuplicationViolations(\n file: string,\n metrics: CodeMetrics,\n crossFileGroups: readonly CrossFileDuplicateBlockGroup[],\n hunks: readonly LineHunk[] | undefined,\n minLines: number\n): Violation[] {\n const groups: BlockLocation[][] = [\n ...metrics.duplication.duplicateBlockGroups.map((group) => group.map((occurrence) => ({ ...occurrence, file }))),\n ...crossFileGroups.map((group) => group.occurrences),\n ];\n const isReported = (block: BlockLocation): boolean =>\n block.file === file &&\n duplicationThreshold.measure(block) >= minLines &&\n (!hunks || hunks.some((hunk) => hunk.headCount > 0 && touchesSpan(hunk, block.startLine, block.endLine)));\n const copiesByBlock = new Map<BlockLocation, BlockLocation[]>();\n for (const group of groups) {\n for (const block of group.filter((occurrence) => isReported(occurrence))) {\n copiesByBlock.set(block, group);\n }\n }\n return mergeOverlapping([...copiesByBlock.keys()]).map(({ merged, sources }) => ({\n kind: 'duplication',\n ...merged,\n exceeded: [\n { metric: metricNameOf(duplicationThreshold), value: duplicationThreshold.measure(merged), limit: minLines },\n ],\n // The region's own occurrences are among the copies of the groups it belongs to.\n partners: mergeOverlapping(sources.flatMap((source) => copiesByBlock.get(source) ?? []))\n .map((partner) => partner.merged)\n .filter((partner) => !overlaps(partner, merged)),\n }));\n}\n\n/** The locations with those overlapping in the same file merged, ordered by file, then line. */\nfunction mergeOverlapping(locations: readonly BlockLocation[]): { merged: BlockLocation; sources: BlockLocation[] }[] {\n const regions: { merged: BlockLocation; sources: BlockLocation[] }[] = [];\n const ordered = locations.toSorted(\n (left, right) => compareStrings(left.file, right.file) || left.startLine - right.startLine\n );\n for (const location of ordered) {\n const last = regions.at(-1);\n if (last && overlaps(last.merged, location)) {\n last.merged.endLine = Math.max(last.merged.endLine, location.endLine);\n last.sources.push(location);\n } else {\n const { file, startLine, endLine } = location;\n regions.push({ merged: { file, startLine, endLine }, sources: [location] });\n }\n }\n return regions;\n}\n\nfunction overlaps(left: BlockLocation, right: BlockLocation): boolean {\n return left.file === right.file && left.startLine <= right.endLine && right.startLine <= left.endLine;\n}\n\n/** Code-unit order, so reports do not depend on the locale. */\nfunction compareStrings(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"thresholdCheck.js","names":[],"sources":["../src/thresholdCheck.ts"],"sourcesContent":["import type { CrossFileDuplicateBlockGroup, CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { LineHunk } from './git.js';\nimport {\n duplicationThreshold,\n fileThresholds,\n functionThresholds,\n levels,\n metricNameOf,\n type Level,\n type LimitsByLevel,\n type Threshold,\n} from './thresholds.js';\nimport type { CodeMetrics, FunctionMetrics } from './types.js';\n\n/** A measured file to check, under the path cross-file duplication knows it by. */\nexport interface CheckedFile {\n file: string;\n metrics: CodeMetrics;\n /** The change to the file; when set, only what the change touches is checked. */\n hunks?: readonly LineHunk[];\n}\n\nexport interface ExceededLimit {\n metric: string;\n value: number;\n /** The most severe level whose limit the value violates. */\n level: Level;\n /** That level's limit: the value is allowed up to it; a duplicated block is allowed below it. */\n limit: number;\n}\n\nexport interface BlockLocation {\n file: string;\n startLine: number;\n endLine: number;\n}\n\nexport interface Violation extends BlockLocation {\n kind: 'function' | 'file' | 'duplication';\n /** The most severe level among `exceeded`. */\n level: Level;\n /** The function's name, for kind `function`. */\n name?: string;\n exceeded: ExceededLimit[];\n /** The other copies of the block, for kind `duplication`. */\n partners?: BlockLocation[];\n}\n\nexport interface CheckResult {\n violations: Violation[];\n checkedFunctionCount: number;\n}\n\nconst violationKinds: readonly Violation['kind'][] = ['file', 'function', 'duplication'];\n\nconst nestedInclusiveMetrics = new Set(\n functionThresholds\n .filter(({ includesNestedFunctions }) => includesNestedFunctions)\n .map((threshold) => metricNameOf(threshold))\n);\n\n/** Every limit the files exceed, errors first, then ordered by path, then line. */\nexport function checkThresholds(\n files: readonly CheckedFile[],\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n limitsOf: (language: string) => LimitsByLevel\n): CheckResult {\n const crossFileGroupsByFile = indexGroupsByFile(crossFileDuplication, new Set(files.map(({ file }) => file)));\n const violations: Violation[] = [];\n let checkedFunctionCount = 0;\n for (const checkedFile of files) {\n const { file, metrics, hunks } = checkedFile;\n const limits = limitsOf(metrics.language);\n // A rename or a mode change lists the file as changed without touching any of its lines.\n const fileExceeded = hunks?.length === 0 ? [] : collectExceeded(fileThresholds, metrics, limits);\n if (fileExceeded.length > 0) {\n violations.push(toViolation({ file, startLine: 1, endLine: metrics.lines.total }, 'file', fileExceeded));\n }\n const checkedFunctions = listCheckedFunctions(checkedFile);\n checkedFunctionCount += checkedFunctions.length;\n const exceedingFunctions = checkedFunctions\n .map((fn) => ({ fn, exceeded: collectExceeded(functionThresholds, fn, limits) }))\n .filter(({ exceeded }) => exceeded.length > 0);\n for (const { fn, exceeded } of exceedingFunctions) {\n const ownExceeded = exceeded.filter((limit) => !isReportedByEnclosingFunction(limit, fn, exceedingFunctions));\n if (ownExceeded.length > 0) {\n const { startLine, endLine } = fn;\n violations.push({\n ...toViolation({ file, startLine, endLine }, 'function', ownExceeded),\n name: fn.name ?? '<anonymous>',\n });\n }\n }\n violations.push(\n ...collectDuplicationViolations(file, metrics, crossFileGroupsByFile.get(file) ?? [], hunks, limits)\n );\n }\n return { violations: violations.toSorted(compareViolations), checkedFunctionCount };\n}\n\n// The kind is compared before the end line because a file violation's end line is corrected after\n// this ordering (to the last line that exists), and the order must not depend on that correction.\nfunction compareViolations(left: Violation, right: Violation): number {\n return (\n levels.indexOf(right.level) - levels.indexOf(left.level) ||\n compareStrings(left.file, right.file) ||\n left.startLine - right.startLine ||\n violationKinds.indexOf(left.kind) - violationKinds.indexOf(right.kind) ||\n left.endLine - right.endLine\n );\n}\n\n/** The file's functions or, for a changed file, those the change touches. */\nfunction listCheckedFunctions({ metrics, hunks }: CheckedFile): FunctionMetrics[] {\n const endsByIndentation = metrics.language === 'python';\n return hunks\n ? metrics.functions.filter((fn) =>\n hunks.some((hunk) => touchesSpan(hunk, fn.startLine, fn.endLine, endsByIndentation))\n )\n : metrics.functions;\n}\n\n/**\n * Whether a function enclosing `fn` exceeds the same limit with a value that covers `fn`, so\n * reporting it for `fn` too would count the same code twice. The enclosing value is at least that\n * of `fn` and both share the file's limits, so its level is never milder.\n */\nfunction isReportedByEnclosingFunction(\n limit: ExceededLimit,\n fn: FunctionMetrics,\n exceedingFunctions: readonly { fn: FunctionMetrics; exceeded: ExceededLimit[] }[]\n): boolean {\n if (!nestedInclusiveMetrics.has(limit.metric)) {\n return false;\n }\n return exceedingFunctions.some(\n (outer) => outer.fn !== fn && encloses(outer.fn, fn) && outer.exceeded.some(({ metric }) => metric === limit.metric)\n );\n}\n\nfunction encloses(outer: FunctionMetrics, inner: FunctionMetrics): boolean {\n const startsBefore =\n outer.startLine < inner.startLine ||\n (outer.startLine === inner.startLine && outer.startColumn <= inner.startColumn);\n const endsAfter =\n outer.endLine > inner.endLine || (outer.endLine === inner.endLine && outer.endColumn >= inner.endColumn);\n return startsBefore && endsAfter;\n}\n\nfunction toViolation(location: BlockLocation, kind: Violation['kind'], exceeded: ExceededLimit[]): Violation {\n const level = exceeded.some((limit) => limit.level === 'error') ? 'error' : 'warning';\n return { kind, level, ...location, exceeded };\n}\n\nfunction collectExceeded<Subject>(\n thresholds: readonly Threshold<Subject>[],\n subject: Subject,\n limits: LimitsByLevel\n): ExceededLimit[] {\n return thresholds.flatMap((threshold) => findExceeded(threshold, subject, limits, (value, limit) => value > limit));\n}\n\n/** The most severe violated limit of the threshold, if any. */\nfunction findExceeded<Subject>(\n threshold: Threshold<Subject>,\n subject: Subject,\n limits: LimitsByLevel,\n violates: (value: number, limit: number) => boolean\n): ExceededLimit[] {\n const value = threshold.measure(subject);\n const level = levels.findLast((candidate) => violates(value, limits[candidate][threshold.key] as number));\n return level\n ? [{ metric: metricNameOf(threshold), value, level, limit: limits[level][threshold.key] as number }]\n : [];\n}\n\n/**\n * Whether the hunk changes the head lines [startLine, endLine]. A hunk without head lines (a pure\n * deletion) sits between its headStart line and the next one. Where indentation ends a span, its\n * last line is a statement rather than a closing delimiter, so a deletion right after that line may\n * have removed the span's tail; `endsByIndentation` counts it as touching, since the head alone\n * cannot tell it from a deletion after the span.\n */\nfunction touchesSpan(hunk: LineHunk, startLine: number, endLine: number, endsByIndentation = false): boolean {\n return hunk.headCount === 0\n ? hunk.headStart >= startLine && (hunk.headStart < endLine || (endsByIndentation && hunk.headStart === endLine))\n : hunk.headStart <= endLine && hunk.headStart + hunk.headCount > startLine;\n}\n\n/** The cross-file groups with an occurrence in each of the given files. */\nfunction indexGroupsByFile(\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n files: ReadonlySet<string>\n): Map<string, CrossFileDuplicateBlockGroup[]> {\n const groupsByFile = new Map<string, CrossFileDuplicateBlockGroup[]>();\n for (const group of crossFileDuplication?.groups ?? []) {\n for (const file of group.files) {\n if (files.has(file)) {\n const groups = groupsByFile.get(file) ?? [];\n groups.push(group);\n groupsByFile.set(file, groups);\n }\n }\n }\n return groupsByFile;\n}\n\n/**\n * One violation per duplicated region of the file: its clone occurrences, within-file and\n * cross-file, that span at least the line count of a level (with `hunks`, only those overlapping\n * added lines), with overlapping occurrences merged into one region.\n */\nfunction collectDuplicationViolations(\n file: string,\n metrics: CodeMetrics,\n crossFileGroups: readonly CrossFileDuplicateBlockGroup[],\n hunks: readonly LineHunk[] | undefined,\n limits: LimitsByLevel\n): Violation[] {\n const findExceededLines = (block: BlockLocation): ExceededLimit[] =>\n findExceeded(duplicationThreshold, block, limits, (value, limit) => value >= limit);\n const groups: BlockLocation[][] = [\n ...metrics.duplication.duplicateBlockGroups.map((group) => group.map((occurrence) => ({ ...occurrence, file }))),\n ...crossFileGroups.map((group) => group.occurrences),\n ];\n const isReported = (block: BlockLocation): boolean =>\n block.file === file &&\n findExceededLines(block).length > 0 &&\n (!hunks || hunks.some((hunk) => hunk.headCount > 0 && touchesSpan(hunk, block.startLine, block.endLine)));\n const copiesByBlock = new Map<BlockLocation, BlockLocation[]>();\n for (const group of groups) {\n for (const block of group.filter((occurrence) => isReported(occurrence))) {\n copiesByBlock.set(block, group);\n }\n }\n return mergeOverlapping([...copiesByBlock.keys()]).map(({ merged, sources }) => ({\n ...toViolation(merged, 'duplication', findExceededLines(merged)),\n // The region's own occurrences are among the copies of the groups it belongs to.\n partners: mergeOverlapping(sources.flatMap((source) => copiesByBlock.get(source) ?? []))\n .map((partner) => partner.merged)\n .filter((partner) => !overlaps(partner, merged)),\n }));\n}\n\n/** The locations with those overlapping in the same file merged, ordered by file, then line. */\nfunction mergeOverlapping(locations: readonly BlockLocation[]): { merged: BlockLocation; sources: BlockLocation[] }[] {\n const regions: { merged: BlockLocation; sources: BlockLocation[] }[] = [];\n const ordered = locations.toSorted(\n (left, right) => compareStrings(left.file, right.file) || left.startLine - right.startLine\n );\n for (const location of ordered) {\n const last = regions.at(-1);\n if (last && overlaps(last.merged, location)) {\n last.merged.endLine = Math.max(last.merged.endLine, location.endLine);\n last.sources.push(location);\n } else {\n const { file, startLine, endLine } = location;\n regions.push({ merged: { file, startLine, endLine }, sources: [location] });\n }\n }\n return regions;\n}\n\nfunction overlaps(left: BlockLocation, right: BlockLocation): boolean {\n return left.file === right.file && left.startLine <= right.endLine && right.startLine <= left.endLine;\n}\n\n/** Code-unit order, so reports do not depend on the locale. */\nfunction compareStrings(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use strict";const e=[{key:`maxFunctionCognitiveComplexity`,label:`cognitive complexity`,defaultLimit:15,hint:`flatten nested branching with early returns and extract nested blocks into named functions.`,measure:e=>e.cognitiveComplexity},{key:`maxFunctionCyclomaticComplexity`,label:`cyclomatic complexity`,defaultLimit:10,hint:`the function has too many independent paths; split it by decision or replace condition chains with a lookup table.`,measure:e=>e.cyclomaticComplexity},{key:`maxFunctionNcss`,label:`NCSS`,defaultLimit:60,hint:`split the function into smaller functions that each do one step.`,measure:e=>e.ncss},{key:`maxFunctionNestingDepth`,label:`nesting depth`,defaultLimit:4,hint:`replace nested conditions with guard clauses or move inner blocks into functions.`,measure:e=>e.nestingDepth},{key:`maxFunctionParameterCount`,label:`parameters`,defaultLimit:7,hint:`group related parameters into one object or split the function.`,measure:e=>e.parameterCount},{key:`maxFunctionHalsteadVolume`,label:`Halstead volume`,defaultLimit:2e3,hint:`the function holds too many operators and operands; move cohesive parts into helper functions.`,measure:e=>e.halstead.volume},{key:`maxFunctionHalsteadDifficulty`,label:`Halstead difficulty`,defaultLimit:20,hint:`the function reuses the same operands many times; split it so each part handles fewer values.`,measure:e=>e.halstead.difficulty},{key:`maxFunctionHalsteadEffort`,label:`Halstead effort`,defaultLimit:3e4,hint:`the function is both large and dense; split it into smaller functions with fewer values each.`,measure:e=>e.halstead.effort},{key:`maxFunctionDepDegree`,label:`DepDegree`,defaultLimit:50,hint:`too many values flow between the variables of the function; split it so each part works on fewer variables.`,measure:e=>e.depDegree}],t=[{key:`maxFileNcss`,label:`file NCSS`,defaultLimit:500,hint:`split the file into smaller modules with one responsibility each.`,measure:e=>e.ncssCount}],n={key:`minDuplicateLines`,label:`duplicated lines`,defaultLimit:10,hint:`extract the repeated code into one shared function or module and call it from every location.`,measure:e=>e.endLine-e.startLine+1},r=[...e,...t,n];function i(e){let t=e.key.slice(3);return t.charAt(0).toLowerCase()+t.slice(1)}function a(e,t){let n={...Object.fromEntries(r.map(({key:e,defaultLimit:t})=>[e,t])),...t?.limits};return r=>({...n,...t&&Object.hasOwn(t.languages,r)?t.languages[r]:void 0,...e})}exports.duplicationThreshold=n,exports.fileThresholds=t,exports.functionThresholds=e,exports.metricNameOf=i,exports.resolveLimits=a,exports.thresholds=r;
1
+ "use strict";const e=[`warning`,`error`],t=[{key:`maxFunctionCognitiveComplexity`,label:`cognitive complexity`,defaultLimits:{warning:15,error:30},hint:`flatten nested branching with early returns and extract nested blocks into named functions.`,measure:e=>e.cognitiveComplexity,includesNestedFunctions:!0},{key:`maxFunctionCyclomaticComplexity`,label:`cyclomatic complexity`,defaultLimits:{warning:1/0,error:1/0},hint:`the function has too many independent paths; split it by decision or replace condition chains with a lookup table.`,measure:e=>e.cyclomaticComplexity},{key:`maxFunctionNcss`,label:`NCSS`,defaultLimits:{warning:60,error:100},hint:`split the function into smaller functions that each do one step.`,measure:e=>e.ncss,includesNestedFunctions:!0},{key:`maxFunctionNestingDepth`,label:`nesting depth`,defaultLimits:{warning:4,error:5},hint:`replace nested conditions with guard clauses or move inner blocks into functions.`,measure:e=>e.nestingDepth},{key:`maxFunctionParameterCount`,label:`parameters`,defaultLimits:{warning:7,error:1/0},hint:`group related parameters into one object or split the function.`,measure:e=>e.parameterCount},{key:`maxFunctionHalsteadVolume`,label:`Halstead volume`,defaultLimits:{warning:1/0,error:1/0},hint:`the function holds too many operators and operands; move cohesive parts into helper functions.`,measure:e=>e.halstead.volume,includesNestedFunctions:!0},{key:`maxFunctionHalsteadDifficulty`,label:`Halstead difficulty`,defaultLimits:{warning:1/0,error:1/0},hint:`the function reuses the same operands many times; split it so each part handles fewer values.`,measure:e=>e.halstead.difficulty},{key:`maxFunctionHalsteadEffort`,label:`Halstead effort`,defaultLimits:{warning:1/0,error:1/0},hint:`the function is both large and dense; split it into smaller functions with fewer values each.`,measure:e=>e.halstead.effort},{key:`maxFunctionDepDegree`,label:`DepDegree`,defaultLimits:{warning:1/0,error:1/0},hint:`too many values flow between the variables of the function; split it so each part works on fewer variables.`,measure:e=>e.depDegree,includesNestedFunctions:!0}],n=[{key:`maxFileNcss`,label:`file NCSS`,defaultLimits:{warning:500,error:1e3},hint:`split the file into smaller modules with one responsibility each.`,measure:e=>e.ncssCount}],r={key:`minDuplicateLines`,label:`duplicated lines`,defaultLimits:{warning:10,error:20},hint:`extract the repeated code into one shared function or module and call it from every location.`,measure:e=>e.endLine-e.startLine+1},i=[...t,...n,r];function a(e){let t=e.key.slice(3);return t.charAt(0).toLowerCase()+t.slice(1)}function o(e,t){let n=(n,r)=>{let a=t?.[n];return{...Object.fromEntries(i.map(({key:e,defaultLimits:t})=>[e,t[n]])),...a?.limits,...a&&Object.hasOwn(a.languages,r)?a.languages[r]:void 0,...e[n]}};return e=>({warning:n(`warning`,e),error:n(`error`,e)})}exports.duplicationThreshold=r,exports.fileThresholds=n,exports.functionThresholds=t,exports.levels=e,exports.metricNameOf=a,exports.resolveLimits=o,exports.thresholds=i;
2
2
  //# sourceMappingURL=thresholds.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"thresholds.cjs","names":[],"sources":["../src/thresholds.ts"],"sourcesContent":["import type { CodeMetrics, FunctionMetrics } from './types.js';\n\n/**\n * One limit of `code-gauge check`. Its `key` is the config setting, the per-language override\n * setting, and (kebab-cased) the command-line option, so a new entry here is all a new thresholded\n * metric needs.\n */\nexport interface Threshold<Subject = never> {\n key: string;\n /** How the report names the measured value. */\n label: string;\n defaultLimit: number;\n /** What to change in the code when the limit is exceeded. */\n hint: string;\n measure: (subject: Subject) => number;\n}\n\n/** Limits by threshold key; `Infinity` disables a limit. */\nexport type Limits = Readonly<Record<string, number>>;\n\nexport const functionThresholds: readonly Threshold<FunctionMetrics>[] = [\n {\n key: 'maxFunctionCognitiveComplexity',\n label: 'cognitive complexity',\n defaultLimit: 15,\n hint: 'flatten nested branching with early returns and extract nested blocks into named functions.',\n measure: (fn) => fn.cognitiveComplexity,\n },\n {\n key: 'maxFunctionCyclomaticComplexity',\n label: 'cyclomatic complexity',\n defaultLimit: 10,\n hint: 'the function has too many independent paths; split it by decision or replace condition chains with a lookup table.',\n measure: (fn) => fn.cyclomaticComplexity,\n },\n {\n key: 'maxFunctionNcss',\n label: 'NCSS',\n defaultLimit: 60,\n hint: 'split the function into smaller functions that each do one step.',\n measure: (fn) => fn.ncss,\n },\n {\n key: 'maxFunctionNestingDepth',\n label: 'nesting depth',\n defaultLimit: 4,\n hint: 'replace nested conditions with guard clauses or move inner blocks into functions.',\n measure: (fn) => fn.nestingDepth,\n },\n {\n key: 'maxFunctionParameterCount',\n label: 'parameters',\n defaultLimit: 7,\n hint: 'group related parameters into one object or split the function.',\n measure: (fn) => fn.parameterCount,\n },\n {\n key: 'maxFunctionHalsteadVolume',\n label: 'Halstead volume',\n defaultLimit: 2000,\n hint: 'the function holds too many operators and operands; move cohesive parts into helper functions.',\n measure: (fn) => fn.halstead.volume,\n },\n {\n key: 'maxFunctionHalsteadDifficulty',\n label: 'Halstead difficulty',\n defaultLimit: 20,\n hint: 'the function reuses the same operands many times; split it so each part handles fewer values.',\n measure: (fn) => fn.halstead.difficulty,\n },\n {\n key: 'maxFunctionHalsteadEffort',\n label: 'Halstead effort',\n defaultLimit: 30_000,\n hint: 'the function is both large and dense; split it into smaller functions with fewer values each.',\n measure: (fn) => fn.halstead.effort,\n },\n {\n key: 'maxFunctionDepDegree',\n label: 'DepDegree',\n defaultLimit: 50,\n hint: 'too many values flow between the variables of the function; split it so each part works on fewer variables.',\n measure: (fn) => fn.depDegree,\n },\n];\n\nexport const fileThresholds: readonly Threshold<CodeMetrics>[] = [\n {\n key: 'maxFileNcss',\n label: 'file NCSS',\n defaultLimit: 500,\n hint: 'split the file into smaller modules with one responsibility each.',\n measure: (file) => file.ncssCount,\n },\n];\n\n/** A duplicated block violates when it spans at least this many lines. */\nexport const duplicationThreshold: Threshold<{ startLine: number; endLine: number }> = {\n key: 'minDuplicateLines',\n label: 'duplicated lines',\n defaultLimit: 10,\n hint: 'extract the repeated code into one shared function or module and call it from every location.',\n measure: (block) => block.endLine - block.startLine + 1,\n};\n\nexport const thresholds: readonly Threshold[] = [...functionThresholds, ...fileThresholds, duplicationThreshold];\n\n/** The name a report gives a threshold's measured value: its key without the `max`/`min` prefix. */\nexport function metricNameOf(threshold: Threshold): string {\n const name = threshold.key.slice(3);\n return name.charAt(0).toLowerCase() + name.slice(1);\n}\n\n/** The `thresholds` config section; a missing key falls back to the less specific layer. */\nexport interface ThresholdsConfig {\n limits: Limits;\n /** Overrides for the files of one language, keyed by language name. */\n languages: Readonly<Record<string, Limits>>;\n}\n\n/**\n * The limits applying to files of each language, with precedence command-line options > the\n * config's per-language overrides > the config's limits > built-in defaults.\n */\nexport function resolveLimits(cliLimits: Limits, config: ThresholdsConfig | undefined): (language: string) => Limits {\n const defaults = Object.fromEntries(thresholds.map(({ key, defaultLimit }) => [key, defaultLimit]));\n const shared = { ...defaults, ...config?.limits };\n return (language) => ({\n ...shared,\n // Object.hasOwn: a language named like an Object.prototype member must not read an inherited value.\n ...(config && Object.hasOwn(config.languages, language) ? config.languages[language] : undefined),\n ...cliLimits,\n });\n}\n"],"mappings":"aAoBA,MAAa,EAA4D,CACvE,CACE,IAAK,iCACL,MAAO,uBACP,aAAc,GACd,KAAM,8FACN,QAAU,GAAO,EAAG,mBACtB,EACA,CACE,IAAK,kCACL,MAAO,wBACP,aAAc,GACd,KAAM,qHACN,QAAU,GAAO,EAAG,oBACtB,EACA,CACE,IAAK,kBACL,MAAO,OACP,aAAc,GACd,KAAM,mEACN,QAAU,GAAO,EAAG,IACtB,EACA,CACE,IAAK,0BACL,MAAO,gBACP,aAAc,EACd,KAAM,oFACN,QAAU,GAAO,EAAG,YACtB,EACA,CACE,IAAK,4BACL,MAAO,aACP,aAAc,EACd,KAAM,kEACN,QAAU,GAAO,EAAG,cACtB,EACA,CACE,IAAK,4BACL,MAAO,kBACP,aAAc,IACd,KAAM,iGACN,QAAU,GAAO,EAAG,SAAS,MAC/B,EACA,CACE,IAAK,gCACL,MAAO,sBACP,aAAc,GACd,KAAM,gGACN,QAAU,GAAO,EAAG,SAAS,UAC/B,EACA,CACE,IAAK,4BACL,MAAO,kBACP,aAAc,IACd,KAAM,gGACN,QAAU,GAAO,EAAG,SAAS,MAC/B,EACA,CACE,IAAK,uBACL,MAAO,YACP,aAAc,GACd,KAAM,8GACN,QAAU,GAAO,EAAG,SACtB,CACF,EAEa,EAAoD,CAC/D,CACE,IAAK,cACL,MAAO,YACP,aAAc,IACd,KAAM,oEACN,QAAU,GAAS,EAAK,SAC1B,CACF,EAGa,EAA0E,CACrF,IAAK,oBACL,MAAO,mBACP,aAAc,GACd,KAAM,gGACN,QAAU,GAAU,EAAM,QAAU,EAAM,UAAY,CACxD,EAEa,EAAmC,CAAC,GAAG,EAAoB,GAAG,EAAgB,CAAoB,EAG/G,SAAgB,EAAa,EAA8B,CACzD,IAAM,EAAO,EAAU,IAAI,MAAM,CAAC,EAClC,OAAO,EAAK,OAAO,CAAC,CAAC,CAAC,YAAY,EAAI,EAAK,MAAM,CAAC,CACpD,CAaA,SAAgB,EAAc,EAAmB,EAAoE,CAEnH,IAAM,EAAS,CAAE,GADA,OAAO,YAAY,EAAW,KAAK,CAAE,MAAK,kBAAmB,CAAC,EAAK,CAAY,CAAC,CACtE,EAAG,GAAG,GAAQ,MAAO,EAChD,MAAQ,KAAc,CACpB,GAAG,EAEH,GAAI,GAAU,OAAO,OAAO,EAAO,UAAW,CAAQ,EAAI,EAAO,UAAU,GAAY,IAAA,GACvF,GAAG,CACL,EACF"}
1
+ {"version":3,"file":"thresholds.cjs","names":[],"sources":["../src/thresholds.ts"],"sourcesContent":["import type { CodeMetrics, FunctionMetrics } from './types.js';\n\n/** The severities of a violation, mildest first. */\nexport const levels = ['warning', 'error'] as const;\nexport type Level = (typeof levels)[number];\n\n/**\n * One threshold of `code-gauge check`. Its `key` is the config setting, the per-language override\n * setting, and (kebab-cased after a level prefix) the command-line options, so a new entry here is\n * all a new thresholded metric needs.\n */\nexport interface Threshold<Subject = never> {\n key: string;\n /** How the report names the measured value. */\n label: string;\n /** `Infinity` leaves the level unchecked unless the config or the command line sets a limit. */\n defaultLimits: Readonly<Record<Level, number>>;\n /** What to change in the code when the limit is exceeded. */\n hint: string;\n measure: (subject: Subject) => number;\n /**\n * The value of a function is at least that of each function nested in it, whose code it counts\n * too. Not so for Halstead difficulty and effort: a ratio can be higher for the nested part alone.\n */\n includesNestedFunctions?: true;\n}\n\n/** Limits by threshold key; `Infinity` disables a limit. */\nexport type Limits = Readonly<Record<string, number>>;\nexport type LimitsByLevel = Readonly<Record<Level, Limits>>;\n\nexport const functionThresholds: readonly Threshold<FunctionMetrics>[] = [\n {\n key: 'maxFunctionCognitiveComplexity',\n label: 'cognitive complexity',\n defaultLimits: { warning: 15, error: 30 },\n hint: 'flatten nested branching with early returns and extract nested blocks into named functions.',\n measure: (fn) => fn.cognitiveComplexity,\n includesNestedFunctions: true,\n },\n {\n key: 'maxFunctionCyclomaticComplexity',\n label: 'cyclomatic complexity',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function has too many independent paths; split it by decision or replace condition chains with a lookup table.',\n measure: (fn) => fn.cyclomaticComplexity,\n },\n {\n key: 'maxFunctionNcss',\n label: 'NCSS',\n defaultLimits: { warning: 60, error: 100 },\n hint: 'split the function into smaller functions that each do one step.',\n measure: (fn) => fn.ncss,\n includesNestedFunctions: true,\n },\n {\n key: 'maxFunctionNestingDepth',\n label: 'nesting depth',\n defaultLimits: { warning: 4, error: 5 },\n hint: 'replace nested conditions with guard clauses or move inner blocks into functions.',\n measure: (fn) => fn.nestingDepth,\n },\n {\n key: 'maxFunctionParameterCount',\n label: 'parameters',\n defaultLimits: { warning: 7, error: Infinity },\n hint: 'group related parameters into one object or split the function.',\n measure: (fn) => fn.parameterCount,\n },\n {\n key: 'maxFunctionHalsteadVolume',\n label: 'Halstead volume',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function holds too many operators and operands; move cohesive parts into helper functions.',\n measure: (fn) => fn.halstead.volume,\n includesNestedFunctions: true,\n },\n {\n key: 'maxFunctionHalsteadDifficulty',\n label: 'Halstead difficulty',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function reuses the same operands many times; split it so each part handles fewer values.',\n measure: (fn) => fn.halstead.difficulty,\n },\n {\n key: 'maxFunctionHalsteadEffort',\n label: 'Halstead effort',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function is both large and dense; split it into smaller functions with fewer values each.',\n measure: (fn) => fn.halstead.effort,\n },\n {\n key: 'maxFunctionDepDegree',\n label: 'DepDegree',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'too many values flow between the variables of the function; split it so each part works on fewer variables.',\n measure: (fn) => fn.depDegree,\n includesNestedFunctions: true,\n },\n];\n\nexport const fileThresholds: readonly Threshold<CodeMetrics>[] = [\n {\n key: 'maxFileNcss',\n label: 'file NCSS',\n defaultLimits: { warning: 500, error: 1000 },\n hint: 'split the file into smaller modules with one responsibility each.',\n measure: (file) => file.ncssCount,\n },\n];\n\n/** A duplicated block violates when it spans at least this many lines. */\nexport const duplicationThreshold: Threshold<{ startLine: number; endLine: number }> = {\n key: 'minDuplicateLines',\n label: 'duplicated lines',\n defaultLimits: { warning: 10, error: 20 },\n hint: 'extract the repeated code into one shared function or module and call it from every location.',\n measure: (block) => block.endLine - block.startLine + 1,\n};\n\nexport const thresholds: readonly Threshold[] = [...functionThresholds, ...fileThresholds, duplicationThreshold];\n\n/** The name a report gives a threshold's measured value: its key without the `max`/`min` prefix. */\nexport function metricNameOf(threshold: Threshold): string {\n const name = threshold.key.slice(3);\n return name.charAt(0).toLowerCase() + name.slice(1);\n}\n\n/** One level of the `thresholds` config section; a missing key falls back to the less specific layer. */\nexport interface LevelThresholdsConfig {\n limits: Limits;\n /** Overrides for the files of one language, keyed by language name. */\n languages: Readonly<Record<string, Limits>>;\n}\n\nexport type ThresholdsConfig = Readonly<Partial<Record<Level, LevelThresholdsConfig>>>;\n\n/**\n * The limits of each level applying to files of each language, with precedence command-line\n * options > the config's per-language overrides > the config's limits > built-in defaults.\n */\nexport function resolveLimits(\n cliLimits: LimitsByLevel,\n config: ThresholdsConfig | undefined\n): (language: string) => LimitsByLevel {\n const resolveLevel = (level: Level, language: string): Limits => {\n const levelConfig = config?.[level];\n return {\n ...Object.fromEntries(thresholds.map(({ key, defaultLimits }) => [key, defaultLimits[level]])),\n ...levelConfig?.limits,\n // Object.hasOwn: a language named like an Object.prototype member must not read an inherited value.\n ...(levelConfig && Object.hasOwn(levelConfig.languages, language) ? levelConfig.languages[language] : undefined),\n ...cliLimits[level],\n };\n };\n return (language) => ({ warning: resolveLevel('warning', language), error: resolveLevel('error', language) });\n}\n"],"mappings":"aAGA,MAAa,EAAS,CAAC,UAAW,OAAO,EA4B5B,EAA4D,CACvE,CACE,IAAK,iCACL,MAAO,uBACP,cAAe,CAAE,QAAS,GAAI,MAAO,EAAG,EACxC,KAAM,8FACN,QAAU,GAAO,EAAG,oBACpB,wBAAyB,EAC3B,EACA,CACE,IAAK,kCACL,MAAO,wBACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,KAAM,qHACN,QAAU,GAAO,EAAG,oBACtB,EACA,CACE,IAAK,kBACL,MAAO,OACP,cAAe,CAAE,QAAS,GAAI,MAAO,GAAI,EACzC,KAAM,mEACN,QAAU,GAAO,EAAG,KACpB,wBAAyB,EAC3B,EACA,CACE,IAAK,0BACL,MAAO,gBACP,cAAe,CAAE,QAAS,EAAG,MAAO,CAAE,EACtC,KAAM,oFACN,QAAU,GAAO,EAAG,YACtB,EACA,CACE,IAAK,4BACL,MAAO,aACP,cAAe,CAAE,QAAS,EAAG,MAAO,GAAS,EAC7C,KAAM,kEACN,QAAU,GAAO,EAAG,cACtB,EACA,CACE,IAAK,4BACL,MAAO,kBACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,KAAM,iGACN,QAAU,GAAO,EAAG,SAAS,OAC7B,wBAAyB,EAC3B,EACA,CACE,IAAK,gCACL,MAAO,sBACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,KAAM,gGACN,QAAU,GAAO,EAAG,SAAS,UAC/B,EACA,CACE,IAAK,4BACL,MAAO,kBACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,KAAM,gGACN,QAAU,GAAO,EAAG,SAAS,MAC/B,EACA,CACE,IAAK,uBACL,MAAO,YACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,KAAM,8GACN,QAAU,GAAO,EAAG,UACpB,wBAAyB,EAC3B,CACF,EAEa,EAAoD,CAC/D,CACE,IAAK,cACL,MAAO,YACP,cAAe,CAAE,QAAS,IAAK,MAAO,GAAK,EAC3C,KAAM,oEACN,QAAU,GAAS,EAAK,SAC1B,CACF,EAGa,EAA0E,CACrF,IAAK,oBACL,MAAO,mBACP,cAAe,CAAE,QAAS,GAAI,MAAO,EAAG,EACxC,KAAM,gGACN,QAAU,GAAU,EAAM,QAAU,EAAM,UAAY,CACxD,EAEa,EAAmC,CAAC,GAAG,EAAoB,GAAG,EAAgB,CAAoB,EAG/G,SAAgB,EAAa,EAA8B,CACzD,IAAM,EAAO,EAAU,IAAI,MAAM,CAAC,EAClC,OAAO,EAAK,OAAO,CAAC,CAAC,CAAC,YAAY,EAAI,EAAK,MAAM,CAAC,CACpD,CAeA,SAAgB,EACd,EACA,EACqC,CACrC,IAAM,GAAgB,EAAc,IAA6B,CAC/D,IAAM,EAAc,IAAS,GAC7B,MAAO,CACL,GAAG,OAAO,YAAY,EAAW,KAAK,CAAE,MAAK,mBAAoB,CAAC,EAAK,EAAc,EAAM,CAAC,CAAC,EAC7F,GAAG,GAAa,OAEhB,GAAI,GAAe,OAAO,OAAO,EAAY,UAAW,CAAQ,EAAI,EAAY,UAAU,GAAY,IAAA,GACtG,GAAG,EAAU,EACf,CACF,EACA,MAAQ,KAAc,CAAE,QAAS,EAAa,UAAW,CAAQ,EAAG,MAAO,EAAa,QAAS,CAAQ,CAAE,EAC7G"}
@@ -1,20 +1,30 @@
1
1
  import type { CodeMetrics, FunctionMetrics } from './types.js';
2
+ /** The severities of a violation, mildest first. */
3
+ export declare const levels: readonly ['warning', 'error'];
4
+ export type Level = (typeof levels)[number];
2
5
  /**
3
- * One limit of `code-gauge check`. Its `key` is the config setting, the per-language override
4
- * setting, and (kebab-cased) the command-line option, so a new entry here is all a new thresholded
5
- * metric needs.
6
+ * One threshold of `code-gauge check`. Its `key` is the config setting, the per-language override
7
+ * setting, and (kebab-cased after a level prefix) the command-line options, so a new entry here is
8
+ * all a new thresholded metric needs.
6
9
  */
7
10
  export interface Threshold<Subject = never> {
8
11
  key: string;
9
12
  /** How the report names the measured value. */
10
13
  label: string;
11
- defaultLimit: number;
14
+ /** `Infinity` leaves the level unchecked unless the config or the command line sets a limit. */
15
+ defaultLimits: Readonly<Record<Level, number>>;
12
16
  /** What to change in the code when the limit is exceeded. */
13
17
  hint: string;
14
18
  measure: (subject: Subject) => number;
19
+ /**
20
+ * The value of a function is at least that of each function nested in it, whose code it counts
21
+ * too. Not so for Halstead difficulty and effort: a ratio can be higher for the nested part alone.
22
+ */
23
+ includesNestedFunctions?: true;
15
24
  }
16
25
  /** Limits by threshold key; `Infinity` disables a limit. */
17
26
  export type Limits = Readonly<Record<string, number>>;
27
+ export type LimitsByLevel = Readonly<Record<Level, Limits>>;
18
28
  export declare const functionThresholds: readonly Threshold<FunctionMetrics>[];
19
29
  export declare const fileThresholds: readonly Threshold<CodeMetrics>[];
20
30
  /** A duplicated block violates when it spans at least this many lines. */
@@ -25,14 +35,15 @@ export declare const duplicationThreshold: Threshold<{
25
35
  export declare const thresholds: readonly Threshold[];
26
36
  /** The name a report gives a threshold's measured value: its key without the `max`/`min` prefix. */
27
37
  export declare function metricNameOf(threshold: Threshold): string;
28
- /** The `thresholds` config section; a missing key falls back to the less specific layer. */
29
- export interface ThresholdsConfig {
38
+ /** One level of the `thresholds` config section; a missing key falls back to the less specific layer. */
39
+ export interface LevelThresholdsConfig {
30
40
  limits: Limits;
31
41
  /** Overrides for the files of one language, keyed by language name. */
32
42
  languages: Readonly<Record<string, Limits>>;
33
43
  }
44
+ export type ThresholdsConfig = Readonly<Partial<Record<Level, LevelThresholdsConfig>>>;
34
45
  /**
35
- * The limits applying to files of each language, with precedence command-line options > the
36
- * config's per-language overrides > the config's limits > built-in defaults.
46
+ * The limits of each level applying to files of each language, with precedence command-line
47
+ * options > the config's per-language overrides > the config's limits > built-in defaults.
37
48
  */
38
- export declare function resolveLimits(cliLimits: Limits, config: ThresholdsConfig | undefined): (language: string) => Limits;
49
+ export declare function resolveLimits(cliLimits: LimitsByLevel, config: ThresholdsConfig | undefined): (language: string) => LimitsByLevel;
@@ -1,2 +1,2 @@
1
- const e=[{key:`maxFunctionCognitiveComplexity`,label:`cognitive complexity`,defaultLimit:15,hint:`flatten nested branching with early returns and extract nested blocks into named functions.`,measure:e=>e.cognitiveComplexity},{key:`maxFunctionCyclomaticComplexity`,label:`cyclomatic complexity`,defaultLimit:10,hint:`the function has too many independent paths; split it by decision or replace condition chains with a lookup table.`,measure:e=>e.cyclomaticComplexity},{key:`maxFunctionNcss`,label:`NCSS`,defaultLimit:60,hint:`split the function into smaller functions that each do one step.`,measure:e=>e.ncss},{key:`maxFunctionNestingDepth`,label:`nesting depth`,defaultLimit:4,hint:`replace nested conditions with guard clauses or move inner blocks into functions.`,measure:e=>e.nestingDepth},{key:`maxFunctionParameterCount`,label:`parameters`,defaultLimit:7,hint:`group related parameters into one object or split the function.`,measure:e=>e.parameterCount},{key:`maxFunctionHalsteadVolume`,label:`Halstead volume`,defaultLimit:2e3,hint:`the function holds too many operators and operands; move cohesive parts into helper functions.`,measure:e=>e.halstead.volume},{key:`maxFunctionHalsteadDifficulty`,label:`Halstead difficulty`,defaultLimit:20,hint:`the function reuses the same operands many times; split it so each part handles fewer values.`,measure:e=>e.halstead.difficulty},{key:`maxFunctionHalsteadEffort`,label:`Halstead effort`,defaultLimit:3e4,hint:`the function is both large and dense; split it into smaller functions with fewer values each.`,measure:e=>e.halstead.effort},{key:`maxFunctionDepDegree`,label:`DepDegree`,defaultLimit:50,hint:`too many values flow between the variables of the function; split it so each part works on fewer variables.`,measure:e=>e.depDegree}],t=[{key:`maxFileNcss`,label:`file NCSS`,defaultLimit:500,hint:`split the file into smaller modules with one responsibility each.`,measure:e=>e.ncssCount}],n={key:`minDuplicateLines`,label:`duplicated lines`,defaultLimit:10,hint:`extract the repeated code into one shared function or module and call it from every location.`,measure:e=>e.endLine-e.startLine+1},r=[...e,...t,n];function i(e){let t=e.key.slice(3);return t.charAt(0).toLowerCase()+t.slice(1)}function a(e,t){let n={...Object.fromEntries(r.map(({key:e,defaultLimit:t})=>[e,t])),...t?.limits};return r=>({...n,...t&&Object.hasOwn(t.languages,r)?t.languages[r]:void 0,...e})}export{n as duplicationThreshold,t as fileThresholds,e as functionThresholds,i as metricNameOf,a as resolveLimits,r as thresholds};
1
+ const e=[`warning`,`error`],t=[{key:`maxFunctionCognitiveComplexity`,label:`cognitive complexity`,defaultLimits:{warning:15,error:30},hint:`flatten nested branching with early returns and extract nested blocks into named functions.`,measure:e=>e.cognitiveComplexity,includesNestedFunctions:!0},{key:`maxFunctionCyclomaticComplexity`,label:`cyclomatic complexity`,defaultLimits:{warning:1/0,error:1/0},hint:`the function has too many independent paths; split it by decision or replace condition chains with a lookup table.`,measure:e=>e.cyclomaticComplexity},{key:`maxFunctionNcss`,label:`NCSS`,defaultLimits:{warning:60,error:100},hint:`split the function into smaller functions that each do one step.`,measure:e=>e.ncss,includesNestedFunctions:!0},{key:`maxFunctionNestingDepth`,label:`nesting depth`,defaultLimits:{warning:4,error:5},hint:`replace nested conditions with guard clauses or move inner blocks into functions.`,measure:e=>e.nestingDepth},{key:`maxFunctionParameterCount`,label:`parameters`,defaultLimits:{warning:7,error:1/0},hint:`group related parameters into one object or split the function.`,measure:e=>e.parameterCount},{key:`maxFunctionHalsteadVolume`,label:`Halstead volume`,defaultLimits:{warning:1/0,error:1/0},hint:`the function holds too many operators and operands; move cohesive parts into helper functions.`,measure:e=>e.halstead.volume,includesNestedFunctions:!0},{key:`maxFunctionHalsteadDifficulty`,label:`Halstead difficulty`,defaultLimits:{warning:1/0,error:1/0},hint:`the function reuses the same operands many times; split it so each part handles fewer values.`,measure:e=>e.halstead.difficulty},{key:`maxFunctionHalsteadEffort`,label:`Halstead effort`,defaultLimits:{warning:1/0,error:1/0},hint:`the function is both large and dense; split it into smaller functions with fewer values each.`,measure:e=>e.halstead.effort},{key:`maxFunctionDepDegree`,label:`DepDegree`,defaultLimits:{warning:1/0,error:1/0},hint:`too many values flow between the variables of the function; split it so each part works on fewer variables.`,measure:e=>e.depDegree,includesNestedFunctions:!0}],n=[{key:`maxFileNcss`,label:`file NCSS`,defaultLimits:{warning:500,error:1e3},hint:`split the file into smaller modules with one responsibility each.`,measure:e=>e.ncssCount}],r={key:`minDuplicateLines`,label:`duplicated lines`,defaultLimits:{warning:10,error:20},hint:`extract the repeated code into one shared function or module and call it from every location.`,measure:e=>e.endLine-e.startLine+1},i=[...t,...n,r];function a(e){let t=e.key.slice(3);return t.charAt(0).toLowerCase()+t.slice(1)}function o(e,t){let n=(n,r)=>{let a=t?.[n];return{...Object.fromEntries(i.map(({key:e,defaultLimits:t})=>[e,t[n]])),...a?.limits,...a&&Object.hasOwn(a.languages,r)?a.languages[r]:void 0,...e[n]}};return e=>({warning:n(`warning`,e),error:n(`error`,e)})}export{r as duplicationThreshold,n as fileThresholds,t as functionThresholds,e as levels,a as metricNameOf,o as resolveLimits,i as thresholds};
2
2
  //# sourceMappingURL=thresholds.js.map