code-gauge 4.5.0 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -6
- package/THIRD-PARTY-NOTICES.txt +8911 -0
- package/dist/cliConfig.cjs +1 -1
- package/dist/cliConfig.cjs.map +1 -1
- package/dist/cliConfig.js +1 -1
- package/dist/crossFileDuplication.cjs +1 -1
- package/dist/crossFileDuplication.cjs.map +1 -1
- package/dist/crossFileDuplication.d.ts +2 -2
- package/dist/crossFileDuplication.js +1 -1
- package/dist/crossFileDuplication.js.map +1 -1
- package/dist/crossFileNearMiss.cjs +1 -1
- package/dist/crossFileNearMiss.cjs.map +1 -1
- package/dist/crossFileNearMiss.d.ts +18 -12
- package/dist/crossFileNearMiss.js +1 -1
- package/dist/crossFileNearMiss.js.map +1 -1
- package/dist/diffCommand.cjs +1 -1
- package/dist/diffCommand.cjs.map +1 -1
- package/dist/diffCommand.js +1 -1
- package/dist/languages.cjs +1 -1
- package/dist/languages.cjs.map +1 -1
- package/dist/languages.js +1 -1
- package/dist/languages.js.map +1 -1
- package/dist/nativeMetrics.cjs +3 -3
- package/dist/nativeMetrics.cjs.map +1 -1
- package/dist/nativeMetrics.d.ts +14 -0
- package/dist/nativeMetrics.js +3 -3
- package/dist/nativeMetrics.js.map +1 -1
- package/dist/scan.cjs +1 -1
- package/dist/scan.js +1 -1
- package/dist/types.d.ts +3 -1
- package/dist/wasmBinding.cjs +2 -0
- package/dist/wasmBinding.cjs.map +1 -0
- package/dist/wasmBinding.d.ts +9 -0
- package/dist/wasmBinding.js +2 -0
- package/dist/wasmBinding.js.map +1 -0
- package/dist/worker.cjs +2 -0
- package/dist/worker.cjs.map +1 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +2 -0
- package/dist/worker.js.map +1 -0
- package/native/Cargo.toml +5 -2
- package/native/build.rs +4 -1
- package/native/code-gauge.wasm +0 -0
- package/native/src/duplication.rs +332 -234
- package/native/src/lib.rs +32 -37
- package/native/src/napi.rs +45 -0
- package/native/src/near_miss.rs +455 -0
- package/native/src/wasm.rs +128 -0
- package/package.json +18 -13
package/dist/diffCommand.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diffCommand.cjs","names":["formatError","resolveTarget","loadConfig","configSearchDirectory","resolveOptions","resolveGateOptions","realpath","resolveRepoRoot","resolveMergeBase","listChangedFiles","listRepositoryFiles","listSymlinkPathsAtRevision","scanListedFiles","formatPath","isScannedPath","evaluateRegressionGate","path","readFile","collectFunctionTokenSequences","getLanguage","readFileAtRevision","measureWithCrossFileData","lstat","stat","measureCrossFileDuplication","collectDuplicatedLineNumbers"],"sources":["../src/diffCommand.ts"],"sourcesContent":["import { lstat, readFile, realpath, stat } from 'node:fs/promises';\nimport path from 'node:path';\nimport { loadConfig, resolveGateOptions, resolveOptions, type ResolvedOptions } from './cliConfig.js';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData } from './duplication.js';\nimport {\n listChangedFiles,\n listRepositoryFiles,\n listSymlinkPathsAtRevision,\n readFileAtRevision,\n resolveMergeBase,\n resolveRepoRoot,\n type ChangedFile,\n} from './git.js';\nimport { collectFunctionTokenSequences } from './metrics.js';\nimport {\n evaluateRegressionGate,\n type CheckedFunctionReport,\n type GateFileInput,\n type GateFunctionValues,\n type GateResult,\n} from './regressionGate.js';\nimport {\n collectDuplicatedLineNumbers,\n configSearchDirectory,\n formatError,\n formatPath,\n getLanguage,\n isScannedPath,\n measureWithCrossFileData,\n resolveTarget,\n scanListedFiles,\n writeStderr,\n writeStdout,\n type FileMetrics,\n} from './scan.js';\nimport type { CodeMetrics, LanguageName } from './types.js';\n\n/** Raw options of the `diff` subcommand; every field but base is undefined unless the flag was passed. */\nexport interface DiffCliOptions {\n base: string;\n config?: string;\n duplicationMinTokens?: number;\n duplicationMaxGapTokens?: number;\n duplicationMinSimilarityPercent?: number;\n includeTests?: boolean;\n json?: boolean;\n full?: boolean;\n}\n\n/** One changed file measured at both revisions, plus its duplication-universe contribution. */\ninterface PreparedFile {\n changed: ChangedFile;\n /** Repository-relative display path: the head path, or the base path for deleted files. */\n displayFile: string;\n /** Whether the file is gated (under the target directory); others only feed the base universe. */\n gated: boolean;\n headFile?: FileMetrics;\n baseMetrics?: CodeMetrics;\n baseCandidates?: CrossFileDuplicationFileData;\n baseFunctionTokens?: Int32Array[];\n headFunctionTokens?: Int32Array[];\n}\n\n/** A scanned file that git considers part of the project, keyed by its repository-relative path. */\ninterface ScannedFile {\n relativePath: string;\n file: FileMetrics;\n}\n\n/**\n * Runs the regression gate: measures the files changed relative to the merge-base with the base\n * ref, at both revisions (`git cat-file`; no checkout, no persisted baseline), and reports only\n * violations. Exit codes: 0 all gates passed, 1 violations, 2 changed files could not be measured.\n */\nexport async function runDiffCommand(target: string, cliOptions: DiffCliOptions): Promise<void> {\n try {\n await runGate(target, cliOptions);\n } catch (error) {\n writeStderr(`Error: ${formatError(error)}\\n`);\n process.exitCode = 2;\n }\n}\n\nasync function runGate(target: string, cliOptions: DiffCliOptions): Promise<void> {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const gateOptions = resolveGateOptions(config);\n\n // The target may be a typo'd path whose ancestors don't exist either; repository discovery must\n // still run so the mistyped target gets its own diagnostic instead of a git spawn failure.\n const repoRoot = await realpath(\n await resolveRepoRoot(await firstExistingDirectory(await configSearchDirectory(resolvedTarget)))\n );\n const mergeBase = await resolveMergeBase(repoRoot, cliOptions.base);\n const changedFiles = await listChangedFiles(repoRoot, mergeBase);\n\n // Every git-visible file (tracked or untracked non-ignored) is measured at head: that provides\n // the head metrics of changed files and the project-wide duplication universe, so copy-paste\n // from unchanged code into changed files is caught. Scanning the explicit git list (instead of\n // walking the tree) keeps ignored artifact directories from ever being parsed: they exist in\n // neither the base commit nor CI, so they would only cost time and skew duplication counts.\n // Unchanged files are byte-identical at both revisions, so the base universe is the same scan\n // with the changed files' contents swapped for their merge-base blobs.\n const repositoryFiles = await listRepositoryFiles(repoRoot);\n const baseSymlinkPaths = await listSymlinkPathsAtRevision(repoRoot, mergeBase);\n const scan = await scanListedFiles(repoRoot, repositoryFiles, options);\n // A run-wide failure (a missing native addon) invalidates the whole gate: surface it once as\n // the fatal error (exit 2) instead of diagnosing every changed file as unmeasured.\n if (scan.fatalError) {\n throw new Error(scan.fatalError);\n }\n const scannedFiles: ScannedFile[] = scan.files.map((file) => ({\n relativePath: formatPath(file.file, scan.displayRoot),\n file,\n }));\n\n // A measurement failure on ANY scannable changed file forces exit 2 — deliberately including\n // files outside a scoped target, because cross-file function matching and the base duplication\n // universe for the gated files depend on them. Failures elsewhere (unchanged files) and\n // unsupported changed paths degrade to warnings.\n const changedPaths = new Set(\n changedFiles\n .flatMap((changed) => [changed.headPath, ...(changed.basePath === undefined ? [] : [changed.basePath])])\n .filter((changedPath) => isScannedPath(changedPath, options))\n );\n const errors: string[] = [];\n const warnings = [...scan.warnings];\n for (const error of scan.errors) {\n if ([...changedPaths].some((changedPath) => error.startsWith(`${changedPath}:`))) {\n errors.push(error);\n } else {\n warnings.push(error);\n }\n }\n\n const { canonicalTarget, targetExists } = await canonicalizeTarget(resolvedTarget);\n const prepared = await prepareChangedFiles(\n changedFiles,\n { repoRoot, mergeBase, canonicalTarget, options, scannedFiles, baseSymlinkPaths },\n errors,\n warnings\n );\n // A gate must not fail open on a mistyped target: a nonexistent path is only acceptable when it\n // still matches changed files (e.g. a fully deleted directory).\n if (!targetExists && !prepared.some((file) => file.gated)) {\n throw new Error(`target \"${target}\" does not exist and matches no changed file`);\n }\n\n // Non-gated files (outside the target, or renamed out of scan scope) still feed function\n // matching and the duplication universes; the evaluator reports nothing for them.\n const { baseCross, headCross } = measureDuplicationUniverses(prepared, scannedFiles, options);\n const inputs = prepared.map((file) => toGateInput(file, baseCross, headCross));\n const result = evaluateRegressionGate(inputs, gateOptions);\n\n if (cliOptions.json) {\n printJsonReport(cliOptions, mergeBase, result, inputs, errors, warnings);\n } else {\n printTextReport(cliOptions, mergeBase, result, errors, warnings);\n }\n\n if (errors.length > 0) {\n process.exitCode = 2;\n } else if (result.violations.length > 0) {\n process.exitCode = 1;\n }\n}\n\n/** The target may not exist (e.g. only deleted files under it); fall back to the resolved path. */\nasync function canonicalizeTarget(resolvedTarget: string): Promise<{ canonicalTarget: string; targetExists: boolean }> {\n try {\n return { canonicalTarget: await realpath(resolvedTarget), targetExists: true };\n } catch {\n return { canonicalTarget: resolvedTarget, targetExists: false };\n }\n}\n\ninterface GateContext {\n repoRoot: string;\n mergeBase: string;\n canonicalTarget: string;\n options: ResolvedOptions;\n scannedFiles: ScannedFile[];\n /** Paths that are symbolic links at the merge-base; like head symlinks, they are not gated. */\n baseSymlinkPaths: Set<string>;\n}\n\nasync function prepareChangedFiles(\n changedFiles: ChangedFile[],\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile[]> {\n const headByPath = new Map(context.scannedFiles.map(({ relativePath, file }) => [relativePath, file]));\n const prepared: PreparedFile[] = [];\n for (const changed of changedFiles) {\n const file = await prepareChangedFile(changed, context, headByPath, errors, warnings);\n if (file) {\n prepared.push(file);\n }\n }\n return prepared;\n}\n\nasync function prepareChangedFile(\n changed: ChangedFile,\n context: GateContext,\n headByPath: Map<string, FileMetrics>,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile | undefined> {\n // Symbolic links are skipped on both sides, mirroring scanListedFiles: git stores only the\n // target string, so a symlink blob is not measurable source.\n const headScannable =\n changed.status !== 'deleted' &&\n isScannedPath(changed.headPath, context.options) &&\n !(await isSymbolicLink(path.join(context.repoRoot, changed.headPath)));\n // A base path outside the scan scope (renamed from a test/ignored directory, or an unsupported\n // extension) was never measurable code: its content gates as new code instead of ratcheting\n // against a blob the scanner would not have measured.\n const baseScannable =\n changed.basePath !== undefined &&\n isScannedPath(changed.basePath, context.options) &&\n !context.baseSymlinkPaths.has(changed.basePath);\n if (!headScannable && !baseScannable) {\n return undefined;\n }\n\n const displayFile = changed.status === 'deleted' ? (changed.basePath as string) : changed.headPath;\n const headFile = headScannable ? headByPath.get(changed.headPath) : undefined;\n if (headScannable && !headFile) {\n reportUnmeasuredChangedFile(changed.headPath, errors);\n return undefined;\n }\n\n const file: PreparedFile = {\n changed,\n displayFile,\n // A file whose head left the scan scope still contributes its base functions to matching and\n // its base blob to the base universe, but nothing about it is gated or reported.\n gated:\n headScannable || changed.status === 'deleted'\n ? isWithinTarget(path.join(context.repoRoot, displayFile), context.canonicalTarget)\n : false,\n headFile,\n };\n\n if (baseScannable && !(await measureBaseRevision(file, changed.basePath as string, context, errors, warnings))) {\n return undefined;\n }\n\n if (headFile) {\n await collectHeadFunctionTokens(file, headFile, context, warnings);\n }\n\n return file;\n}\n\n/**\n * The scan covers exactly the git-visible list, so a scannable changed path can only be missing\n * after a measurement failure (already recorded as an error) or a silent exclusion (an alias of\n * an already-visited file, or absence from the git list). Failing loudly keeps the gate from\n * passing with the file unchecked.\n */\nfunction reportUnmeasuredChangedFile(headPath: string, errors: string[]): void {\n if (!errors.some((error) => error.startsWith(`${headPath}:`))) {\n errors.push(`${headPath}: changed file was not measured`);\n }\n}\n\nasync function collectHeadFunctionTokens(\n file: PreparedFile,\n headFile: FileMetrics,\n context: GateContext,\n warnings: string[]\n): Promise<void> {\n try {\n const headContent = await readFile(headFile.file, 'utf8');\n file.headFunctionTokens = collectFunctionTokenSequences(headContent, {\n language: getLanguage(file.changed.headPath, context.options) as LanguageName,\n duplication: context.options.duplication,\n });\n } catch (error) {\n // Only rename re-matching degrades without token sequences; the head metrics still gate.\n warnings.push(`${file.displayFile}: function token sequences unavailable: ${formatError(error)}`);\n }\n}\n\n/**\n * Measures the merge-base blob into `file`; false (with an error recorded) only when the metrics\n * themselves cannot be measured. The auxiliary collections (duplication candidates, token\n * sequences) may fail independently of the metrics, so their failure only degrades duplication\n * data and rename re-matching — the function-level ratchets still run.\n */\nasync function measureBaseRevision(\n file: PreparedFile,\n basePath: string,\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<boolean> {\n const measureOptions = {\n language: getLanguage(basePath, context.options) as LanguageName,\n duplication: context.options.duplication,\n };\n let baseContent;\n try {\n baseContent = await readFileAtRevision(context.repoRoot, context.mergeBase, basePath);\n const measured = measureWithCrossFileData(baseContent, measureOptions);\n file.baseMetrics = measured.metrics;\n file.baseCandidates = measured.crossFileData;\n if (measured.crossFileError !== undefined) {\n warnings.push(`${basePath} (at merge-base): duplication candidates unavailable: ${measured.crossFileError}`);\n }\n } catch (error) {\n errors.push(`${basePath} (at merge-base): ${formatError(error)}`);\n return false;\n }\n try {\n file.baseFunctionTokens = collectFunctionTokenSequences(baseContent, measureOptions);\n } catch (error) {\n warnings.push(`${basePath} (at merge-base): function token sequences unavailable: ${formatError(error)}`);\n }\n return true;\n}\n\nasync function isSymbolicLink(absolutePath: string): Promise<boolean> {\n const stats = await lstat(absolutePath).catch(() => {});\n return stats?.isSymbolicLink() ?? false;\n}\n\n/** Walks up to the nearest existing DIRECTORY, so git commands never spawn in a missing or non-directory cwd. */\nasync function firstExistingDirectory(directory: string): Promise<string> {\n let current = directory;\n while (true) {\n const stats = await stat(current).catch(() => {});\n if (stats?.isDirectory()) {\n return current;\n }\n const parent = path.dirname(current);\n if (parent === current) {\n return current;\n }\n current = parent;\n }\n}\n\nfunction isWithinTarget(candidate: string, targetDirectory: string): boolean {\n const relative = path.relative(targetDirectory, candidate);\n return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));\n}\n\nfunction measureDuplicationUniverses(\n prepared: PreparedFile[],\n scannedFiles: ScannedFile[],\n options: ResolvedOptions\n): { baseCross?: CrossFileDuplicationMetrics; headCross?: CrossFileDuplicationMetrics } {\n const headSources = scannedFiles.flatMap(({ relativePath, file }) =>\n file.duplicationCandidates ? [{ file: relativePath, ...file.duplicationCandidates }] : []\n );\n\n const changedHeadPaths = new Set(\n prepared.flatMap((file) => (file.changed.status === 'deleted' ? [] : [file.changed.headPath]))\n );\n const baseSources = headSources.filter((source) => !changedHeadPaths.has(source.file));\n for (const file of prepared) {\n if (file.baseCandidates && file.changed.basePath !== undefined) {\n baseSources.push({ file: file.changed.basePath, ...file.baseCandidates });\n }\n }\n\n return {\n baseCross: baseSources.length >= 2 ? measureCrossFileDuplication(baseSources, options.duplication) : undefined,\n headCross: headSources.length >= 2 ? measureCrossFileDuplication(headSources, options.duplication) : undefined,\n };\n}\n\nfunction toGateInput(\n file: PreparedFile,\n baseCross: CrossFileDuplicationMetrics | undefined,\n headCross: CrossFileDuplicationMetrics | undefined\n): GateFileInput {\n return {\n file: file.displayFile,\n baseMetrics: file.baseMetrics,\n headMetrics: file.headFile?.metrics,\n baseFunctionTokens: file.baseFunctionTokens,\n headFunctionTokens: file.headFunctionTokens,\n baseDuplicatedLineCount:\n file.baseMetrics === undefined || file.changed.basePath === undefined\n ? 0\n : countDuplicatedLines(file.baseMetrics, baseCross, file.changed.basePath),\n headDuplicatedLineCount:\n file.changed.status === 'deleted'\n ? 0\n : countDuplicatedLines(file.headFile?.metrics, headCross, file.changed.headPath),\n duplicationPartners: collectPartners(headCross, file.changed.headPath),\n gated: file.gated,\n };\n}\n\nfunction countDuplicatedLines(\n metrics: CodeMetrics | undefined,\n cross: CrossFileDuplicationMetrics | undefined,\n file: string\n): number {\n return collectDuplicatedLineNumbers(metrics, cross, file).size;\n}\n\nfunction collectPartners(cross: CrossFileDuplicationMetrics | undefined, file: string): string[] {\n if (!cross) {\n return [];\n }\n const partners = new Set<string>();\n for (const group of cross.groups) {\n if (group.files.includes(file)) {\n for (const partner of group.files) {\n if (partner !== file) {\n partners.add(partner);\n }\n }\n }\n }\n return [...partners].toSorted();\n}\n\nfunction printTextReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n errors: string[],\n warnings: string[]\n): void {\n const shortBase = mergeBase.slice(0, 12);\n if (errors.length > 0) {\n // Unmeasured files were not gated, so \"0 violations\" would be vacuous; never claim a pass.\n writeStdout(\n `Regression gate could not complete: ${errors.length} measurement failures (details on stderr)` +\n `${result.violations.length > 0 ? `; ${result.violations.length} violations in the measured files` : ''} (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n printViolations(result);\n } else if (result.violations.length === 0) {\n writeStdout(\n `Regression gate passed: ${result.checkedFileCount} changed files, ${result.checkedFunctionCount} functions checked (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n } else {\n writeStdout(\n `Regression gate vs ${cliOptions.base} (merge-base ${shortBase}): ${result.violations.length} violations\\n`\n );\n printViolations(result);\n }\n\n if (cliOptions.full) {\n printFullDetails(result);\n }\n\n for (const warning of warnings) {\n writeStderr(`Warning: ${warning}\\n`);\n }\n for (const error of errors) {\n writeStderr(`Error: ${error}\\n`);\n }\n}\n\nfunction printViolations(result: GateResult): void {\n for (const [index, violation] of result.violations.entries()) {\n writeStdout(`${index + 1}. ${violation.message}\\n`);\n }\n}\n\n/** Base -> head values of every checked function; kept behind --full for humans and trending. */\nfunction printFullDetails(result: GateResult): void {\n if (result.checkedFunctions.length === 0) {\n return;\n }\n writeStdout('\\nChecked functions (base -> head):\\n');\n for (const report of result.checkedFunctions) {\n writeStdout(`- ${formatFunctionReport(report)}\\n`);\n }\n}\n\nfunction formatFunctionReport(report: CheckedFunctionReport): string {\n const range = (\n select: (values: GateFunctionValues) => number,\n format: (value: number) => string = String\n ): string => {\n const head = format(select(report.head));\n return report.base ? `${format(select(report.base))} -> ${head}` : head;\n };\n const values = [\n `cognitive ${range((fn) => fn.cognitiveComplexity)}`,\n `NCSS ${range((fn) => fn.ncss)}`,\n `nesting ${range((fn) => fn.nestingDepth)}`,\n `DepDegree ${range((fn) => fn.depDegree)}`,\n `volume ${range(\n (fn) => fn.halsteadVolume,\n (value) => value.toFixed(1)\n )}`,\n ];\n return `${report.file}:${report.startLine}-${report.endLine} ${report.name}${report.base ? '' : ' (new)'}: ${values.join(', ')}`;\n}\n\nfunction printJsonReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n inputs: GateFileInput[],\n errors: string[],\n warnings: string[]\n): void {\n const report: Record<string, unknown> = {\n base: cliOptions.base,\n mergeBase,\n passed: result.violations.length === 0 && errors.length === 0,\n violations: result.violations,\n checkedFileCount: result.checkedFileCount,\n checkedFunctionCount: result.checkedFunctionCount,\n newFunctionCount: result.newFunctionCount,\n errors,\n warnings,\n };\n if (cliOptions.full) {\n report.files = inputs\n .filter((input) => input.gated !== false)\n .map((input) => ({\n file: input.file,\n baseFunctionCount: input.baseMetrics?.functions.length ?? 0,\n headFunctionCount: input.headMetrics?.functions.length ?? 0,\n baseNcss: input.baseMetrics?.ncssCount ?? 0,\n headNcss: input.headMetrics?.ncssCount ?? 0,\n baseMaxCognitiveComplexity: input.baseMetrics?.maxCognitiveComplexity ?? 0,\n headMaxCognitiveComplexity: input.headMetrics?.maxCognitiveComplexity ?? 0,\n baseDuplicatedLineCount: input.baseDuplicatedLineCount,\n headDuplicatedLineCount: input.headDuplicatedLineCount,\n duplicationPartners: input.duplicationPartners,\n functions: result.checkedFunctions.filter((fn) => fn.file === input.file),\n }));\n }\n writeStdout(JSON.stringify(report, undefined, 2) + '\\n');\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"diffCommand.cjs","names":["formatError","resolveTarget","loadConfig","configSearchDirectory","resolveOptions","resolveGateOptions","realpath","resolveRepoRoot","resolveMergeBase","listChangedFiles","listRepositoryFiles","listSymlinkPathsAtRevision","scanListedFiles","formatPath","isScannedPath","evaluateRegressionGate","path","readFile","collectFunctionTokenSequences","getLanguage","readFileAtRevision","measureWithCrossFileData","lstat","stat","measureCrossFileDuplication","collectDuplicatedLineNumbers"],"sources":["../src/diffCommand.ts"],"sourcesContent":["import { lstat, readFile, realpath, stat } from 'node:fs/promises';\nimport path from 'node:path';\nimport { loadConfig, resolveGateOptions, resolveOptions, type ResolvedOptions } from './cliConfig.js';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData } from './duplication.js';\nimport {\n listChangedFiles,\n listRepositoryFiles,\n listSymlinkPathsAtRevision,\n readFileAtRevision,\n resolveMergeBase,\n resolveRepoRoot,\n type ChangedFile,\n} from './git.js';\nimport { collectFunctionTokenSequences } from './metrics.js';\nimport {\n evaluateRegressionGate,\n type CheckedFunctionReport,\n type GateFileInput,\n type GateFunctionValues,\n type GateResult,\n} from './regressionGate.js';\nimport {\n collectDuplicatedLineNumbers,\n configSearchDirectory,\n formatError,\n formatPath,\n getLanguage,\n isScannedPath,\n measureWithCrossFileData,\n resolveTarget,\n scanListedFiles,\n writeStderr,\n writeStdout,\n type FileMetrics,\n} from './scan.js';\nimport type { CodeMetrics, LanguageName } from './types.js';\n\n/** Raw options of the `diff` subcommand; every field but base is undefined unless the flag was passed. */\nexport interface DiffCliOptions {\n base: string;\n config?: string;\n duplicationMinTokens?: number;\n duplicationMaxGapTokens?: number;\n duplicationMinSimilarityPercent?: number;\n includeTests?: boolean;\n json?: boolean;\n full?: boolean;\n}\n\n/** One changed file measured at both revisions, plus its duplication-universe contribution. */\ninterface PreparedFile {\n changed: ChangedFile;\n /** Repository-relative display path: the head path, or the base path for deleted files. */\n displayFile: string;\n /** Whether the file is gated (under the target directory); others only feed the base universe. */\n gated: boolean;\n headFile?: FileMetrics;\n baseMetrics?: CodeMetrics;\n baseCandidates?: CrossFileDuplicationFileData;\n baseFunctionTokens?: Int32Array[];\n headFunctionTokens?: Int32Array[];\n}\n\n/** A scanned file that git considers part of the project, keyed by its repository-relative path. */\ninterface ScannedFile {\n relativePath: string;\n file: FileMetrics;\n}\n\n/**\n * Runs the regression gate: measures the files changed relative to the merge-base with the base\n * ref, at both revisions (`git cat-file`; no checkout, no persisted baseline), and reports only\n * violations. Exit codes: 0 all gates passed, 1 violations, 2 changed files could not be measured.\n */\nexport async function runDiffCommand(target: string, cliOptions: DiffCliOptions): Promise<void> {\n try {\n await runGate(target, cliOptions);\n } catch (error) {\n writeStderr(`Error: ${formatError(error)}\\n`);\n process.exitCode = 2;\n }\n}\n\nasync function runGate(target: string, cliOptions: DiffCliOptions): Promise<void> {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const gateOptions = resolveGateOptions(config);\n\n // The target may be a typo'd path whose ancestors don't exist either; repository discovery must\n // still run so the mistyped target gets its own diagnostic instead of a git spawn failure.\n const repoRoot = await realpath(\n await resolveRepoRoot(await firstExistingDirectory(await configSearchDirectory(resolvedTarget)))\n );\n const mergeBase = await resolveMergeBase(repoRoot, cliOptions.base);\n const changedFiles = await listChangedFiles(repoRoot, mergeBase);\n\n // Every git-visible file (tracked or untracked non-ignored) is measured at head: that provides\n // the head metrics of changed files and the project-wide duplication universe, so copy-paste\n // from unchanged code into changed files is caught. Scanning the explicit git list (instead of\n // walking the tree) keeps ignored artifact directories from ever being parsed: they exist in\n // neither the base commit nor CI, so they would only cost time and skew duplication counts.\n // Unchanged files are byte-identical at both revisions, so the base universe is the same scan\n // with the changed files' contents swapped for their merge-base blobs.\n const repositoryFiles = await listRepositoryFiles(repoRoot);\n const baseSymlinkPaths = await listSymlinkPathsAtRevision(repoRoot, mergeBase);\n const scan = await scanListedFiles(repoRoot, repositoryFiles, options);\n // A run-wide failure (a missing native addon) invalidates the whole gate: surface it once as\n // the fatal error (exit 2) instead of diagnosing every changed file as unmeasured.\n if (scan.fatalError) {\n throw new Error(scan.fatalError);\n }\n const scannedFiles: ScannedFile[] = scan.files.map((file) => ({\n relativePath: formatPath(file.file, scan.displayRoot),\n file,\n }));\n\n // A measurement failure on ANY scannable changed file forces exit 2 — deliberately including\n // files outside a scoped target, because cross-file function matching and the base duplication\n // universe for the gated files depend on them. Failures elsewhere (unchanged files) and\n // unsupported changed paths degrade to warnings.\n const changedPaths = new Set(\n changedFiles\n .flatMap((changed) => [changed.headPath, ...(changed.basePath === undefined ? [] : [changed.basePath])])\n .filter((changedPath) => isScannedPath(changedPath, options))\n );\n const errors: string[] = [];\n const warnings = [...scan.warnings];\n for (const error of scan.errors) {\n if ([...changedPaths].some((changedPath) => error.startsWith(`${changedPath}:`))) {\n errors.push(error);\n } else {\n warnings.push(error);\n }\n }\n\n const { canonicalTarget, targetExists } = await canonicalizeTarget(resolvedTarget);\n const prepared = await prepareChangedFiles(\n changedFiles,\n { repoRoot, mergeBase, canonicalTarget, options, scannedFiles, baseSymlinkPaths },\n errors,\n warnings\n );\n // A gate must not fail open on a mistyped target: a nonexistent path is only acceptable when it\n // still matches changed files (e.g. a fully deleted directory).\n if (!targetExists && !prepared.some((file) => file.gated)) {\n throw new Error(`target \"${target}\" does not exist and matches no changed file`);\n }\n\n // Non-gated files (outside the target, or renamed out of scan scope) still feed function\n // matching and the duplication universes; the evaluator reports nothing for them.\n const { baseCross, headCross } = measureDuplicationUniverses(prepared, scannedFiles, options);\n const inputs = prepared.map((file) => toGateInput(file, baseCross, headCross));\n const result = evaluateRegressionGate(inputs, gateOptions);\n\n if (cliOptions.json) {\n printJsonReport(cliOptions, mergeBase, result, inputs, errors, warnings);\n } else {\n printTextReport(cliOptions, mergeBase, result, errors, warnings);\n }\n\n if (errors.length > 0) {\n process.exitCode = 2;\n } else if (result.violations.length > 0) {\n process.exitCode = 1;\n }\n}\n\n/** The target may not exist (e.g. only deleted files under it); fall back to the resolved path. */\nasync function canonicalizeTarget(resolvedTarget: string): Promise<{ canonicalTarget: string; targetExists: boolean }> {\n try {\n return { canonicalTarget: await realpath(resolvedTarget), targetExists: true };\n } catch {\n return { canonicalTarget: resolvedTarget, targetExists: false };\n }\n}\n\ninterface GateContext {\n repoRoot: string;\n mergeBase: string;\n canonicalTarget: string;\n options: ResolvedOptions;\n scannedFiles: ScannedFile[];\n /** Paths that are symbolic links at the merge-base; like head symlinks, they are not gated. */\n baseSymlinkPaths: Set<string>;\n}\n\nasync function prepareChangedFiles(\n changedFiles: ChangedFile[],\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile[]> {\n const headByPath = new Map(context.scannedFiles.map(({ relativePath, file }) => [relativePath, file]));\n const prepared: PreparedFile[] = [];\n for (const changed of changedFiles) {\n const file = await prepareChangedFile(changed, context, headByPath, errors, warnings);\n if (file) {\n prepared.push(file);\n }\n }\n return prepared;\n}\n\nasync function prepareChangedFile(\n changed: ChangedFile,\n context: GateContext,\n headByPath: Map<string, FileMetrics>,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile | undefined> {\n // Symbolic links are skipped on both sides, mirroring scanListedFiles: git stores only the\n // target string, so a symlink blob is not measurable source.\n const headScannable =\n changed.status !== 'deleted' &&\n isScannedPath(changed.headPath, context.options) &&\n !(await isSymbolicLink(path.join(context.repoRoot, changed.headPath)));\n // A base path outside the scan scope (renamed from a test/ignored directory, or an unsupported\n // extension) was never measurable code: its content gates as new code instead of ratcheting\n // against a blob the scanner would not have measured.\n const baseScannable =\n changed.basePath !== undefined &&\n isScannedPath(changed.basePath, context.options) &&\n !context.baseSymlinkPaths.has(changed.basePath);\n if (!headScannable && !baseScannable) {\n return undefined;\n }\n\n const displayFile = changed.status === 'deleted' ? (changed.basePath as string) : changed.headPath;\n const headFile = headScannable ? headByPath.get(changed.headPath) : undefined;\n if (headScannable && !headFile) {\n reportUnmeasuredChangedFile(changed.headPath, errors);\n return undefined;\n }\n\n const file: PreparedFile = {\n changed,\n displayFile,\n // A file whose head left the scan scope still contributes its base functions to matching and\n // its base blob to the base universe, but nothing about it is gated or reported.\n gated:\n headScannable || changed.status === 'deleted'\n ? isWithinTarget(path.join(context.repoRoot, displayFile), context.canonicalTarget)\n : false,\n headFile,\n };\n\n if (baseScannable && !(await measureBaseRevision(file, changed.basePath as string, context, errors, warnings))) {\n return undefined;\n }\n\n if (headFile) {\n await collectHeadFunctionTokens(file, headFile, context, warnings);\n }\n\n return file;\n}\n\n/**\n * The scan covers exactly the git-visible list, so a scannable changed path can only be missing\n * after a measurement failure (already recorded as an error) or a silent exclusion (an alias of\n * an already-visited file, or absence from the git list). Failing loudly keeps the gate from\n * passing with the file unchecked.\n */\nfunction reportUnmeasuredChangedFile(headPath: string, errors: string[]): void {\n if (!errors.some((error) => error.startsWith(`${headPath}:`))) {\n errors.push(`${headPath}: changed file was not measured`);\n }\n}\n\nasync function collectHeadFunctionTokens(\n file: PreparedFile,\n headFile: FileMetrics,\n context: GateContext,\n warnings: string[]\n): Promise<void> {\n try {\n const headContent = await readFile(headFile.file, 'utf8');\n file.headFunctionTokens = collectFunctionTokenSequences(headContent, {\n language: getLanguage(file.changed.headPath, context.options) as LanguageName,\n duplication: context.options.duplication,\n });\n } catch (error) {\n // Only rename re-matching degrades without token sequences; the head metrics still gate.\n warnings.push(`${file.displayFile}: function token sequences unavailable: ${formatError(error)}`);\n }\n}\n\n/**\n * Measures the merge-base blob into `file`; false (with an error recorded) only when the metrics\n * themselves cannot be measured. The auxiliary collections (duplication candidates, token\n * sequences) may fail independently of the metrics, so their failure only degrades duplication\n * data and rename re-matching — the function-level ratchets still run.\n */\nasync function measureBaseRevision(\n file: PreparedFile,\n basePath: string,\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<boolean> {\n const measureOptions = {\n language: getLanguage(basePath, context.options) as LanguageName,\n duplication: context.options.duplication,\n };\n let baseContent;\n try {\n baseContent = await readFileAtRevision(context.repoRoot, context.mergeBase, basePath);\n const measured = measureWithCrossFileData(baseContent, measureOptions);\n file.baseMetrics = measured.metrics;\n file.baseCandidates = measured.crossFileData;\n if (measured.crossFileError !== undefined) {\n warnings.push(`${basePath} (at merge-base): duplication candidates unavailable: ${measured.crossFileError}`);\n }\n } catch (error) {\n errors.push(`${basePath} (at merge-base): ${formatError(error)}`);\n return false;\n }\n try {\n file.baseFunctionTokens = collectFunctionTokenSequences(baseContent, measureOptions);\n } catch (error) {\n warnings.push(`${basePath} (at merge-base): function token sequences unavailable: ${formatError(error)}`);\n }\n return true;\n}\n\nasync function isSymbolicLink(absolutePath: string): Promise<boolean> {\n const stats = await lstat(absolutePath).catch(() => {});\n return stats?.isSymbolicLink() ?? false;\n}\n\n/** Walks up to the nearest existing DIRECTORY, so git commands never spawn in a missing or non-directory cwd. */\nasync function firstExistingDirectory(directory: string): Promise<string> {\n let current = directory;\n while (true) {\n const stats = await stat(current).catch(() => {});\n if (stats?.isDirectory()) {\n return current;\n }\n const parent = path.dirname(current);\n if (parent === current) {\n return current;\n }\n current = parent;\n }\n}\n\nfunction isWithinTarget(candidate: string, targetDirectory: string): boolean {\n const relative = path.relative(targetDirectory, candidate);\n return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));\n}\n\nfunction measureDuplicationUniverses(\n prepared: PreparedFile[],\n scannedFiles: ScannedFile[],\n options: ResolvedOptions\n): { baseCross?: CrossFileDuplicationMetrics; headCross?: CrossFileDuplicationMetrics } {\n const headSources = scannedFiles.flatMap(({ relativePath, file }) =>\n file.duplicationCandidates ? [{ file: relativePath, ...file.duplicationCandidates }] : []\n );\n\n const changedHeadPaths = new Set(\n prepared.flatMap((file) => (file.changed.status === 'deleted' ? [] : [file.changed.headPath]))\n );\n const baseSources = headSources.filter((source) => !changedHeadPaths.has(source.file));\n for (const file of prepared) {\n if (file.baseCandidates && file.changed.basePath !== undefined) {\n baseSources.push({ file: file.changed.basePath, ...file.baseCandidates });\n }\n }\n\n return {\n baseCross: baseSources.length >= 2 ? measureCrossFileDuplication(baseSources, options.duplication) : undefined,\n headCross: headSources.length >= 2 ? measureCrossFileDuplication(headSources, options.duplication) : undefined,\n };\n}\n\nfunction toGateInput(\n file: PreparedFile,\n baseCross: CrossFileDuplicationMetrics | undefined,\n headCross: CrossFileDuplicationMetrics | undefined\n): GateFileInput {\n return {\n file: file.displayFile,\n baseMetrics: file.baseMetrics,\n headMetrics: file.headFile?.metrics,\n baseFunctionTokens: file.baseFunctionTokens,\n headFunctionTokens: file.headFunctionTokens,\n baseDuplicatedLineCount:\n file.baseMetrics === undefined || file.changed.basePath === undefined\n ? 0\n : countDuplicatedLines(file.baseMetrics, baseCross, file.changed.basePath),\n headDuplicatedLineCount:\n file.changed.status === 'deleted'\n ? 0\n : countDuplicatedLines(file.headFile?.metrics, headCross, file.changed.headPath),\n duplicationPartners: collectPartners(headCross, file.changed.headPath),\n gated: file.gated,\n };\n}\n\nfunction countDuplicatedLines(\n metrics: CodeMetrics | undefined,\n cross: CrossFileDuplicationMetrics | undefined,\n file: string\n): number {\n return collectDuplicatedLineNumbers(metrics, cross, file).size;\n}\n\nfunction collectPartners(cross: CrossFileDuplicationMetrics | undefined, file: string): string[] {\n if (!cross) {\n return [];\n }\n const partners = new Set<string>();\n for (const group of cross.groups) {\n if (group.files.includes(file)) {\n for (const partner of group.files) {\n if (partner !== file) {\n partners.add(partner);\n }\n }\n }\n }\n return [...partners].toSorted();\n}\n\nfunction printTextReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n errors: string[],\n warnings: string[]\n): void {\n const shortBase = mergeBase.slice(0, 12);\n if (errors.length > 0) {\n // Unmeasured files were not gated, so \"0 violations\" would be vacuous; never claim a pass.\n writeStdout(\n `Regression gate could not complete: ${errors.length} measurement failures (details on stderr)` +\n `${result.violations.length > 0 ? `; ${result.violations.length} violations in the measured files` : ''} (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n printViolations(result);\n } else if (result.violations.length === 0) {\n writeStdout(\n `Regression gate passed: ${result.checkedFileCount} changed files, ${result.checkedFunctionCount} functions checked (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n } else {\n writeStdout(\n `Regression gate vs ${cliOptions.base} (merge-base ${shortBase}): ${result.violations.length} violations\\n`\n );\n printViolations(result);\n }\n\n if (cliOptions.full) {\n printFullDetails(result);\n }\n\n for (const warning of warnings) {\n writeStderr(`Warning: ${warning}\\n`);\n }\n for (const error of errors) {\n writeStderr(`Error: ${error}\\n`);\n }\n}\n\nfunction printViolations(result: GateResult): void {\n for (const [index, violation] of result.violations.entries()) {\n writeStdout(`${index + 1}. ${violation.message}\\n`);\n }\n}\n\n/** Base -> head values of every checked function; kept behind --full for humans and trending. */\nfunction printFullDetails(result: GateResult): void {\n if (result.checkedFunctions.length === 0) {\n return;\n }\n writeStdout('\\nChecked functions (base -> head):\\n');\n for (const report of result.checkedFunctions) {\n writeStdout(`- ${formatFunctionReport(report)}\\n`);\n }\n}\n\nfunction formatFunctionReport(report: CheckedFunctionReport): string {\n const range = (\n select: (values: GateFunctionValues) => number,\n format: (value: number) => string = String\n ): string => {\n const head = format(select(report.head));\n return report.base ? `${format(select(report.base))} -> ${head}` : head;\n };\n const values = [\n `cognitive ${range((fn) => fn.cognitiveComplexity)}`,\n `NCSS ${range((fn) => fn.ncss)}`,\n `nesting ${range((fn) => fn.nestingDepth)}`,\n `DepDegree ${range((fn) => fn.depDegree)}`,\n `volume ${range(\n (fn) => fn.halsteadVolume,\n (value) => value.toFixed(1)\n )}`,\n ];\n return `${report.file}:${report.startLine}-${report.endLine} ${report.name}${report.base ? '' : ' (new)'}: ${values.join(', ')}`;\n}\n\nfunction printJsonReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n inputs: GateFileInput[],\n errors: string[],\n warnings: string[]\n): void {\n const report: Record<string, unknown> = {\n base: cliOptions.base,\n mergeBase,\n passed: result.violations.length === 0 && errors.length === 0,\n violations: result.violations,\n checkedFileCount: result.checkedFileCount,\n checkedFunctionCount: result.checkedFunctionCount,\n newFunctionCount: result.newFunctionCount,\n errors,\n warnings,\n };\n if (cliOptions.full) {\n report.files = inputs\n .filter((input) => input.gated !== false)\n .map((input) => ({\n file: input.file,\n baseFunctionCount: input.baseMetrics?.functions.length ?? 0,\n headFunctionCount: input.headMetrics?.functions.length ?? 0,\n baseNcss: input.baseMetrics?.ncssCount ?? 0,\n headNcss: input.headMetrics?.ncssCount ?? 0,\n baseMaxCognitiveComplexity: input.baseMetrics?.maxCognitiveComplexity ?? 0,\n headMaxCognitiveComplexity: input.headMetrics?.maxCognitiveComplexity ?? 0,\n baseDuplicatedLineCount: input.baseDuplicatedLineCount,\n headDuplicatedLineCount: input.headDuplicatedLineCount,\n duplicationPartners: input.duplicationPartners,\n functions: result.checkedFunctions.filter((fn) => fn.file === input.file),\n }));\n }\n writeStdout(JSON.stringify(report, undefined, 2) + '\\n');\n}\n"],"mappings":"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"}
|
package/dist/diffCommand.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{collectFunctionTokenSequences as t}from"./metrics.js";import{evaluateRegressionGate as n}from"./regressionGate.js";import{loadConfig as r,resolveGateOptions as i,resolveOptions as a}from"./cliConfig.js";import{listChangedFiles as o,listRepositoryFiles as s,listSymlinkPathsAtRevision as c,readFileAtRevision as l,resolveMergeBase as u,resolveRepoRoot as d}from"./git.js";import{collectDuplicatedLineNumbers as f,configSearchDirectory as p,formatError as m,formatPath as h,getLanguage as g,isScannedPath as _,measureWithCrossFileData as v,resolveTarget as y,scanListedFiles as b,writeStderr as x,writeStdout as S}from"./scan.js";import
|
|
1
|
+
import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{collectFunctionTokenSequences as t}from"./metrics.js";import{evaluateRegressionGate as n}from"./regressionGate.js";import{loadConfig as r,resolveGateOptions as i,resolveOptions as a}from"./cliConfig.js";import{listChangedFiles as o,listRepositoryFiles as s,listSymlinkPathsAtRevision as c,readFileAtRevision as l,resolveMergeBase as u,resolveRepoRoot as d}from"./git.js";import{collectDuplicatedLineNumbers as f,configSearchDirectory as p,formatError as m,formatPath as h,getLanguage as g,isScannedPath as _,measureWithCrossFileData as v,resolveTarget as y,scanListedFiles as b,writeStderr as x,writeStdout as S}from"./scan.js";import{lstat as C,readFile as w,realpath as T,stat as E}from"node:fs/promises";import D from"node:path";async function O(e,t){try{await k(e,t)}catch(e){x(`Error: ${m(e)}\n`),process.exitCode=2}}async function k(e,t){let l=y(e),f=await r(t.config,await p(l)),m=a(t,f),g=i(f),v=await T(await d(await L(await p(l)))),x=await u(v,t.base),S=await o(v,x),C=await s(v),w=await c(v,x),E=await b(v,C,m);if(E.fatalError)throw Error(E.fatalError);let D=E.files.map(e=>({relativePath:h(e.file,E.displayRoot),file:e})),O=new Set(S.flatMap(e=>[e.headPath,...e.basePath===void 0?[]:[e.basePath]]).filter(e=>_(e,m))),k=[],M=[...E.warnings];for(let e of E.errors)[...O].some(t=>e.startsWith(`${t}:`))?k.push(e):M.push(e);let{canonicalTarget:N,targetExists:P}=await A(l),F=await j(S,{repoRoot:v,mergeBase:x,canonicalTarget:N,options:m,scannedFiles:D,baseSymlinkPaths:w},k,M);if(!P&&!F.some(e=>e.gated))throw Error(`target "${e}" does not exist and matches no changed file`);let{baseCross:I,headCross:R}=z(F,D,m),V=F.map(e=>B(e,I,R)),H=n(V,g);t.json?q(t,x,H,V,k,M):U(t,x,H,k,M),k.length>0?process.exitCode=2:H.violations.length>0&&(process.exitCode=1)}async function A(e){try{return{canonicalTarget:await T(e),targetExists:!0}}catch{return{canonicalTarget:e,targetExists:!1}}}async function j(e,t,n,r){let i=new Map(t.scannedFiles.map(({relativePath:e,file:t})=>[e,t])),a=[];for(let o of e){let e=await M(o,t,i,n,r);e&&a.push(e)}return a}async function M(e,t,n,r,i){let a=e.status!==`deleted`&&_(e.headPath,t.options)&&!await I(D.join(t.repoRoot,e.headPath)),o=e.basePath!==void 0&&_(e.basePath,t.options)&&!t.baseSymlinkPaths.has(e.basePath);if(!a&&!o)return;let s=e.status===`deleted`?e.basePath:e.headPath,c=a?n.get(e.headPath):void 0;if(a&&!c){N(e.headPath,r);return}let l={changed:e,displayFile:s,gated:a||e.status===`deleted`?R(D.join(t.repoRoot,s),t.canonicalTarget):!1,headFile:c};if(!o||await F(l,e.basePath,t,r,i))return c&&await P(l,c,t,i),l}function N(e,t){t.some(t=>t.startsWith(`${e}:`))||t.push(`${e}: changed file was not measured`)}async function P(e,n,r,i){try{let i=await w(n.file,`utf8`);e.headFunctionTokens=t(i,{language:g(e.changed.headPath,r.options),duplication:r.options.duplication})}catch(t){i.push(`${e.displayFile}: function token sequences unavailable: ${m(t)}`)}}async function F(e,n,r,i,a){let o={language:g(n,r.options),duplication:r.options.duplication},s;try{s=await l(r.repoRoot,r.mergeBase,n);let t=v(s,o);e.baseMetrics=t.metrics,e.baseCandidates=t.crossFileData,t.crossFileError!==void 0&&a.push(`${n} (at merge-base): duplication candidates unavailable: ${t.crossFileError}`)}catch(e){return i.push(`${n} (at merge-base): ${m(e)}`),!1}try{e.baseFunctionTokens=t(s,o)}catch(e){a.push(`${n} (at merge-base): function token sequences unavailable: ${m(e)}`)}return!0}async function I(e){return(await C(e).catch(()=>{}))?.isSymbolicLink()??!1}async function L(e){let t=e;for(;;){if((await E(t).catch(()=>{}))?.isDirectory())return t;let e=D.dirname(t);if(e===t)return t;t=e}}function R(e,t){let n=D.relative(t,e);return n===``||!n.startsWith(`..${D.sep}`)&&n!==`..`&&!D.isAbsolute(n)}function z(t,n,r){let i=n.flatMap(({relativePath:e,file:t})=>t.duplicationCandidates?[{file:e,...t.duplicationCandidates}]:[]),a=new Set(t.flatMap(e=>e.changed.status===`deleted`?[]:[e.changed.headPath])),o=i.filter(e=>!a.has(e.file));for(let e of t)e.baseCandidates&&e.changed.basePath!==void 0&&o.push({file:e.changed.basePath,...e.baseCandidates});return{baseCross:o.length>=2?e(o,r.duplication):void 0,headCross:i.length>=2?e(i,r.duplication):void 0}}function B(e,t,n){return{file:e.displayFile,baseMetrics:e.baseMetrics,headMetrics:e.headFile?.metrics,baseFunctionTokens:e.baseFunctionTokens,headFunctionTokens:e.headFunctionTokens,baseDuplicatedLineCount:e.baseMetrics===void 0||e.changed.basePath===void 0?0:V(e.baseMetrics,t,e.changed.basePath),headDuplicatedLineCount:e.changed.status===`deleted`?0:V(e.headFile?.metrics,n,e.changed.headPath),duplicationPartners:H(n,e.changed.headPath),gated:e.gated}}function V(e,t,n){return f(e,t,n).size}function H(e,t){if(!e)return[];let n=new Set;for(let r of e.groups)if(r.files.includes(t))for(let e of r.files)e!==t&&n.add(e);return[...n].toSorted()}function U(e,t,n,r,i){let a=t.slice(0,12);r.length>0?(S(`Regression gate could not complete: ${r.length} measurement failures (details on stderr)${n.violations.length>0?`; ${n.violations.length} violations in the measured files`:``} (base ${e.base}, merge-base ${a}).\n`),W(n)):n.violations.length===0?S(`Regression gate passed: ${n.checkedFileCount} changed files, ${n.checkedFunctionCount} functions checked (base ${e.base}, merge-base ${a}).\n`):(S(`Regression gate vs ${e.base} (merge-base ${a}): ${n.violations.length} violations\n`),W(n)),e.full&&G(n);for(let e of i)x(`Warning: ${e}\n`);for(let e of r)x(`Error: ${e}\n`)}function W(e){for(let[t,n]of e.violations.entries())S(`${t+1}. ${n.message}\n`)}function G(e){if(e.checkedFunctions.length!==0){S(`
|
|
2
2
|
Checked functions (base -> head):
|
|
3
3
|
`);for(let t of e.checkedFunctions)S(`- ${K(t)}\n`)}}function K(e){let t=(t,n=String)=>{let r=n(t(e.head));return e.base?`${n(t(e.base))} -> ${r}`:r},n=[`cognitive ${t(e=>e.cognitiveComplexity)}`,`NCSS ${t(e=>e.ncss)}`,`nesting ${t(e=>e.nestingDepth)}`,`DepDegree ${t(e=>e.depDegree)}`,`volume ${t(e=>e.halsteadVolume,e=>e.toFixed(1))}`];return`${e.file}:${e.startLine}-${e.endLine} ${e.name}${e.base?``:` (new)`}: ${n.join(`, `)}`}function q(e,t,n,r,i,a){let o={base:e.base,mergeBase:t,passed:n.violations.length===0&&i.length===0,violations:n.violations,checkedFileCount:n.checkedFileCount,checkedFunctionCount:n.checkedFunctionCount,newFunctionCount:n.newFunctionCount,errors:i,warnings:a};e.full&&(o.files=r.filter(e=>e.gated!==!1).map(e=>({file:e.file,baseFunctionCount:e.baseMetrics?.functions.length??0,headFunctionCount:e.headMetrics?.functions.length??0,baseNcss:e.baseMetrics?.ncssCount??0,headNcss:e.headMetrics?.ncssCount??0,baseMaxCognitiveComplexity:e.baseMetrics?.maxCognitiveComplexity??0,headMaxCognitiveComplexity:e.headMetrics?.maxCognitiveComplexity??0,baseDuplicatedLineCount:e.baseDuplicatedLineCount,headDuplicatedLineCount:e.headDuplicatedLineCount,duplicationPartners:e.duplicationPartners,functions:n.checkedFunctions.filter(t=>t.file===e.file)}))),S(JSON.stringify(o,void 0,2)+`
|
|
4
4
|
`)}export{O as runDiffCommand};
|
package/dist/languages.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const e=
|
|
1
|
+
"use strict";const e=[{name:`javascript`,aliases:[`js`,`mjs`,`cjs`]},{name:`jsx`},{name:`typescript`,aliases:[`ts`]},{name:`tsx`},{name:`python`,aliases:[`py`]},{name:`go`},{name:`rust`,aliases:[`rs`]},{name:`java`},{name:`ruby`,aliases:[`rb`]},{name:`c`},{name:`cpp`,aliases:[`c++`,`cxx`]},{name:`csharp`,aliases:[`cs`,`c#`]},{name:`kotlin`,aliases:[`kt`,`kts`]}];function t(t=e){let n=new Map;for(let e of t){n.set(e.name,e);for(let t of e.aliases??[])n.set(t,e)}return n}const n=e.map(e=>e.name),r=new Map([[`.c`,`c`],[`.c++`,`cpp`],[`.cc`,`cpp`],[`.cjs`,`javascript`],[`.cp`,`cpp`],[`.cpp`,`cpp`],[`.cs`,`csharp`],[`.tcc`,`cpp`],[`.cts`,`typescript`],[`.cxx`,`cpp`],[`.go`,`go`],[`.h`,`cpp`],[`.hh`,`cpp`],[`.hpp`,`cpp`],[`.hxx`,`cpp`],[`.java`,`java`],[`.js`,`javascript`],[`.jsx`,`jsx`],[`.kt`,`kotlin`],[`.kts`,`kotlin`],[`.mjs`,`javascript`],[`.mts`,`typescript`],[`.py`,`python`],[`.rb`,`ruby`],[`.rs`,`rust`],[`.ts`,`typescript`],[`.tsx`,`tsx`]]);function i(e){let t=a(e);return t===`.C`?`cpp`:r.get(t.toLowerCase())}function a(e){let t=Math.max(e.lastIndexOf(`/`),globalThis.process?.platform===`win32`?e.lastIndexOf(`\\`):-1),n=e.slice(t+1),r=n.lastIndexOf(`.`);return r>0?n.slice(r):``}exports.createLanguageRegistry=t,exports.defaultLanguages=e,exports.detectLanguage=i,exports.supportedLanguages=n;
|
|
2
2
|
//# sourceMappingURL=languages.cjs.map
|
package/dist/languages.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"languages.cjs","names":[
|
|
1
|
+
{"version":3,"file":"languages.cjs","names":[],"sources":["../src/languages.ts"],"sourcesContent":["import type { LanguageDefinition, LanguageName, SupportedLanguage } from './types.js';\n\n/**\n * The built-in languages. Grammars and per-language node-type configuration live in the Rust\n * addon (native/src/languages.rs); this list only names the languages and their aliases so the\n * CLI and API can resolve and enumerate them without crossing the N-API boundary.\n */\nexport const defaultLanguages: readonly LanguageDefinition[] = [\n { name: 'javascript', aliases: ['js', 'mjs', 'cjs'] },\n { name: 'jsx' },\n { name: 'typescript', aliases: ['ts'] },\n { name: 'tsx' },\n { name: 'python', aliases: ['py'] },\n { name: 'go' },\n { name: 'rust', aliases: ['rs'] },\n { name: 'java' },\n { name: 'ruby', aliases: ['rb'] },\n { name: 'c' },\n { name: 'cpp', aliases: ['c++', 'cxx'] },\n { name: 'csharp', aliases: ['cs', 'c#'] },\n { name: 'kotlin', aliases: ['kt', 'kts'] },\n];\n\nexport function createLanguageRegistry(\n languages: readonly LanguageDefinition[] = defaultLanguages\n): Map<LanguageName, LanguageDefinition> {\n const registry = new Map<LanguageName, LanguageDefinition>();\n\n for (const language of languages) {\n registry.set(language.name, language);\n for (const alias of language.aliases ?? []) {\n registry.set(alias, language);\n }\n }\n\n return registry;\n}\n\nexport const supportedLanguages = defaultLanguages.map((language) => language.name);\n\nconst languageByExtension = new Map<string, SupportedLanguage>([\n ['.c', 'c'],\n ['.c++', 'cpp'],\n ['.cc', 'cpp'],\n ['.cjs', 'javascript'],\n ['.cp', 'cpp'],\n ['.cpp', 'cpp'],\n ['.cs', 'csharp'],\n ['.tcc', 'cpp'],\n ['.cts', 'typescript'],\n ['.cxx', 'cpp'],\n ['.go', 'go'],\n // Headers may be C or C++; the C++ grammar parses both.\n ['.h', 'cpp'],\n ['.hh', 'cpp'],\n ['.hpp', 'cpp'],\n ['.hxx', 'cpp'],\n ['.java', 'java'],\n ['.js', 'javascript'],\n ['.jsx', 'jsx'],\n ['.kt', 'kotlin'],\n ['.kts', 'kotlin'],\n ['.mjs', 'javascript'],\n ['.mts', 'typescript'],\n ['.py', 'python'],\n ['.rb', 'ruby'],\n ['.rs', 'rust'],\n ['.ts', 'typescript'],\n ['.tsx', 'tsx'],\n]);\n\n/** Detects the language of a source file from its extension, or `undefined` when unsupported. */\nexport function detectLanguage(filePath: string): SupportedLanguage | undefined {\n const extension = extname(filePath);\n // GCC treats an uppercase `.C` as C++; lowercasing first would misparse it with the C grammar.\n if (extension === '.C') {\n return 'cpp';\n }\n return languageByExtension.get(extension.toLowerCase());\n}\n\n/**\n * path.extname() without node:path, which runtimes such as Cloudflare Workers lack. Like Node.js,\n * backslashes separate paths only on Windows; elsewhere they are ordinary file-name characters.\n */\nfunction extname(filePath: string): string {\n const separatorIndex = Math.max(\n filePath.lastIndexOf('/'),\n globalThis.process?.platform === 'win32' ? filePath.lastIndexOf('\\\\') : -1\n );\n const baseName = filePath.slice(separatorIndex + 1);\n const dotIndex = baseName.lastIndexOf('.');\n return dotIndex > 0 ? baseName.slice(dotIndex) : '';\n}\n"],"mappings":"aAOA,MAAa,EAAkD,CAC7D,CAAE,KAAM,aAAc,QAAS,CAAC,KAAM,MAAO,KAAK,CAAE,EACpD,CAAE,KAAM,KAAM,EACd,CAAE,KAAM,aAAc,QAAS,CAAC,IAAI,CAAE,EACtC,CAAE,KAAM,KAAM,EACd,CAAE,KAAM,SAAU,QAAS,CAAC,IAAI,CAAE,EAClC,CAAE,KAAM,IAAK,EACb,CAAE,KAAM,OAAQ,QAAS,CAAC,IAAI,CAAE,EAChC,CAAE,KAAM,MAAO,EACf,CAAE,KAAM,OAAQ,QAAS,CAAC,IAAI,CAAE,EAChC,CAAE,KAAM,GAAI,EACZ,CAAE,KAAM,MAAO,QAAS,CAAC,MAAO,KAAK,CAAE,EACvC,CAAE,KAAM,SAAU,QAAS,CAAC,KAAM,IAAI,CAAE,EACxC,CAAE,KAAM,SAAU,QAAS,CAAC,KAAM,KAAK,CAAE,CAC3C,EAEA,SAAgB,EACd,EAA2C,EACJ,CACvC,IAAM,EAAW,IAAI,IAErB,IAAK,IAAM,KAAY,EAAW,CAChC,EAAS,IAAI,EAAS,KAAM,CAAQ,EACpC,IAAK,IAAM,KAAS,EAAS,SAAW,CAAC,EACvC,EAAS,IAAI,EAAO,CAAQ,CAEhC,CAEA,OAAO,CACT,CAEA,MAAa,EAAqB,EAAiB,IAAK,GAAa,EAAS,IAAI,EAE5E,EAAsB,IAAI,IAA+B,CAC7D,CAAC,KAAM,GAAG,EACV,CAAC,OAAQ,KAAK,EACd,CAAC,MAAO,KAAK,EACb,CAAC,OAAQ,YAAY,EACrB,CAAC,MAAO,KAAK,EACb,CAAC,OAAQ,KAAK,EACd,CAAC,MAAO,QAAQ,EAChB,CAAC,OAAQ,KAAK,EACd,CAAC,OAAQ,YAAY,EACrB,CAAC,OAAQ,KAAK,EACd,CAAC,MAAO,IAAI,EAEZ,CAAC,KAAM,KAAK,EACZ,CAAC,MAAO,KAAK,EACb,CAAC,OAAQ,KAAK,EACd,CAAC,OAAQ,KAAK,EACd,CAAC,QAAS,MAAM,EAChB,CAAC,MAAO,YAAY,EACpB,CAAC,OAAQ,KAAK,EACd,CAAC,MAAO,QAAQ,EAChB,CAAC,OAAQ,QAAQ,EACjB,CAAC,OAAQ,YAAY,EACrB,CAAC,OAAQ,YAAY,EACrB,CAAC,MAAO,QAAQ,EAChB,CAAC,MAAO,MAAM,EACd,CAAC,MAAO,MAAM,EACd,CAAC,MAAO,YAAY,EACpB,CAAC,OAAQ,KAAK,CAChB,CAAC,EAGD,SAAgB,EAAe,EAAiD,CAC9E,IAAM,EAAY,EAAQ,CAAQ,EAKlC,OAHI,IAAc,KACT,MAEF,EAAoB,IAAI,EAAU,YAAY,CAAC,CACxD,CAMA,SAAS,EAAQ,EAA0B,CACzC,IAAM,EAAiB,KAAK,IAC1B,EAAS,YAAY,GAAG,EACxB,WAAW,SAAS,WAAa,QAAU,EAAS,YAAY,IAAI,EAAI,EAC1E,EACM,EAAW,EAAS,MAAM,EAAiB,CAAC,EAC5C,EAAW,EAAS,YAAY,GAAG,EACzC,OAAO,EAAW,EAAI,EAAS,MAAM,CAAQ,EAAI,EACnD"}
|
package/dist/languages.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
const e=[{name:`javascript`,aliases:[`js`,`mjs`,`cjs`]},{name:`jsx`},{name:`typescript`,aliases:[`ts`]},{name:`tsx`},{name:`python`,aliases:[`py`]},{name:`go`},{name:`rust`,aliases:[`rs`]},{name:`java`},{name:`ruby`,aliases:[`rb`]},{name:`c`},{name:`cpp`,aliases:[`c++`,`cxx`]},{name:`csharp`,aliases:[`cs`,`c#`]},{name:`kotlin`,aliases:[`kt`,`kts`]}];function t(t=e){let n=new Map;for(let e of t){n.set(e.name,e);for(let t of e.aliases??[])n.set(t,e)}return n}const n=e.map(e=>e.name),r=new Map([[`.c`,`c`],[`.c++`,`cpp`],[`.cc`,`cpp`],[`.cjs`,`javascript`],[`.cp`,`cpp`],[`.cpp`,`cpp`],[`.cs`,`csharp`],[`.tcc`,`cpp`],[`.cts`,`typescript`],[`.cxx`,`cpp`],[`.go`,`go`],[`.h`,`cpp`],[`.hh`,`cpp`],[`.hpp`,`cpp`],[`.hxx`,`cpp`],[`.java`,`java`],[`.js`,`javascript`],[`.jsx`,`jsx`],[`.kt`,`kotlin`],[`.kts`,`kotlin`],[`.mjs`,`javascript`],[`.mts`,`typescript`],[`.py`,`python`],[`.rb`,`ruby`],[`.rs`,`rust`],[`.ts`,`typescript`],[`.tsx`,`tsx`]]);function i(e){let t=a(e);return t===`.C`?`cpp`:r.get(t.toLowerCase())}function a(e){let t=Math.max(e.lastIndexOf(`/`),globalThis.process?.platform===`win32`?e.lastIndexOf(`\\`):-1),n=e.slice(t+1),r=n.lastIndexOf(`.`);return r>0?n.slice(r):``}export{t as createLanguageRegistry,e as defaultLanguages,i as detectLanguage,n as supportedLanguages};
|
|
2
2
|
//# sourceMappingURL=languages.js.map
|
package/dist/languages.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"languages.js","names":[],"sources":["../src/languages.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"languages.js","names":[],"sources":["../src/languages.ts"],"sourcesContent":["import type { LanguageDefinition, LanguageName, SupportedLanguage } from './types.js';\n\n/**\n * The built-in languages. Grammars and per-language node-type configuration live in the Rust\n * addon (native/src/languages.rs); this list only names the languages and their aliases so the\n * CLI and API can resolve and enumerate them without crossing the N-API boundary.\n */\nexport const defaultLanguages: readonly LanguageDefinition[] = [\n { name: 'javascript', aliases: ['js', 'mjs', 'cjs'] },\n { name: 'jsx' },\n { name: 'typescript', aliases: ['ts'] },\n { name: 'tsx' },\n { name: 'python', aliases: ['py'] },\n { name: 'go' },\n { name: 'rust', aliases: ['rs'] },\n { name: 'java' },\n { name: 'ruby', aliases: ['rb'] },\n { name: 'c' },\n { name: 'cpp', aliases: ['c++', 'cxx'] },\n { name: 'csharp', aliases: ['cs', 'c#'] },\n { name: 'kotlin', aliases: ['kt', 'kts'] },\n];\n\nexport function createLanguageRegistry(\n languages: readonly LanguageDefinition[] = defaultLanguages\n): Map<LanguageName, LanguageDefinition> {\n const registry = new Map<LanguageName, LanguageDefinition>();\n\n for (const language of languages) {\n registry.set(language.name, language);\n for (const alias of language.aliases ?? []) {\n registry.set(alias, language);\n }\n }\n\n return registry;\n}\n\nexport const supportedLanguages = defaultLanguages.map((language) => language.name);\n\nconst languageByExtension = new Map<string, SupportedLanguage>([\n ['.c', 'c'],\n ['.c++', 'cpp'],\n ['.cc', 'cpp'],\n ['.cjs', 'javascript'],\n ['.cp', 'cpp'],\n ['.cpp', 'cpp'],\n ['.cs', 'csharp'],\n ['.tcc', 'cpp'],\n ['.cts', 'typescript'],\n ['.cxx', 'cpp'],\n ['.go', 'go'],\n // Headers may be C or C++; the C++ grammar parses both.\n ['.h', 'cpp'],\n ['.hh', 'cpp'],\n ['.hpp', 'cpp'],\n ['.hxx', 'cpp'],\n ['.java', 'java'],\n ['.js', 'javascript'],\n ['.jsx', 'jsx'],\n ['.kt', 'kotlin'],\n ['.kts', 'kotlin'],\n ['.mjs', 'javascript'],\n ['.mts', 'typescript'],\n ['.py', 'python'],\n ['.rb', 'ruby'],\n ['.rs', 'rust'],\n ['.ts', 'typescript'],\n ['.tsx', 'tsx'],\n]);\n\n/** Detects the language of a source file from its extension, or `undefined` when unsupported. */\nexport function detectLanguage(filePath: string): SupportedLanguage | undefined {\n const extension = extname(filePath);\n // GCC treats an uppercase `.C` as C++; lowercasing first would misparse it with the C grammar.\n if (extension === '.C') {\n return 'cpp';\n }\n return languageByExtension.get(extension.toLowerCase());\n}\n\n/**\n * path.extname() without node:path, which runtimes such as Cloudflare Workers lack. Like Node.js,\n * backslashes separate paths only on Windows; elsewhere they are ordinary file-name characters.\n */\nfunction extname(filePath: string): string {\n const separatorIndex = Math.max(\n filePath.lastIndexOf('/'),\n globalThis.process?.platform === 'win32' ? filePath.lastIndexOf('\\\\') : -1\n );\n const baseName = filePath.slice(separatorIndex + 1);\n const dotIndex = baseName.lastIndexOf('.');\n return dotIndex > 0 ? baseName.slice(dotIndex) : '';\n}\n"],"mappings":"AAOA,MAAa,EAAkD,CAC7D,CAAE,KAAM,aAAc,QAAS,CAAC,KAAM,MAAO,KAAK,CAAE,EACpD,CAAE,KAAM,KAAM,EACd,CAAE,KAAM,aAAc,QAAS,CAAC,IAAI,CAAE,EACtC,CAAE,KAAM,KAAM,EACd,CAAE,KAAM,SAAU,QAAS,CAAC,IAAI,CAAE,EAClC,CAAE,KAAM,IAAK,EACb,CAAE,KAAM,OAAQ,QAAS,CAAC,IAAI,CAAE,EAChC,CAAE,KAAM,MAAO,EACf,CAAE,KAAM,OAAQ,QAAS,CAAC,IAAI,CAAE,EAChC,CAAE,KAAM,GAAI,EACZ,CAAE,KAAM,MAAO,QAAS,CAAC,MAAO,KAAK,CAAE,EACvC,CAAE,KAAM,SAAU,QAAS,CAAC,KAAM,IAAI,CAAE,EACxC,CAAE,KAAM,SAAU,QAAS,CAAC,KAAM,KAAK,CAAE,CAC3C,EAEA,SAAgB,EACd,EAA2C,EACJ,CACvC,IAAM,EAAW,IAAI,IAErB,IAAK,IAAM,KAAY,EAAW,CAChC,EAAS,IAAI,EAAS,KAAM,CAAQ,EACpC,IAAK,IAAM,KAAS,EAAS,SAAW,CAAC,EACvC,EAAS,IAAI,EAAO,CAAQ,CAEhC,CAEA,OAAO,CACT,CAEA,MAAa,EAAqB,EAAiB,IAAK,GAAa,EAAS,IAAI,EAE5E,EAAsB,IAAI,IAA+B,CAC7D,CAAC,KAAM,GAAG,EACV,CAAC,OAAQ,KAAK,EACd,CAAC,MAAO,KAAK,EACb,CAAC,OAAQ,YAAY,EACrB,CAAC,MAAO,KAAK,EACb,CAAC,OAAQ,KAAK,EACd,CAAC,MAAO,QAAQ,EAChB,CAAC,OAAQ,KAAK,EACd,CAAC,OAAQ,YAAY,EACrB,CAAC,OAAQ,KAAK,EACd,CAAC,MAAO,IAAI,EAEZ,CAAC,KAAM,KAAK,EACZ,CAAC,MAAO,KAAK,EACb,CAAC,OAAQ,KAAK,EACd,CAAC,OAAQ,KAAK,EACd,CAAC,QAAS,MAAM,EAChB,CAAC,MAAO,YAAY,EACpB,CAAC,OAAQ,KAAK,EACd,CAAC,MAAO,QAAQ,EAChB,CAAC,OAAQ,QAAQ,EACjB,CAAC,OAAQ,YAAY,EACrB,CAAC,OAAQ,YAAY,EACrB,CAAC,MAAO,QAAQ,EAChB,CAAC,MAAO,MAAM,EACd,CAAC,MAAO,MAAM,EACd,CAAC,MAAO,YAAY,EACpB,CAAC,OAAQ,KAAK,CAChB,CAAC,EAGD,SAAgB,EAAe,EAAiD,CAC9E,IAAM,EAAY,EAAQ,CAAQ,EAKlC,OAHI,IAAc,KACT,MAEF,EAAoB,IAAI,EAAU,YAAY,CAAC,CACxD,CAMA,SAAS,EAAQ,EAA0B,CACzC,IAAM,EAAiB,KAAK,IAC1B,EAAS,YAAY,GAAG,EACxB,WAAW,SAAS,WAAa,QAAU,EAAS,YAAY,IAAI,EAAI,EAC1E,EACM,EAAW,EAAS,MAAM,EAAiB,CAAC,EAC5C,EAAW,EAAS,YAAY,GAAG,EACzC,OAAO,EAAW,EAAI,EAAS,MAAM,CAAQ,EAAI,EACnD"}
|
package/dist/nativeMetrics.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
`)[0]:String(
|
|
3
|
-
`)}`),
|
|
1
|
+
"use strict";function e(e,t,n,a,o=!1){return JSON.parse(l().measureCodeNative(r(e),t,n,i(a?.minTokens),i(a?.maxGapTokens),i(a?.minSimilarityPercent),o))}function t(e,t,n){return JSON.parse(l().collectCrossFileDataNative(r(e),t,i(n)))}function n(e,t){return JSON.parse(l().collectFunctionTokenSequencesNative(r(e),t)).map(e=>Int32Array.from(e))}function r(e){return e.isWellFormed()?e:e.toWellFormed()}function i(e){return e===void 0||Number.isNaN(e)?void 0:Math.min(Math.max(Math.trunc(e),0),4294967295)}var a=class extends Error{};let o,s;function c(e){o=e}function l(){if(o)return o;if(s)throw s;let e=process.getBuiltinModule(`node:module`).createRequire(require("url").pathToFileURL(__filename).href),t=[`code-gauge-${u()}`,`../native/code-gauge.node`],n=[];for(let r of t){let t;try{t=e(r)}catch(e){n.push(` ${r}: ${e instanceof Error?e.message.split(`
|
|
2
|
+
`)[0]:String(e)}`);continue}let i=t.payloadVersion?.();if(i!==7){n.push(` ${r}: payload version ${i??`unknown`} does not match the expected 7; rebuild the addon with \`bun run build-native\``);continue}return o=t,t}throw s=new a(`The code-gauge native addon is not available for ${u()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain); when installing with npm, also allow install scripts for code-gauge so its postinstall build can run.\n${n.join(`
|
|
3
|
+
`)}`),s}function u(){let e=`${process.platform}-${process.arch}`;return process.platform===`win32`?`${e}-msvc`:process.platform===`linux`?(process.report?.getReport())?.header?.glibcVersionRuntime?`${e}-gnu`:`${e}-musl`:e}exports.NativeAddonError=a,exports.collectCrossFileDataNative=t,exports.collectFunctionTokenSequencesNative=n,exports.measureCodeNative=e,exports.setNativeBinding=c;
|
|
4
4
|
//# sourceMappingURL=nativeMetrics.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nativeMetrics.cjs","names":["createRequire"],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ninterface NativeBinding {\n measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens?: number,\n maxGapTokens?: number,\n minSimilarityPercent?: number,\n includeCrossFileData?: boolean\n ): string;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields instead of failing with a clear rebuild message.\n */\nconst expectedPayloadVersion = 7;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData\n )\n ) as NativeMetricsPayload;\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value)\n ? undefined\n : Math.min(Math.max(Math.trunc(value), 0), 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n const requireNative = createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${error instanceof Error ? error.message.split('\\n')[0] : String(error)}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n 'toolchain); when installing with npm, also allow install scripts for code-gauge so its ' +\n `postinstall build can run.\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"0CAgEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAC5C,IAAA,GACA,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,CAAK,EAAG,CAAC,EAAG,UAAa,CAC5D,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAEJ,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAGR,IAAM,GAAA,EAAgBA,EAAAA,cAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,IAA6B,EAC7C,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,IAAI,aAAiB,MAAQ,EAAM,QAAQ,MAAM;CAAI,CAAC,CAAC,GAAK,OAAO,CAAK,GAAG,EACxG,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAY,EAAwB,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAOA,KANA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,2NAGrC,EAAS,KAAK;CAAI,GACrD,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
|
|
1
|
+
{"version":3,"file":"nativeMetrics.cjs","names":[],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\nexport interface NativeBinding {\n measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens?: number,\n maxGapTokens?: number,\n minSimilarityPercent?: number,\n includeCrossFileData?: boolean\n ): string;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields instead of failing with a clear rebuild message.\n */\nexport const expectedPayloadVersion = 7;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData\n )\n ) as NativeMetricsPayload;\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value)\n ? undefined\n : Math.min(Math.max(Math.trunc(value), 0), 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\n/** Replaces the N-API addon, e.g. with the WebAssembly build on runtimes without N-API. */\nexport function setNativeBinding(binding: NativeBinding): void {\n cachedBinding = binding;\n}\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n // node:module is loaded lazily so that runtimes without it can bundle this module.\n const requireNative = process.getBuiltinModule('node:module').createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${error instanceof Error ? error.message.split('\\n')[0] : String(error)}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n 'toolchain); when installing with npm, also allow install scripts for code-gauge so its ' +\n `postinstall build can run.\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"aA+DA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAC5C,IAAA,GACA,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,CAAK,EAAG,CAAC,EAAG,UAAa,CAC5D,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAGJ,SAAgB,EAAiB,EAA8B,CAC7D,EAAgB,CAClB,CAEA,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAIR,IAAM,EAAgB,QAAQ,iBAAiB,aAAa,CAAC,CAAC,cAAA,QAAA,KAAA,CAAA,CAAA,cAAA,UAAA,CAAA,CAAA,IAA6B,EACrF,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,IAAI,aAAiB,MAAQ,EAAM,QAAQ,MAAM;CAAI,CAAC,CAAC,GAAK,OAAO,CAAK,GAAG,EACxG,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAA,EAAoC,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAOA,KANA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,2NAGrC,EAAS,KAAK;CAAI,GACrD,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
|
package/dist/nativeMetrics.d.ts
CHANGED
|
@@ -30,6 +30,18 @@ export interface NativeCrossFileDataPayload {
|
|
|
30
30
|
/** 1-based lines that are neither blank nor comment-only, sorted ascending. */
|
|
31
31
|
codeLineNumbers: number[];
|
|
32
32
|
}
|
|
33
|
+
export interface NativeBinding {
|
|
34
|
+
measureCodeNative(code: string, language: string, includeSyntaxTree: boolean, minTokens?: number, maxGapTokens?: number, minSimilarityPercent?: number, includeCrossFileData?: boolean): string;
|
|
35
|
+
collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;
|
|
36
|
+
collectFunctionTokenSequencesNative(code: string, language: string): string;
|
|
37
|
+
payloadVersion?(): number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a
|
|
41
|
+
* `git pull` untouched, so without this handshake it would silently return payloads missing
|
|
42
|
+
* newer fields instead of failing with a clear rebuild message.
|
|
43
|
+
*/
|
|
44
|
+
export declare const expectedPayloadVersion = 7;
|
|
33
45
|
/**
|
|
34
46
|
* Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;
|
|
35
47
|
* with `includeCrossFileData`, the payload also carries the file's cross-file contribution.
|
|
@@ -46,3 +58,5 @@ export declare function collectFunctionTokenSequencesNative(code: string, langua
|
|
|
46
58
|
*/
|
|
47
59
|
export declare class NativeAddonError extends Error {
|
|
48
60
|
}
|
|
61
|
+
/** Replaces the N-API addon, e.g. with the WebAssembly build on runtimes without N-API. */
|
|
62
|
+
export declare function setNativeBinding(binding: NativeBinding): void;
|
package/dist/nativeMetrics.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
`)[0]:String(
|
|
3
|
-
`)}`),
|
|
1
|
+
function e(e,t,n,a,o=!1){return JSON.parse(l().measureCodeNative(r(e),t,n,i(a?.minTokens),i(a?.maxGapTokens),i(a?.minSimilarityPercent),o))}function t(e,t,n){return JSON.parse(l().collectCrossFileDataNative(r(e),t,i(n)))}function n(e,t){return JSON.parse(l().collectFunctionTokenSequencesNative(r(e),t)).map(e=>Int32Array.from(e))}function r(e){return e.isWellFormed()?e:e.toWellFormed()}function i(e){return e===void 0||Number.isNaN(e)?void 0:Math.min(Math.max(Math.trunc(e),0),4294967295)}var a=class extends Error{};let o,s;function c(e){o=e}function l(){if(o)return o;if(s)throw s;let e=process.getBuiltinModule(`node:module`).createRequire(import.meta.url),t=[`code-gauge-${u()}`,`../native/code-gauge.node`],n=[];for(let r of t){let t;try{t=e(r)}catch(e){n.push(` ${r}: ${e instanceof Error?e.message.split(`
|
|
2
|
+
`)[0]:String(e)}`);continue}let i=t.payloadVersion?.();if(i!==7){n.push(` ${r}: payload version ${i??`unknown`} does not match the expected 7; rebuild the addon with \`bun run build-native\``);continue}return o=t,t}throw s=new a(`The code-gauge native addon is not available for ${u()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain); when installing with npm, also allow install scripts for code-gauge so its postinstall build can run.\n${n.join(`
|
|
3
|
+
`)}`),s}function u(){let e=`${process.platform}-${process.arch}`;return process.platform===`win32`?`${e}-msvc`:process.platform===`linux`?(process.report?.getReport())?.header?.glibcVersionRuntime?`${e}-gnu`:`${e}-musl`:e}export{a as NativeAddonError,t as collectCrossFileDataNative,n as collectFunctionTokenSequencesNative,e as measureCodeNative,c as setNativeBinding};
|
|
4
4
|
//# sourceMappingURL=nativeMetrics.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nativeMetrics.js","names":[],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ninterface NativeBinding {\n measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens?: number,\n maxGapTokens?: number,\n minSimilarityPercent?: number,\n includeCrossFileData?: boolean\n ): string;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields instead of failing with a clear rebuild message.\n */\nconst expectedPayloadVersion = 7;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData\n )\n ) as NativeMetricsPayload;\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value)\n ? undefined\n : Math.min(Math.max(Math.trunc(value), 0), 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n const requireNative = createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${error instanceof Error ? error.message.split('\\n')[0] : String(error)}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n 'toolchain); when installing with npm, also allow install scripts for code-gauge so its ' +\n `postinstall build can run.\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"4CAgEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAC5C,IAAA,GACA,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,CAAK,EAAG,CAAC,EAAG,UAAa,CAC5D,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAEJ,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAGR,IAAM,EAAgB,EAAc,YAAY,GAAG,EAC7C,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,IAAI,aAAiB,MAAQ,EAAM,QAAQ,MAAM;CAAI,CAAC,CAAC,GAAK,OAAO,CAAK,GAAG,EACxG,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAY,EAAwB,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAOA,KANA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,2NAGrC,EAAS,KAAK;CAAI,GACrD,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
|
|
1
|
+
{"version":3,"file":"nativeMetrics.js","names":[],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\nexport interface NativeBinding {\n measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens?: number,\n maxGapTokens?: number,\n minSimilarityPercent?: number,\n includeCrossFileData?: boolean\n ): string;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields instead of failing with a clear rebuild message.\n */\nexport const expectedPayloadVersion = 7;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData\n )\n ) as NativeMetricsPayload;\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value)\n ? undefined\n : Math.min(Math.max(Math.trunc(value), 0), 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\n/** Replaces the N-API addon, e.g. with the WebAssembly build on runtimes without N-API. */\nexport function setNativeBinding(binding: NativeBinding): void {\n cachedBinding = binding;\n}\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n // node:module is loaded lazily so that runtimes without it can bundle this module.\n const requireNative = process.getBuiltinModule('node:module').createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${error instanceof Error ? error.message.split('\\n')[0] : String(error)}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n 'toolchain); when installing with npm, also allow install scripts for code-gauge so its ' +\n `postinstall build can run.\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"AA+DA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAC5C,IAAA,GACA,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,CAAK,EAAG,CAAC,EAAG,UAAa,CAC5D,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAGJ,SAAgB,EAAiB,EAA8B,CAC7D,EAAgB,CAClB,CAEA,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAIR,IAAM,EAAgB,QAAQ,iBAAiB,aAAa,CAAC,CAAC,cAAc,YAAY,GAAG,EACrF,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,IAAI,aAAiB,MAAQ,EAAM,QAAQ,MAAM;CAAI,CAAC,CAAC,GAAK,OAAO,CAAK,GAAG,EACxG,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAA,EAAoC,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAOA,KANA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,2NAGrC,EAAS,KAAK;CAAI,GACrD,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
|
package/dist/scan.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./crossFileDuplication.cjs"),n=require("./languages.cjs"),r=require("./nativeMetrics.cjs"),i=require("./metrics.cjs");let a=require("node:path");
|
|
1
|
+
"use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./crossFileDuplication.cjs"),n=require("./languages.cjs"),r=require("./nativeMetrics.cjs"),i=require("./metrics.cjs");let a=require("node:fs/promises"),o=require("node:path");o=e.__toESM(o,1);let s=require("node:os");s=e.__toESM(s,1);const c=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`]),l=new Set([`__tests__`,`test`,`tests`,`spec`]),u=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-](?:test|spec))\.[^.]+$/iu,d=/Tests?\.(?:java|kt|cs)$/u;function f(e){return e===`~`?s.default.homedir():e.startsWith(`~/`)?o.default.join(s.default.homedir(),e.slice(2)):o.default.resolve(e)}async function p(e){try{return(await(0,a.stat)(e)).isDirectory()?e:o.default.dirname(e)}catch{return o.default.dirname(e)}}async function m(e,t){let n=[],r=[],i=[],s=e;try{s=await(0,a.realpath)(e)}catch{}let c=o.default.dirname(s),l;try{l=await(0,a.stat)(s)}catch(e){let t=`${j(s,c)}: ${P(e)}`;return{displayRoot:c,files:n,errors:[t],warnings:i,fatalError:t}}if(l.isFile()){let e=o.default.dirname(s),a=A(s,t,!0);if(!a){let t=`${j(s,e)}: unsupported file type`;return{displayRoot:e,files:n,errors:[t],warnings:i,fatalError:t}}let c=_(t,n,r,i,e);try{await C(s,a,`single-file`,c,s)}catch(t){return g(t,e,n,r,i)}return{displayRoot:e,files:n,errors:r,warnings:i}}try{await b(s,_(t,n,r,i,s))}catch(e){return g(e,s,n,r,i)}return{displayRoot:s,files:n,errors:r,warnings:i}}async function h(e,t,n){let r=[],i=[],s=[],c=_(n,r,i,s,e);for(let l of t){let t=k(l,n)?A(l,n):void 0;if(!t)continue;let u=o.default.join(e,l);if(!(await(0,a.lstat)(u).catch(()=>{}))?.isSymbolicLink())try{await C(u,t,`directory`,c)}catch(t){return g(t,e,r,i,s)}}return{displayRoot:e,files:r,errors:i,warnings:s}}function g(e,t,n,i,a){if(!(e instanceof r.NativeAddonError))throw e;let o=P(e);return{displayRoot:t,files:n,errors:[...i,o],warnings:a,fatalError:o}}function _(e,t,n,r,i){return{options:e,files:t,errors:n,warnings:r,visitedDirectories:new Set,visitedFiles:new Set,rootDirectory:i}}async function v(e,t,n){try{return await e()}catch(e){n.errors.push(`${j(t,n.rootDirectory)}: ${P(e)}`);return}}async function y(e,t){let n=await v(()=>(0,a.realpath)(e),e,t);return n!==void 0&&O(n,t.rootDirectory)?n:void 0}async function b(e,t){let n=await y(e,t);if(n===void 0||t.visitedDirectories.has(n))return;t.visitedDirectories.add(n);let r=await v(()=>(0,a.readdir)(e,{withFileTypes:!0}),e,t);if(r!==void 0)for(let n of r){let r=o.default.join(e,n.name);if(n.isSymbolicLink()){await x(n.name,r,t);continue}if(n.isDirectory()){if(D(n.name,t.options))continue;await b(r,t);continue}n.isFile()&&await S(r,t)}}async function x(e,t,n){let r=await y(t,n);if(r===void 0)return;let i=await v(()=>(0,a.stat)(t),t,n);if(i!==void 0){if(i.isDirectory()){if(D(e,n.options)||D(o.default.basename(r),n.options))return;await b(t,n);return}i.isFile()&&await S(t,n,r,r)}}async function S(e,t,n=e,r){let i=A(n,t.options);i&&await C(e,i,`directory`,t,r)}async function C(e,t,n,o,s){try{let r=s??await(0,a.realpath)(e);if(o.visitedFiles.has(r))return;o.visitedFiles.add(r);let c=await(0,a.readFile)(e,`utf8`),l={language:t,duplication:o.options.duplication};if(n===`single-file`){o.files.push({file:e,metrics:i.measureCode(c,l)});return}let{metrics:u,crossFileData:d,crossFileError:f}=w(c,l);f!==void 0&&o.warnings.push(`${j(e,o.rootDirectory)}: cross-file duplication candidates unavailable: ${f}`),o.files.push({file:e,metrics:u,duplicationCandidates:d})}catch(t){if(t instanceof r.NativeAddonError)throw t;o.errors.push(`${j(e,o.rootDirectory)}: ${P(t)}`)}}function w(e,t){try{return i.measureCodeWithCrossFileData(e,t)}catch(n){if(n instanceof r.NativeAddonError)throw n;return{metrics:i.measureCode(e,t),crossFileError:P(n)}}}function T(e,n){if(e.fatalError||e.files.length<2)return;let r=e.files.flatMap(({file:t,duplicationCandidates:n})=>n?[{file:j(t,e.displayRoot),...n}]:[]);r.length<2||(e.crossFileDuplication=t.measureCrossFileDuplication(r,n.duplication))}function E(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 D(e,t){return c.has(e)?!0:!t.includeTests&&l.has(e)}function O(e,t){let n=o.default.relative(t,e);return n===``||n!==`..`&&!n.startsWith(`..${o.default.sep}`)&&!o.default.isAbsolute(n)}function k(e,t){let n=e.split(`/`);for(let e of n.slice(0,-1))if(c.has(e)||!t.includeTests&&l.has(e))return!1;return A(e,t)!==void 0}function A(e,t,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||t.includeTests||!u.test(o.default.basename(e))&&!d.test(o.default.basename(e))))return n.detectLanguage(e)}function j(e,t){return o.default.relative(t,e)||o.default.basename(e)}function M(e){process.stdout.write(e)}function N(e){process.stderr.write(e)}function P(e){return e instanceof Error?e.message:String(e)}exports.addCrossFileDuplication=T,exports.collectDuplicatedLineNumbers=E,exports.configSearchDirectory=p,exports.formatError=P,exports.formatPath=j,exports.getLanguage=A,exports.isScannedPath=k,exports.measureWithCrossFileData=w,exports.resolveTarget=f,exports.scanListedFiles=h,exports.scanTarget=m,exports.writeStderr=N,exports.writeStdout=M;
|
|
2
2
|
//# sourceMappingURL=scan.cjs.map
|
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,measureCodeWithCrossFileData as i}from"./metrics.js";import
|
|
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,measureCodeWithCrossFileData 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)}}async function v(e,t){let n=[],r=[],i=[],a=e;try{a=await c(e)}catch{}let o=u.dirname(a),s;try{s=await l(a)}catch(e){let t=`${F(a,o)}: ${R(e)}`;return{displayRoot:o,files:n,errors:[t],warnings:i,fatalError:t}}if(s.isFile()){let e=u.dirname(a),o=P(a,t,!0);if(!o){let t=`${F(a,e)}: unsupported file type`;return{displayRoot:e,files:n,errors:[t],warnings:i,fatalError:t}}let s=x(t,n,r,i,e);try{await D(a,o,`single-file`,s,a)}catch(t){return b(t,e,n,r,i)}return{displayRoot:e,files:n,errors:r,warnings:i}}try{await w(a,x(t,n,r,i,a))}catch(e){return b(e,a,n,r,i)}return{displayRoot:a,files:n,errors:r,warnings:i}}async function y(e,t,n){let r=[],i=[],o=[],s=x(n,r,i,o,e);for(let c of t){let t=N(c,n)?P(c,n):void 0;if(!t)continue;let l=u.join(e,c);if(!(await a(l).catch(()=>{}))?.isSymbolicLink())try{await D(l,t,`directory`,s)}catch(t){return b(t,e,r,i,o)}}return{displayRoot:e,files:r,errors:i,warnings:o}}function b(e,t,r,i,a){if(!(e instanceof n))throw e;let o=R(e);return{displayRoot:t,files:r,errors:[...i,o],warnings:a,fatalError:o}}function x(e,t,n,r,i){return{options:e,files:t,errors:n,warnings:r,visitedDirectories:new Set,visitedFiles:new Set,rootDirectory:i}}async function S(e,t,n){try{return await e()}catch(e){n.errors.push(`${F(t,n.rootDirectory)}: ${R(e)}`);return}}async function C(e,t){let n=await S(()=>c(e),e,t);return n!==void 0&&M(n,t.rootDirectory)?n:void 0}async function w(e,t){let n=await C(e,t);if(n===void 0||t.visitedDirectories.has(n))return;t.visitedDirectories.add(n);let r=await S(()=>s(e,{withFileTypes:!0}),e,t);if(r!==void 0)for(let n of r){let r=u.join(e,n.name);if(n.isSymbolicLink()){await T(n.name,r,t);continue}if(n.isDirectory()){if(j(n.name,t.options))continue;await w(r,t);continue}n.isFile()&&await E(r,t)}}async function T(e,t,n){let r=await C(t,n);if(r===void 0)return;let i=await S(()=>l(t),t,n);if(i!==void 0){if(i.isDirectory()){if(j(e,n.options)||j(u.basename(r),n.options))return;await w(t,n);return}i.isFile()&&await E(t,n,r,r)}}async function E(e,t,n=e,r){let i=P(n,t.options);i&&await D(e,i,`directory`,t,r)}async function D(e,t,i,a,s){try{let n=s??await c(e);if(a.visitedFiles.has(n))return;a.visitedFiles.add(n);let l=await o(e,`utf8`),u={language:t,duplication:a.options.duplication};if(i===`single-file`){a.files.push({file:e,metrics:r(l,u)});return}let{metrics:d,crossFileData:f,crossFileError:p}=O(l,u);p!==void 0&&a.warnings.push(`${F(e,a.rootDirectory)}: cross-file duplication candidates unavailable: ${p}`),a.files.push({file:e,metrics:d,duplicationCandidates:f})}catch(t){if(t instanceof n)throw t;a.errors.push(`${F(e,a.rootDirectory)}: ${R(t)}`)}}function O(e,t){try{return i(e,t)}catch(i){if(i instanceof n)throw i;return{metrics:r(e,t),crossFileError:R(i)}}}function k(t,n){if(t.fatalError||t.files.length<2)return;let r=t.files.flatMap(({file:e,duplicationCandidates:n})=>n?[{file:F(e,t.displayRoot),...n}]:[]);r.length<2||(t.crossFileDuplication=e(r,n.duplication))}function A(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 j(e,t){return f.has(e)?!0:!t.includeTests&&p.has(e)}function M(e,t){let n=u.relative(t,e);return n===``||n!==`..`&&!n.startsWith(`..${u.sep}`)&&!u.isAbsolute(n)}function N(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 P(e,t)!==void 0}function P(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 F(e,t){return u.relative(t,e)||u.basename(e)}function I(e){process.stdout.write(e)}function L(e){process.stderr.write(e)}function R(e){return e instanceof Error?e.message:String(e)}export{k as addCrossFileDuplication,A as collectDuplicatedLineNumbers,_ as configSearchDirectory,R as formatError,F as formatPath,P as getLanguage,N as isScannedPath,O as measureWithCrossFileData,g as resolveTarget,y as scanListedFiles,v as scanTarget,L as writeStderr,I as writeStdout};
|
|
2
2
|
//# sourceMappingURL=scan.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -22,7 +22,9 @@ export interface DuplicationOptions {
|
|
|
22
22
|
/**
|
|
23
23
|
* Minimum similarity percent (1-100) for near-miss (Type-3) clone blocks, measured as the
|
|
24
24
|
* token-level longest common subsequence relative to the larger block (NiCad-style per-fragment
|
|
25
|
-
* similarity)
|
|
25
|
+
* similarity), also after putting both blocks' top-level statements in a canonical order (so
|
|
26
|
+
* reordered copies match), or relative to the larger matched core when a copy is embedded in
|
|
27
|
+
* added code. 100 disables near-miss detection and reports exact (Type-1/2) matches plus gapped
|
|
26
28
|
* merges only (default 70). Applies to within-file and cross-file detection.
|
|
27
29
|
*/
|
|
28
30
|
minSimilarityPercent?: number;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";const e=require("./nativeMetrics.cjs"),t=new TextEncoder,n=new TextDecoder;function r(e){let t,r,s=a=>{if(!t){if(r)throw r;try{t=i(e)}catch(e){throw r=e,e}}let{exports:o,stderr:s}=t;s.length=0;let c;try{c=a(o)}catch(e){throw t=void 0,Error(`The code-gauge WebAssembly module crashed: ${s.join(``).trim()||String(e)}`,{cause:e})}let l=n.decode(new Uint8Array(o.memory.buffer,o.result_ptr(),o.result_len()));if(c!==0)throw Error(l);return l};return{measureCodeNative:(e,t,n,r,i,c,l)=>s(s=>s.measure_code(...a(s,e),...a(s,t),Number(n),o(r),o(i),o(c),Number(l??!1))),collectCrossFileDataNative:(e,t,n)=>s(r=>r.collect_cross_file_data(...a(r,e),...a(r,t),o(n))),collectFunctionTokenSequencesNative:(e,t)=>s(n=>n.collect_function_token_sequences(...a(n,e),...a(n,t)))}}function i(t){let n=[],r,i=new WebAssembly.Instance(t,{wasi_snapshot_preview1:c(()=>r,n)}).exports;r=i.memory;let a=i.payload_version();if(a!==7)throw new e.NativeAddonError(`The code-gauge WebAssembly module has payload version ${a}, but 7 is expected; reinstall code-gauge, or in a code-gauge repository checkout, rebuild it with \`bun run build-wasm\``);return{exports:i,stderr:n}}function a(e,n){let r=t.encode(n),i=e.alloc(r.length);return new Uint8Array(e.memory.buffer,i,r.length).set(r),[i,r.length]}function o(e){return e??-1}const s=65536;function c(e,t){let r=()=>new DataView(e().buffer),i=()=>8;return{environ_get:()=>0,environ_sizes_get:(e,t)=>(r().setUint32(e,0,!0),r().setUint32(t,0,!0),0),clock_time_get:(e,t,n)=>(r().setBigUint64(n,BigInt(Date.now())*1000000n,!0),0),random_get:(t,n)=>{for(let r=0;r<n;r+=s)crypto.getRandomValues(new Uint8Array(e().buffer,t+r,Math.min(s,n-r)));return 0},fd_write:(i,a,o,s)=>{let c=r(),l=0;for(let r=0;r<o;r++){let o=c.getUint32(a+r*8,!0),s=c.getUint32(a+r*8+4,!0);i===2&&t.push(n.decode(new Uint8Array(e().buffer,o,s))),l+=s}return c.setUint32(s,l,!0),0},fd_close:i,fd_fdstat_get:i,fd_fdstat_set_flags:i,fd_read:i,fd_seek:i,proc_exit:e=>{throw Error(`The code-gauge WebAssembly module exited with code ${e}`)}}}exports.createWasmBinding=r;
|
|
2
|
+
//# sourceMappingURL=wasmBinding.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wasmBinding.cjs","names":["NativeAddonError"],"sources":["../src/wasmBinding.ts"],"sourcesContent":["import { expectedPayloadVersion, NativeAddonError, type NativeBinding } from './nativeMetrics.js';\n\n/** The C ABI exported by native/src/wasm.rs. */\ninterface WasmExports {\n memory: WebAssembly.Memory;\n payload_version(): number;\n alloc(length: number): number;\n result_ptr(): number;\n result_len(): number;\n measure_code(\n codePtr: number,\n codeLength: number,\n languagePtr: number,\n languageLength: number,\n includeSyntaxTree: number,\n minTokens: number,\n maxGapTokens: number,\n minSimilarityPercent: number,\n includeCrossFileData: number\n ): number;\n collect_cross_file_data(\n codePtr: number,\n codeLength: number,\n languagePtr: number,\n languageLength: number,\n minTokens: number\n ): number;\n collect_function_token_sequences(\n codePtr: number,\n codeLength: number,\n languagePtr: number,\n languageLength: number\n ): number;\n}\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\n/**\n * Wraps the WebAssembly build of the native addon (native/src/wasm.rs) as a NativeBinding. The\n * module is instantiated synchronously on first use and again after a trap (a panic or stack\n * overflow), because a trap leaves the instance's memory and stack pointer in an undefined state.\n * An instantiation failure (e.g., a payload version mismatch) is memoized like the N-API loader's,\n * since instantiating the same module again would fail again.\n */\nexport function createWasmBinding(module: WebAssembly.Module): NativeBinding {\n let instance: { exports: WasmExports; stderr: string[] } | undefined;\n let instantiationFailure: unknown;\n\n const call = (invoke: (exports: WasmExports) => number): string => {\n if (!instance) {\n if (instantiationFailure) {\n throw instantiationFailure;\n }\n try {\n instance = instantiate(module);\n } catch (error) {\n instantiationFailure = error;\n throw error;\n }\n }\n const { exports, stderr } = instance;\n stderr.length = 0;\n let status: number;\n try {\n status = invoke(exports);\n } catch (error) {\n instance = undefined;\n throw new Error(`The code-gauge WebAssembly module crashed: ${stderr.join('').trim() || String(error)}`, {\n cause: error,\n });\n }\n const result = decoder.decode(new Uint8Array(exports.memory.buffer, exports.result_ptr(), exports.result_len()));\n if (status !== 0) {\n throw new Error(result);\n }\n return result;\n };\n\n return {\n measureCodeNative: (\n code,\n language,\n includeSyntaxTree,\n minTokens,\n maxGapTokens,\n minSimilarityPercent,\n includeCrossFileData\n ) =>\n call((exports) =>\n exports.measure_code(\n ...passString(exports, code),\n ...passString(exports, language),\n Number(includeSyntaxTree),\n toOptionalU32(minTokens),\n toOptionalU32(maxGapTokens),\n toOptionalU32(minSimilarityPercent),\n Number(includeCrossFileData ?? false)\n )\n ),\n collectCrossFileDataNative: (code, language, minTokens) =>\n call((exports) =>\n exports.collect_cross_file_data(\n ...passString(exports, code),\n ...passString(exports, language),\n toOptionalU32(minTokens)\n )\n ),\n collectFunctionTokenSequencesNative: (code, language) =>\n call((exports) =>\n exports.collect_function_token_sequences(...passString(exports, code), ...passString(exports, language))\n ),\n };\n}\n\nfunction instantiate(module: WebAssembly.Module): { exports: WasmExports; stderr: string[] } {\n const stderr: string[] = [];\n let memory: WebAssembly.Memory | undefined;\n const instance = new WebAssembly.Instance(module, {\n wasi_snapshot_preview1: createWasiImports(() => memory as WebAssembly.Memory, stderr),\n });\n const exports = instance.exports as unknown as WasmExports;\n memory = exports.memory;\n const version = exports.payload_version();\n if (version !== expectedPayloadVersion) {\n throw new NativeAddonError(\n `The code-gauge WebAssembly module has payload version ${version}, but ${expectedPayloadVersion} is ` +\n 'expected; reinstall code-gauge, or in a code-gauge repository checkout, rebuild it with `bun run build-wasm`'\n );\n }\n return { exports, stderr };\n}\n\n/** Copies a string into a buffer whose ownership passes to the called export. */\nfunction passString(exports: WasmExports, text: string): [number, number] {\n // TextEncoder replaces lone surrogates with U+FFFD, like toWellFormed() does for the N-API addon.\n const bytes = encoder.encode(text);\n const pointer = exports.alloc(bytes.length);\n new Uint8Array(exports.memory.buffer, pointer, bytes.length).set(bytes);\n return [pointer, bytes.length];\n}\n\n/** native/src/wasm.rs reads a negative value as an absent setting. */\nfunction toOptionalU32(value: number | undefined): number {\n return value ?? -1;\n}\n\nconst WASI_ERRNO_SUCCESS = 0;\nconst WASI_ERRNO_BADF = 8;\n// crypto.getRandomValues() rejects requests larger than this.\nconst MAX_RANDOM_BYTES = 65_536;\n\n/**\n * The WASI preview 1 functions the module imports. The metrics code performs no I/O, so file\n * descriptors are unavailable except for writes, whose stderr output (e.g., a panic message) is\n * kept for the error raised when the module traps.\n */\nfunction createWasiImports(\n getMemory: () => WebAssembly.Memory,\n stderr: string[]\n): Record<string, (...args: never[]) => number> {\n const view = (): DataView => new DataView(getMemory().buffer);\n const unavailable = (): number => WASI_ERRNO_BADF;\n return {\n environ_get: () => WASI_ERRNO_SUCCESS,\n environ_sizes_get: (countPointer: number, sizePointer: number) => {\n view().setUint32(countPointer, 0, true);\n view().setUint32(sizePointer, 0, true);\n return WASI_ERRNO_SUCCESS;\n },\n clock_time_get: (_clockId: number, _precision: bigint, timePointer: number) => {\n view().setBigUint64(timePointer, BigInt(Date.now()) * 1_000_000n, true);\n return WASI_ERRNO_SUCCESS;\n },\n random_get: (pointer: number, length: number) => {\n for (let offset = 0; offset < length; offset += MAX_RANDOM_BYTES) {\n crypto.getRandomValues(\n new Uint8Array(getMemory().buffer, pointer + offset, Math.min(MAX_RANDOM_BYTES, length - offset))\n );\n }\n return WASI_ERRNO_SUCCESS;\n },\n fd_write: (fd: number, iovsPointer: number, iovsLength: number, writtenPointer: number) => {\n const memoryView = view();\n let written = 0;\n for (let index = 0; index < iovsLength; index++) {\n const pointer = memoryView.getUint32(iovsPointer + index * 8, true);\n const length = memoryView.getUint32(iovsPointer + index * 8 + 4, true);\n if (fd === 2) {\n stderr.push(decoder.decode(new Uint8Array(getMemory().buffer, pointer, length)));\n }\n written += length;\n }\n memoryView.setUint32(writtenPointer, written, true);\n return WASI_ERRNO_SUCCESS;\n },\n fd_close: unavailable,\n fd_fdstat_get: unavailable,\n fd_fdstat_set_flags: unavailable,\n fd_read: unavailable,\n fd_seek: unavailable,\n proc_exit: (code: number) => {\n throw new Error(`The code-gauge WebAssembly module exited with code ${code}`);\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type NativeBinding } from './nativeMetrics.js';
|
|
2
|
+
/**
|
|
3
|
+
* Wraps the WebAssembly build of the native addon (native/src/wasm.rs) as a NativeBinding. The
|
|
4
|
+
* module is instantiated synchronously on first use and again after a trap (a panic or stack
|
|
5
|
+
* overflow), because a trap leaves the instance's memory and stack pointer in an undefined state.
|
|
6
|
+
* An instantiation failure (e.g., a payload version mismatch) is memoized like the N-API loader's,
|
|
7
|
+
* since instantiating the same module again would fail again.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createWasmBinding(module: WebAssembly.Module): NativeBinding;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{NativeAddonError as e}from"./nativeMetrics.js";const t=new TextEncoder,n=new TextDecoder;function r(e){let t,r,s=a=>{if(!t){if(r)throw r;try{t=i(e)}catch(e){throw r=e,e}}let{exports:o,stderr:s}=t;s.length=0;let c;try{c=a(o)}catch(e){throw t=void 0,Error(`The code-gauge WebAssembly module crashed: ${s.join(``).trim()||String(e)}`,{cause:e})}let l=n.decode(new Uint8Array(o.memory.buffer,o.result_ptr(),o.result_len()));if(c!==0)throw Error(l);return l};return{measureCodeNative:(e,t,n,r,i,c,l)=>s(s=>s.measure_code(...a(s,e),...a(s,t),Number(n),o(r),o(i),o(c),Number(l??!1))),collectCrossFileDataNative:(e,t,n)=>s(r=>r.collect_cross_file_data(...a(r,e),...a(r,t),o(n))),collectFunctionTokenSequencesNative:(e,t)=>s(n=>n.collect_function_token_sequences(...a(n,e),...a(n,t)))}}function i(t){let n=[],r,i=new WebAssembly.Instance(t,{wasi_snapshot_preview1:c(()=>r,n)}).exports;r=i.memory;let a=i.payload_version();if(a!==7)throw new e(`The code-gauge WebAssembly module has payload version ${a}, but 7 is expected; reinstall code-gauge, or in a code-gauge repository checkout, rebuild it with \`bun run build-wasm\``);return{exports:i,stderr:n}}function a(e,n){let r=t.encode(n),i=e.alloc(r.length);return new Uint8Array(e.memory.buffer,i,r.length).set(r),[i,r.length]}function o(e){return e??-1}const s=65536;function c(e,t){let r=()=>new DataView(e().buffer),i=()=>8;return{environ_get:()=>0,environ_sizes_get:(e,t)=>(r().setUint32(e,0,!0),r().setUint32(t,0,!0),0),clock_time_get:(e,t,n)=>(r().setBigUint64(n,BigInt(Date.now())*1000000n,!0),0),random_get:(t,n)=>{for(let r=0;r<n;r+=s)crypto.getRandomValues(new Uint8Array(e().buffer,t+r,Math.min(s,n-r)));return 0},fd_write:(i,a,o,s)=>{let c=r(),l=0;for(let r=0;r<o;r++){let o=c.getUint32(a+r*8,!0),s=c.getUint32(a+r*8+4,!0);i===2&&t.push(n.decode(new Uint8Array(e().buffer,o,s))),l+=s}return c.setUint32(s,l,!0),0},fd_close:i,fd_fdstat_get:i,fd_fdstat_set_flags:i,fd_read:i,fd_seek:i,proc_exit:e=>{throw Error(`The code-gauge WebAssembly module exited with code ${e}`)}}}export{r as createWasmBinding};
|
|
2
|
+
//# sourceMappingURL=wasmBinding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wasmBinding.js","names":[],"sources":["../src/wasmBinding.ts"],"sourcesContent":["import { expectedPayloadVersion, NativeAddonError, type NativeBinding } from './nativeMetrics.js';\n\n/** The C ABI exported by native/src/wasm.rs. */\ninterface WasmExports {\n memory: WebAssembly.Memory;\n payload_version(): number;\n alloc(length: number): number;\n result_ptr(): number;\n result_len(): number;\n measure_code(\n codePtr: number,\n codeLength: number,\n languagePtr: number,\n languageLength: number,\n includeSyntaxTree: number,\n minTokens: number,\n maxGapTokens: number,\n minSimilarityPercent: number,\n includeCrossFileData: number\n ): number;\n collect_cross_file_data(\n codePtr: number,\n codeLength: number,\n languagePtr: number,\n languageLength: number,\n minTokens: number\n ): number;\n collect_function_token_sequences(\n codePtr: number,\n codeLength: number,\n languagePtr: number,\n languageLength: number\n ): number;\n}\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\n/**\n * Wraps the WebAssembly build of the native addon (native/src/wasm.rs) as a NativeBinding. The\n * module is instantiated synchronously on first use and again after a trap (a panic or stack\n * overflow), because a trap leaves the instance's memory and stack pointer in an undefined state.\n * An instantiation failure (e.g., a payload version mismatch) is memoized like the N-API loader's,\n * since instantiating the same module again would fail again.\n */\nexport function createWasmBinding(module: WebAssembly.Module): NativeBinding {\n let instance: { exports: WasmExports; stderr: string[] } | undefined;\n let instantiationFailure: unknown;\n\n const call = (invoke: (exports: WasmExports) => number): string => {\n if (!instance) {\n if (instantiationFailure) {\n throw instantiationFailure;\n }\n try {\n instance = instantiate(module);\n } catch (error) {\n instantiationFailure = error;\n throw error;\n }\n }\n const { exports, stderr } = instance;\n stderr.length = 0;\n let status: number;\n try {\n status = invoke(exports);\n } catch (error) {\n instance = undefined;\n throw new Error(`The code-gauge WebAssembly module crashed: ${stderr.join('').trim() || String(error)}`, {\n cause: error,\n });\n }\n const result = decoder.decode(new Uint8Array(exports.memory.buffer, exports.result_ptr(), exports.result_len()));\n if (status !== 0) {\n throw new Error(result);\n }\n return result;\n };\n\n return {\n measureCodeNative: (\n code,\n language,\n includeSyntaxTree,\n minTokens,\n maxGapTokens,\n minSimilarityPercent,\n includeCrossFileData\n ) =>\n call((exports) =>\n exports.measure_code(\n ...passString(exports, code),\n ...passString(exports, language),\n Number(includeSyntaxTree),\n toOptionalU32(minTokens),\n toOptionalU32(maxGapTokens),\n toOptionalU32(minSimilarityPercent),\n Number(includeCrossFileData ?? false)\n )\n ),\n collectCrossFileDataNative: (code, language, minTokens) =>\n call((exports) =>\n exports.collect_cross_file_data(\n ...passString(exports, code),\n ...passString(exports, language),\n toOptionalU32(minTokens)\n )\n ),\n collectFunctionTokenSequencesNative: (code, language) =>\n call((exports) =>\n exports.collect_function_token_sequences(...passString(exports, code), ...passString(exports, language))\n ),\n };\n}\n\nfunction instantiate(module: WebAssembly.Module): { exports: WasmExports; stderr: string[] } {\n const stderr: string[] = [];\n let memory: WebAssembly.Memory | undefined;\n const instance = new WebAssembly.Instance(module, {\n wasi_snapshot_preview1: createWasiImports(() => memory as WebAssembly.Memory, stderr),\n });\n const exports = instance.exports as unknown as WasmExports;\n memory = exports.memory;\n const version = exports.payload_version();\n if (version !== expectedPayloadVersion) {\n throw new NativeAddonError(\n `The code-gauge WebAssembly module has payload version ${version}, but ${expectedPayloadVersion} is ` +\n 'expected; reinstall code-gauge, or in a code-gauge repository checkout, rebuild it with `bun run build-wasm`'\n );\n }\n return { exports, stderr };\n}\n\n/** Copies a string into a buffer whose ownership passes to the called export. */\nfunction passString(exports: WasmExports, text: string): [number, number] {\n // TextEncoder replaces lone surrogates with U+FFFD, like toWellFormed() does for the N-API addon.\n const bytes = encoder.encode(text);\n const pointer = exports.alloc(bytes.length);\n new Uint8Array(exports.memory.buffer, pointer, bytes.length).set(bytes);\n return [pointer, bytes.length];\n}\n\n/** native/src/wasm.rs reads a negative value as an absent setting. */\nfunction toOptionalU32(value: number | undefined): number {\n return value ?? -1;\n}\n\nconst WASI_ERRNO_SUCCESS = 0;\nconst WASI_ERRNO_BADF = 8;\n// crypto.getRandomValues() rejects requests larger than this.\nconst MAX_RANDOM_BYTES = 65_536;\n\n/**\n * The WASI preview 1 functions the module imports. The metrics code performs no I/O, so file\n * descriptors are unavailable except for writes, whose stderr output (e.g., a panic message) is\n * kept for the error raised when the module traps.\n */\nfunction createWasiImports(\n getMemory: () => WebAssembly.Memory,\n stderr: string[]\n): Record<string, (...args: never[]) => number> {\n const view = (): DataView => new DataView(getMemory().buffer);\n const unavailable = (): number => WASI_ERRNO_BADF;\n return {\n environ_get: () => WASI_ERRNO_SUCCESS,\n environ_sizes_get: (countPointer: number, sizePointer: number) => {\n view().setUint32(countPointer, 0, true);\n view().setUint32(sizePointer, 0, true);\n return WASI_ERRNO_SUCCESS;\n },\n clock_time_get: (_clockId: number, _precision: bigint, timePointer: number) => {\n view().setBigUint64(timePointer, BigInt(Date.now()) * 1_000_000n, true);\n return WASI_ERRNO_SUCCESS;\n },\n random_get: (pointer: number, length: number) => {\n for (let offset = 0; offset < length; offset += MAX_RANDOM_BYTES) {\n crypto.getRandomValues(\n new Uint8Array(getMemory().buffer, pointer + offset, Math.min(MAX_RANDOM_BYTES, length - offset))\n );\n }\n return WASI_ERRNO_SUCCESS;\n },\n fd_write: (fd: number, iovsPointer: number, iovsLength: number, writtenPointer: number) => {\n const memoryView = view();\n let written = 0;\n for (let index = 0; index < iovsLength; index++) {\n const pointer = memoryView.getUint32(iovsPointer + index * 8, true);\n const length = memoryView.getUint32(iovsPointer + index * 8 + 4, true);\n if (fd === 2) {\n stderr.push(decoder.decode(new Uint8Array(getMemory().buffer, pointer, length)));\n }\n written += length;\n }\n memoryView.setUint32(writtenPointer, written, true);\n return WASI_ERRNO_SUCCESS;\n },\n fd_close: unavailable,\n fd_fdstat_get: unavailable,\n fd_fdstat_set_flags: unavailable,\n fd_read: unavailable,\n fd_seek: unavailable,\n proc_exit: (code: number) => {\n throw new Error(`The code-gauge WebAssembly module exited with code ${code}`);\n },\n };\n}\n"],"mappings":"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"}
|