code-gauge 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +83 -21
  2. package/dist/cli.cjs +3 -3
  3. package/dist/cli.cjs.map +1 -1
  4. package/dist/cli.js +3 -3
  5. package/dist/cli.js.map +1 -1
  6. package/dist/cliConfig.cjs +1 -1
  7. package/dist/cliConfig.cjs.map +1 -1
  8. package/dist/cliConfig.d.ts +11 -0
  9. package/dist/cliConfig.js +1 -1
  10. package/dist/cliConfig.js.map +1 -1
  11. package/dist/crossFileDuplication.cjs +1 -1
  12. package/dist/crossFileDuplication.cjs.map +1 -1
  13. package/dist/crossFileDuplication.js +1 -1
  14. package/dist/crossFileDuplication.js.map +1 -1
  15. package/dist/diffCommand.cjs +5 -0
  16. package/dist/diffCommand.cjs.map +1 -0
  17. package/dist/diffCommand.d.ts +17 -0
  18. package/dist/diffCommand.js +5 -0
  19. package/dist/diffCommand.js.map +1 -0
  20. package/dist/duplication.cjs +1 -1
  21. package/dist/duplication.cjs.map +1 -1
  22. package/dist/duplication.d.ts +20 -24
  23. package/dist/duplication.js +1 -1
  24. package/dist/duplication.js.map +1 -1
  25. package/dist/git.cjs +2 -0
  26. package/dist/git.cjs.map +1 -0
  27. package/dist/git.d.ts +27 -0
  28. package/dist/git.js +2 -0
  29. package/dist/git.js.map +1 -0
  30. package/dist/index.cjs +1 -1
  31. package/dist/index.d.ts +3 -2
  32. package/dist/index.js +1 -1
  33. package/dist/languages.cjs +1 -1
  34. package/dist/languages.cjs.map +1 -1
  35. package/dist/languages.d.ts +5 -0
  36. package/dist/languages.js +1 -1
  37. package/dist/languages.js.map +1 -1
  38. package/dist/metrics.cjs +1 -1
  39. package/dist/metrics.cjs.map +1 -1
  40. package/dist/metrics.d.ts +18 -5
  41. package/dist/metrics.js +1 -1
  42. package/dist/metrics.js.map +1 -1
  43. package/dist/nativeMetrics.cjs +3 -1
  44. package/dist/nativeMetrics.cjs.map +1 -1
  45. package/dist/nativeMetrics.d.ts +29 -10
  46. package/dist/nativeMetrics.js +3 -1
  47. package/dist/nativeMetrics.js.map +1 -1
  48. package/dist/regressionGate.cjs +2 -0
  49. package/dist/regressionGate.cjs.map +1 -0
  50. package/dist/regressionGate.d.ts +106 -0
  51. package/dist/regressionGate.js +2 -0
  52. package/dist/regressionGate.js.map +1 -0
  53. package/dist/scan.cjs +2 -0
  54. package/dist/scan.cjs.map +1 -0
  55. package/dist/scan.d.ts +55 -0
  56. package/dist/scan.js +2 -0
  57. package/dist/scan.js.map +1 -0
  58. package/dist/types.d.ts +18 -13
  59. package/native/Cargo.lock +523 -0
  60. package/native/Cargo.toml +45 -0
  61. package/native/build.rs +3 -0
  62. package/native/src/complexity.rs +627 -0
  63. package/native/src/dep_degree.rs +253 -0
  64. package/native/src/duplication.rs +2007 -0
  65. package/native/src/functions.rs +345 -0
  66. package/native/src/languages.rs +647 -0
  67. package/native/src/lib.rs +101 -0
  68. package/native/src/measure.rs +590 -0
  69. package/native/src/ncss.rs +263 -0
  70. package/native/src/types.rs +135 -0
  71. package/native/src/util.rs +139 -0
  72. package/package.json +16 -19
  73. package/scripts/buildNative.mjs +25 -0
  74. package/scripts/installNative.mjs +96 -0
  75. package/dist/ncss.cjs +0 -2
  76. package/dist/ncss.cjs.map +0 -1
  77. package/dist/ncss.d.ts +0 -17
  78. package/dist/ncss.js +0 -2
  79. package/dist/ncss.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.cjs","names":["os","path","stat","realpath","lstat","NativeAddonError","readdir","readFile","measureCode","collectCrossFileDuplicationFileData","measureCrossFileDuplication"],"sources":["../src/scan.ts"],"sourcesContent":["import { lstat, readdir, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData } from './duplication.js';\nimport { collectCrossFileDuplicationFileData, measureCode } from './metrics.js';\nimport { NativeAddonError } from './nativeMetrics.js';\nimport type { CodeMetrics, DuplicationOptions, LanguageName } from './types.js';\n\n/** The scan settings shared by every command (a structural subset of each command's options). */\nexport interface ScanOptions {\n duplication: Required<DuplicationOptions>;\n includeTests: boolean;\n}\n\nexport interface FileMetrics {\n file: string;\n metrics: CodeMetrics;\n /** Cross-file duplicate candidates and token/statement data, collected only for directory scans. */\n duplicationCandidates?: CrossFileDuplicationFileData;\n}\n\nexport interface ScanResult {\n crossFileDuplication?: CrossFileDuplicationMetrics;\n displayRoot: string;\n errors: string[];\n /** Non-fatal degradations (e.g. cross-file candidates unavailable); the file is still measured. */\n warnings: string[];\n fatalError?: string;\n files: FileMetrics[];\n}\n\nconst languageByExtension = new Map<string, LanguageName>([\n ['.c', 'c'],\n ['.c++', 'cpp'],\n ['.cc', 'cpp'],\n ['.cjs', 'javascript'],\n ['.cp', 'cpp'],\n ['.cpp', 'cpp'],\n ['.tcc', 'cpp'],\n ['.cts', 'typescript'],\n ['.cxx', 'cpp'],\n ['.go', 'go'],\n // Headers may be C or C++; the C++ grammar parses both.\n ['.h', 'cpp'],\n ['.hh', 'cpp'],\n ['.hpp', 'cpp'],\n ['.hxx', 'cpp'],\n ['.java', 'java'],\n ['.js', 'javascript'],\n ['.jsx', 'jsx'],\n ['.mjs', 'javascript'],\n ['.mts', 'typescript'],\n ['.py', 'python'],\n ['.rb', 'ruby'],\n ['.rs', 'rust'],\n ['.ts', 'typescript'],\n ['.tsx', 'tsx'],\n]);\n\nconst ignoredDirectoryNames = new Set([\n '.agents',\n '.claude',\n '.cursor',\n '.git',\n '.next',\n '.playwright-cli',\n '.tox',\n '.tmp',\n '.turbo',\n '.venv',\n '.yarn',\n '__fixtures__',\n '__generated__',\n '__pycache__',\n 'coverage',\n 'dist',\n 'fixtures',\n 'generated',\n 'node_modules',\n 'target',\n 'test-fixtures',\n 'vendor',\n 'venv',\n]);\n\nconst testDirectoryNames = new Set(['__tests__', 'test', 'tests', 'spec']);\nconst testFilePattern = /(?:^test(?:[_-].*)?|\\.(?:spec|test)|[_-](?:test|spec))\\.[^.]+$/iu;\n// JUnit tests use a case-sensitive `Test.java` suffix; case-insensitive matching would catch\n// production files like `contest.java`.\nconst javaTestFilePattern = /Test\\.java$/u;\n\nexport function resolveTarget(target: string): string {\n if (target === '~') {\n return os.homedir();\n }\n\n if (target.startsWith('~/')) {\n return path.join(os.homedir(), target.slice(2));\n }\n\n return path.resolve(target);\n}\n\n/** Returns the directory from which the config file search should start (the target itself if it is a directory). */\nexport async function configSearchDirectory(target: string): Promise<string> {\n try {\n const targetStat = await stat(target);\n return targetStat.isDirectory() ? target : path.dirname(target);\n } catch {\n return path.dirname(target);\n }\n}\n\n/** Shared state of one scan, threaded through the directory walk instead of positional plumbing. */\ninterface ScanContext {\n options: ScanOptions;\n files: FileMetrics[];\n errors: string[];\n warnings: string[];\n visitedDirectories: Set<string>;\n visitedFiles: Set<string>;\n /** Scan root: paths are displayed relative to it, and symbolic links may not escape it. */\n rootDirectory: string;\n}\n\nexport async function scanTarget(target: string, options: ScanOptions): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n let canonicalTarget = target;\n try {\n canonicalTarget = await realpath(target);\n } catch {\n // stat below reports missing targets with the original path.\n }\n\n const fallbackDisplayRoot = path.dirname(canonicalTarget);\n let targetStat;\n\n try {\n targetStat = await stat(canonicalTarget);\n } catch (error) {\n const fatalError = `${formatPath(canonicalTarget, fallbackDisplayRoot)}: ${formatError(error)}`;\n return { displayRoot: fallbackDisplayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n if (targetStat.isFile()) {\n const displayRoot = path.dirname(canonicalTarget);\n const language = getLanguage(canonicalTarget, options, true);\n if (!language) {\n const fatalError = `${formatPath(canonicalTarget, displayRoot)}: unsupported file type`;\n return { displayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n const context = makeScanContext(options, files, errors, warnings, displayRoot);\n try {\n await measureFile(canonicalTarget, language, 'single-file', context, canonicalTarget);\n } catch (error) {\n return toFatalResult(error, displayRoot, files, errors, warnings);\n }\n return { displayRoot, files, errors, warnings };\n }\n\n try {\n await scanDirectory(canonicalTarget, makeScanContext(options, files, errors, warnings, canonicalTarget));\n } catch (error) {\n return toFatalResult(error, canonicalTarget, files, errors, warnings);\n }\n return { displayRoot: canonicalTarget, files, errors, warnings };\n}\n\n/**\n * Measures an explicit list of repository-relative files (the diff gate's git-visible allowlist)\n * instead of walking the directory tree, so ignored artifact directories are never parsed. Paths\n * outside the scan scope (ignored/test directories, unsupported or test file names) are skipped\n * with the same rules as the walk.\n */\nexport async function scanListedFiles(\n rootDirectory: string,\n relativePaths: Iterable<string>,\n options: ScanOptions\n): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n const context = makeScanContext(options, files, errors, warnings, rootDirectory);\n for (const relativePath of relativePaths) {\n const language = isScannedPath(relativePath, options) ? getLanguage(relativePath, options) : undefined;\n if (!language) {\n continue;\n }\n const absolutePath = path.join(rootDirectory, relativePath);\n // Symbolic links are not source files: git stores only their target string, so measuring\n // through them would diverge from what any revision of the repository actually contains.\n const stats = await lstat(absolutePath).catch(() => {});\n if (stats?.isSymbolicLink()) {\n continue;\n }\n try {\n await measureFile(absolutePath, language, 'directory', context);\n } catch (error) {\n return toFatalResult(error, rootDirectory, files, errors, warnings);\n }\n }\n return { displayRoot: rootDirectory, files, errors, warnings };\n}\n\n/** A run-wide failure (a missing native addon) as a fatal result; anything else keeps throwing. */\nfunction toFatalResult(\n error: unknown,\n displayRoot: string,\n files: FileMetrics[],\n errors: string[],\n warnings: string[]\n): ScanResult {\n if (!(error instanceof NativeAddonError)) {\n throw error;\n }\n const fatalError = formatError(error);\n // Errors the walk accumulated before the fatal failure stay reported alongside it.\n return { displayRoot, files, errors: [...errors, fatalError], warnings, fatalError };\n}\n\nfunction makeScanContext(\n options: ScanOptions,\n files: FileMetrics[],\n errors: string[],\n warnings: string[],\n rootDirectory: string\n): ScanContext {\n return { options, files, errors, warnings, visitedDirectories: new Set(), visitedFiles: new Set(), rootDirectory };\n}\n\n/** Runs a filesystem operation, recording a scan error and returning undefined when it fails. */\nasync function tryFileSystem<T>(\n operation: () => Promise<T>,\n target: string,\n context: ScanContext\n): Promise<T | undefined> {\n try {\n return await operation();\n } catch (error) {\n context.errors.push(`${formatPath(target, context.rootDirectory)}: ${formatError(error)}`);\n return undefined;\n }\n}\n\n/** Resolves the path (recording errors); undefined when that fails or the result escapes the root. */\nasync function resolveWithinRoot(target: string, context: ScanContext): Promise<string | undefined> {\n const resolved = await tryFileSystem(() => realpath(target), target, context);\n return resolved !== undefined && isWithinDirectory(resolved, context.rootDirectory) ? resolved : undefined;\n}\n\nasync function scanDirectory(directory: string, context: ScanContext): Promise<void> {\n const resolvedDirectory = await resolveWithinRoot(directory, context);\n if (resolvedDirectory === undefined || context.visitedDirectories.has(resolvedDirectory)) {\n return;\n }\n context.visitedDirectories.add(resolvedDirectory);\n\n const entries = await tryFileSystem(() => readdir(directory, { withFileTypes: true }), directory, context);\n if (entries === undefined) {\n return;\n }\n\n for (const entry of entries) {\n const entryPath = path.join(directory, entry.name);\n if (entry.isSymbolicLink()) {\n await scanSymbolicLink(entry.name, entryPath, context);\n continue;\n }\n\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name, context.options)) {\n continue;\n }\n await scanDirectory(entryPath, context);\n continue;\n }\n\n if (entry.isFile()) {\n await measureScannableFile(entryPath, context);\n }\n }\n}\n\nasync function scanSymbolicLink(name: string, entryPath: string, context: ScanContext): Promise<void> {\n const resolvedPath = await resolveWithinRoot(entryPath, context);\n if (resolvedPath === undefined) {\n return;\n }\n\n const entryStat = await tryFileSystem(() => stat(entryPath), entryPath, context);\n if (entryStat === undefined) {\n return;\n }\n\n if (entryStat.isDirectory()) {\n if (\n shouldSkipDirectory(name, context.options) ||\n shouldSkipDirectory(path.basename(resolvedPath), context.options)\n ) {\n return;\n }\n await scanDirectory(entryPath, context);\n return;\n }\n\n if (entryStat.isFile()) {\n await measureScannableFile(entryPath, context, resolvedPath, resolvedPath);\n }\n}\n\nasync function measureScannableFile(\n file: string,\n context: ScanContext,\n languageFile = file,\n realFile?: string\n): Promise<void> {\n const language = getLanguage(languageFile, context.options);\n if (language) {\n await measureFile(file, language, 'directory', context, realFile);\n }\n}\n\nasync function measureFile(\n file: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext,\n realFile?: string\n): Promise<void> {\n try {\n const resolvedFile = realFile ?? (await realpath(file));\n if (context.visitedFiles.has(resolvedFile)) {\n return;\n }\n context.visitedFiles.add(resolvedFile);\n\n const code = await readFile(file, 'utf8');\n const measureOptions = { language, duplication: context.options.duplication };\n const fileMetrics: FileMetrics = { file, metrics: measureCode(code, measureOptions) };\n // Only directory scans compare files against each other; a single-file target has no peers.\n // Candidate collection is an auxiliary pass: if it fails where measureCode succeeded (an\n // addon error specific to this pass), that must not discard the measured metrics.\n if (mode === 'directory') {\n try {\n fileMetrics.duplicationCandidates = collectCrossFileDuplicationFileData(code, measureOptions);\n } catch (error) {\n // A warning, not an error: the file's metrics are complete, only its participation in\n // cross-file matching is lost, so it is not \"skipped\" and must not fail --fail-on-error.\n context.warnings.push(\n `${formatPath(file, context.rootDirectory)}: cross-file duplication candidates unavailable: ${formatError(error)}`\n );\n }\n }\n context.files.push(fileMetrics);\n } catch (error) {\n // A missing native addon fails every file identically: propagate it once as a fatal scan\n // error instead of recording one \"skipped\" entry per file behind a successful exit code.\n if (error instanceof NativeAddonError) {\n throw error;\n }\n context.errors.push(`${formatPath(file, context.rootDirectory)}: ${formatError(error)}`);\n }\n}\n\n/** Runs after the scan so every measured file's candidates participate. */\nexport function addCrossFileDuplication(result: ScanResult, options: ScanOptions): void {\n if (result.fatalError || result.files.length < 2) {\n return;\n }\n const sourceFiles = result.files.flatMap(({ file, duplicationCandidates }) =>\n duplicationCandidates ? [{ file: formatPath(file, result.displayRoot), ...duplicationCandidates }] : []\n );\n if (sourceFiles.length < 2) {\n return;\n }\n result.crossFileDuplication = measureCrossFileDuplication(sourceFiles, options.duplication);\n}\n\n/**\n * Distinct 1-based code lines covered by within-file or cross-file duplicated content. Both\n * sources expose the exact lines carrying matched tokens (never block bounding ranges, which\n * would over-count comment/blank lines and the unmatched gap of a merged clone), so the union is\n * a subset of the file's code lines and a ratio derived over code lines can never exceed 1.\n */\nexport function collectDuplicatedLineNumbers(\n metrics: CodeMetrics | undefined,\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n formattedFile: string\n): Set<number> {\n const lines = new Set(metrics?.duplication.duplicateLineNumbers);\n // Object.hasOwn: a file named like an Object.prototype member must not read an inherited value.\n const crossFileLines =\n crossFileDuplication && Object.hasOwn(crossFileDuplication.duplicateLineNumbersByFile, formattedFile)\n ? (crossFileDuplication.duplicateLineNumbersByFile[formattedFile] ?? [])\n : [];\n for (const line of crossFileLines) {\n lines.add(line);\n }\n return lines;\n}\n\nfunction shouldSkipDirectory(name: string, options: ScanOptions): boolean {\n if (ignoredDirectoryNames.has(name)) {\n return true;\n }\n\n if (options.includeTests) {\n return false;\n }\n\n return testDirectoryNames.has(name);\n}\n\nfunction isWithinDirectory(candidate: string, directory: string): boolean {\n const relative = path.relative(directory, candidate);\n return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));\n}\n\n/**\n * Whether a repository-relative path would be scanned: no ignored or excluded-test directory\n * segment and a supported, non-test file name. The diff gate uses this for base-revision\n * eligibility, so code renamed into scan scope gates as new code instead of ratcheting against\n * a blob the scanner would never have measured.\n */\nexport function isScannedPath(relativePath: string, options: ScanOptions): boolean {\n const segments = relativePath.split('/');\n for (const segment of segments.slice(0, -1)) {\n if (ignoredDirectoryNames.has(segment) || (!options.includeTests && testDirectoryNames.has(segment))) {\n return false;\n }\n }\n return getLanguage(relativePath, options) !== undefined;\n}\n\nexport function getLanguage(file: string, options: ScanOptions, explicitTarget = false): LanguageName | undefined {\n const lowerFile = file.toLowerCase();\n if (\n !explicitTarget &&\n (lowerFile.endsWith('.d.ts') ||\n lowerFile.endsWith('.d.mts') ||\n lowerFile.endsWith('.d.cts') ||\n lowerFile.endsWith('.min.js') ||\n lowerFile.endsWith('.pnp.cjs'))\n ) {\n return undefined;\n }\n\n if (\n !explicitTarget &&\n !options.includeTests &&\n (testFilePattern.test(path.basename(file)) || javaTestFilePattern.test(path.basename(file)))\n ) {\n return undefined;\n }\n\n // GCC treats an uppercase `.C` as C++; lowercasing first would misparse it with the C grammar.\n if (path.extname(file) === '.C') {\n return 'cpp';\n }\n\n return languageByExtension.get(path.extname(lowerFile));\n}\n\nexport function formatPath(file: string, base: string): string {\n return path.relative(base, file) || path.basename(file);\n}\n\nexport function writeStdout(message: string): void {\n process.stdout.write(message);\n}\n\nexport function writeStderr(message: string): void {\n process.stderr.write(message);\n}\n\nexport function formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
package/dist/scan.d.ts ADDED
@@ -0,0 +1,55 @@
1
+ import { type CrossFileDuplicationMetrics } from './crossFileDuplication.js';
2
+ import type { CrossFileDuplicationFileData } from './duplication.js';
3
+ import type { CodeMetrics, DuplicationOptions, LanguageName } from './types.js';
4
+ /** The scan settings shared by every command (a structural subset of each command's options). */
5
+ export interface ScanOptions {
6
+ duplication: Required<DuplicationOptions>;
7
+ includeTests: boolean;
8
+ }
9
+ export interface FileMetrics {
10
+ file: string;
11
+ metrics: CodeMetrics;
12
+ /** Cross-file duplicate candidates and token/statement data, collected only for directory scans. */
13
+ duplicationCandidates?: CrossFileDuplicationFileData;
14
+ }
15
+ export interface ScanResult {
16
+ crossFileDuplication?: CrossFileDuplicationMetrics;
17
+ displayRoot: string;
18
+ errors: string[];
19
+ /** Non-fatal degradations (e.g. cross-file candidates unavailable); the file is still measured. */
20
+ warnings: string[];
21
+ fatalError?: string;
22
+ files: FileMetrics[];
23
+ }
24
+ export declare function resolveTarget(target: string): string;
25
+ /** Returns the directory from which the config file search should start (the target itself if it is a directory). */
26
+ export declare function configSearchDirectory(target: string): Promise<string>;
27
+ export declare function scanTarget(target: string, options: ScanOptions): Promise<ScanResult>;
28
+ /**
29
+ * Measures an explicit list of repository-relative files (the diff gate's git-visible allowlist)
30
+ * instead of walking the directory tree, so ignored artifact directories are never parsed. Paths
31
+ * outside the scan scope (ignored/test directories, unsupported or test file names) are skipped
32
+ * with the same rules as the walk.
33
+ */
34
+ export declare function scanListedFiles(rootDirectory: string, relativePaths: Iterable<string>, options: ScanOptions): Promise<ScanResult>;
35
+ /** Runs after the scan so every measured file's candidates participate. */
36
+ export declare function addCrossFileDuplication(result: ScanResult, options: ScanOptions): void;
37
+ /**
38
+ * Distinct 1-based code lines covered by within-file or cross-file duplicated content. Both
39
+ * sources expose the exact lines carrying matched tokens (never block bounding ranges, which
40
+ * would over-count comment/blank lines and the unmatched gap of a merged clone), so the union is
41
+ * a subset of the file's code lines and a ratio derived over code lines can never exceed 1.
42
+ */
43
+ export declare function collectDuplicatedLineNumbers(metrics: CodeMetrics | undefined, crossFileDuplication: CrossFileDuplicationMetrics | undefined, formattedFile: string): Set<number>;
44
+ /**
45
+ * Whether a repository-relative path would be scanned: no ignored or excluded-test directory
46
+ * segment and a supported, non-test file name. The diff gate uses this for base-revision
47
+ * eligibility, so code renamed into scan scope gates as new code instead of ratcheting against
48
+ * a blob the scanner would never have measured.
49
+ */
50
+ export declare function isScannedPath(relativePath: string, options: ScanOptions): boolean;
51
+ export declare function getLanguage(file: string, options: ScanOptions, explicitTarget?: boolean): LanguageName | undefined;
52
+ export declare function formatPath(file: string, base: string): string;
53
+ export declare function writeStdout(message: string): void;
54
+ export declare function writeStderr(message: string): void;
55
+ export declare function formatError(error: unknown): string;
package/dist/scan.js ADDED
@@ -0,0 +1,2 @@
1
+ import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{NativeAddonError as t}from"./nativeMetrics.js";import{collectCrossFileDuplicationFileData as n,measureCode as r}from"./metrics.js";import{lstat as i,readFile as a,readdir as o,realpath as s,stat as c}from"node:fs/promises";import l from"node:path";import u from"node:os";const d=new Map([[`.c`,`c`],[`.c++`,`cpp`],[`.cc`,`cpp`],[`.cjs`,`javascript`],[`.cp`,`cpp`],[`.cpp`,`cpp`],[`.tcc`,`cpp`],[`.cts`,`typescript`],[`.cxx`,`cpp`],[`.go`,`go`],[`.h`,`cpp`],[`.hh`,`cpp`],[`.hpp`,`cpp`],[`.hxx`,`cpp`],[`.java`,`java`],[`.js`,`javascript`],[`.jsx`,`jsx`],[`.mjs`,`javascript`],[`.mts`,`typescript`],[`.py`,`python`],[`.rb`,`ruby`],[`.rs`,`rust`],[`.ts`,`typescript`],[`.tsx`,`tsx`]]),f=new Set([`.agents`,`.claude`,`.cursor`,`.git`,`.next`,`.playwright-cli`,`.tox`,`.tmp`,`.turbo`,`.venv`,`.yarn`,`__fixtures__`,`__generated__`,`__pycache__`,`coverage`,`dist`,`fixtures`,`generated`,`node_modules`,`target`,`test-fixtures`,`vendor`,`venv`]),p=new Set([`__tests__`,`test`,`tests`,`spec`]),m=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-](?:test|spec))\.[^.]+$/iu,h=/Test\.java$/u;function g(e){return e===`~`?u.homedir():e.startsWith(`~/`)?l.join(u.homedir(),e.slice(2)):l.resolve(e)}async function _(e){try{return(await c(e)).isDirectory()?e:l.dirname(e)}catch{return l.dirname(e)}}async function v(e,t){let n=[],r=[],i=[],a=e;try{a=await s(e)}catch{}let o=l.dirname(a),u;try{u=await c(a)}catch(e){let t=`${P(a,o)}: ${L(e)}`;return{displayRoot:o,files:n,errors:[t],warnings:i,fatalError:t}}if(u.isFile()){let e=l.dirname(a),o=N(a,t,!0);if(!o){let t=`${P(a,e)}: unsupported file type`;return{displayRoot:e,files:n,errors:[t],warnings:i,fatalError:t}}let s=x(t,n,r,i,e);try{await D(a,o,`single-file`,s,a)}catch(t){return b(t,e,n,r,i)}return{displayRoot:e,files:n,errors:r,warnings:i}}try{await w(a,x(t,n,r,i,a))}catch(e){return b(e,a,n,r,i)}return{displayRoot:a,files:n,errors:r,warnings:i}}async function y(e,t,n){let r=[],a=[],o=[],s=x(n,r,a,o,e);for(let c of t){let t=M(c,n)?N(c,n):void 0;if(!t)continue;let u=l.join(e,c);if(!(await i(u).catch(()=>{}))?.isSymbolicLink())try{await D(u,t,`directory`,s)}catch(t){return b(t,e,r,a,o)}}return{displayRoot:e,files:r,errors:a,warnings:o}}function b(e,n,r,i,a){if(!(e instanceof t))throw e;let o=L(e);return{displayRoot:n,files:r,errors:[...i,o],warnings:a,fatalError:o}}function x(e,t,n,r,i){return{options:e,files:t,errors:n,warnings:r,visitedDirectories:new Set,visitedFiles:new Set,rootDirectory:i}}async function S(e,t,n){try{return await e()}catch(e){n.errors.push(`${P(t,n.rootDirectory)}: ${L(e)}`);return}}async function C(e,t){let n=await S(()=>s(e),e,t);return n!==void 0&&j(n,t.rootDirectory)?n:void 0}async function w(e,t){let n=await C(e,t);if(n===void 0||t.visitedDirectories.has(n))return;t.visitedDirectories.add(n);let r=await S(()=>o(e,{withFileTypes:!0}),e,t);if(r!==void 0)for(let n of r){let r=l.join(e,n.name);if(n.isSymbolicLink()){await T(n.name,r,t);continue}if(n.isDirectory()){if(A(n.name,t.options))continue;await w(r,t);continue}n.isFile()&&await E(r,t)}}async function T(e,t,n){let r=await C(t,n);if(r===void 0)return;let i=await S(()=>c(t),t,n);if(i!==void 0){if(i.isDirectory()){if(A(e,n.options)||A(l.basename(r),n.options))return;await w(t,n);return}i.isFile()&&await E(t,n,r,r)}}async function E(e,t,n=e,r){let i=N(n,t.options);i&&await D(e,i,`directory`,t,r)}async function D(e,i,o,c,l){try{let t=l??await s(e);if(c.visitedFiles.has(t))return;c.visitedFiles.add(t);let u=await a(e,`utf8`),d={language:i,duplication:c.options.duplication},f={file:e,metrics:r(u,d)};if(o===`directory`)try{f.duplicationCandidates=n(u,d)}catch(t){c.warnings.push(`${P(e,c.rootDirectory)}: cross-file duplication candidates unavailable: ${L(t)}`)}c.files.push(f)}catch(n){if(n instanceof t)throw n;c.errors.push(`${P(e,c.rootDirectory)}: ${L(n)}`)}}function O(t,n){if(t.fatalError||t.files.length<2)return;let r=t.files.flatMap(({file:e,duplicationCandidates:n})=>n?[{file:P(e,t.displayRoot),...n}]:[]);r.length<2||(t.crossFileDuplication=e(r,n.duplication))}function k(e,t,n){let r=new Set(e?.duplication.duplicateLineNumbers),i=t&&Object.hasOwn(t.duplicateLineNumbersByFile,n)?t.duplicateLineNumbersByFile[n]??[]:[];for(let e of i)r.add(e);return r}function A(e,t){return f.has(e)?!0:!t.includeTests&&p.has(e)}function j(e,t){let n=l.relative(t,e);return n===``||n!==`..`&&!n.startsWith(`..${l.sep}`)&&!l.isAbsolute(n)}function M(e,t){let n=e.split(`/`);for(let e of n.slice(0,-1))if(f.has(e)||!t.includeTests&&p.has(e))return!1;return N(e,t)!==void 0}function N(e,t,n=!1){let r=e.toLowerCase();if(!(!n&&(r.endsWith(`.d.ts`)||r.endsWith(`.d.mts`)||r.endsWith(`.d.cts`)||r.endsWith(`.min.js`)||r.endsWith(`.pnp.cjs`)))&&!(!n&&!t.includeTests&&(m.test(l.basename(e))||h.test(l.basename(e)))))return l.extname(e)===`.C`?`cpp`:d.get(l.extname(r))}function P(e,t){return l.relative(t,e)||l.basename(e)}function F(e){process.stdout.write(e)}function I(e){process.stderr.write(e)}function L(e){return e instanceof Error?e.message:String(e)}export{O as addCrossFileDuplication,k as collectDuplicatedLineNumbers,_ as configSearchDirectory,L as formatError,P as formatPath,N as getLanguage,M as isScannedPath,g as resolveTarget,y as scanListedFiles,v as scanTarget,I as writeStderr,F as writeStdout};
2
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.js","names":[],"sources":["../src/scan.ts"],"sourcesContent":["import { lstat, readdir, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData } from './duplication.js';\nimport { collectCrossFileDuplicationFileData, measureCode } from './metrics.js';\nimport { NativeAddonError } from './nativeMetrics.js';\nimport type { CodeMetrics, DuplicationOptions, LanguageName } from './types.js';\n\n/** The scan settings shared by every command (a structural subset of each command's options). */\nexport interface ScanOptions {\n duplication: Required<DuplicationOptions>;\n includeTests: boolean;\n}\n\nexport interface FileMetrics {\n file: string;\n metrics: CodeMetrics;\n /** Cross-file duplicate candidates and token/statement data, collected only for directory scans. */\n duplicationCandidates?: CrossFileDuplicationFileData;\n}\n\nexport interface ScanResult {\n crossFileDuplication?: CrossFileDuplicationMetrics;\n displayRoot: string;\n errors: string[];\n /** Non-fatal degradations (e.g. cross-file candidates unavailable); the file is still measured. */\n warnings: string[];\n fatalError?: string;\n files: FileMetrics[];\n}\n\nconst languageByExtension = new Map<string, LanguageName>([\n ['.c', 'c'],\n ['.c++', 'cpp'],\n ['.cc', 'cpp'],\n ['.cjs', 'javascript'],\n ['.cp', 'cpp'],\n ['.cpp', 'cpp'],\n ['.tcc', 'cpp'],\n ['.cts', 'typescript'],\n ['.cxx', 'cpp'],\n ['.go', 'go'],\n // Headers may be C or C++; the C++ grammar parses both.\n ['.h', 'cpp'],\n ['.hh', 'cpp'],\n ['.hpp', 'cpp'],\n ['.hxx', 'cpp'],\n ['.java', 'java'],\n ['.js', 'javascript'],\n ['.jsx', 'jsx'],\n ['.mjs', 'javascript'],\n ['.mts', 'typescript'],\n ['.py', 'python'],\n ['.rb', 'ruby'],\n ['.rs', 'rust'],\n ['.ts', 'typescript'],\n ['.tsx', 'tsx'],\n]);\n\nconst ignoredDirectoryNames = new Set([\n '.agents',\n '.claude',\n '.cursor',\n '.git',\n '.next',\n '.playwright-cli',\n '.tox',\n '.tmp',\n '.turbo',\n '.venv',\n '.yarn',\n '__fixtures__',\n '__generated__',\n '__pycache__',\n 'coverage',\n 'dist',\n 'fixtures',\n 'generated',\n 'node_modules',\n 'target',\n 'test-fixtures',\n 'vendor',\n 'venv',\n]);\n\nconst testDirectoryNames = new Set(['__tests__', 'test', 'tests', 'spec']);\nconst testFilePattern = /(?:^test(?:[_-].*)?|\\.(?:spec|test)|[_-](?:test|spec))\\.[^.]+$/iu;\n// JUnit tests use a case-sensitive `Test.java` suffix; case-insensitive matching would catch\n// production files like `contest.java`.\nconst javaTestFilePattern = /Test\\.java$/u;\n\nexport function resolveTarget(target: string): string {\n if (target === '~') {\n return os.homedir();\n }\n\n if (target.startsWith('~/')) {\n return path.join(os.homedir(), target.slice(2));\n }\n\n return path.resolve(target);\n}\n\n/** Returns the directory from which the config file search should start (the target itself if it is a directory). */\nexport async function configSearchDirectory(target: string): Promise<string> {\n try {\n const targetStat = await stat(target);\n return targetStat.isDirectory() ? target : path.dirname(target);\n } catch {\n return path.dirname(target);\n }\n}\n\n/** Shared state of one scan, threaded through the directory walk instead of positional plumbing. */\ninterface ScanContext {\n options: ScanOptions;\n files: FileMetrics[];\n errors: string[];\n warnings: string[];\n visitedDirectories: Set<string>;\n visitedFiles: Set<string>;\n /** Scan root: paths are displayed relative to it, and symbolic links may not escape it. */\n rootDirectory: string;\n}\n\nexport async function scanTarget(target: string, options: ScanOptions): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n let canonicalTarget = target;\n try {\n canonicalTarget = await realpath(target);\n } catch {\n // stat below reports missing targets with the original path.\n }\n\n const fallbackDisplayRoot = path.dirname(canonicalTarget);\n let targetStat;\n\n try {\n targetStat = await stat(canonicalTarget);\n } catch (error) {\n const fatalError = `${formatPath(canonicalTarget, fallbackDisplayRoot)}: ${formatError(error)}`;\n return { displayRoot: fallbackDisplayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n if (targetStat.isFile()) {\n const displayRoot = path.dirname(canonicalTarget);\n const language = getLanguage(canonicalTarget, options, true);\n if (!language) {\n const fatalError = `${formatPath(canonicalTarget, displayRoot)}: unsupported file type`;\n return { displayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n const context = makeScanContext(options, files, errors, warnings, displayRoot);\n try {\n await measureFile(canonicalTarget, language, 'single-file', context, canonicalTarget);\n } catch (error) {\n return toFatalResult(error, displayRoot, files, errors, warnings);\n }\n return { displayRoot, files, errors, warnings };\n }\n\n try {\n await scanDirectory(canonicalTarget, makeScanContext(options, files, errors, warnings, canonicalTarget));\n } catch (error) {\n return toFatalResult(error, canonicalTarget, files, errors, warnings);\n }\n return { displayRoot: canonicalTarget, files, errors, warnings };\n}\n\n/**\n * Measures an explicit list of repository-relative files (the diff gate's git-visible allowlist)\n * instead of walking the directory tree, so ignored artifact directories are never parsed. Paths\n * outside the scan scope (ignored/test directories, unsupported or test file names) are skipped\n * with the same rules as the walk.\n */\nexport async function scanListedFiles(\n rootDirectory: string,\n relativePaths: Iterable<string>,\n options: ScanOptions\n): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n const context = makeScanContext(options, files, errors, warnings, rootDirectory);\n for (const relativePath of relativePaths) {\n const language = isScannedPath(relativePath, options) ? getLanguage(relativePath, options) : undefined;\n if (!language) {\n continue;\n }\n const absolutePath = path.join(rootDirectory, relativePath);\n // Symbolic links are not source files: git stores only their target string, so measuring\n // through them would diverge from what any revision of the repository actually contains.\n const stats = await lstat(absolutePath).catch(() => {});\n if (stats?.isSymbolicLink()) {\n continue;\n }\n try {\n await measureFile(absolutePath, language, 'directory', context);\n } catch (error) {\n return toFatalResult(error, rootDirectory, files, errors, warnings);\n }\n }\n return { displayRoot: rootDirectory, files, errors, warnings };\n}\n\n/** A run-wide failure (a missing native addon) as a fatal result; anything else keeps throwing. */\nfunction toFatalResult(\n error: unknown,\n displayRoot: string,\n files: FileMetrics[],\n errors: string[],\n warnings: string[]\n): ScanResult {\n if (!(error instanceof NativeAddonError)) {\n throw error;\n }\n const fatalError = formatError(error);\n // Errors the walk accumulated before the fatal failure stay reported alongside it.\n return { displayRoot, files, errors: [...errors, fatalError], warnings, fatalError };\n}\n\nfunction makeScanContext(\n options: ScanOptions,\n files: FileMetrics[],\n errors: string[],\n warnings: string[],\n rootDirectory: string\n): ScanContext {\n return { options, files, errors, warnings, visitedDirectories: new Set(), visitedFiles: new Set(), rootDirectory };\n}\n\n/** Runs a filesystem operation, recording a scan error and returning undefined when it fails. */\nasync function tryFileSystem<T>(\n operation: () => Promise<T>,\n target: string,\n context: ScanContext\n): Promise<T | undefined> {\n try {\n return await operation();\n } catch (error) {\n context.errors.push(`${formatPath(target, context.rootDirectory)}: ${formatError(error)}`);\n return undefined;\n }\n}\n\n/** Resolves the path (recording errors); undefined when that fails or the result escapes the root. */\nasync function resolveWithinRoot(target: string, context: ScanContext): Promise<string | undefined> {\n const resolved = await tryFileSystem(() => realpath(target), target, context);\n return resolved !== undefined && isWithinDirectory(resolved, context.rootDirectory) ? resolved : undefined;\n}\n\nasync function scanDirectory(directory: string, context: ScanContext): Promise<void> {\n const resolvedDirectory = await resolveWithinRoot(directory, context);\n if (resolvedDirectory === undefined || context.visitedDirectories.has(resolvedDirectory)) {\n return;\n }\n context.visitedDirectories.add(resolvedDirectory);\n\n const entries = await tryFileSystem(() => readdir(directory, { withFileTypes: true }), directory, context);\n if (entries === undefined) {\n return;\n }\n\n for (const entry of entries) {\n const entryPath = path.join(directory, entry.name);\n if (entry.isSymbolicLink()) {\n await scanSymbolicLink(entry.name, entryPath, context);\n continue;\n }\n\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name, context.options)) {\n continue;\n }\n await scanDirectory(entryPath, context);\n continue;\n }\n\n if (entry.isFile()) {\n await measureScannableFile(entryPath, context);\n }\n }\n}\n\nasync function scanSymbolicLink(name: string, entryPath: string, context: ScanContext): Promise<void> {\n const resolvedPath = await resolveWithinRoot(entryPath, context);\n if (resolvedPath === undefined) {\n return;\n }\n\n const entryStat = await tryFileSystem(() => stat(entryPath), entryPath, context);\n if (entryStat === undefined) {\n return;\n }\n\n if (entryStat.isDirectory()) {\n if (\n shouldSkipDirectory(name, context.options) ||\n shouldSkipDirectory(path.basename(resolvedPath), context.options)\n ) {\n return;\n }\n await scanDirectory(entryPath, context);\n return;\n }\n\n if (entryStat.isFile()) {\n await measureScannableFile(entryPath, context, resolvedPath, resolvedPath);\n }\n}\n\nasync function measureScannableFile(\n file: string,\n context: ScanContext,\n languageFile = file,\n realFile?: string\n): Promise<void> {\n const language = getLanguage(languageFile, context.options);\n if (language) {\n await measureFile(file, language, 'directory', context, realFile);\n }\n}\n\nasync function measureFile(\n file: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext,\n realFile?: string\n): Promise<void> {\n try {\n const resolvedFile = realFile ?? (await realpath(file));\n if (context.visitedFiles.has(resolvedFile)) {\n return;\n }\n context.visitedFiles.add(resolvedFile);\n\n const code = await readFile(file, 'utf8');\n const measureOptions = { language, duplication: context.options.duplication };\n const fileMetrics: FileMetrics = { file, metrics: measureCode(code, measureOptions) };\n // Only directory scans compare files against each other; a single-file target has no peers.\n // Candidate collection is an auxiliary pass: if it fails where measureCode succeeded (an\n // addon error specific to this pass), that must not discard the measured metrics.\n if (mode === 'directory') {\n try {\n fileMetrics.duplicationCandidates = collectCrossFileDuplicationFileData(code, measureOptions);\n } catch (error) {\n // A warning, not an error: the file's metrics are complete, only its participation in\n // cross-file matching is lost, so it is not \"skipped\" and must not fail --fail-on-error.\n context.warnings.push(\n `${formatPath(file, context.rootDirectory)}: cross-file duplication candidates unavailable: ${formatError(error)}`\n );\n }\n }\n context.files.push(fileMetrics);\n } catch (error) {\n // A missing native addon fails every file identically: propagate it once as a fatal scan\n // error instead of recording one \"skipped\" entry per file behind a successful exit code.\n if (error instanceof NativeAddonError) {\n throw error;\n }\n context.errors.push(`${formatPath(file, context.rootDirectory)}: ${formatError(error)}`);\n }\n}\n\n/** Runs after the scan so every measured file's candidates participate. */\nexport function addCrossFileDuplication(result: ScanResult, options: ScanOptions): void {\n if (result.fatalError || result.files.length < 2) {\n return;\n }\n const sourceFiles = result.files.flatMap(({ file, duplicationCandidates }) =>\n duplicationCandidates ? [{ file: formatPath(file, result.displayRoot), ...duplicationCandidates }] : []\n );\n if (sourceFiles.length < 2) {\n return;\n }\n result.crossFileDuplication = measureCrossFileDuplication(sourceFiles, options.duplication);\n}\n\n/**\n * Distinct 1-based code lines covered by within-file or cross-file duplicated content. Both\n * sources expose the exact lines carrying matched tokens (never block bounding ranges, which\n * would over-count comment/blank lines and the unmatched gap of a merged clone), so the union is\n * a subset of the file's code lines and a ratio derived over code lines can never exceed 1.\n */\nexport function collectDuplicatedLineNumbers(\n metrics: CodeMetrics | undefined,\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n formattedFile: string\n): Set<number> {\n const lines = new Set(metrics?.duplication.duplicateLineNumbers);\n // Object.hasOwn: a file named like an Object.prototype member must not read an inherited value.\n const crossFileLines =\n crossFileDuplication && Object.hasOwn(crossFileDuplication.duplicateLineNumbersByFile, formattedFile)\n ? (crossFileDuplication.duplicateLineNumbersByFile[formattedFile] ?? [])\n : [];\n for (const line of crossFileLines) {\n lines.add(line);\n }\n return lines;\n}\n\nfunction shouldSkipDirectory(name: string, options: ScanOptions): boolean {\n if (ignoredDirectoryNames.has(name)) {\n return true;\n }\n\n if (options.includeTests) {\n return false;\n }\n\n return testDirectoryNames.has(name);\n}\n\nfunction isWithinDirectory(candidate: string, directory: string): boolean {\n const relative = path.relative(directory, candidate);\n return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));\n}\n\n/**\n * Whether a repository-relative path would be scanned: no ignored or excluded-test directory\n * segment and a supported, non-test file name. The diff gate uses this for base-revision\n * eligibility, so code renamed into scan scope gates as new code instead of ratcheting against\n * a blob the scanner would never have measured.\n */\nexport function isScannedPath(relativePath: string, options: ScanOptions): boolean {\n const segments = relativePath.split('/');\n for (const segment of segments.slice(0, -1)) {\n if (ignoredDirectoryNames.has(segment) || (!options.includeTests && testDirectoryNames.has(segment))) {\n return false;\n }\n }\n return getLanguage(relativePath, options) !== undefined;\n}\n\nexport function getLanguage(file: string, options: ScanOptions, explicitTarget = false): LanguageName | undefined {\n const lowerFile = file.toLowerCase();\n if (\n !explicitTarget &&\n (lowerFile.endsWith('.d.ts') ||\n lowerFile.endsWith('.d.mts') ||\n lowerFile.endsWith('.d.cts') ||\n lowerFile.endsWith('.min.js') ||\n lowerFile.endsWith('.pnp.cjs'))\n ) {\n return undefined;\n }\n\n if (\n !explicitTarget &&\n !options.includeTests &&\n (testFilePattern.test(path.basename(file)) || javaTestFilePattern.test(path.basename(file)))\n ) {\n return undefined;\n }\n\n // GCC treats an uppercase `.C` as C++; lowercasing first would misparse it with the C grammar.\n if (path.extname(file) === '.C') {\n return 'cpp';\n }\n\n return languageByExtension.get(path.extname(lowerFile));\n}\n\nexport function formatPath(file: string, base: string): string {\n return path.relative(base, file) || path.basename(file);\n}\n\nexport function writeStdout(message: string): void {\n process.stdout.write(message);\n}\n\nexport function writeStderr(message: string): void {\n process.stderr.write(message);\n}\n\nexport function formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"8VAgCA,MAAM,EAAsB,IAAI,IAA0B,CACxD,CAAC,KAAM,GAAG,EACV,CAAC,OAAQ,KAAK,EACd,CAAC,MAAO,KAAK,EACb,CAAC,OAAQ,YAAY,EACrB,CAAC,MAAO,KAAK,EACb,CAAC,OAAQ,KAAK,EACd,CAAC,OAAQ,KAAK,EACd,CAAC,OAAQ,YAAY,EACrB,CAAC,OAAQ,KAAK,EACd,CAAC,MAAO,IAAI,EAEZ,CAAC,KAAM,KAAK,EACZ,CAAC,MAAO,KAAK,EACb,CAAC,OAAQ,KAAK,EACd,CAAC,OAAQ,KAAK,EACd,CAAC,QAAS,MAAM,EAChB,CAAC,MAAO,YAAY,EACpB,CAAC,OAAQ,KAAK,EACd,CAAC,OAAQ,YAAY,EACrB,CAAC,OAAQ,YAAY,EACrB,CAAC,MAAO,QAAQ,EAChB,CAAC,MAAO,MAAM,EACd,CAAC,MAAO,MAAM,EACd,CAAC,MAAO,YAAY,EACpB,CAAC,OAAQ,KAAK,CAChB,CAAC,EAEK,EAAwB,IAAI,IAAI,CACpC,UACA,UACA,UACA,OACA,QACA,kBACA,OACA,OACA,SACA,QACA,QACA,eACA,gBACA,cACA,WACA,OACA,WACA,YACA,eACA,SACA,gBACA,SACA,MACF,CAAC,EAEK,EAAqB,IAAI,IAAI,CAAC,YAAa,OAAQ,QAAS,MAAM,CAAC,EACnE,EAAkB,mEAGlB,EAAsB,eAE5B,SAAgB,EAAc,EAAwB,CASpD,OARI,IAAW,IACN,EAAG,QAAQ,EAGhB,EAAO,WAAW,IAAI,EACjB,EAAK,KAAK,EAAG,QAAQ,EAAG,EAAO,MAAM,CAAC,CAAC,EAGzC,EAAK,QAAQ,CAAM,CAC5B,CAGA,eAAsB,EAAsB,EAAiC,CAC3E,GAAI,CAEF,OAAO,MADkB,EAAK,CAAM,EAAA,CAClB,YAAY,EAAI,EAAS,EAAK,QAAQ,CAAM,CAChE,MAAQ,CACN,OAAO,EAAK,QAAQ,CAAM,CAC5B,CACF,CAcA,eAAsB,EAAW,EAAgB,EAA2C,CAC1F,IAAM,EAAuB,CAAC,EACxB,EAAmB,CAAC,EACpB,EAAqB,CAAC,EACxB,EAAkB,EACtB,GAAI,CACF,EAAkB,MAAM,EAAS,CAAM,CACzC,MAAQ,CAER,CAEA,IAAM,EAAsB,EAAK,QAAQ,CAAe,EACpD,EAEJ,GAAI,CACF,EAAa,MAAM,EAAK,CAAe,CACzC,OAAS,EAAO,CACd,IAAM,EAAa,GAAG,EAAW,EAAiB,CAAmB,EAAE,IAAI,EAAY,CAAK,IAC5F,MAAO,CAAE,YAAa,EAAqB,QAAO,OAAQ,CAAC,CAAU,EAAG,WAAU,YAAW,CAC/F,CAEA,GAAI,EAAW,OAAO,EAAG,CACvB,IAAM,EAAc,EAAK,QAAQ,CAAe,EAC1C,EAAW,EAAY,EAAiB,EAAS,EAAI,EAC3D,GAAI,CAAC,EAAU,CACb,IAAM,EAAa,GAAG,EAAW,EAAiB,CAAW,EAAE,yBAC/D,MAAO,CAAE,cAAa,QAAO,OAAQ,CAAC,CAAU,EAAG,WAAU,YAAW,CAC1E,CAEA,IAAM,EAAU,EAAgB,EAAS,EAAO,EAAQ,EAAU,CAAW,EAC7E,GAAI,CACF,MAAM,EAAY,EAAiB,EAAU,cAAe,EAAS,CAAe,CACtF,OAAS,EAAO,CACd,OAAO,EAAc,EAAO,EAAa,EAAO,EAAQ,CAAQ,CAClE,CACA,MAAO,CAAE,cAAa,QAAO,SAAQ,UAAS,CAChD,CAEA,GAAI,CACF,MAAM,EAAc,EAAiB,EAAgB,EAAS,EAAO,EAAQ,EAAU,CAAe,CAAC,CACzG,OAAS,EAAO,CACd,OAAO,EAAc,EAAO,EAAiB,EAAO,EAAQ,CAAQ,CACtE,CACA,MAAO,CAAE,YAAa,EAAiB,QAAO,SAAQ,UAAS,CACjE,CAQA,eAAsB,EACpB,EACA,EACA,EACqB,CACrB,IAAM,EAAuB,CAAC,EACxB,EAAmB,CAAC,EACpB,EAAqB,CAAC,EACtB,EAAU,EAAgB,EAAS,EAAO,EAAQ,EAAU,CAAa,EAC/E,IAAK,IAAM,KAAgB,EAAe,CACxC,IAAM,EAAW,EAAc,EAAc,CAAO,EAAI,EAAY,EAAc,CAAO,EAAI,IAAA,GAC7F,GAAI,CAAC,EACH,SAEF,IAAM,EAAe,EAAK,KAAK,EAAe,CAAY,EAI1D,KAAI,MADgB,EAAM,CAAY,CAAC,CAAC,UAAY,CAAC,CAAC,EAAA,EAC3C,eAAe,EAG1B,GAAI,CACF,MAAM,EAAY,EAAc,EAAU,YAAa,CAAO,CAChE,OAAS,EAAO,CACd,OAAO,EAAc,EAAO,EAAe,EAAO,EAAQ,CAAQ,CACpE,CACF,CACA,MAAO,CAAE,YAAa,EAAe,QAAO,SAAQ,UAAS,CAC/D,CAGA,SAAS,EACP,EACA,EACA,EACA,EACA,EACY,CACZ,GAAI,EAAE,aAAiB,GACrB,MAAM,EAER,IAAM,EAAa,EAAY,CAAK,EAEpC,MAAO,CAAE,cAAa,QAAO,OAAQ,CAAC,GAAG,EAAQ,CAAU,EAAG,WAAU,YAAW,CACrF,CAEA,SAAS,EACP,EACA,EACA,EACA,EACA,EACa,CACb,MAAO,CAAE,UAAS,QAAO,SAAQ,WAAU,mBAAoB,IAAI,IAAO,aAAc,IAAI,IAAO,eAAc,CACnH,CAGA,eAAe,EACb,EACA,EACA,EACwB,CACxB,GAAI,CACF,OAAO,MAAM,EAAU,CACzB,OAAS,EAAO,CACd,EAAQ,OAAO,KAAK,GAAG,EAAW,EAAQ,EAAQ,aAAa,EAAE,IAAI,EAAY,CAAK,GAAG,EACzF,MACF,CACF,CAGA,eAAe,EAAkB,EAAgB,EAAmD,CAClG,IAAM,EAAW,MAAM,MAAoB,EAAS,CAAM,EAAG,EAAQ,CAAO,EAC5E,OAAO,IAAa,IAAA,IAAa,EAAkB,EAAU,EAAQ,aAAa,EAAI,EAAW,IAAA,EACnG,CAEA,eAAe,EAAc,EAAmB,EAAqC,CACnF,IAAM,EAAoB,MAAM,EAAkB,EAAW,CAAO,EACpE,GAAI,IAAsB,IAAA,IAAa,EAAQ,mBAAmB,IAAI,CAAiB,EACrF,OAEF,EAAQ,mBAAmB,IAAI,CAAiB,EAEhD,IAAM,EAAU,MAAM,MAAoB,EAAQ,EAAW,CAAE,cAAe,EAAK,CAAC,EAAG,EAAW,CAAO,EACrG,OAAY,IAAA,GAIhB,IAAK,IAAM,KAAS,EAAS,CAC3B,IAAM,EAAY,EAAK,KAAK,EAAW,EAAM,IAAI,EACjD,GAAI,EAAM,eAAe,EAAG,CAC1B,MAAM,EAAiB,EAAM,KAAM,EAAW,CAAO,EACrD,QACF,CAEA,GAAI,EAAM,YAAY,EAAG,CACvB,GAAI,EAAoB,EAAM,KAAM,EAAQ,OAAO,EACjD,SAEF,MAAM,EAAc,EAAW,CAAO,EACtC,QACF,CAEI,EAAM,OAAO,GACf,MAAM,EAAqB,EAAW,CAAO,CAEjD,CACF,CAEA,eAAe,EAAiB,EAAc,EAAmB,EAAqC,CACpG,IAAM,EAAe,MAAM,EAAkB,EAAW,CAAO,EAC/D,GAAI,IAAiB,IAAA,GACnB,OAGF,IAAM,EAAY,MAAM,MAAoB,EAAK,CAAS,EAAG,EAAW,CAAO,EAC3E,OAAc,IAAA,GAIlB,IAAI,EAAU,YAAY,EAAG,CAC3B,GACE,EAAoB,EAAM,EAAQ,OAAO,GACzC,EAAoB,EAAK,SAAS,CAAY,EAAG,EAAQ,OAAO,EAEhE,OAEF,MAAM,EAAc,EAAW,CAAO,EACtC,MACF,CAEI,EAAU,OAAO,GACnB,MAAM,EAAqB,EAAW,EAAS,EAAc,CAAY,CAH3E,CAKF,CAEA,eAAe,EACb,EACA,EACA,EAAe,EACf,EACe,CACf,IAAM,EAAW,EAAY,EAAc,EAAQ,OAAO,EACtD,GACF,MAAM,EAAY,EAAM,EAAU,YAAa,EAAS,CAAQ,CAEpE,CAEA,eAAe,EACb,EACA,EACA,EACA,EACA,EACe,CACf,GAAI,CACF,IAAM,EAAe,GAAa,MAAM,EAAS,CAAI,EACrD,GAAI,EAAQ,aAAa,IAAI,CAAY,EACvC,OAEF,EAAQ,aAAa,IAAI,CAAY,EAErC,IAAM,EAAO,MAAM,EAAS,EAAM,MAAM,EAClC,EAAiB,CAAE,WAAU,YAAa,EAAQ,QAAQ,WAAY,EACtE,EAA2B,CAAE,OAAM,QAAS,EAAY,EAAM,CAAc,CAAE,EAIpF,GAAI,IAAS,YACX,GAAI,CACF,EAAY,sBAAwB,EAAoC,EAAM,CAAc,CAC9F,OAAS,EAAO,CAGd,EAAQ,SAAS,KACf,GAAG,EAAW,EAAM,EAAQ,aAAa,EAAE,mDAAmD,EAAY,CAAK,GACjH,CACF,CAEF,EAAQ,MAAM,KAAK,CAAW,CAChC,OAAS,EAAO,CAGd,GAAI,aAAiB,EACnB,MAAM,EAER,EAAQ,OAAO,KAAK,GAAG,EAAW,EAAM,EAAQ,aAAa,EAAE,IAAI,EAAY,CAAK,GAAG,CACzF,CACF,CAGA,SAAgB,EAAwB,EAAoB,EAA4B,CACtF,GAAI,EAAO,YAAc,EAAO,MAAM,OAAS,EAC7C,OAEF,IAAM,EAAc,EAAO,MAAM,SAAS,CAAE,OAAM,2BAChD,EAAwB,CAAC,CAAE,KAAM,EAAW,EAAM,EAAO,WAAW,EAAG,GAAG,CAAsB,CAAC,EAAI,CAAC,CACxG,EACI,EAAY,OAAS,IAGzB,EAAO,qBAAuB,EAA4B,EAAa,EAAQ,WAAW,EAC5F,CAQA,SAAgB,EACd,EACA,EACA,EACa,CACb,IAAM,EAAQ,IAAI,IAAI,GAAS,YAAY,oBAAoB,EAEzD,EACJ,GAAwB,OAAO,OAAO,EAAqB,2BAA4B,CAAa,EAC/F,EAAqB,2BAA2B,IAAkB,CAAC,EACpE,CAAC,EACP,IAAK,IAAM,KAAQ,EACjB,EAAM,IAAI,CAAI,EAEhB,OAAO,CACT,CAEA,SAAS,EAAoB,EAAc,EAA+B,CASxE,OARI,EAAsB,IAAI,CAAI,EACzB,GAGT,CAAI,EAAQ,cAIL,EAAmB,IAAI,CAAI,CACpC,CAEA,SAAS,EAAkB,EAAmB,EAA4B,CACxE,IAAM,EAAW,EAAK,SAAS,EAAW,CAAS,EACnD,OAAO,IAAa,IAAO,IAAa,MAAQ,CAAC,EAAS,WAAW,KAAK,EAAK,KAAK,GAAK,CAAC,EAAK,WAAW,CAAQ,CACpH,CAQA,SAAgB,EAAc,EAAsB,EAA+B,CACjF,IAAM,EAAW,EAAa,MAAM,GAAG,EACvC,IAAK,IAAM,KAAW,EAAS,MAAM,EAAG,EAAE,EACxC,GAAI,EAAsB,IAAI,CAAO,GAAM,CAAC,EAAQ,cAAgB,EAAmB,IAAI,CAAO,EAChG,MAAO,GAGX,OAAO,EAAY,EAAc,CAAO,IAAM,IAAA,EAChD,CAEA,SAAgB,EAAY,EAAc,EAAsB,EAAiB,GAAiC,CAChH,IAAM,EAAY,EAAK,YAAY,EAEjC,MAAC,IACA,EAAU,SAAS,OAAO,GACzB,EAAU,SAAS,QAAQ,GAC3B,EAAU,SAAS,QAAQ,GAC3B,EAAU,SAAS,SAAS,GAC5B,EAAU,SAAS,UAAU,KAM/B,GAAC,GACD,CAAC,EAAQ,eACR,EAAgB,KAAK,EAAK,SAAS,CAAI,CAAC,GAAK,EAAoB,KAAK,EAAK,SAAS,CAAI,CAAC,IAU5F,OAJI,EAAK,QAAQ,CAAI,IAAM,KAClB,MAGF,EAAoB,IAAI,EAAK,QAAQ,CAAS,CAAC,CACxD,CAEA,SAAgB,EAAW,EAAc,EAAsB,CAC7D,OAAO,EAAK,SAAS,EAAM,CAAI,GAAK,EAAK,SAAS,CAAI,CACxD,CAEA,SAAgB,EAAY,EAAuB,CACjD,QAAQ,OAAO,MAAM,CAAO,CAC9B,CAEA,SAAgB,EAAY,EAAuB,CACjD,QAAQ,OAAO,MAAM,CAAO,CAC9B,CAEA,SAAgB,EAAY,EAAwB,CAClD,OAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAC9D"}
package/dist/types.d.ts CHANGED
@@ -1,20 +1,12 @@
1
1
  export type SupportedLanguage = 'c' | 'cpp' | 'go' | 'java' | 'javascript' | 'jsx' | 'python' | 'ruby' | 'rust' | 'typescript' | 'tsx';
2
2
  export type LanguageName = SupportedLanguage | (string & {});
3
- export type ParserLanguage = unknown;
3
+ /**
4
+ * A built-in language as enumerated by the API. Grammars and per-language node-type configuration
5
+ * live in the Rust addon, so a definition only names the language and its accepted aliases.
6
+ */
4
7
  export interface LanguageDefinition {
5
8
  name: LanguageName;
6
- parserLanguage: ParserLanguage;
7
9
  aliases?: readonly string[];
8
- functionNodeTypes?: readonly string[];
9
- decisionNodeTypes?: readonly string[];
10
- nestingNodeTypes?: readonly string[];
11
- /** Node types that each count as one non-commenting source statement (NCSS). */
12
- ncssNodeTypes?: readonly string[];
13
- /**
14
- * Node types whose direct named children count as statements even without a dedicated statement
15
- * node type (expression-oriented grammars: Ruby bodies, Rust trailing block expressions).
16
- */
17
- ncssContainerNodeTypes?: readonly string[];
18
10
  }
19
11
  /** Detection settings for within-file and cross-file duplication. */
20
12
  export interface DuplicationOptions {
@@ -38,7 +30,7 @@ export interface DuplicationOptions {
38
30
  export interface MeasureOptions {
39
31
  language: LanguageName;
40
32
  includeSyntaxTree?: boolean;
41
- /** Duplication detection settings; non-default values disable the native backend for the call. */
33
+ /** Duplication detection settings. */
42
34
  duplication?: DuplicationOptions;
43
35
  }
44
36
  export interface LineMetrics {
@@ -78,6 +70,19 @@ export interface FunctionMetrics {
78
70
  */
79
71
  ncss: number;
80
72
  parameterCount: number;
73
+ /**
74
+ * Halstead metrics of the function's whole subtree. Nested function bodies are included, so
75
+ * summing over functions counts shared regions more than once (like ncss).
76
+ */
77
+ halstead: HalsteadMetrics;
78
+ /**
79
+ * Approximate def-use dependency degree (DepDegree, Beyer & Fararooy 2010): the number of
80
+ * variable reads whose name has a preceding definition (declaration, assignment, or parameter)
81
+ * within the same function. A file-local single-assignment approximation — each read is charged
82
+ * one reaching definition — which is stable enough for regression ratcheting where only the
83
+ * delta matters.
84
+ */
85
+ depDegree: number;
81
86
  }
82
87
  /**
83
88
  * Within-file structural duplication: copy-pasted regions whose normalized token sequence repeats.