code-gauge 4.7.1 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +35 -10
  2. package/THIRD-PARTY-NOTICES.txt +595 -29
  3. package/dist/cli.cjs +3 -3
  4. package/dist/cli.cjs.map +1 -1
  5. package/dist/cli.js +3 -3
  6. package/dist/cli.js.map +1 -1
  7. package/dist/cliConfig.cjs +1 -1
  8. package/dist/cliConfig.cjs.map +1 -1
  9. package/dist/cliConfig.d.ts +13 -3
  10. package/dist/cliConfig.js +1 -1
  11. package/dist/cliConfig.js.map +1 -1
  12. package/dist/crossFileDuplication.cjs +1 -1
  13. package/dist/crossFileDuplication.cjs.map +1 -1
  14. package/dist/crossFileDuplication.js +1 -1
  15. package/dist/crossFileDuplication.js.map +1 -1
  16. package/dist/crossFileNearMiss.cjs +1 -1
  17. package/dist/crossFileNearMiss.cjs.map +1 -1
  18. package/dist/crossFileNearMiss.js +1 -1
  19. package/dist/crossFileNearMiss.js.map +1 -1
  20. package/dist/diffCommand.cjs +3 -3
  21. package/dist/diffCommand.cjs.map +1 -1
  22. package/dist/diffCommand.js +3 -3
  23. package/dist/diffCommand.js.map +1 -1
  24. package/dist/duplicateSelection.cjs +1 -1
  25. package/dist/duplicateSelection.cjs.map +1 -1
  26. package/dist/duplicateSelection.js +1 -1
  27. package/dist/duplicateSelection.js.map +1 -1
  28. package/dist/duplication.cjs +1 -1
  29. package/dist/duplication.cjs.map +1 -1
  30. package/dist/duplication.js +1 -1
  31. package/dist/duplication.js.map +1 -1
  32. package/dist/exclusion.cjs +2 -0
  33. package/dist/exclusion.cjs.map +1 -0
  34. package/dist/exclusion.d.ts +30 -0
  35. package/dist/exclusion.js +2 -0
  36. package/dist/exclusion.js.map +1 -0
  37. package/dist/git.cjs +1 -1
  38. package/dist/git.cjs.map +1 -1
  39. package/dist/git.d.ts +16 -0
  40. package/dist/git.js +1 -1
  41. package/dist/git.js.map +1 -1
  42. package/dist/index.cjs +1 -1
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.js +1 -1
  45. package/dist/languages.cjs +1 -1
  46. package/dist/languages.cjs.map +1 -1
  47. package/dist/languages.js +1 -1
  48. package/dist/languages.js.map +1 -1
  49. package/dist/newDuplication.cjs +2 -0
  50. package/dist/newDuplication.cjs.map +1 -0
  51. package/dist/newDuplication.d.ts +40 -0
  52. package/dist/newDuplication.js +2 -0
  53. package/dist/newDuplication.js.map +1 -0
  54. package/dist/regressionGate.cjs +1 -1
  55. package/dist/regressionGate.cjs.map +1 -1
  56. package/dist/regressionGate.d.ts +8 -6
  57. package/dist/regressionGate.js +1 -1
  58. package/dist/regressionGate.js.map +1 -1
  59. package/dist/scan.cjs +1 -1
  60. package/dist/scan.cjs.map +1 -1
  61. package/dist/scan.d.ts +12 -3
  62. package/dist/scan.js +1 -1
  63. package/dist/scan.js.map +1 -1
  64. package/dist/worker.cjs +1 -1
  65. package/dist/worker.cjs.map +1 -1
  66. package/dist/worker.js +1 -1
  67. package/dist/worker.js.map +1 -1
  68. package/native/Cargo.lock +47 -15
  69. package/native/Cargo.toml +9 -9
  70. package/native/code-gauge.wasm +0 -0
  71. package/native/src/complexity.rs +26 -33
  72. package/native/src/dep_degree.rs +43 -32
  73. package/native/src/duplication.rs +158 -40
  74. package/native/src/functions.rs +20 -62
  75. package/native/src/languages.rs +28 -7
  76. package/native/src/measure.rs +16 -27
  77. package/native/src/ncss.rs +32 -23
  78. package/native/src/tree_index.rs +9 -0
  79. package/native/src/util.rs +12 -57
  80. package/package.json +10 -10
package/dist/scan.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"scan.cjs","names":["os","path","stat","realpath","lstat","readdir","readFile","measureCode","NativeAddonError","measureCodeWithCrossFileDataAsync","measureCrossFileDuplication","detectLanguage"],"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 { detectLanguage } from './languages.js';\nimport { measureCode, measureCodeWithCrossFileDataAsync } from './metrics.js';\nimport { NativeAddonError } from './nativeMetrics.js';\nimport type { CodeMetrics, DuplicationOptions, LanguageName, MeasureOptions } 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 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 // .NET SDK intermediate output (generated sources such as `*.GlobalUsings.g.cs`).\n 'obj',\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 (Java/Kotlin) and xUnit/NUnit (C#) tests use case-sensitive `Test`/`Tests` class-name\n// suffixes; case-insensitive matching would catch production files like `contest.java`.\nconst suffixTestFilePattern = /Tests?\\.(?:java|kt|cs)$/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 /**\n * Outcomes in walk order. Files are measured concurrently, so a measurement is recorded as a\n * promise here and applied in this order once the walk ends, keeping results deterministic.\n */\n outcomes: (ScanOutcome | Promise<ScanOutcome>)[];\n /** Measurements in flight, bounded so file contents and payloads do not pile up during the walk. */\n inFlight: Set<Promise<ScanOutcome>>;\n /** Set once a measurement fails fatally; the walk then starts no further work. */\n fatalSeen: boolean;\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\n/**\n * A missing native addon fails every file identically, so it ends the scan as one fatal error\n * instead of one \"skipped\" entry per file behind a successful exit code.\n */\ntype ScanOutcome = { file: FileMetrics; warning?: string } | { error: string } | { fatal: NativeAddonError };\n\n// Twice the addon's worker count keeps its pool busy while finished payloads are parsed.\nconst maxMeasurementsInFlight = os.availableParallelism() * 2;\n\nexport async function scanTarget(target: string, options: ScanOptions): Promise<ScanResult> {\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, displayRoot);\n await measureFile(canonicalTarget, language, 'single-file', context, canonicalTarget);\n return settleScan(context, displayRoot);\n }\n\n const context = makeScanContext(options, canonicalTarget);\n await scanDirectory(canonicalTarget, context);\n return settleScan(context, canonicalTarget);\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 context = makeScanContext(options, rootDirectory);\n for (const relativePath of relativePaths) {\n if (context.fatalSeen) {\n break;\n }\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 await measureFile(absolutePath, language, 'directory', context);\n }\n return settleScan(context, rootDirectory);\n}\n\n/** Applies the scan's outcomes in walk order once every measurement has settled. */\nasync function settleScan(context: ScanContext, displayRoot: string): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n for (const pending of context.outcomes) {\n const outcome = await pending;\n if ('fatal' in outcome) {\n const fatalError = formatError(outcome.fatal);\n // Errors the walk recorded before the fatal failure stay reported alongside it.\n return { displayRoot, files, errors: [...errors, fatalError], warnings, fatalError };\n }\n if ('error' in outcome) {\n errors.push(outcome.error);\n continue;\n }\n files.push(outcome.file);\n if (outcome.warning !== undefined) {\n warnings.push(outcome.warning);\n }\n }\n return { displayRoot, files, errors, warnings };\n}\n\nfunction makeScanContext(options: ScanOptions, rootDirectory: string): ScanContext {\n return {\n options,\n outcomes: [],\n inFlight: new Set(),\n fatalSeen: false,\n visitedDirectories: new Set(),\n visitedFiles: new Set(),\n rootDirectory,\n };\n}\n\nfunction recordError(context: ScanContext, target: string, error: unknown): void {\n context.outcomes.push({ error: `${formatPath(target, context.rootDirectory)}: ${formatError(error)}` });\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 recordError(context, target, 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 if (context.fatalSeen) {\n return;\n }\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\n/**\n * Resolves and deduplicates the file in walk order, then starts measuring it concurrently; its\n * outcome is recorded in walk order (see ScanContext.outcomes).\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 if (context.fatalSeen) {\n return;\n }\n let resolvedFile;\n try {\n resolvedFile = realFile ?? (await realpath(file));\n } catch (error) {\n recordError(context, file, error);\n return;\n }\n if (context.visitedFiles.has(resolvedFile)) {\n return;\n }\n context.visitedFiles.add(resolvedFile);\n\n // Any settled measurement frees its slot (removed by the callback below, which runs before the\n // race resumes), so one slow file never idles the pool.\n while (context.inFlight.size >= maxMeasurementsInFlight) {\n await Promise.race(context.inFlight);\n }\n const outcome = readAndMeasureFile(file, language, mode, context);\n context.inFlight.add(outcome);\n void outcome.then(() => context.inFlight.delete(outcome));\n context.outcomes.push(outcome);\n}\n\nasync function readAndMeasureFile(\n file: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext\n): Promise<ScanOutcome> {\n try {\n const code = await readFile(file, 'utf8');\n const measureOptions = { language, duplication: context.options.duplication };\n // Only directory scans compare files against each other; a single-file target has no peers.\n if (mode === 'single-file') {\n return { file: { file, metrics: measureCode(code, measureOptions) } };\n }\n const { metrics, crossFileData, crossFileError } = await measureWithCrossFileData(code, measureOptions);\n return {\n file: { file, metrics, duplicationCandidates: crossFileData },\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 warning:\n crossFileError === undefined\n ? undefined\n : `${formatPath(file, context.rootDirectory)}: cross-file duplication candidates unavailable: ${crossFileError}`,\n };\n } catch (error) {\n if (error instanceof NativeAddonError) {\n context.fatalSeen = true;\n return { fatal: error };\n }\n return { error: `${formatPath(file, context.rootDirectory)}: ${formatError(error)}` };\n }\n}\n\n/**\n * Measures a file together with its cross-file contribution from one parse. The contribution is\n * auxiliary: if collecting it fails where plain measurement succeeds (e.g. a payload too large to\n * cross the addon boundary), the metrics are still returned with the failure message, which\n * callers report as a warning rather than an error.\n */\nexport async function measureWithCrossFileData(\n code: string,\n measureOptions: MeasureOptions\n): Promise<{ metrics: CodeMetrics; crossFileData?: CrossFileDuplicationFileData; crossFileError?: string }> {\n try {\n return await measureCodeWithCrossFileDataAsync(code, measureOptions);\n } catch (error) {\n if (error instanceof NativeAddonError) {\n throw error;\n }\n return { metrics: measureCode(code, measureOptions), crossFileError: 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)) || suffixTestFilePattern.test(path.basename(file)))\n ) {\n return undefined;\n }\n\n return detectLanguage(file);\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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"}
1
+ {"version":3,"file":"scan.cjs","names":["os","path","stat","realpath","lstat","readFile","readdir","measureCode","NativeAddonError","measureCodeWithCrossFileDataAsync","measureCrossFileDuplication","detectLanguage"],"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 type { Exclusion } from './exclusion.js';\nimport { detectLanguage } from './languages.js';\nimport { measureCode, measureCodeWithCrossFileDataAsync } from './metrics.js';\nimport { NativeAddonError } from './nativeMetrics.js';\nimport type { CodeMetrics, DuplicationOptions, LanguageName, MeasureOptions } 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 * Loads the exclusion covering the files a directory scan found (absolute paths); an explicitly\n * targeted file is always measured.\n */\n loadExclusion?: (absolutePaths: readonly string[]) => Promise<Exclusion>;\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 /** Files skipped because their content is generated code (see Exclusion.isGeneratedCode). */\n generatedFiles: string[];\n fatalError?: string;\n files: FileMetrics[];\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 // .NET SDK intermediate output (generated sources such as `*.GlobalUsings.g.cs`).\n 'obj',\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 (Java/Kotlin) and xUnit/NUnit (C#) tests use case-sensitive `Test`/`Tests` class-name\n// suffixes; case-insensitive matching would catch production files like `contest.java`.\nconst suffixTestFilePattern = /Tests?\\.(?:java|kt|cs)$/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 /**\n * Outcomes in discovery order. Files are measured concurrently, so a measurement is recorded as\n * a promise here and applied in this order once all have settled, keeping results deterministic.\n */\n outcomes: (ScanOutcome | Promise<ScanOutcome>)[];\n /** Measurements in flight, bounded so file contents and payloads do not pile up. */\n inFlight: Set<Promise<ScanOutcome>>;\n /** Set once a measurement fails fatally; no further measurement starts. */\n fatalSeen: boolean;\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 /** Files the walk found, measured once the exclusion covering all of them is loaded. */\n candidates: ScanCandidate[];\n exclusion?: Exclusion;\n}\n\ninterface ScanCandidate {\n file: string;\n language: LanguageName;\n /** The resolved path when the file was reached through a symbolic link. */\n realFile?: string;\n}\n\n/**\n * A missing native addon fails every file identically, so it ends the scan as one fatal error\n * instead of one \"skipped\" entry per file behind a successful exit code.\n */\ntype ScanOutcome =\n | { file: FileMetrics; warning?: string }\n | { generatedFile: string }\n | { error: string }\n | { fatal: NativeAddonError };\n\n// Twice the addon's worker count keeps its pool busy while finished payloads are parsed.\nconst maxMeasurementsInFlight = os.availableParallelism() * 2;\n\nexport async function scanTarget(target: string, options: ScanOptions): Promise<ScanResult> {\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 {\n displayRoot: fallbackDisplayRoot,\n files: [],\n errors: [fatalError],\n warnings: [],\n generatedFiles: [],\n fatalError,\n };\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: [], generatedFiles: [], fatalError };\n }\n\n const context = makeScanContext(options, displayRoot);\n await measureFile(canonicalTarget, language, 'single-file', context, canonicalTarget);\n return settleScan(context, displayRoot);\n }\n\n const context = makeScanContext(options, canonicalTarget);\n await scanDirectory(canonicalTarget, context);\n await measureCandidates(context);\n return settleScan(context, canonicalTarget);\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 explicitFiles: ReadonlySet<string> = new Set()\n): Promise<ScanResult> {\n const context = makeScanContext(options, rootDirectory);\n for (const relativePath of relativePaths) {\n const absolutePath = path.join(rootDirectory, relativePath);\n const language = isScannedPath(relativePath, options, explicitFiles.has(absolutePath))\n ? getLanguage(relativePath, options, explicitFiles.has(absolutePath))\n : undefined;\n if (!language) {\n continue;\n }\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 context.candidates.push({ file: absolutePath, language });\n }\n }\n await measureCandidates(context);\n return settleScan(context, rootDirectory);\n}\n\nasync function measureCandidates(context: ScanContext): Promise<void> {\n const exclusion = await context.options.loadExclusion?.(\n context.candidates.flatMap(({ file, realFile }) => (realFile === undefined ? [file] : [file, realFile]))\n );\n context.exclusion = exclusion;\n // Every path reaching the same real file, in discovery order, so one alias cannot decide alone.\n const pathsByRealFile = new Map<string, ScanCandidate[]>();\n for (const candidate of context.candidates) {\n let realFile;\n try {\n realFile = candidate.realFile ?? (await realpath(candidate.file));\n } catch (error) {\n recordError(context, candidate.file, error);\n continue;\n }\n pathsByRealFile.set(realFile, [...(pathsByRealFile.get(realFile) ?? []), candidate]);\n }\n for (const [realFile, candidates] of pathsByRealFile) {\n if (context.fatalSeen) {\n return;\n }\n const chosen = await chooseMeasuredPath(realFile, candidates, exclusion);\n if (chosen !== undefined) {\n await measureFile(chosen.file, chosen.language, 'directory', context, realFile);\n }\n }\n}\n\n/**\n * The path to measure a real file through. A path is excluded when its own exclusion or the real\n * file's applies, so the real path, when the scan reached it directly, is never less measurable\n * than an alias; otherwise the first alias not excluded, preferring one whose own attributes do\n * not judge the content generated.\n */\nasync function chooseMeasuredPath(\n realFile: string,\n candidates: readonly ScanCandidate[],\n exclusion: Exclusion | undefined\n): Promise<ScanCandidate | undefined> {\n if (exclusion === undefined) {\n return candidates[0];\n }\n if (exclusion.isExcludedPath(realFile)) {\n return undefined;\n }\n const measurable = candidates.filter(({ file }) => !exclusion.isExcludedPath(file));\n const direct = measurable.find(({ file }) => file === realFile);\n if (direct !== undefined || measurable.length <= 1) {\n return direct ?? measurable[0];\n }\n const code = await readFile(realFile, 'utf8').catch(() => {});\n return measurable.find(({ file }) => code !== undefined && !exclusion.isGeneratedCode(file, code)) ?? measurable[0];\n}\n\n/** Applies the scan's outcomes in discovery order once every measurement has settled. */\nasync function settleScan(context: ScanContext, displayRoot: string): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n const generatedFiles: string[] = [];\n for (const pending of context.outcomes) {\n const outcome = await pending;\n if ('fatal' in outcome) {\n const fatalError = formatError(outcome.fatal);\n // Errors the walk recorded before the fatal failure stay reported alongside it.\n return { displayRoot, files, errors: [...errors, fatalError], warnings, generatedFiles, fatalError };\n }\n if ('generatedFile' in outcome) {\n generatedFiles.push(outcome.generatedFile);\n continue;\n }\n if ('error' in outcome) {\n errors.push(outcome.error);\n continue;\n }\n files.push(outcome.file);\n if (outcome.warning !== undefined) {\n warnings.push(outcome.warning);\n }\n }\n return { displayRoot, files, errors, warnings, generatedFiles };\n}\n\nfunction makeScanContext(options: ScanOptions, rootDirectory: string): ScanContext {\n return {\n options,\n outcomes: [],\n inFlight: new Set(),\n fatalSeen: false,\n visitedDirectories: new Set(),\n visitedFiles: new Set(),\n rootDirectory,\n candidates: [],\n };\n}\n\nfunction recordError(context: ScanContext, target: string, error: unknown): void {\n context.outcomes.push({ error: `${formatPath(target, context.rootDirectory)}: ${formatError(error)}` });\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 recordError(context, target, 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 // readdir order differs between file systems; sorting keeps which path reaches a file first,\n // and so the scan's result, the same everywhere.\n entries.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));\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 // Under a symbolically linked directory the real path differs, and exclusion checks both.\n const realFile = resolvedDirectory === directory ? undefined : path.join(resolvedDirectory, entry.name);\n collectScannableFile(entryPath, context, entryPath, realFile);\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 collectScannableFile(entryPath, context, resolvedPath, resolvedPath);\n }\n}\n\nfunction collectScannableFile(file: string, context: ScanContext, languageFile = file, realFile?: string): void {\n const language = getLanguage(languageFile, context.options);\n if (language) {\n context.candidates.push({ file, language, realFile });\n }\n}\n\n/**\n * Resolves and deduplicates the file in discovery order, then starts measuring it concurrently; its\n * outcome is recorded in discovery order (see ScanContext.outcomes).\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 if (context.fatalSeen) {\n return;\n }\n let resolvedFile;\n try {\n resolvedFile = realFile ?? (await realpath(file));\n } catch (error) {\n recordError(context, file, error);\n return;\n }\n if (context.visitedFiles.has(resolvedFile)) {\n return;\n }\n context.visitedFiles.add(resolvedFile);\n\n // Any settled measurement frees its slot (removed by the callback below, which runs before the\n // race resumes), so one slow file never idles the pool.\n while (context.inFlight.size >= maxMeasurementsInFlight) {\n await Promise.race(context.inFlight);\n }\n const outcome = readAndMeasureFile(file, resolvedFile, language, mode, context);\n context.inFlight.add(outcome);\n void outcome.then(() => context.inFlight.delete(outcome));\n context.outcomes.push(outcome);\n}\n\nasync function readAndMeasureFile(\n file: string,\n resolvedFile: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext\n): Promise<ScanOutcome> {\n try {\n const code = await readFile(file, 'utf8');\n const measureOptions = { language, duplication: context.options.duplication };\n // Only directory scans compare files against each other; a single-file target has no peers.\n if (mode === 'single-file') {\n return { file: { file, metrics: measureCode(code, measureOptions) } };\n }\n // Like path exclusion, either the scanned path or the real one can judge the code generated.\n const exclusion = context.exclusion;\n if (exclusion && (exclusion.isGeneratedCode(file, code) || exclusion.isGeneratedCode(resolvedFile, code))) {\n return { generatedFile: file };\n }\n const { metrics, crossFileData, crossFileError } = await measureWithCrossFileData(code, measureOptions);\n return {\n file: { file, metrics, duplicationCandidates: crossFileData },\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 warning:\n crossFileError === undefined\n ? undefined\n : `${formatPath(file, context.rootDirectory)}: cross-file duplication candidates unavailable: ${crossFileError}`,\n };\n } catch (error) {\n if (error instanceof NativeAddonError) {\n context.fatalSeen = true;\n return { fatal: error };\n }\n return { error: `${formatPath(file, context.rootDirectory)}: ${formatError(error)}` };\n }\n}\n\n/**\n * Measures a file together with its cross-file contribution from one parse. The contribution is\n * auxiliary: if collecting it fails where plain measurement succeeds (e.g. a payload too large to\n * cross the addon boundary), the metrics are still returned with the failure message, which\n * callers report as a warning rather than an error.\n */\nexport async function measureWithCrossFileData(\n code: string,\n measureOptions: MeasureOptions\n): Promise<{ metrics: CodeMetrics; crossFileData?: CrossFileDuplicationFileData; crossFileError?: string }> {\n try {\n return await measureCodeWithCrossFileDataAsync(code, measureOptions);\n } catch (error) {\n if (error instanceof NativeAddonError) {\n throw error;\n }\n return { metrics: measureCode(code, measureOptions), crossFileError: 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. An explicitly targeted file only needs a supported\n * language, as in the ranking command.\n */\nexport function isScannedPath(relativePath: string, options: ScanOptions, explicitTarget = false): boolean {\n if (explicitTarget) {\n return getLanguage(relativePath, options, true) !== undefined;\n }\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)) || suffixTestFilePattern.test(path.basename(file)))\n ) {\n return undefined;\n }\n\n return detectLanguage(file);\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 CHANGED
@@ -1,10 +1,16 @@
1
1
  import { type CrossFileDuplicationMetrics } from './crossFileDuplication.js';
2
2
  import type { CrossFileDuplicationFileData } from './duplication.js';
3
+ import type { Exclusion } from './exclusion.js';
3
4
  import type { CodeMetrics, DuplicationOptions, LanguageName, MeasureOptions } from './types.js';
4
5
  /** The scan settings shared by every command (a structural subset of each command's options). */
5
6
  export interface ScanOptions {
6
7
  duplication: Required<DuplicationOptions>;
7
8
  includeTests: boolean;
9
+ /**
10
+ * Loads the exclusion covering the files a directory scan found (absolute paths); an explicitly
11
+ * targeted file is always measured.
12
+ */
13
+ loadExclusion?: (absolutePaths: readonly string[]) => Promise<Exclusion>;
8
14
  }
9
15
  export interface FileMetrics {
10
16
  file: string;
@@ -18,6 +24,8 @@ export interface ScanResult {
18
24
  errors: string[];
19
25
  /** Non-fatal degradations (e.g. cross-file candidates unavailable); the file is still measured. */
20
26
  warnings: string[];
27
+ /** Files skipped because their content is generated code (see Exclusion.isGeneratedCode). */
28
+ generatedFiles: string[];
21
29
  fatalError?: string;
22
30
  files: FileMetrics[];
23
31
  }
@@ -31,7 +39,7 @@ export declare function scanTarget(target: string, options: ScanOptions): Promis
31
39
  * outside the scan scope (ignored/test directories, unsupported or test file names) are skipped
32
40
  * with the same rules as the walk.
33
41
  */
34
- export declare function scanListedFiles(rootDirectory: string, relativePaths: Iterable<string>, options: ScanOptions): Promise<ScanResult>;
42
+ export declare function scanListedFiles(rootDirectory: string, relativePaths: Iterable<string>, options: ScanOptions, explicitFiles?: ReadonlySet<string>): Promise<ScanResult>;
35
43
  /**
36
44
  * Measures a file together with its cross-file contribution from one parse. The contribution is
37
45
  * auxiliary: if collecting it fails where plain measurement succeeds (e.g. a payload too large to
@@ -56,9 +64,10 @@ export declare function collectDuplicatedLineNumbers(metrics: CodeMetrics | unde
56
64
  * Whether a repository-relative path would be scanned: no ignored or excluded-test directory
57
65
  * segment and a supported, non-test file name. The diff gate uses this for base-revision
58
66
  * eligibility, so code renamed into scan scope gates as new code instead of ratcheting against
59
- * a blob the scanner would never have measured.
67
+ * a blob the scanner would never have measured. An explicitly targeted file only needs a supported
68
+ * language, as in the ranking command.
60
69
  */
61
- export declare function isScannedPath(relativePath: string, options: ScanOptions): boolean;
70
+ export declare function isScannedPath(relativePath: string, options: ScanOptions, explicitTarget?: boolean): boolean;
62
71
  export declare function getLanguage(file: string, options: ScanOptions, explicitTarget?: boolean): LanguageName | undefined;
63
72
  export declare function formatPath(file: string, base: string): string;
64
73
  export declare function writeStdout(message: string): void;
package/dist/scan.js CHANGED
@@ -1,2 +1,2 @@
1
- import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{detectLanguage as t}from"./languages.js";import{NativeAddonError as n}from"./nativeMetrics.js";import{measureCode as r,measureCodeWithCrossFileDataAsync as i}from"./metrics.js";import{lstat as a,readFile as o,readdir as s,realpath as c,stat as l}from"node:fs/promises";import u from"node:path";import d from"node:os";const f=new Set([`.agents`,`.claude`,`.cursor`,`.git`,`.next`,`.playwright-cli`,`.tox`,`.tmp`,`.turbo`,`.venv`,`.yarn`,`__fixtures__`,`__generated__`,`__pycache__`,`coverage`,`dist`,`fixtures`,`generated`,`node_modules`,`obj`,`target`,`test-fixtures`,`vendor`,`venv`]),p=new Set([`__tests__`,`test`,`tests`,`spec`]),m=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-](?:test|spec))\.[^.]+$/iu,h=/Tests?\.(?:java|kt|cs)$/u;function g(e){return e===`~`?d.homedir():e.startsWith(`~/`)?u.join(d.homedir(),e.slice(2)):u.resolve(e)}async function _(e){try{return(await l(e)).isDirectory()?e:u.dirname(e)}catch{return u.dirname(e)}}const v=d.availableParallelism()*2;async function y(e,t){let n=e;try{n=await c(e)}catch{}let r=u.dirname(n),i;try{i=await l(n)}catch(e){let t=`${R(n,r)}: ${V(e)}`;return{displayRoot:r,files:[],errors:[t],warnings:[],fatalError:t}}if(i.isFile()){let e=u.dirname(n),r=L(n,t,!0);if(!r){let t=`${R(n,e)}: unsupported file type`;return{displayRoot:e,files:[],errors:[t],warnings:[],fatalError:t}}let i=S(t,e);return await k(n,r,`single-file`,i,n),x(i,e)}let a=S(t,n);return await E(n,a),x(a,n)}async function b(e,t,n){let r=S(n,e);for(let i of t){if(r.fatalSeen)break;let t=I(i,n)?L(i,n):void 0;if(!t)continue;let o=u.join(e,i);(await a(o).catch(()=>{}))?.isSymbolicLink()||await k(o,t,`directory`,r)}return x(r,e)}async function x(e,t){let n=[],r=[],i=[];for(let a of e.outcomes){let e=await a;if(`fatal`in e){let a=V(e.fatal);return{displayRoot:t,files:n,errors:[...r,a],warnings:i,fatalError:a}}if(`error`in e){r.push(e.error);continue}n.push(e.file),e.warning!==void 0&&i.push(e.warning)}return{displayRoot:t,files:n,errors:r,warnings:i}}function S(e,t){return{options:e,outcomes:[],inFlight:new Set,fatalSeen:!1,visitedDirectories:new Set,visitedFiles:new Set,rootDirectory:t}}function C(e,t,n){e.outcomes.push({error:`${R(t,e.rootDirectory)}: ${V(n)}`})}async function w(e,t,n){try{return await e()}catch(e){C(n,t,e);return}}async function T(e,t){let n=await w(()=>c(e),e,t);return n!==void 0&&F(n,t.rootDirectory)?n:void 0}async function E(e,t){let n=await T(e,t);if(n===void 0||t.visitedDirectories.has(n))return;t.visitedDirectories.add(n);let r=await w(()=>s(e,{withFileTypes:!0}),e,t);if(r!==void 0)for(let n of r){if(t.fatalSeen)return;let r=u.join(e,n.name);if(n.isSymbolicLink()){await D(n.name,r,t);continue}if(n.isDirectory()){if(P(n.name,t.options))continue;await E(r,t);continue}n.isFile()&&await O(r,t)}}async function D(e,t,n){let r=await T(t,n);if(r===void 0)return;let i=await w(()=>l(t),t,n);if(i!==void 0){if(i.isDirectory()){if(P(e,n.options)||P(u.basename(r),n.options))return;await E(t,n);return}i.isFile()&&await O(t,n,r,r)}}async function O(e,t,n=e,r){let i=L(n,t.options);i&&await k(e,i,`directory`,t,r)}async function k(e,t,n,r,i){if(r.fatalSeen)return;let a;try{a=i??await c(e)}catch(t){C(r,e,t);return}if(r.visitedFiles.has(a))return;for(r.visitedFiles.add(a);r.inFlight.size>=v;)await Promise.race(r.inFlight);let o=A(e,t,n,r);r.inFlight.add(o),o.then(()=>r.inFlight.delete(o)),r.outcomes.push(o)}async function A(e,t,i,a){try{let n=await o(e,`utf8`),s={language:t,duplication:a.options.duplication};if(i===`single-file`)return{file:{file:e,metrics:r(n,s)}};let{metrics:c,crossFileData:l,crossFileError:u}=await j(n,s);return{file:{file:e,metrics:c,duplicationCandidates:l},warning:u===void 0?void 0:`${R(e,a.rootDirectory)}: cross-file duplication candidates unavailable: ${u}`}}catch(t){return t instanceof n?(a.fatalSeen=!0,{fatal:t}):{error:`${R(e,a.rootDirectory)}: ${V(t)}`}}}async function j(e,t){try{return await i(e,t)}catch(i){if(i instanceof n)throw i;return{metrics:r(e,t),crossFileError:V(i)}}}function M(t,n){if(t.fatalError||t.files.length<2)return;let r=t.files.flatMap(({file:e,duplicationCandidates:n})=>n?[{file:R(e,t.displayRoot),...n}]:[]);r.length<2||(t.crossFileDuplication=e(r,n.duplication))}function N(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 P(e,t){return f.has(e)?!0:!t.includeTests&&p.has(e)}function F(e,t){let n=u.relative(t,e);return n===``||n!==`..`&&!n.startsWith(`..${u.sep}`)&&!u.isAbsolute(n)}function I(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 L(e,t)!==void 0}function L(e,n,r=!1){let i=e.toLowerCase();if(!(!r&&(i.endsWith(`.d.ts`)||i.endsWith(`.d.mts`)||i.endsWith(`.d.cts`)||i.endsWith(`.min.js`)||i.endsWith(`.pnp.cjs`)))&&(r||n.includeTests||!m.test(u.basename(e))&&!h.test(u.basename(e))))return t(e)}function R(e,t){return u.relative(t,e)||u.basename(e)}function z(e){process.stdout.write(e)}function B(e){process.stderr.write(e)}function V(e){return e instanceof Error?e.message:String(e)}export{M as addCrossFileDuplication,N as collectDuplicatedLineNumbers,_ as configSearchDirectory,V as formatError,R as formatPath,L as getLanguage,I as isScannedPath,j as measureWithCrossFileData,g as resolveTarget,b as scanListedFiles,y as scanTarget,B as writeStderr,z as writeStdout};
1
+ import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{detectLanguage as t}from"./languages.js";import{NativeAddonError as n}from"./nativeMetrics.js";import{measureCode as r,measureCodeWithCrossFileDataAsync as i}from"./metrics.js";import{lstat as a,readFile as o,readdir as s,realpath as c,stat as l}from"node:fs/promises";import u from"node:path";import d from"node:os";const f=/* @__PURE__ */ new Set([`.agents`,`.claude`,`.cursor`,`.git`,`.next`,`.playwright-cli`,`.tox`,`.tmp`,`.turbo`,`.venv`,`.yarn`,`__fixtures__`,`__generated__`,`__pycache__`,`coverage`,`dist`,`fixtures`,`generated`,`node_modules`,`obj`,`target`,`test-fixtures`,`vendor`,`venv`]),p=/* @__PURE__ */ new Set([`__tests__`,`test`,`tests`,`spec`]),m=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-](?:test|spec))\.[^.]+$/iu,h=/Tests?\.(?:java|kt|cs)$/u;function g(e){return e===`~`?d.homedir():e.startsWith(`~/`)?u.join(d.homedir(),e.slice(2)):u.resolve(e)}async function _(e){try{return(await l(e)).isDirectory()?e:u.dirname(e)}catch{return u.dirname(e)}}const v=d.availableParallelism()*2;async function y(e,t){let n=e;try{n=await c(e)}catch{}let r=u.dirname(n),i;try{i=await l(n)}catch(e){let t=`${B(n,r)}: ${U(e)}`;return{displayRoot:r,files:[],errors:[t],warnings:[],generatedFiles:[],fatalError:t}}if(i.isFile()){let e=u.dirname(n),r=z(n,t,!0);if(!r){let t=`${B(n,e)}: unsupported file type`;return{displayRoot:e,files:[],errors:[t],warnings:[],generatedFiles:[],fatalError:t}}let i=w(t,e);return await j(n,r,`single-file`,i,n),C(i,e)}let a=w(t,n);return await O(n,a),await x(a),C(a,n)}async function b(e,t,n,r=/* @__PURE__ */ new Set){let i=w(n,e);for(let o of t){let t=u.join(e,o),s=R(o,n,r.has(t))?z(o,n,r.has(t)):void 0;s&&((await a(t).catch(()=>{}))?.isSymbolicLink()||i.candidates.push({file:t,language:s}))}return await x(i),C(i,e)}async function x(e){let t=await e.options.loadExclusion?.(e.candidates.flatMap(({file:e,realFile:t})=>t===void 0?[e]:[e,t]));e.exclusion=t;let n=/* @__PURE__ */ new Map;for(let t of e.candidates){let r;try{r=t.realFile??await c(t.file)}catch(n){T(e,t.file,n);continue}n.set(r,[...n.get(r)??[],t])}for(let[r,i]of n){if(e.fatalSeen)return;let n=await S(r,i,t);n!==void 0&&await j(n.file,n.language,`directory`,e,r)}}async function S(e,t,n){if(n===void 0)return t[0];if(n.isExcludedPath(e))return;let r=t.filter(({file:e})=>!n.isExcludedPath(e)),i=r.find(({file:t})=>t===e);if(i!==void 0||r.length<=1)return i??r[0];let a=await o(e,`utf8`).catch(()=>{});return r.find(({file:e})=>a!==void 0&&!n.isGeneratedCode(e,a))??r[0]}async function C(e,t){let n=[],r=[],i=[],a=[];for(let o of e.outcomes){let e=await o;if(`fatal`in e){let o=U(e.fatal);return{displayRoot:t,files:n,errors:[...r,o],warnings:i,generatedFiles:a,fatalError:o}}if(`generatedFile`in e){a.push(e.generatedFile);continue}if(`error`in e){r.push(e.error);continue}n.push(e.file),e.warning!==void 0&&i.push(e.warning)}return{displayRoot:t,files:n,errors:r,warnings:i,generatedFiles:a}}function w(e,t){return{options:e,outcomes:[],inFlight:/* @__PURE__ */ new Set,fatalSeen:!1,visitedDirectories:/* @__PURE__ */ new Set,visitedFiles:/* @__PURE__ */ new Set,rootDirectory:t,candidates:[]}}function T(e,t,n){e.outcomes.push({error:`${B(t,e.rootDirectory)}: ${U(n)}`})}async function E(e,t,n){try{return await e()}catch(e){T(n,t,e);return}}async function D(e,t){let n=await E(()=>c(e),e,t);return n!==void 0&&L(n,t.rootDirectory)?n:void 0}async function O(e,t){let n=await D(e,t);if(n===void 0||t.visitedDirectories.has(n))return;t.visitedDirectories.add(n);let r=await E(()=>s(e,{withFileTypes:!0}),e,t);if(r!==void 0){r.sort((e,t)=>e.name<t.name?-1:+(e.name>t.name));for(let i of r){let r=u.join(e,i.name);if(i.isSymbolicLink()){await k(i.name,r,t);continue}if(i.isDirectory()){if(I(i.name,t.options))continue;await O(r,t);continue}i.isFile()&&A(r,t,r,n===e?void 0:u.join(n,i.name))}}}async function k(e,t,n){let r=await D(t,n);if(r===void 0)return;let i=await E(()=>l(t),t,n);if(i!==void 0){if(i.isDirectory()){if(I(e,n.options)||I(u.basename(r),n.options))return;await O(t,n);return}i.isFile()&&A(t,n,r,r)}}function A(e,t,n=e,r){let i=z(n,t.options);i&&t.candidates.push({file:e,language:i,realFile:r})}async function j(e,t,n,r,i){if(r.fatalSeen)return;let a;try{a=i??await c(e)}catch(t){T(r,e,t);return}if(r.visitedFiles.has(a))return;for(r.visitedFiles.add(a);r.inFlight.size>=v;)await Promise.race(r.inFlight);let o=M(e,a,t,n,r);r.inFlight.add(o),o.then(()=>r.inFlight.delete(o)),r.outcomes.push(o)}async function M(e,t,i,a,s){try{let n=await o(e,`utf8`),c={language:i,duplication:s.options.duplication};if(a===`single-file`)return{file:{file:e,metrics:r(n,c)}};let l=s.exclusion;if(l&&(l.isGeneratedCode(e,n)||l.isGeneratedCode(t,n)))return{generatedFile:e};let{metrics:u,crossFileData:d,crossFileError:f}=await N(n,c);return{file:{file:e,metrics:u,duplicationCandidates:d},warning:f===void 0?void 0:`${B(e,s.rootDirectory)}: cross-file duplication candidates unavailable: ${f}`}}catch(t){return t instanceof n?(s.fatalSeen=!0,{fatal:t}):{error:`${B(e,s.rootDirectory)}: ${U(t)}`}}}async function N(e,t){try{return await i(e,t)}catch(i){if(i instanceof n)throw i;return{metrics:r(e,t),crossFileError:U(i)}}}function P(t,n){if(t.fatalError||t.files.length<2)return;let r=t.files.flatMap(({file:e,duplicationCandidates:n})=>n?[{file:B(e,t.displayRoot),...n}]:[]);r.length<2||(t.crossFileDuplication=e(r,n.duplication))}function F(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 I(e,t){return f.has(e)?!0:!t.includeTests&&p.has(e)}function L(e,t){let n=u.relative(t,e);return n===``||n!==`..`&&!n.startsWith(`..${u.sep}`)&&!u.isAbsolute(n)}function R(e,t,n=!1){if(n)return z(e,t,!0)!==void 0;let r=e.split(`/`);for(let e of r.slice(0,-1))if(f.has(e)||!t.includeTests&&p.has(e))return!1;return z(e,t)!==void 0}function z(e,n,r=!1){let i=e.toLowerCase();if(!(!r&&(i.endsWith(`.d.ts`)||i.endsWith(`.d.mts`)||i.endsWith(`.d.cts`)||i.endsWith(`.min.js`)||i.endsWith(`.pnp.cjs`)))&&(r||n.includeTests||!m.test(u.basename(e))&&!h.test(u.basename(e))))return t(e)}function B(e,t){return u.relative(t,e)||u.basename(e)}function V(e){process.stdout.write(e)}function H(e){process.stderr.write(e)}function U(e){return e instanceof Error?e.message:String(e)}export{P as addCrossFileDuplication,F as collectDuplicatedLineNumbers,_ as configSearchDirectory,U as formatError,B as formatPath,z as getLanguage,R as isScannedPath,N as measureWithCrossFileData,g as resolveTarget,b as scanListedFiles,y as scanTarget,H as writeStderr,V as writeStdout};
2
2
  //# sourceMappingURL=scan.js.map
package/dist/scan.js.map CHANGED
@@ -1 +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 { detectLanguage } from './languages.js';\nimport { measureCode, measureCodeWithCrossFileDataAsync } from './metrics.js';\nimport { NativeAddonError } from './nativeMetrics.js';\nimport type { CodeMetrics, DuplicationOptions, LanguageName, MeasureOptions } 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 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 // .NET SDK intermediate output (generated sources such as `*.GlobalUsings.g.cs`).\n 'obj',\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 (Java/Kotlin) and xUnit/NUnit (C#) tests use case-sensitive `Test`/`Tests` class-name\n// suffixes; case-insensitive matching would catch production files like `contest.java`.\nconst suffixTestFilePattern = /Tests?\\.(?:java|kt|cs)$/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 /**\n * Outcomes in walk order. Files are measured concurrently, so a measurement is recorded as a\n * promise here and applied in this order once the walk ends, keeping results deterministic.\n */\n outcomes: (ScanOutcome | Promise<ScanOutcome>)[];\n /** Measurements in flight, bounded so file contents and payloads do not pile up during the walk. */\n inFlight: Set<Promise<ScanOutcome>>;\n /** Set once a measurement fails fatally; the walk then starts no further work. */\n fatalSeen: boolean;\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\n/**\n * A missing native addon fails every file identically, so it ends the scan as one fatal error\n * instead of one \"skipped\" entry per file behind a successful exit code.\n */\ntype ScanOutcome = { file: FileMetrics; warning?: string } | { error: string } | { fatal: NativeAddonError };\n\n// Twice the addon's worker count keeps its pool busy while finished payloads are parsed.\nconst maxMeasurementsInFlight = os.availableParallelism() * 2;\n\nexport async function scanTarget(target: string, options: ScanOptions): Promise<ScanResult> {\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, displayRoot);\n await measureFile(canonicalTarget, language, 'single-file', context, canonicalTarget);\n return settleScan(context, displayRoot);\n }\n\n const context = makeScanContext(options, canonicalTarget);\n await scanDirectory(canonicalTarget, context);\n return settleScan(context, canonicalTarget);\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 context = makeScanContext(options, rootDirectory);\n for (const relativePath of relativePaths) {\n if (context.fatalSeen) {\n break;\n }\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 await measureFile(absolutePath, language, 'directory', context);\n }\n return settleScan(context, rootDirectory);\n}\n\n/** Applies the scan's outcomes in walk order once every measurement has settled. */\nasync function settleScan(context: ScanContext, displayRoot: string): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n for (const pending of context.outcomes) {\n const outcome = await pending;\n if ('fatal' in outcome) {\n const fatalError = formatError(outcome.fatal);\n // Errors the walk recorded before the fatal failure stay reported alongside it.\n return { displayRoot, files, errors: [...errors, fatalError], warnings, fatalError };\n }\n if ('error' in outcome) {\n errors.push(outcome.error);\n continue;\n }\n files.push(outcome.file);\n if (outcome.warning !== undefined) {\n warnings.push(outcome.warning);\n }\n }\n return { displayRoot, files, errors, warnings };\n}\n\nfunction makeScanContext(options: ScanOptions, rootDirectory: string): ScanContext {\n return {\n options,\n outcomes: [],\n inFlight: new Set(),\n fatalSeen: false,\n visitedDirectories: new Set(),\n visitedFiles: new Set(),\n rootDirectory,\n };\n}\n\nfunction recordError(context: ScanContext, target: string, error: unknown): void {\n context.outcomes.push({ error: `${formatPath(target, context.rootDirectory)}: ${formatError(error)}` });\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 recordError(context, target, 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 if (context.fatalSeen) {\n return;\n }\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\n/**\n * Resolves and deduplicates the file in walk order, then starts measuring it concurrently; its\n * outcome is recorded in walk order (see ScanContext.outcomes).\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 if (context.fatalSeen) {\n return;\n }\n let resolvedFile;\n try {\n resolvedFile = realFile ?? (await realpath(file));\n } catch (error) {\n recordError(context, file, error);\n return;\n }\n if (context.visitedFiles.has(resolvedFile)) {\n return;\n }\n context.visitedFiles.add(resolvedFile);\n\n // Any settled measurement frees its slot (removed by the callback below, which runs before the\n // race resumes), so one slow file never idles the pool.\n while (context.inFlight.size >= maxMeasurementsInFlight) {\n await Promise.race(context.inFlight);\n }\n const outcome = readAndMeasureFile(file, language, mode, context);\n context.inFlight.add(outcome);\n void outcome.then(() => context.inFlight.delete(outcome));\n context.outcomes.push(outcome);\n}\n\nasync function readAndMeasureFile(\n file: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext\n): Promise<ScanOutcome> {\n try {\n const code = await readFile(file, 'utf8');\n const measureOptions = { language, duplication: context.options.duplication };\n // Only directory scans compare files against each other; a single-file target has no peers.\n if (mode === 'single-file') {\n return { file: { file, metrics: measureCode(code, measureOptions) } };\n }\n const { metrics, crossFileData, crossFileError } = await measureWithCrossFileData(code, measureOptions);\n return {\n file: { file, metrics, duplicationCandidates: crossFileData },\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 warning:\n crossFileError === undefined\n ? undefined\n : `${formatPath(file, context.rootDirectory)}: cross-file duplication candidates unavailable: ${crossFileError}`,\n };\n } catch (error) {\n if (error instanceof NativeAddonError) {\n context.fatalSeen = true;\n return { fatal: error };\n }\n return { error: `${formatPath(file, context.rootDirectory)}: ${formatError(error)}` };\n }\n}\n\n/**\n * Measures a file together with its cross-file contribution from one parse. The contribution is\n * auxiliary: if collecting it fails where plain measurement succeeds (e.g. a payload too large to\n * cross the addon boundary), the metrics are still returned with the failure message, which\n * callers report as a warning rather than an error.\n */\nexport async function measureWithCrossFileData(\n code: string,\n measureOptions: MeasureOptions\n): Promise<{ metrics: CodeMetrics; crossFileData?: CrossFileDuplicationFileData; crossFileError?: string }> {\n try {\n return await measureCodeWithCrossFileDataAsync(code, measureOptions);\n } catch (error) {\n if (error instanceof NativeAddonError) {\n throw error;\n }\n return { metrics: measureCode(code, measureOptions), crossFileError: 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)) || suffixTestFilePattern.test(path.basename(file)))\n ) {\n return undefined;\n }\n\n return detectLanguage(file);\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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"}
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 type { Exclusion } from './exclusion.js';\nimport { detectLanguage } from './languages.js';\nimport { measureCode, measureCodeWithCrossFileDataAsync } from './metrics.js';\nimport { NativeAddonError } from './nativeMetrics.js';\nimport type { CodeMetrics, DuplicationOptions, LanguageName, MeasureOptions } 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 * Loads the exclusion covering the files a directory scan found (absolute paths); an explicitly\n * targeted file is always measured.\n */\n loadExclusion?: (absolutePaths: readonly string[]) => Promise<Exclusion>;\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 /** Files skipped because their content is generated code (see Exclusion.isGeneratedCode). */\n generatedFiles: string[];\n fatalError?: string;\n files: FileMetrics[];\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 // .NET SDK intermediate output (generated sources such as `*.GlobalUsings.g.cs`).\n 'obj',\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 (Java/Kotlin) and xUnit/NUnit (C#) tests use case-sensitive `Test`/`Tests` class-name\n// suffixes; case-insensitive matching would catch production files like `contest.java`.\nconst suffixTestFilePattern = /Tests?\\.(?:java|kt|cs)$/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 /**\n * Outcomes in discovery order. Files are measured concurrently, so a measurement is recorded as\n * a promise here and applied in this order once all have settled, keeping results deterministic.\n */\n outcomes: (ScanOutcome | Promise<ScanOutcome>)[];\n /** Measurements in flight, bounded so file contents and payloads do not pile up. */\n inFlight: Set<Promise<ScanOutcome>>;\n /** Set once a measurement fails fatally; no further measurement starts. */\n fatalSeen: boolean;\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 /** Files the walk found, measured once the exclusion covering all of them is loaded. */\n candidates: ScanCandidate[];\n exclusion?: Exclusion;\n}\n\ninterface ScanCandidate {\n file: string;\n language: LanguageName;\n /** The resolved path when the file was reached through a symbolic link. */\n realFile?: string;\n}\n\n/**\n * A missing native addon fails every file identically, so it ends the scan as one fatal error\n * instead of one \"skipped\" entry per file behind a successful exit code.\n */\ntype ScanOutcome =\n | { file: FileMetrics; warning?: string }\n | { generatedFile: string }\n | { error: string }\n | { fatal: NativeAddonError };\n\n// Twice the addon's worker count keeps its pool busy while finished payloads are parsed.\nconst maxMeasurementsInFlight = os.availableParallelism() * 2;\n\nexport async function scanTarget(target: string, options: ScanOptions): Promise<ScanResult> {\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 {\n displayRoot: fallbackDisplayRoot,\n files: [],\n errors: [fatalError],\n warnings: [],\n generatedFiles: [],\n fatalError,\n };\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: [], generatedFiles: [], fatalError };\n }\n\n const context = makeScanContext(options, displayRoot);\n await measureFile(canonicalTarget, language, 'single-file', context, canonicalTarget);\n return settleScan(context, displayRoot);\n }\n\n const context = makeScanContext(options, canonicalTarget);\n await scanDirectory(canonicalTarget, context);\n await measureCandidates(context);\n return settleScan(context, canonicalTarget);\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 explicitFiles: ReadonlySet<string> = new Set()\n): Promise<ScanResult> {\n const context = makeScanContext(options, rootDirectory);\n for (const relativePath of relativePaths) {\n const absolutePath = path.join(rootDirectory, relativePath);\n const language = isScannedPath(relativePath, options, explicitFiles.has(absolutePath))\n ? getLanguage(relativePath, options, explicitFiles.has(absolutePath))\n : undefined;\n if (!language) {\n continue;\n }\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 context.candidates.push({ file: absolutePath, language });\n }\n }\n await measureCandidates(context);\n return settleScan(context, rootDirectory);\n}\n\nasync function measureCandidates(context: ScanContext): Promise<void> {\n const exclusion = await context.options.loadExclusion?.(\n context.candidates.flatMap(({ file, realFile }) => (realFile === undefined ? [file] : [file, realFile]))\n );\n context.exclusion = exclusion;\n // Every path reaching the same real file, in discovery order, so one alias cannot decide alone.\n const pathsByRealFile = new Map<string, ScanCandidate[]>();\n for (const candidate of context.candidates) {\n let realFile;\n try {\n realFile = candidate.realFile ?? (await realpath(candidate.file));\n } catch (error) {\n recordError(context, candidate.file, error);\n continue;\n }\n pathsByRealFile.set(realFile, [...(pathsByRealFile.get(realFile) ?? []), candidate]);\n }\n for (const [realFile, candidates] of pathsByRealFile) {\n if (context.fatalSeen) {\n return;\n }\n const chosen = await chooseMeasuredPath(realFile, candidates, exclusion);\n if (chosen !== undefined) {\n await measureFile(chosen.file, chosen.language, 'directory', context, realFile);\n }\n }\n}\n\n/**\n * The path to measure a real file through. A path is excluded when its own exclusion or the real\n * file's applies, so the real path, when the scan reached it directly, is never less measurable\n * than an alias; otherwise the first alias not excluded, preferring one whose own attributes do\n * not judge the content generated.\n */\nasync function chooseMeasuredPath(\n realFile: string,\n candidates: readonly ScanCandidate[],\n exclusion: Exclusion | undefined\n): Promise<ScanCandidate | undefined> {\n if (exclusion === undefined) {\n return candidates[0];\n }\n if (exclusion.isExcludedPath(realFile)) {\n return undefined;\n }\n const measurable = candidates.filter(({ file }) => !exclusion.isExcludedPath(file));\n const direct = measurable.find(({ file }) => file === realFile);\n if (direct !== undefined || measurable.length <= 1) {\n return direct ?? measurable[0];\n }\n const code = await readFile(realFile, 'utf8').catch(() => {});\n return measurable.find(({ file }) => code !== undefined && !exclusion.isGeneratedCode(file, code)) ?? measurable[0];\n}\n\n/** Applies the scan's outcomes in discovery order once every measurement has settled. */\nasync function settleScan(context: ScanContext, displayRoot: string): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n const generatedFiles: string[] = [];\n for (const pending of context.outcomes) {\n const outcome = await pending;\n if ('fatal' in outcome) {\n const fatalError = formatError(outcome.fatal);\n // Errors the walk recorded before the fatal failure stay reported alongside it.\n return { displayRoot, files, errors: [...errors, fatalError], warnings, generatedFiles, fatalError };\n }\n if ('generatedFile' in outcome) {\n generatedFiles.push(outcome.generatedFile);\n continue;\n }\n if ('error' in outcome) {\n errors.push(outcome.error);\n continue;\n }\n files.push(outcome.file);\n if (outcome.warning !== undefined) {\n warnings.push(outcome.warning);\n }\n }\n return { displayRoot, files, errors, warnings, generatedFiles };\n}\n\nfunction makeScanContext(options: ScanOptions, rootDirectory: string): ScanContext {\n return {\n options,\n outcomes: [],\n inFlight: new Set(),\n fatalSeen: false,\n visitedDirectories: new Set(),\n visitedFiles: new Set(),\n rootDirectory,\n candidates: [],\n };\n}\n\nfunction recordError(context: ScanContext, target: string, error: unknown): void {\n context.outcomes.push({ error: `${formatPath(target, context.rootDirectory)}: ${formatError(error)}` });\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 recordError(context, target, 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 // readdir order differs between file systems; sorting keeps which path reaches a file first,\n // and so the scan's result, the same everywhere.\n entries.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));\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 // Under a symbolically linked directory the real path differs, and exclusion checks both.\n const realFile = resolvedDirectory === directory ? undefined : path.join(resolvedDirectory, entry.name);\n collectScannableFile(entryPath, context, entryPath, realFile);\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 collectScannableFile(entryPath, context, resolvedPath, resolvedPath);\n }\n}\n\nfunction collectScannableFile(file: string, context: ScanContext, languageFile = file, realFile?: string): void {\n const language = getLanguage(languageFile, context.options);\n if (language) {\n context.candidates.push({ file, language, realFile });\n }\n}\n\n/**\n * Resolves and deduplicates the file in discovery order, then starts measuring it concurrently; its\n * outcome is recorded in discovery order (see ScanContext.outcomes).\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 if (context.fatalSeen) {\n return;\n }\n let resolvedFile;\n try {\n resolvedFile = realFile ?? (await realpath(file));\n } catch (error) {\n recordError(context, file, error);\n return;\n }\n if (context.visitedFiles.has(resolvedFile)) {\n return;\n }\n context.visitedFiles.add(resolvedFile);\n\n // Any settled measurement frees its slot (removed by the callback below, which runs before the\n // race resumes), so one slow file never idles the pool.\n while (context.inFlight.size >= maxMeasurementsInFlight) {\n await Promise.race(context.inFlight);\n }\n const outcome = readAndMeasureFile(file, resolvedFile, language, mode, context);\n context.inFlight.add(outcome);\n void outcome.then(() => context.inFlight.delete(outcome));\n context.outcomes.push(outcome);\n}\n\nasync function readAndMeasureFile(\n file: string,\n resolvedFile: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext\n): Promise<ScanOutcome> {\n try {\n const code = await readFile(file, 'utf8');\n const measureOptions = { language, duplication: context.options.duplication };\n // Only directory scans compare files against each other; a single-file target has no peers.\n if (mode === 'single-file') {\n return { file: { file, metrics: measureCode(code, measureOptions) } };\n }\n // Like path exclusion, either the scanned path or the real one can judge the code generated.\n const exclusion = context.exclusion;\n if (exclusion && (exclusion.isGeneratedCode(file, code) || exclusion.isGeneratedCode(resolvedFile, code))) {\n return { generatedFile: file };\n }\n const { metrics, crossFileData, crossFileError } = await measureWithCrossFileData(code, measureOptions);\n return {\n file: { file, metrics, duplicationCandidates: crossFileData },\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 warning:\n crossFileError === undefined\n ? undefined\n : `${formatPath(file, context.rootDirectory)}: cross-file duplication candidates unavailable: ${crossFileError}`,\n };\n } catch (error) {\n if (error instanceof NativeAddonError) {\n context.fatalSeen = true;\n return { fatal: error };\n }\n return { error: `${formatPath(file, context.rootDirectory)}: ${formatError(error)}` };\n }\n}\n\n/**\n * Measures a file together with its cross-file contribution from one parse. The contribution is\n * auxiliary: if collecting it fails where plain measurement succeeds (e.g. a payload too large to\n * cross the addon boundary), the metrics are still returned with the failure message, which\n * callers report as a warning rather than an error.\n */\nexport async function measureWithCrossFileData(\n code: string,\n measureOptions: MeasureOptions\n): Promise<{ metrics: CodeMetrics; crossFileData?: CrossFileDuplicationFileData; crossFileError?: string }> {\n try {\n return await measureCodeWithCrossFileDataAsync(code, measureOptions);\n } catch (error) {\n if (error instanceof NativeAddonError) {\n throw error;\n }\n return { metrics: measureCode(code, measureOptions), crossFileError: 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. An explicitly targeted file only needs a supported\n * language, as in the ranking command.\n */\nexport function isScannedPath(relativePath: string, options: ScanOptions, explicitTarget = false): boolean {\n if (explicitTarget) {\n return getLanguage(relativePath, options, true) !== undefined;\n }\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)) || suffixTestFilePattern.test(path.basename(file)))\n ) {\n return undefined;\n }\n\n return detectLanguage(file);\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/worker.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./crossFileDuplication.cjs"),n=require("./languages.cjs"),r=require("./nativeMetrics.cjs"),i=require("./metrics.cjs"),a=require("./regressionGate.cjs");require("./index.cjs");const o=require("./wasmBinding.cjs");let s=require("../native/code-gauge.wasm");s=e.__toESM(s,1),r.setNativeBinding(o.createWasmBinding(s.default)),exports.TreeMeasurer=i.TreeMeasurer,exports.collectCrossFileDuplicationFileData=i.collectCrossFileDuplicationFileData,exports.collectDuplicationCandidates=i.collectDuplicationCandidates,exports.collectFunctionTokenSequences=i.collectFunctionTokenSequences,exports.defaultGateOptions=a.defaultGateOptions,exports.defaultLanguages=n.defaultLanguages,exports.defaultMeasurer=i.defaultMeasurer,exports.detectLanguage=n.detectLanguage,exports.evaluateRegressionGate=a.evaluateRegressionGate,exports.measureCode=i.measureCode,exports.measureCrossFileDuplication=t.measureCrossFileDuplication,exports.supportedLanguages=n.supportedLanguages;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./crossFileDuplication.cjs"),n=require("./languages.cjs"),r=require("./nativeMetrics.cjs"),i=require("./metrics.cjs"),a=require("./newDuplication.cjs"),o=require("./regressionGate.cjs");require("./index.cjs");const s=require("./wasmBinding.cjs");let c=require("../native/code-gauge.wasm");c=e.__toESM(c,1),r.setNativeBinding(s.createWasmBinding(c.default)),exports.TreeMeasurer=i.TreeMeasurer,exports.collectCrossFileDuplicationFileData=i.collectCrossFileDuplicationFileData,exports.collectDuplicationCandidates=i.collectDuplicationCandidates,exports.collectFunctionTokenSequences=i.collectFunctionTokenSequences,exports.defaultGateOptions=o.defaultGateOptions,exports.defaultLanguages=n.defaultLanguages,exports.defaultMeasurer=i.defaultMeasurer,exports.detectLanguage=n.detectLanguage,exports.evaluateRegressionGate=o.evaluateRegressionGate,exports.findNewlyDuplicatedLines=a.findNewlyDuplicatedLines,exports.measureCode=i.measureCode,exports.measureCrossFileDuplication=t.measureCrossFileDuplication,exports.supportedLanguages=n.supportedLanguages;
2
2
  //# sourceMappingURL=worker.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"worker.cjs","names":["setNativeBinding","createWasmBinding","wasmModule"],"sources":["../src/worker.ts"],"sourcesContent":["// The entry point for Cloudflare Workers (the `workerd` export condition), which cannot load N-API\n// addons: the same API runs on the WebAssembly build of the native addon.\nimport wasmModule from '../native/code-gauge.wasm';\nimport { setNativeBinding } from './nativeMetrics.js';\nimport { createWasmBinding } from './wasmBinding.js';\n\nsetNativeBinding(createWasmBinding(wasmModule));\n\nexport * from './index.js';\n"],"mappings":"+ZAMAA,EAAAA,iBAAiBC,EAAAA,kBAAkBC,EAAAA,OAAU,CAAC"}
1
+ {"version":3,"file":"worker.cjs","names":["setNativeBinding","createWasmBinding","wasmModule"],"sources":["../src/worker.ts"],"sourcesContent":["// The entry point for Cloudflare Workers (the `workerd` export condition), which cannot load N-API\n// addons: the same API runs on the WebAssembly build of the native addon.\nimport wasmModule from '../native/code-gauge.wasm';\nimport { setNativeBinding } from './nativeMetrics.js';\nimport { createWasmBinding } from './wasmBinding.js';\n\nsetNativeBinding(createWasmBinding(wasmModule));\n\nexport * from './index.js';\n"],"mappings":"icAMAA,EAAAA,iBAAiBC,EAAAA,kBAAkBC,EAAAA,OAAU,CAAC"}
package/dist/worker.js CHANGED
@@ -1,2 +1,2 @@
1
- import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{defaultLanguages as t,detectLanguage as n,supportedLanguages as r}from"./languages.js";import{setNativeBinding as i}from"./nativeMetrics.js";import{TreeMeasurer as a,collectCrossFileDuplicationFileData as o,collectDuplicationCandidates as s,collectFunctionTokenSequences as c,defaultMeasurer as l,measureCode as u}from"./metrics.js";import{defaultGateOptions as d,evaluateRegressionGate as f}from"./regressionGate.js";import"./index.js";import{createWasmBinding as p}from"./wasmBinding.js";import m from"../native/code-gauge.wasm";i(p(m));export{a as TreeMeasurer,o as collectCrossFileDuplicationFileData,s as collectDuplicationCandidates,c as collectFunctionTokenSequences,d as defaultGateOptions,t as defaultLanguages,l as defaultMeasurer,n as detectLanguage,f as evaluateRegressionGate,u as measureCode,e as measureCrossFileDuplication,r as supportedLanguages};
1
+ import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{defaultLanguages as t,detectLanguage as n,supportedLanguages as r}from"./languages.js";import{setNativeBinding as i}from"./nativeMetrics.js";import{TreeMeasurer as a,collectCrossFileDuplicationFileData as o,collectDuplicationCandidates as s,collectFunctionTokenSequences as c,defaultMeasurer as l,measureCode as u}from"./metrics.js";import{findNewlyDuplicatedLines as d}from"./newDuplication.js";import{defaultGateOptions as f,evaluateRegressionGate as p}from"./regressionGate.js";import"./index.js";import{createWasmBinding as m}from"./wasmBinding.js";import h from"../native/code-gauge.wasm";i(m(h));export{a as TreeMeasurer,o as collectCrossFileDuplicationFileData,s as collectDuplicationCandidates,c as collectFunctionTokenSequences,f as defaultGateOptions,t as defaultLanguages,l as defaultMeasurer,n as detectLanguage,p as evaluateRegressionGate,d as findNewlyDuplicatedLines,u as measureCode,e as measureCrossFileDuplication,r as supportedLanguages};
2
2
  //# sourceMappingURL=worker.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"worker.js","names":[],"sources":["../src/worker.ts"],"sourcesContent":["// The entry point for Cloudflare Workers (the `workerd` export condition), which cannot load N-API\n// addons: the same API runs on the WebAssembly build of the native addon.\nimport wasmModule from '../native/code-gauge.wasm';\nimport { setNativeBinding } from './nativeMetrics.js';\nimport { createWasmBinding } from './wasmBinding.js';\n\nsetNativeBinding(createWasmBinding(wasmModule));\n\nexport * from './index.js';\n"],"mappings":"kmBAMA,EAAiB,EAAkB,CAAU,CAAC"}
1
+ {"version":3,"file":"worker.js","names":[],"sources":["../src/worker.ts"],"sourcesContent":["// The entry point for Cloudflare Workers (the `workerd` export condition), which cannot load N-API\n// addons: the same API runs on the WebAssembly build of the native addon.\nimport wasmModule from '../native/code-gauge.wasm';\nimport { setNativeBinding } from './nativeMetrics.js';\nimport { createWasmBinding } from './wasmBinding.js';\n\nsetNativeBinding(createWasmBinding(wasmModule));\n\nexport * from './index.js';\n"],"mappings":"iqBAMA,EAAiB,EAAkB,CAAU,CAAC"}
package/native/Cargo.lock CHANGED
@@ -19,9 +19,13 @@ checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
19
19
 
20
20
  [[package]]
21
21
  name = "cc"
22
- version = "1.0.106"
22
+ version = "1.4.7"
23
23
  source = "registry+https://github.com/rust-lang/crates.io-index"
24
- checksum = "066fce287b1d4eafef758e89e09d724a24808a9196fe9756b8ca90e86d0719a2"
24
+ checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313"
25
+ dependencies = [
26
+ "find-msvc-tools",
27
+ "shlex",
28
+ ]
25
29
 
26
30
  [[package]]
27
31
  name = "cfg-if"
@@ -48,11 +52,11 @@ dependencies = [
48
52
  "tree-sitter-go",
49
53
  "tree-sitter-java",
50
54
  "tree-sitter-javascript",
51
- "tree-sitter-kotlin",
52
55
  "tree-sitter-python",
53
56
  "tree-sitter-ruby",
54
57
  "tree-sitter-rust",
55
58
  "tree-sitter-typescript",
59
+ "willbooster-tree-sitter-kotlin",
56
60
  ]
57
61
 
58
62
  [[package]]
@@ -76,6 +80,12 @@ version = "1.0.2"
76
80
  source = "registry+https://github.com/rust-lang/crates.io-index"
77
81
  checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
78
82
 
83
+ [[package]]
84
+ name = "find-msvc-tools"
85
+ version = "0.1.13"
86
+ source = "registry+https://github.com/rust-lang/crates.io-index"
87
+ checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b"
88
+
79
89
  [[package]]
80
90
  name = "futures"
81
91
  version = "0.3.33"
@@ -366,6 +376,7 @@ version = "1.0.150"
366
376
  source = "registry+https://github.com/rust-lang/crates.io-index"
367
377
  checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
368
378
  dependencies = [
379
+ "indexmap",
369
380
  "itoa",
370
381
  "memchr",
371
382
  "serde",
@@ -373,12 +384,24 @@ dependencies = [
373
384
  "zmij",
374
385
  ]
375
386
 
387
+ [[package]]
388
+ name = "shlex"
389
+ version = "2.0.1"
390
+ source = "registry+https://github.com/rust-lang/crates.io-index"
391
+ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
392
+
376
393
  [[package]]
377
394
  name = "slab"
378
395
  version = "0.4.12"
379
396
  source = "registry+https://github.com/rust-lang/crates.io-index"
380
397
  checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
381
398
 
399
+ [[package]]
400
+ name = "streaming-iterator"
401
+ version = "0.1.9"
402
+ source = "registry+https://github.com/rust-lang/crates.io-index"
403
+ checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
404
+
382
405
  [[package]]
383
406
  name = "syn"
384
407
  version = "2.0.119"
@@ -403,12 +426,16 @@ dependencies = [
403
426
 
404
427
  [[package]]
405
428
  name = "tree-sitter"
406
- version = "0.22.6"
429
+ version = "0.25.10"
407
430
  source = "registry+https://github.com/rust-lang/crates.io-index"
408
- checksum = "df7cc499ceadd4dcdf7ec6d4cbc34ece92c3fa07821e287aedecd4416c516dca"
431
+ checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87"
409
432
  dependencies = [
410
433
  "cc",
411
434
  "regex",
435
+ "regex-syntax",
436
+ "serde_json",
437
+ "streaming-iterator",
438
+ "tree-sitter-language",
412
439
  ]
413
440
 
414
441
  [[package]]
@@ -463,23 +490,19 @@ dependencies = [
463
490
 
464
491
  [[package]]
465
492
  name = "tree-sitter-javascript"
466
- version = "0.21.4"
493
+ version = "0.25.0"
467
494
  source = "registry+https://github.com/rust-lang/crates.io-index"
468
- checksum = "8710a71bc6779e33811a8067bdda3ed08bed1733296ff915e44faf60f8c533d7"
495
+ checksum = "68204f2abc0627a90bdf06e605f5c470aa26fdcb2081ea553a04bdad756693f5"
469
496
  dependencies = [
470
497
  "cc",
471
- "tree-sitter",
498
+ "tree-sitter-language",
472
499
  ]
473
500
 
474
501
  [[package]]
475
- name = "tree-sitter-kotlin"
476
- version = "0.3.8"
502
+ name = "tree-sitter-language"
503
+ version = "0.1.8"
477
504
  source = "registry+https://github.com/rust-lang/crates.io-index"
478
- checksum = "54ff60aeb036f5762515ceb31404512ea4f9599764bcd3857074bb82867bdd34"
479
- dependencies = [
480
- "cc",
481
- "tree-sitter",
482
- ]
505
+ checksum = "ca0d1bf6fdd806e43ae5198f82f527056d359def39e54e67a0f478ac09dac081"
483
506
 
484
507
  [[package]]
485
508
  name = "tree-sitter-python"
@@ -533,6 +556,15 @@ version = "1.13.3"
533
556
  source = "registry+https://github.com/rust-lang/crates.io-index"
534
557
  checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
535
558
 
559
+ [[package]]
560
+ name = "willbooster-tree-sitter-kotlin"
561
+ version = "0.0.0"
562
+ source = "git+https://github.com/WillBooster/tree-sitter-kotlin?tag=v1.1.0#a05912e6f07e2cedd44bfb117940361089d4c3e3"
563
+ dependencies = [
564
+ "cc",
565
+ "tree-sitter-language",
566
+ ]
567
+
536
568
  [[package]]
537
569
  name = "windows-link"
538
570
  version = "0.2.1"
package/native/Cargo.toml CHANGED
@@ -16,20 +16,20 @@ regex = "1"
16
16
  rustc-hash = "2"
17
17
 
18
18
  # Grammar crates are pinned exactly: every metric (and the golden-based test suite) is calibrated
19
- # against these grammar versions, so upgrades are deliberate, test-verified changes. The versions
20
- # match the npm grammar packages the former node-tree-sitter backend used, keeping historical
21
- # outputs stable. Core 0.22.6 parses identically to node-tree-sitter 0.21.1's vendored core
22
- # (0.21.0 does not: it lacks several parser fixes and can abort on malformed input); its one
23
- # behavioral difference — extra (error-recovery) children no longer inherit fields in
24
- # ts_node_field_name_for_child — is compensated in util::find_children_by_field_name.
25
- tree-sitter = "=0.22.6"
19
+ # against these grammar versions, so upgrades are deliberate, test-verified changes. The Kotlin
20
+ # grammar is WillBooster's fork, which is not on crates.io, pinned to a release tag. Core 0.25 is
21
+ # the first to load that grammar's ABI 15 parser, and tree-sitter-javascript 0.21.x pins
22
+ # `cc ~1.0`, which conflicts with the core's `cc ^1.2`, hence 0.25.0. The core reports no field for
23
+ # extra (error-recovery) children in ts_node_field_name_for_child, which
24
+ # util::find_children_by_field_name compensates for.
25
+ tree-sitter = "=0.25.10"
26
26
  tree-sitter-c = "=0.21.4"
27
27
  tree-sitter-c-sharp = "=0.21.3"
28
28
  tree-sitter-cpp = "=0.22.3"
29
29
  tree-sitter-go = "=0.21.2"
30
30
  tree-sitter-java = "=0.21.0"
31
- tree-sitter-kotlin = "=0.3.8"
32
- tree-sitter-javascript = "=0.21.4"
31
+ tree-sitter-kotlin = { package = "willbooster-tree-sitter-kotlin", git = "https://github.com/WillBooster/tree-sitter-kotlin", tag = "v1.1.0" }
32
+ tree-sitter-javascript = "=0.25.0"
33
33
  tree-sitter-python = "=0.21.0"
34
34
  tree-sitter-ruby = "=0.21.0"
35
35
  # 0.21.0 breaks MSVC (its build script passes the GCC-only -Wno-unused-parameter
Binary file