code-gauge 9.0.0 → 10.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.
Files changed (43) hide show
  1. package/README.md +41 -18
  2. package/dist/checkCommand.cjs +2 -2
  3. package/dist/checkCommand.cjs.map +1 -1
  4. package/dist/checkCommand.d.ts +3 -1
  5. package/dist/checkCommand.js +2 -2
  6. package/dist/checkCommand.js.map +1 -1
  7. package/dist/cli.cjs +3 -3
  8. package/dist/cli.cjs.map +1 -1
  9. package/dist/cli.js +3 -3
  10. package/dist/cli.js.map +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/metrics.cjs +1 -1
  13. package/dist/metrics.cjs.map +1 -1
  14. package/dist/metrics.js +1 -1
  15. package/dist/metrics.js.map +1 -1
  16. package/dist/nativeMetrics.cjs +1 -1
  17. package/dist/nativeMetrics.cjs.map +1 -1
  18. package/dist/nativeMetrics.d.ts +4 -2
  19. package/dist/nativeMetrics.js +1 -1
  20. package/dist/nativeMetrics.js.map +1 -1
  21. package/dist/thresholdCheck.cjs +1 -1
  22. package/dist/thresholdCheck.cjs.map +1 -1
  23. package/dist/thresholdCheck.d.ts +18 -4
  24. package/dist/thresholdCheck.js +1 -1
  25. package/dist/thresholdCheck.js.map +1 -1
  26. package/dist/thresholds.cjs +1 -1
  27. package/dist/thresholds.cjs.map +1 -1
  28. package/dist/thresholds.d.ts +5 -3
  29. package/dist/thresholds.js +1 -1
  30. package/dist/thresholds.js.map +1 -1
  31. package/dist/types.d.ts +37 -0
  32. package/dist/wasmBinding.cjs +1 -1
  33. package/dist/wasmBinding.cjs.map +1 -1
  34. package/dist/wasmBinding.js +1 -1
  35. package/dist/wasmBinding.js.map +1 -1
  36. package/native/code-gauge.wasm +0 -0
  37. package/native/src/complexity.rs +173 -19
  38. package/native/src/container.rs +413 -0
  39. package/native/src/functions.rs +2 -5
  40. package/native/src/lib.rs +2 -1
  41. package/native/src/measure.rs +24 -8
  42. package/native/src/types.rs +21 -0
  43. package/package.json +8 -8
@@ -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).\\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
+ {"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' | 'cognitiveBlocks'> {\n /** Absent when the function has none. */\n cognitiveBlocks?: FunctionMetrics['cognitiveBlocks'];\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 = 10;\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":"qEAqEA,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,GAAoC,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,iFAE1D,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`],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;
1
+ "use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./thresholds.cjs");let n=require("node:assert");n=e.__toESM(n,1);const r=[`file`,`function`,`duplication`],i=new Set(t.functionThresholds.filter(({includesNestedFunctions:e})=>e).map(e=>t.metricNameOf(e)));function a(e,n,r,i){let a=g(n,new Set(e.map(({file:e})=>e))),l=[],d=0;for(let n of e){let{file:e,metrics:o,hunks:m}=n,h=r(o.language),g=m?.length===0?[]:p(t.fileThresholds,o,h);g.length>0&&l.push(f({file:e,startLine:1,endLine:o.lines.total},`file`,g));let v=s(n);d+=v.length;let y=v.map(e=>({fn:e,exceeded:p(t.functionThresholds,e,h)})).filter(({exceeded:e})=>e.length>0);for(let{fn:t,exceeded:n}of y){let r=n.filter(e=>!c(e,t,y));r.length>0&&l.push(u(e,t,r))}l.push(..._(n,a.get(e)??[],h,i))}return{violations:l.toSorted(o),checkedFunctionCount:d}}function o(e,n){return t.levels.indexOf(n.level)-t.levels.indexOf(e.level)||x(e.file,n.file)||e.startLine-n.startLine||r.indexOf(e.kind)-r.indexOf(n.kind)||e.endLine-n.endLine}function s({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 c(e,t,n){return i.has(e.metric)?n.some(n=>n.fn!==t&&l(n.fn,t)&&n.exceeded.some(({metric:t})=>t===e.metric)):!1}function l(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 u(e,t,n){let{startLine:r,endLine:i}=t,a=n.some(({metric:e})=>e===`functionCognitiveComplexity`);return{...f({file:e,startLine:r,endLine:i},`function`,n),name:d(t),...a&&t.cognitiveBlocks.length>0&&{largestBlocks:t.cognitiveBlocks}}}function d({name:e=`<anonymous>`,containerName:t}){return t?`${t}.${e}`:e}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,n,r,i){let a=e.measure(n),o=t.levels.findLast(t=>i(a,r[t][e.key]));return o?[{metric:t.metricNameOf(e),value:a,level:o,limit:r[o][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 _({file:e,metrics:n,hunks:r},i,a,o){let s=e=>m(t.duplicationThreshold,{codeLineCount:y(e,o)},a,(e,t)=>e>=t),c=[...n.duplication.duplicateBlockGroups.map(t=>t.map(t=>({...t,file:e}))),...i.map(e=>e.occurrences)],l=t=>t.file===e&&s(t).length>0&&(!r||r.some(e=>e.headCount>0&&h(e,t.startLine,t.endLine))),u=/* @__PURE__ */ new Map;for(let e of c)for(let t of e.filter(e=>l(e)))u.set(t,e);return v([...u.keys()]).map(({merged:e,sources:t})=>({...f(e,`duplication`,s(e)),partners:v(t.flatMap(e=>u.get(e)??[])).map(e=>e.merged).filter(t=>!b(t,e)).toSorted((e,t)=>y(t,o)-y(e,o))}))}function v(e){let t=[],n=e.toSorted((e,t)=>x(e.file,t.file)||e.startLine-t.startLine);for(let e of n){let n=t.at(-1);if(n&&b(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){let r=t.get(e.file);n.default.ok(r,`No code lines for ${e.file}, which holds a duplicated block.`);let i=0;for(let t=e.startLine;t<=e.endLine;t++)r.has(t)&&(i+=1);return i}function b(e,t){return e.file===t.file&&e.startLine<=t.endLine&&t.startLine<=e.endLine}function x(e,t){return e<t?-1:+(e>t)}exports.checkThresholds=a,exports.qualifyFunctionName=d;
2
2
  //# sourceMappingURL=thresholdCheck.cjs.map
@@ -1 +1 @@
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
+ {"version":3,"file":"thresholdCheck.cjs","names":["functionThresholds","metricNameOf","fileThresholds","levels","duplicationThreshold"],"sources":["../src/thresholdCheck.ts"],"sourcesContent":["import assert from 'node:assert';\nimport 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, CognitiveBlock, 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\n/**\n * By file path, the 1-based lines that are neither blank nor comment-only, by which duplicated\n * blocks are measured and their copies ranked: of the checked files and of every other measured\n * file a copy may lie in.\n */\nexport type CodeLineNumbersByFile = ReadonlyMap<string, ReadonlySet<number>>;\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, qualified with the class it is a member of, for kind `function`. */\n name?: string;\n exceeded: ExceededLimit[];\n /**\n * The parts adding the most to the cognitive complexity, largest first, for a function that\n * exceeds a limit of it and has such parts: operator sequences and jumps outside any branching\n * construct add to the complexity without being one.\n */\n largestBlocks?: CognitiveBlock[];\n /** The other copies of the block, most code lines first, 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 cognitiveComplexityMetric = 'functionCognitiveComplexity';\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 codeLineNumbersByFile: CodeLineNumbersByFile\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 violations.push(toFunctionViolation(file, fn, ownExceeded));\n }\n }\n violations.push(\n ...collectDuplicationViolations(checkedFile, crossFileGroupsByFile.get(file) ?? [], limits, codeLineNumbersByFile)\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 toFunctionViolation(file: string, fn: FunctionMetrics, exceeded: ExceededLimit[]): Violation {\n const { startLine, endLine } = fn;\n const exceedsCognitiveComplexity = exceeded.some(({ metric }) => metric === cognitiveComplexityMetric);\n return {\n ...toViolation({ file, startLine, endLine }, 'function', exceeded),\n name: qualifyFunctionName(fn),\n ...(exceedsCognitiveComplexity && fn.cognitiveBlocks.length > 0 && { largestBlocks: fn.cognitiveBlocks }),\n };\n}\n\n/** How reports name a function: with the type or module it is a member of, so that it can be found. */\nexport function qualifyFunctionName({ name = '<anonymous>', containerName }: FunctionMetrics): string {\n return containerName ? `${containerName}.${name}` : name;\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 hold at least the code lines of a level (with `hunks`, only those overlapping\n * added lines), with overlapping occurrences merged into one region.\n */\nfunction collectDuplicationViolations(\n { file, metrics, hunks }: CheckedFile,\n crossFileGroups: readonly CrossFileDuplicateBlockGroup[],\n limits: LimitsByLevel,\n codeLineNumbersByFile: CodeLineNumbersByFile\n): Violation[] {\n const findExceededLines = (block: BlockLocation): ExceededLimit[] =>\n findExceeded(\n duplicationThreshold,\n { codeLineCount: countCodeLines(block, codeLineNumbersByFile) },\n limits,\n (value, limit) => value >= limit\n );\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 // The region merges every clone that overlaps it, so its copies range from whole copies of it\n // to a few lines matching one fragment; the largest say the most about what to share.\n partners: mergeOverlapping(sources.flatMap((source) => copiesByBlock.get(source) ?? []))\n .map((partner) => partner.merged)\n .filter((partner) => !overlaps(partner, merged))\n .toSorted(\n (left, right) => countCodeLines(right, codeLineNumbersByFile) - countCodeLines(left, codeLineNumbersByFile)\n ),\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\n/** The length of a duplicated block. */\nfunction countCodeLines(block: BlockLocation, codeLineNumbersByFile: CodeLineNumbersByFile): number {\n const codeLineNumbers = codeLineNumbersByFile.get(block.file);\n assert.ok(codeLineNumbers, `No code lines for ${block.file}, which holds a duplicated block.`);\n let count = 0;\n for (let line = block.startLine; line <= block.endLine; line++) {\n if (codeLineNumbers.has(line)) count += 1;\n }\n return count;\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,7 +1,7 @@
1
1
  import type { CrossFileDuplicationMetrics } from './crossFileDuplication.js';
2
2
  import type { LineHunk } from './git.js';
3
3
  import { type Level, type LimitsByLevel } from './thresholds.js';
4
- import type { CodeMetrics } from './types.js';
4
+ import type { CodeMetrics, CognitiveBlock, FunctionMetrics } from './types.js';
5
5
  /** A measured file to check, under the path cross-file duplication knows it by. */
6
6
  export interface CheckedFile {
7
7
  file: string;
@@ -9,6 +9,12 @@ export interface CheckedFile {
9
9
  /** The change to the file; when set, only what the change touches is checked. */
10
10
  hunks?: readonly LineHunk[];
11
11
  }
12
+ /**
13
+ * By file path, the 1-based lines that are neither blank nor comment-only, by which duplicated
14
+ * blocks are measured and their copies ranked: of the checked files and of every other measured
15
+ * file a copy may lie in.
16
+ */
17
+ export type CodeLineNumbersByFile = ReadonlyMap<string, ReadonlySet<number>>;
12
18
  export interface ExceededLimit {
13
19
  metric: string;
14
20
  value: number;
@@ -26,10 +32,16 @@ export interface Violation extends BlockLocation {
26
32
  kind: 'function' | 'file' | 'duplication';
27
33
  /** The most severe level among `exceeded`. */
28
34
  level: Level;
29
- /** The function's name, for kind `function`. */
35
+ /** The function's name, qualified with the class it is a member of, for kind `function`. */
30
36
  name?: string;
31
37
  exceeded: ExceededLimit[];
32
- /** The other copies of the block, for kind `duplication`. */
38
+ /**
39
+ * The parts adding the most to the cognitive complexity, largest first, for a function that
40
+ * exceeds a limit of it and has such parts: operator sequences and jumps outside any branching
41
+ * construct add to the complexity without being one.
42
+ */
43
+ largestBlocks?: CognitiveBlock[];
44
+ /** The other copies of the block, most code lines first, for kind `duplication`. */
33
45
  partners?: BlockLocation[];
34
46
  }
35
47
  export interface CheckResult {
@@ -37,4 +49,6 @@ export interface CheckResult {
37
49
  checkedFunctionCount: number;
38
50
  }
39
51
  /** 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;
52
+ export declare function checkThresholds(files: readonly CheckedFile[], crossFileDuplication: CrossFileDuplicationMetrics | undefined, limitsOf: (language: string) => LimitsByLevel, codeLineNumbersByFile: CodeLineNumbersByFile): CheckResult;
53
+ /** How reports name a function: with the type or module it is a member of, so that it can be found. */
54
+ export declare function qualifyFunctionName({ name, containerName }: FunctionMetrics): string;
@@ -1,2 +1,2 @@
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};
1
+ import{duplicationThreshold as e,fileThresholds as t,functionThresholds as n,levels as r,metricNameOf as i}from"./thresholds.js";import a from"node:assert";const o=[`file`,`function`,`duplication`],s=new Set(n.filter(({includesNestedFunctions:e})=>e).map(e=>i(e)));function c(e,r,i,a){let o=y(r,new Set(e.map(({file:e})=>e))),s=[],c=0;for(let r of e){let{file:e,metrics:l,hunks:f}=r,m=i(l.language),_=f?.length===0?[]:g(t,l,m);_.length>0&&s.push(h({file:e,startLine:1,endLine:l.lines.total},`file`,_));let v=u(r);c+=v.length;let y=v.map(e=>({fn:e,exceeded:g(n,e,m)})).filter(({exceeded:e})=>e.length>0);for(let{fn:t,exceeded:n}of y){let r=n.filter(e=>!d(e,t,y));r.length>0&&s.push(p(e,t,r))}s.push(...b(r,o.get(e)??[],m,a))}return{violations:s.toSorted(l),checkedFunctionCount:c}}function l(e,t){return r.indexOf(t.level)-r.indexOf(e.level)||w(e.file,t.file)||e.startLine-t.startLine||o.indexOf(e.kind)-o.indexOf(t.kind)||e.endLine-t.endLine}function u({metrics:e,hunks:t}){let n=e.language===`python`;return t?e.functions.filter(e=>t.some(t=>v(t,e.startLine,e.endLine,n))):e.functions}function d(e,t,n){return s.has(e.metric)?n.some(n=>n.fn!==t&&f(n.fn,t)&&n.exceeded.some(({metric:t})=>t===e.metric)):!1}function f(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 p(e,t,n){let{startLine:r,endLine:i}=t,a=n.some(({metric:e})=>e===`functionCognitiveComplexity`);return{...h({file:e,startLine:r,endLine:i},`function`,n),name:m(t),...a&&t.cognitiveBlocks.length>0&&{largestBlocks:t.cognitiveBlocks}}}function m({name:e=`<anonymous>`,containerName:t}){return t?`${t}.${e}`:e}function h(e,t,n){return{kind:t,level:n.some(e=>e.level===`error`)?`error`:`warning`,...e,exceeded:n}}function g(e,t,n){return e.flatMap(e=>_(e,t,n,(e,t)=>e>t))}function _(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 v(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 y(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 b({file:t,metrics:n,hunks:r},i,a,o){let s=t=>_(e,{codeLineCount:S(t,o)},a,(e,t)=>e>=t),c=[...n.duplication.duplicateBlockGroups.map(e=>e.map(e=>({...e,file:t}))),...i.map(e=>e.occurrences)],l=e=>e.file===t&&s(e).length>0&&(!r||r.some(t=>t.headCount>0&&v(t,e.startLine,e.endLine))),u=/* @__PURE__ */ new Map;for(let e of c)for(let t of e.filter(e=>l(e)))u.set(t,e);return x([...u.keys()]).map(({merged:e,sources:t})=>({...h(e,`duplication`,s(e)),partners:x(t.flatMap(e=>u.get(e)??[])).map(e=>e.merged).filter(t=>!C(t,e)).toSorted((e,t)=>S(t,o)-S(e,o))}))}function x(e){let t=[],n=e.toSorted((e,t)=>w(e.file,t.file)||e.startLine-t.startLine);for(let e of n){let n=t.at(-1);if(n&&C(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 S(e,t){let n=t.get(e.file);a.ok(n,`No code lines for ${e.file}, which holds a duplicated block.`);let r=0;for(let t=e.startLine;t<=e.endLine;t++)n.has(t)&&(r+=1);return r}function C(e,t){return e.file===t.file&&e.startLine<=t.endLine&&t.startLine<=e.endLine}function w(e,t){return e<t?-1:+(e>t)}export{c as checkThresholds,m as qualifyFunctionName};
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 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
+ {"version":3,"file":"thresholdCheck.js","names":[],"sources":["../src/thresholdCheck.ts"],"sourcesContent":["import assert from 'node:assert';\nimport 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, CognitiveBlock, 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\n/**\n * By file path, the 1-based lines that are neither blank nor comment-only, by which duplicated\n * blocks are measured and their copies ranked: of the checked files and of every other measured\n * file a copy may lie in.\n */\nexport type CodeLineNumbersByFile = ReadonlyMap<string, ReadonlySet<number>>;\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, qualified with the class it is a member of, for kind `function`. */\n name?: string;\n exceeded: ExceededLimit[];\n /**\n * The parts adding the most to the cognitive complexity, largest first, for a function that\n * exceeds a limit of it and has such parts: operator sequences and jumps outside any branching\n * construct add to the complexity without being one.\n */\n largestBlocks?: CognitiveBlock[];\n /** The other copies of the block, most code lines first, 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 cognitiveComplexityMetric = 'functionCognitiveComplexity';\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 codeLineNumbersByFile: CodeLineNumbersByFile\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 violations.push(toFunctionViolation(file, fn, ownExceeded));\n }\n }\n violations.push(\n ...collectDuplicationViolations(checkedFile, crossFileGroupsByFile.get(file) ?? [], limits, codeLineNumbersByFile)\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 toFunctionViolation(file: string, fn: FunctionMetrics, exceeded: ExceededLimit[]): Violation {\n const { startLine, endLine } = fn;\n const exceedsCognitiveComplexity = exceeded.some(({ metric }) => metric === cognitiveComplexityMetric);\n return {\n ...toViolation({ file, startLine, endLine }, 'function', exceeded),\n name: qualifyFunctionName(fn),\n ...(exceedsCognitiveComplexity && fn.cognitiveBlocks.length > 0 && { largestBlocks: fn.cognitiveBlocks }),\n };\n}\n\n/** How reports name a function: with the type or module it is a member of, so that it can be found. */\nexport function qualifyFunctionName({ name = '<anonymous>', containerName }: FunctionMetrics): string {\n return containerName ? `${containerName}.${name}` : name;\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 hold at least the code lines of a level (with `hunks`, only those overlapping\n * added lines), with overlapping occurrences merged into one region.\n */\nfunction collectDuplicationViolations(\n { file, metrics, hunks }: CheckedFile,\n crossFileGroups: readonly CrossFileDuplicateBlockGroup[],\n limits: LimitsByLevel,\n codeLineNumbersByFile: CodeLineNumbersByFile\n): Violation[] {\n const findExceededLines = (block: BlockLocation): ExceededLimit[] =>\n findExceeded(\n duplicationThreshold,\n { codeLineCount: countCodeLines(block, codeLineNumbersByFile) },\n limits,\n (value, limit) => value >= limit\n );\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 // The region merges every clone that overlaps it, so its copies range from whole copies of it\n // to a few lines matching one fragment; the largest say the most about what to share.\n partners: mergeOverlapping(sources.flatMap((source) => copiesByBlock.get(source) ?? []))\n .map((partner) => partner.merged)\n .filter((partner) => !overlaps(partner, merged))\n .toSorted(\n (left, right) => countCodeLines(right, codeLineNumbersByFile) - countCodeLines(left, codeLineNumbersByFile)\n ),\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\n/** The length of a duplicated block. */\nfunction countCodeLines(block: BlockLocation, codeLineNumbersByFile: CodeLineNumbersByFile): number {\n const codeLineNumbers = codeLineNumbersByFile.get(block.file);\n assert.ok(codeLineNumbers, `No code lines for ${block.file}, which holds a duplicated block.`);\n let count = 0;\n for (let line = block.startLine; line <= block.endLine; line++) {\n if (codeLineNumbers.has(line)) count += 1;\n }\n return count;\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=[`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:30,error:60},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:1/0,error:1/0},hint:`replace nested conditions with guard clauses or move inner blocks into functions.`,measure:e=>e.nestingDepth},{key:`maxFunctionParameterCount`,label:`parameters`,defaultLimits:{warning:6,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:400,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:15,error:1/0},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;
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:30,error:60},hint:`move each group of statements that forms one step into a named function.`,measure:e=>e.ncss,includesNestedFunctions:!0},{key:`maxFunctionNestingDepth`,label:`nesting depth`,defaultLimits:{warning:1/0,error:1/0},hint:`replace nested conditions with guard clauses or move inner blocks into functions.`,measure:e=>e.nestingDepth},{key:`maxFunctionParameterCount`,label:`parameters`,defaultLimits:{warning:6,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:400,error:1e3},hint:`split the file into modules with one responsibility each, for example by moving a class or the helpers that serve only one function into a file of their own.`,measure:e=>e.ncssCount}],r={key:`minDuplicateLines`,label:`duplicated lines`,defaultLimits:{warning:15,error:1/0},hint:`extract the repeated code into one shared function or module and call it from every location.`,measure:e=>e.codeLineCount},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/** 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: 30, error: 60 },\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: Infinity, error: Infinity },\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: 6, 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: 400, 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: 15, error: Infinity },\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,EAAG,EACxC,KAAM,mEACN,QAAU,GAAO,EAAG,KACpB,wBAAyB,EAC3B,EACA,CACE,IAAK,0BACL,MAAO,gBACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,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,GAAS,EAC9C,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
+ {"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: 30, error: 60 },\n hint: 'move each group of statements that forms one step into a named function.',\n measure: (fn) => fn.ncss,\n includesNestedFunctions: true,\n },\n {\n key: 'maxFunctionNestingDepth',\n label: 'nesting depth',\n defaultLimits: { warning: Infinity, error: Infinity },\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: 6, 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: 400, error: 1000 },\n hint: 'split the file into modules with one responsibility each, for example by moving a class or the helpers that serve only one function into a file of their own.',\n measure: (file) => file.ncssCount,\n },\n];\n\n/**\n * A duplicated block violates when it holds at least this many code lines: comment-only and blank\n * lines do not count, so a copy reads the same length whether or not it is commented.\n */\nexport const duplicationThreshold: Threshold<{ codeLineCount: number }> = {\n key: 'minDuplicateLines',\n label: 'duplicated lines',\n defaultLimits: { warning: 15, error: Infinity },\n hint: 'extract the repeated code into one shared function or module and call it from every location.',\n measure: (block) => block.codeLineCount,\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,EAAG,EACxC,KAAM,2EACN,QAAU,GAAO,EAAG,KACpB,wBAAyB,EAC3B,EACA,CACE,IAAK,0BACL,MAAO,gBACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,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,gKACN,QAAU,GAAS,EAAK,SAC1B,CACF,EAMa,EAA6D,CACxE,IAAK,oBACL,MAAO,mBACP,cAAe,CAAE,QAAS,GAAI,MAAO,GAAS,EAC9C,KAAM,gGACN,QAAU,GAAU,EAAM,aAC5B,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"}
@@ -27,10 +27,12 @@ export type Limits = Readonly<Record<string, number>>;
27
27
  export type LimitsByLevel = Readonly<Record<Level, Limits>>;
28
28
  export declare const functionThresholds: readonly Threshold<FunctionMetrics>[];
29
29
  export declare const fileThresholds: readonly Threshold<CodeMetrics>[];
30
- /** A duplicated block violates when it spans at least this many lines. */
30
+ /**
31
+ * A duplicated block violates when it holds at least this many code lines: comment-only and blank
32
+ * lines do not count, so a copy reads the same length whether or not it is commented.
33
+ */
31
34
  export declare const duplicationThreshold: Threshold<{
32
- startLine: number;
33
- endLine: number;
35
+ codeLineCount: number;
34
36
  }>;
35
37
  export declare const thresholds: readonly Threshold[];
36
38
  /** The name a report gives a threshold's measured value: its key without the `max`/`min` prefix. */
@@ -1,2 +1,2 @@
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:30,error:60},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:1/0,error:1/0},hint:`replace nested conditions with guard clauses or move inner blocks into functions.`,measure:e=>e.nestingDepth},{key:`maxFunctionParameterCount`,label:`parameters`,defaultLimits:{warning:6,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:400,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:15,error:1/0},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};
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:30,error:60},hint:`move each group of statements that forms one step into a named function.`,measure:e=>e.ncss,includesNestedFunctions:!0},{key:`maxFunctionNestingDepth`,label:`nesting depth`,defaultLimits:{warning:1/0,error:1/0},hint:`replace nested conditions with guard clauses or move inner blocks into functions.`,measure:e=>e.nestingDepth},{key:`maxFunctionParameterCount`,label:`parameters`,defaultLimits:{warning:6,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:400,error:1e3},hint:`split the file into modules with one responsibility each, for example by moving a class or the helpers that serve only one function into a file of their own.`,measure:e=>e.ncssCount}],r={key:`minDuplicateLines`,label:`duplicated lines`,defaultLimits:{warning:15,error:1/0},hint:`extract the repeated code into one shared function or module and call it from every location.`,measure:e=>e.codeLineCount},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
@@ -1 +1 @@
1
- {"version":3,"file":"thresholds.js","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: 30, error: 60 },\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: Infinity, error: Infinity },\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: 6, 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: 400, 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: 15, error: Infinity },\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,EAAG,EACxC,KAAM,mEACN,QAAU,GAAO,EAAG,KACpB,wBAAyB,EAC3B,EACA,CACE,IAAK,0BACL,MAAO,gBACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,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,GAAS,EAC9C,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
+ {"version":3,"file":"thresholds.js","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: 30, error: 60 },\n hint: 'move each group of statements that forms one step into a named function.',\n measure: (fn) => fn.ncss,\n includesNestedFunctions: true,\n },\n {\n key: 'maxFunctionNestingDepth',\n label: 'nesting depth',\n defaultLimits: { warning: Infinity, error: Infinity },\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: 6, 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: 400, error: 1000 },\n hint: 'split the file into modules with one responsibility each, for example by moving a class or the helpers that serve only one function into a file of their own.',\n measure: (file) => file.ncssCount,\n },\n];\n\n/**\n * A duplicated block violates when it holds at least this many code lines: comment-only and blank\n * lines do not count, so a copy reads the same length whether or not it is commented.\n */\nexport const duplicationThreshold: Threshold<{ codeLineCount: number }> = {\n key: 'minDuplicateLines',\n label: 'duplicated lines',\n defaultLimits: { warning: 15, error: Infinity },\n hint: 'extract the repeated code into one shared function or module and call it from every location.',\n measure: (block) => block.codeLineCount,\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,EAAG,EACxC,KAAM,2EACN,QAAU,GAAO,EAAG,KACpB,wBAAyB,EAC3B,EACA,CACE,IAAK,0BACL,MAAO,gBACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,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,gKACN,QAAU,GAAS,EAAK,SAC1B,CACF,EAMa,EAA6D,CACxE,IAAK,oBACL,MAAO,mBACP,cAAe,CAAE,QAAS,GAAI,MAAO,GAAS,EAC9C,KAAM,gGACN,QAAU,GAAU,EAAM,aAC5B,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"}
package/dist/types.d.ts CHANGED
@@ -56,8 +56,35 @@ export interface HalsteadMetrics {
56
56
  difficulty: number;
57
57
  effort: number;
58
58
  }
59
+ export interface CognitiveBlock {
60
+ /** The name of the nested function the block is, when it is one with a name. */
61
+ name?: string;
62
+ startLine: number;
63
+ endLine: number;
64
+ cognitiveComplexity: number;
65
+ }
59
66
  export interface FunctionMetrics {
60
67
  name?: string;
68
+ /**
69
+ * The name of the declaration whose member a declared function (a method, an accessor, a
70
+ * constructor, a function of a namespace or module) is, read from the syntax alone: a class,
71
+ * struct, union, interface, enum, record, object, trait, namespace or module, or Rust `impl`
72
+ * enclosing it, the receiver type of a Go method, or the object a Ruby singleton method or
73
+ * singleton class names. A C++ owner is spelled with the namespaces and classes around it
74
+ * (`ns::Rules`), whether the function is defined inside them or outside as `ns::Rules::decide`;
75
+ * an unnamed or inline namespace and template arguments add nothing, and a friend defined in a class
76
+ * belongs to the namespace around that class. Names are not looked up: a scope a definition
77
+ * spells is taken as the enclosing scope of that name when there is one, also where C++ would
78
+ * find a like-named member of it (`void gauge::f()` inside `namespace gauge` holding a
79
+ * `struct gauge`).
80
+ *
81
+ * Absent for a function written as a value (a lambda, a closure, a function expression), which
82
+ * keeps the name of what it is bound to, for a function nested in another function, for a
83
+ * member of an anonymous class or of an object literal, and for a function local to an
84
+ * initializer block. So a method of a class declared inside a function has it, and a closure
85
+ * inside a method does not.
86
+ */
87
+ containerName?: string;
61
88
  /**
62
89
  * The tree-sitter node type of the function (e.g. `method_declaration`, `arrow_function`,
63
90
  * `lambda_expression`), letting consumers distinguish declared methods from lambdas — e.g. to
@@ -81,6 +108,14 @@ export interface FunctionMetrics {
81
108
  */
82
109
  cyclomaticComplexity: number;
83
110
  cognitiveComplexity: number;
111
+ /**
112
+ * The parts of the function adding the most cognitive complexity, largest first (at most 3),
113
+ * each with the complexity its span adds to this function: the outermost branching constructs
114
+ * and nested functions of its body, where one that merely wraps most of the function (it holds
115
+ * more than half of the function's complexity, and the blocks inside it hold at least three
116
+ * quarters of its own) is replaced by the blocks inside it, unless it is a named function.
117
+ */
118
+ cognitiveBlocks: CognitiveBlock[];
84
119
  nestingDepth: number;
85
120
  /**
86
121
  * Non-commenting source statements in the function, PMD-style: the declaration itself, each
@@ -145,6 +180,8 @@ export interface CodeMetrics {
145
180
  language: LanguageName;
146
181
  bytes: number;
147
182
  lines: LineMetrics;
183
+ /** The 1-based lines counted in `lines.code` (neither blank nor comment-only), sorted ascending. */
184
+ codeLineNumbers: number[];
148
185
  functions: FunctionMetrics[];
149
186
  /**
150
187
  * The file's cyclomatic complexity as McCabe's v = e - n + 2p over its components: the sum of
@@ -1,2 +1,2 @@
1
- "use strict";const e=require("./nativeMetrics.cjs"),t=new TextEncoder,n=new TextDecoder;function r(e){let t,r,s=a=>{if(!t){if(r)throw r;try{t=i(e)}catch(e){throw r=e,e}}let{exports:o,stderr:s}=t;s.length=0;let c;try{c=a(o)}catch(e){throw t=void 0,Error(`The code-gauge WebAssembly module crashed: ${s.join(``).trim()||String(e)}`,{cause:e})}let l=n.decode(new Uint8Array(o.memory.buffer,o.result_ptr(),o.result_len()));if(c!==0)throw Error(l);return l},c=(e,t,n,r,i,c,l)=>s(s=>s.measure_code(...a(s,e),...a(s,t),Number(n),o(r),o(i),o(c),Number(l)));return{measureCodeNative:c,measureCodeNativeAsync:async(...e)=>c(...e),collectCrossFileDataNative:(e,t,n)=>s(r=>r.collect_cross_file_data(...a(r,e),...a(r,t),o(n))),collectFunctionTokenSequencesNative:(e,t)=>s(n=>n.collect_function_token_sequences(...a(n,e),...a(n,t)))}}function i(t){let n=[],r,i=new WebAssembly.Instance(t,{wasi_snapshot_preview1:c(()=>r,n)}).exports;r=i.memory;let a=i.payload_version();if(a!==8)throw new e.NativeAddonError(`The code-gauge WebAssembly module has payload version ${a}, but 8 is expected; reinstall code-gauge, or in a code-gauge repository checkout, rebuild it with \`bun run build-wasm\``);return{exports:i,stderr:n}}function a(e,n){let r=t.encode(n),i=e.alloc(r.length);return new Uint8Array(e.memory.buffer,i,r.length).set(r),[i,r.length]}function o(e){return e??-1}const s=65536;function c(e,t){let r=()=>new DataView(e().buffer),i=()=>8;return{environ_get:()=>0,environ_sizes_get:(e,t)=>(r().setUint32(e,0,!0),r().setUint32(t,0,!0),0),clock_time_get:(e,t,n)=>(r().setBigUint64(n,BigInt(Date.now())*1000000n,!0),0),random_get:(t,n)=>{for(let r=0;r<n;r+=s)crypto.getRandomValues(new Uint8Array(e().buffer,t+r,Math.min(s,n-r)));return 0},fd_write:(i,a,o,s)=>{let c=r(),l=0;for(let r=0;r<o;r++){let o=c.getUint32(a+r*8,!0),s=c.getUint32(a+r*8+4,!0);i===2&&t.push(n.decode(new Uint8Array(e().buffer,o,s))),l+=s}return c.setUint32(s,l,!0),0},fd_close:i,fd_fdstat_get:i,fd_fdstat_set_flags:i,fd_read:i,fd_seek:i,proc_exit:e=>{throw Error(`The code-gauge WebAssembly module exited with code ${e}`)}}}exports.createWasmBinding=r;
1
+ "use strict";const e=require("./nativeMetrics.cjs"),t=new TextEncoder,n=new TextDecoder;function r(e){let t,r,s=a=>{if(!t){if(r)throw r;try{t=i(e)}catch(e){throw r=e,e}}let{exports:o,stderr:s}=t;s.length=0;let c;try{c=a(o)}catch(e){throw t=void 0,Error(`The code-gauge WebAssembly module crashed: ${s.join(``).trim()||String(e)}`,{cause:e})}let l=n.decode(new Uint8Array(o.memory.buffer,o.result_ptr(),o.result_len()));if(c!==0)throw Error(l);return l},c=(e,t,n,r,i,c,l)=>s(s=>s.measure_code(...a(s,e),...a(s,t),Number(n),o(r),o(i),o(c),Number(l)));return{measureCodeNative:c,measureCodeNativeAsync:async(...e)=>c(...e),collectCrossFileDataNative:(e,t,n)=>s(r=>r.collect_cross_file_data(...a(r,e),...a(r,t),o(n))),collectFunctionTokenSequencesNative:(e,t)=>s(n=>n.collect_function_token_sequences(...a(n,e),...a(n,t)))}}function i(t){let n=[],r,i=new WebAssembly.Instance(t,{wasi_snapshot_preview1:c(()=>r,n)}).exports;r=i.memory;let a=i.payload_version();if(a!==10)throw new e.NativeAddonError(`The code-gauge WebAssembly module has payload version ${a}, but 10 is expected; reinstall code-gauge, or in a code-gauge repository checkout, rebuild it with \`bun run build-wasm\``);return{exports:i,stderr:n}}function a(e,n){let r=t.encode(n),i=e.alloc(r.length);return new Uint8Array(e.memory.buffer,i,r.length).set(r),[i,r.length]}function o(e){return e??-1}const s=65536;function c(e,t){let r=()=>new DataView(e().buffer),i=()=>8;return{environ_get:()=>0,environ_sizes_get:(e,t)=>(r().setUint32(e,0,!0),r().setUint32(t,0,!0),0),clock_time_get:(e,t,n)=>(r().setBigUint64(n,BigInt(Date.now())*1000000n,!0),0),random_get:(t,n)=>{for(let r=0;r<n;r+=s)crypto.getRandomValues(new Uint8Array(e().buffer,t+r,Math.min(s,n-r)));return 0},fd_write:(i,a,o,s)=>{let c=r(),l=0;for(let r=0;r<o;r++){let o=c.getUint32(a+r*8,!0),s=c.getUint32(a+r*8+4,!0);i===2&&t.push(n.decode(new Uint8Array(e().buffer,o,s))),l+=s}return c.setUint32(s,l,!0),0},fd_close:i,fd_fdstat_get:i,fd_fdstat_set_flags:i,fd_read:i,fd_seek:i,proc_exit:e=>{throw Error(`The code-gauge WebAssembly module exited with code ${e}`)}}}exports.createWasmBinding=r;
2
2
  //# sourceMappingURL=wasmBinding.cjs.map