code-gauge 5.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"diffCommand.js","names":[],"sources":["../src/diffCommand.ts"],"sourcesContent":["import { lstat, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { loadConfig, resolveGateOptions, resolveOptions, type ResolvedOptions } from './cliConfig.js';\nimport {\n measureCrossFileDuplication,\n type CrossFileDuplicateOccurrence,\n type CrossFileDuplicationMetrics,\n} from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData, Token } from './duplication.js';\nimport { keepPaths, loadRepositoryExclusion, type Exclusion } from './exclusion.js';\nimport {\n listChangedFiles,\n listLineHunks,\n listRepositoryFiles,\n listSymlinkPathsAtRevision,\n readFileAtRevision,\n resolveMergeBase,\n resolveRepoRoot,\n type ChangedFile,\n} from './git.js';\nimport { collectFunctionTokenSequences } from './metrics.js';\nimport {\n findNewlyDuplicatedLines,\n indexPartnersByLine,\n type CloneOccurrence,\n type DuplicationChange,\n type LineHunk,\n} from './newDuplication.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 headContent?: string;\n baseMetrics?: CodeMetrics;\n baseContent?: string;\n /**\n * The line diff from the measured base to the measured head; with one side unmeasured, every\n * line of the other counts as added or deleted.\n */\n hunks?: LineHunk[];\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 { canonicalTarget, targetExists } = await canonicalizeTarget(resolvedTarget);\n const targetStat = await stat(canonicalTarget).catch(() => {});\n const targetFile = targetStat?.isFile() ? canonicalTarget : undefined;\n // A targeted file git does not list (an ignored one) is still measured, as an addition.\n const unlistedTarget = targetFile && path.relative(repoRoot, targetFile).split(path.sep).join('/');\n if (unlistedTarget !== undefined && !unlistedTarget.startsWith('../') && !repositoryFiles.has(unlistedTarget)) {\n repositoryFiles.add(unlistedTarget);\n changedFiles.push({ status: 'added', headPath: unlistedTarget });\n }\n const explicitFiles = listExplicitlyTargetedPaths(targetFile, repoRoot, changedFiles);\n // Base blobs are excluded by the attributes of the revision they come from. A changed\n // .gitattributes can re-include files whose bytes did not change, so then every file's base\n // attributes are needed to find them.\n const attributesChanged = changedFiles.some((changed) =>\n [changed.headPath, changed.basePath].some(\n (file) => file !== undefined && path.posix.basename(file) === '.gitattributes'\n )\n );\n const basePaths = changedFiles.flatMap((changed) => (changed.basePath === undefined ? [] : [changed.basePath]));\n const [headAttributesExclusion, baseAttributesExclusion] = await Promise.all([\n loadRepositoryExclusion(repoRoot, repositoryFiles, options.exclude),\n loadRepositoryExclusion(\n repoRoot,\n attributesChanged ? [...new Set([...basePaths, ...repositoryFiles])] : basePaths,\n options.exclude,\n mergeBase\n ),\n ]);\n const headExclusion = keepPaths(headAttributesExclusion, explicitFiles);\n const baseExclusion = keepPaths(baseAttributesExclusion, explicitFiles);\n const scan = await scanListedFiles(\n repoRoot,\n repositoryFiles,\n { ...options, loadExclusion: () => Promise.resolve(headExclusion) },\n explicitFiles\n );\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 if (attributesChanged) {\n changedFiles.push(\n ...(await listReincludedFiles(scannedFiles, changedFiles, repoRoot, headExclusion, baseExclusion))\n );\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, explicitFiles.has(path.join(repoRoot, changedPath))))\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 prepared = await prepareChangedFiles(\n changedFiles,\n {\n repoRoot,\n mergeBase,\n canonicalTarget,\n options,\n scannedFiles,\n baseSymlinkPaths,\n scanErrors: [...errors],\n headExclusion,\n baseExclusion,\n explicitFiles,\n generatedHeadFiles: new Set(scan.generatedFiles),\n },\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 renamedPaths = new Map(\n prepared.flatMap(({ changed }) =>\n changed.status === 'renamed' && changed.basePath !== undefined\n ? [[changed.basePath, changed.headPath] as const]\n : []\n )\n );\n const headPathOf = (basePath: string): string => renamedPaths.get(basePath) ?? basePath;\n const lineSignaturesOf = createLineSignatureIndex(scannedFiles);\n const newlyDuplicatedLines = findNewlyDuplicatedLines(\n prepared.map((file) => toDuplicationChange(file, baseCross, headCross, headPathOf, lineSignaturesOf))\n );\n const inputs = prepared.map((file, index) => toGateInput(file, newlyDuplicatedLines[index] ?? [], 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/** Content with a generated-code marker, to ask whether an exclusion honors the marker for a path. */\nconst markedCode = '/* @generated */';\n\n/**\n * Unchanged files the head scan measured but the merge-base would not have: excluded by its\n * attributes, or generated by content without the `-linguist-generated` override the working tree\n * adds. Their code is newly measured, so it gates as a modification from an unmeasurable base. The\n * head side comes from the scan, which already applied scope, symbolic-link, and exclusion rules.\n */\nasync function listReincludedFiles(\n scannedFiles: readonly ScannedFile[],\n changedFiles: readonly ChangedFile[],\n repoRoot: string,\n headExclusion: Exclusion,\n baseExclusion: Exclusion\n): Promise<ChangedFile[]> {\n const changedPaths = new Set(changedFiles.flatMap((changed) => [changed.headPath, changed.basePath]));\n const reincluded: ChangedFile[] = [];\n for (const { relativePath, file } of scannedFiles) {\n const absolutePath = path.join(repoRoot, relativePath);\n if (changedPaths.has(relativePath)) {\n continue;\n }\n // The same content is generated at one revision only when its override differs, so the\n // content is read just for files the working tree newly marks -linguist-generated.\n const overrideAdded =\n baseExclusion.isGeneratedCode(absolutePath, markedCode) &&\n !headExclusion.isGeneratedCode(absolutePath, markedCode);\n if (\n baseExclusion.isExcludedPath(absolutePath) ||\n (overrideAdded && baseExclusion.isGeneratedCode(absolutePath, await readFile(file.file, 'utf8')))\n ) {\n reincluded.push({ status: 'modified', basePath: relativePath, headPath: relativePath });\n }\n }\n return reincluded;\n}\n\n/**\n * An explicitly targeted file is measured even when excluded, like the ranking command's single\n * file target: the file itself, and its base path when it was renamed.\n */\nfunction listExplicitlyTargetedPaths(\n targetFile: string | undefined,\n repoRoot: string,\n changedFiles: ChangedFile[]\n): Set<string> {\n if (targetFile === undefined) {\n return new Set();\n }\n const paths = new Set([targetFile]);\n for (const changed of changedFiles) {\n if (changed.basePath !== undefined && path.join(repoRoot, changed.headPath) === targetFile) {\n paths.add(path.join(repoRoot, changed.basePath));\n }\n }\n return paths;\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 /** Errors the head scan recorded against changed files. */\n scanErrors: readonly string[];\n headExclusion: Exclusion;\n baseExclusion: Exclusion;\n /** The explicitly targeted file (and its renamed base path), measured despite every exclusion. */\n explicitFiles: ReadonlySet<string>;\n /** Absolute paths the head scan skipped as generated code. */\n generatedHeadFiles: ReadonlySet<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 // Files are prepared concurrently (git reads and base measurements); each records its diagnostics\n // on its own, and they are appended in changed-file order so the report stays deterministic.\n const results = await mapConcurrently(changedFiles, os.availableParallelism() * 2, async (changed) => {\n const fileErrors: string[] = [];\n const fileWarnings: string[] = [];\n const file = await prepareChangedFile(changed, context, headByPath, fileErrors, fileWarnings);\n return { file, fileErrors, fileWarnings };\n });\n const prepared: PreparedFile[] = [];\n for (const { file, fileErrors, fileWarnings } of results) {\n errors.push(...fileErrors);\n warnings.push(...fileWarnings);\n if (file) {\n prepared.push(file);\n }\n }\n return prepared;\n}\n\n/** `Promise.all(items.map(map))` with at most `limit` calls pending at once, results in input order. */\nasync function mapConcurrently<T, R>(items: readonly T[], limit: number, map: (item: T) => Promise<R>): Promise<R[]> {\n const results: R[] = [];\n let nextIndex = 0;\n const work = async (): Promise<void> => {\n while (nextIndex < items.length) {\n const index = nextIndex;\n nextIndex += 1;\n results[index] = await map(items[index] as T);\n }\n };\n await Promise.all(Array.from({ length: Math.min(limit, items.length) }, work));\n return results;\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 absoluteHeadPath = path.join(context.repoRoot, changed.headPath);\n const headScannable =\n changed.status !== 'deleted' &&\n isScannedPath(changed.headPath, context.options, context.explicitFiles.has(absoluteHeadPath)) &&\n !context.headExclusion.isExcludedPath(absoluteHeadPath) &&\n !context.generatedHeadFiles.has(absoluteHeadPath) &&\n !(await isSymbolicLink(absoluteHeadPath));\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 absoluteBasePath = changed.basePath === undefined ? '' : path.join(context.repoRoot, changed.basePath);\n const baseScannable =\n changed.basePath !== undefined &&\n isScannedPath(changed.basePath, context.options, context.explicitFiles.has(absoluteBasePath)) &&\n !context.baseExclusion.isExcludedPath(absoluteBasePath) &&\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, context.scanErrors, 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) {\n const outcome = await measureBaseRevision(file, changed.basePath as string, context, errors, warnings);\n // A base that turns out to be generated code gates its head as new code, like any other base\n // outside the scan scope.\n if (outcome === 'failed' || (outcome === 'generated' && !headFile)) {\n return undefined;\n }\n }\n\n try {\n if (headFile) {\n // The duplication gate needs the head lines, so an unreadable head fails like an unmeasured one.\n file.headContent = await readFile(headFile.file, 'utf8');\n collectHeadFunctionTokens(file, file.headContent, context, warnings);\n }\n file.hunks = await diffMeasuredRevisions(file, context);\n } catch (error) {\n errors.push(`${displayFile}: ${formatError(error)}`);\n return undefined;\n }\n return file;\n}\n\nasync function diffMeasuredRevisions(file: PreparedFile, context: GateContext): Promise<LineHunk[]> {\n const { changed, baseContent, headContent } = file;\n if (baseContent !== undefined && headContent !== undefined) {\n return await listLineHunks(context.repoRoot, context.mergeBase, changed.basePath as string, changed.headPath);\n }\n return [\n {\n baseStart: 1,\n baseCount: baseContent === undefined ? 0 : splitLines(baseContent).length,\n headStart: 1,\n headCount: headContent === undefined ? 0 : splitLines(headContent).length,\n },\n ];\n}\n\n/** Line numbering of the measured metrics (classifyLines splits on every line terminator). */\nfunction splitLines(content: string): string[] {\n return content.split(/\\r\\n|\\n|\\r/u);\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, scanErrors: readonly string[], errors: string[]): void {\n if (!scanErrors.some((error) => error.startsWith(`${headPath}:`))) {\n errors.push(`${headPath}: changed file was not measured`);\n }\n}\n\nfunction collectHeadFunctionTokens(\n file: PreparedFile,\n headContent: string,\n context: GateContext,\n warnings: string[]\n): void {\n try {\n file.headFunctionTokens = collectFunctionTokenSequences(headContent, {\n language: languageOf(file.changed.headPath, context),\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`; 'failed' (with an error recorded) only when the\n * metrics themselves cannot be measured, and 'generated' (nothing measured) for generated code.\n * The auxiliary collections (duplication candidates, token sequences) may fail independently of\n * the metrics, so their failure only degrades duplication data and rename re-matching — the\n * 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<'measured' | 'generated' | 'failed'> {\n const measureOptions = {\n language: languageOf(basePath, context),\n duplication: context.options.duplication,\n };\n let baseContent;\n try {\n baseContent = await readFileAtRevision(context.repoRoot, context.mergeBase, basePath);\n if (context.baseExclusion.isGeneratedCode(path.join(context.repoRoot, basePath), baseContent)) {\n return 'generated';\n }\n const measured = await measureWithCrossFileData(baseContent, measureOptions);\n file.baseMetrics = measured.metrics;\n file.baseContent = baseContent;\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 'failed';\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 'measured';\n}\n\n/** The language of a scannable path; an explicitly targeted file skips the test-file name rules. */\nfunction languageOf(relativePath: string, context: GateContext): LanguageName {\n const explicitTarget = context.explicitFiles.has(path.join(context.repoRoot, relativePath));\n return getLanguage(relativePath, context.options, explicitTarget) as LanguageName;\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 toDuplicationChange(\n file: PreparedFile,\n baseCross: CrossFileDuplicationMetrics | undefined,\n headCross: CrossFileDuplicationMetrics | undefined,\n headPathOf: (basePath: string) => string,\n lineSignaturesOf: (file: string) => LineSignatures | undefined\n): DuplicationChange {\n const headOccurrences = collectOccurrences(file.headFile?.metrics, headCross, file.changed.headPath);\n return {\n baseLines: file.baseContent === undefined ? undefined : splitLines(file.baseContent),\n headLines: file.headContent === undefined ? undefined : splitLines(file.headContent),\n baseDuplicatedLines:\n file.baseMetrics === undefined || file.changed.basePath === undefined\n ? new Set()\n : collectDuplicatedLineNumbers(file.baseMetrics, baseCross, file.changed.basePath),\n headDuplicatedLines:\n file.headFile === undefined\n ? new Set()\n : keepRepeatedLines(\n collectDuplicatedLineNumbers(file.headFile.metrics, headCross, file.changed.headPath),\n file.changed.headPath,\n headOccurrences,\n lineSignaturesOf\n ),\n baseOccurrences: collectOccurrences(file.baseMetrics, baseCross, file.changed.basePath, headPathOf),\n headOccurrences,\n hunks: file.hunks ?? [],\n };\n}\n\n/** A file's per-line normalized token signatures and how many lines carry each. */\ninterface LineSignatures {\n byLine: Map<number, string>;\n lineCounts: Map<string, number>;\n}\n\n/** Line signatures of the scanned head files, computed on first use. */\nfunction createLineSignatureIndex(scannedFiles: ScannedFile[]): (file: string) => LineSignatures | undefined {\n const tokensByFile = new Map(\n scannedFiles.map(({ relativePath, file }) => [relativePath, file.duplicationCandidates?.tokens])\n );\n const signaturesByFile = new Map<string, LineSignatures | undefined>();\n return (file) => {\n if (!signaturesByFile.has(file)) {\n const tokens = tokensByFile.get(file);\n signaturesByFile.set(file, tokens && signLines(tokens));\n }\n return signaturesByFile.get(file);\n };\n}\n\n/**\n * Each line's tokens with identifiers numbered by first occurrence, as clone detection anonymizes\n * them.\n */\nfunction signLines(tokens: readonly Token[]): LineSignatures {\n const byLine = new Map<number, string>();\n const identifierIndexesByLine = new Map<number, Map<string, number>>();\n for (const token of tokens) {\n const line = token.startRow + 1;\n let text = token.text;\n if (token.kind === 'id') {\n // Numbered by first occurrence within the line, so `a + a` and `a + b` stay distinct.\n const identifierIndexes = identifierIndexesByLine.get(line) ?? new Map<string, number>();\n identifierIndexesByLine.set(line, identifierIndexes);\n if (!identifierIndexes.has(token.text)) {\n identifierIndexes.set(token.text, identifierIndexes.size);\n }\n text = `\\u0001${identifierIndexes.get(token.text)}`;\n }\n byLine.set(line, `${byLine.get(line) ?? ''}${text}\\u0000`);\n }\n const lineCounts = new Map<string, number>();\n for (const signature of byLine.values()) {\n lineCounts.set(signature, (lineCounts.get(signature) ?? 0) + 1);\n }\n return { byLine, lineCounts };\n}\n\n/**\n * The duplicated lines whose normalized tokens a partner file of their clones (the file itself,\n * on another line, for `''`) repeats on some line. Near-miss coverage also marks lines the copies\n * do not share, such as a new line inserted into an old near-miss clone, which copied nothing.\n */\nfunction keepRepeatedLines(\n lines: ReadonlySet<number>,\n file: string,\n occurrences: readonly CloneOccurrence[],\n lineSignaturesOf: (file: string) => LineSignatures | undefined\n): Set<number> {\n const own = lineSignaturesOf(file);\n if (own === undefined) {\n return new Set(lines);\n }\n const repeats = (signature: string, partner: string): boolean => {\n if (partner === '') {\n return (own.lineCounts.get(signature) ?? 0) > 1;\n }\n const partnerSignatures = lineSignaturesOf(partner);\n return partnerSignatures === undefined || partnerSignatures.lineCounts.has(signature);\n };\n const partnersByLine = indexPartnersByLine(occurrences);\n return new Set(\n [...lines].filter((line) => {\n const signature = own.byLine.get(line);\n return (\n signature === undefined || [...(partnersByLine.get(line) ?? [])].some((partner) => repeats(signature, partner))\n );\n })\n );\n}\n\n/**\n * The file's clone occurrences with their partner files (`''` for the file itself), partner paths\n * renamed by `renamePartner` so base partners compare with head ones.\n */\nfunction collectOccurrences(\n metrics: CodeMetrics | undefined,\n cross: CrossFileDuplicationMetrics | undefined,\n file: string | undefined,\n renamePartner: (partner: string) => string = (partner) => partner\n): CloneOccurrence[] {\n if (metrics === undefined || file === undefined) {\n return [];\n }\n const withinFile = metrics.duplication.duplicateBlockGroups.flatMap((group) =>\n group.map(({ startLine, endLine }) => ({ startLine, endLine, partners: [''] }))\n );\n const crossFile = (cross?.groups ?? []).flatMap((group) => {\n const own = group.occurrences.filter((occurrence) => occurrence.file === file);\n const partners = [\n ...group.files.filter((partner) => partner !== file).map(renamePartner),\n ...(own.length > 1 ? [''] : []),\n ];\n return own.map(({ startLine, endLine }) => ({ startLine, endLine, partners }));\n });\n return [...withinFile, ...crossFile];\n}\n\nfunction toGateInput(\n file: PreparedFile,\n newlyDuplicatedLines: number[],\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 newlyDuplicatedLines,\n duplicationPartners: collectPartners(file, newlyDuplicatedLines, headCross),\n gated: file.gated,\n };\n}\n\n/** Other files sharing a cross-file clone that covers the given lines. */\nfunction collectPartners(\n file: PreparedFile,\n lines: number[],\n headCross: CrossFileDuplicationMetrics | undefined\n): string[] {\n const headPath = file.changed.headPath;\n const covers = (occurrence: CrossFileDuplicateOccurrence): boolean =>\n occurrence.file === headPath && lines.some((line) => line >= occurrence.startLine && line <= occurrence.endLine);\n const partners = new Set<string>();\n for (const group of headCross?.groups ?? []) {\n if (group.occurrences.some(covers)) {\n for (const partner of group.files) {\n if (partner !== headPath) {\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 newlyDuplicatedLines: input.newlyDuplicatedLines,\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.js","names":[],"sources":["../src/diffCommand.ts"],"sourcesContent":["import { lstat, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { getErrorMessage, mapConcurrently } from '@willbooster/shared-lib';\nimport { loadConfig, resolveGateOptions, resolveOptions, type ResolvedOptions } from './cliConfig.js';\nimport {\n measureCrossFileDuplication,\n type CrossFileDuplicateOccurrence,\n type CrossFileDuplicationMetrics,\n} from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData, Token } from './duplication.js';\nimport { keepPaths, loadRepositoryExclusion, type Exclusion } from './exclusion.js';\nimport {\n listChangedFiles,\n listLineHunks,\n listRepositoryFiles,\n listSymlinkPathsAtRevision,\n readFileAtRevision,\n resolveMergeBase,\n resolveRepoRoot,\n type ChangedFile,\n} from './git.js';\nimport { collectFunctionTokenSequences } from './metrics.js';\nimport {\n findNewlyDuplicatedLines,\n indexPartnersByLine,\n type CloneOccurrence,\n type DuplicationChange,\n type LineHunk,\n} from './newDuplication.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 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 headContent?: string;\n baseMetrics?: CodeMetrics;\n baseContent?: string;\n /**\n * The line diff from the measured base to the measured head; with one side unmeasured, every\n * line of the other counts as added or deleted.\n */\n hunks?: LineHunk[];\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: ${getErrorMessage(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 { canonicalTarget, targetExists } = await canonicalizeTarget(resolvedTarget);\n const targetStat = await stat(canonicalTarget).catch(() => {});\n const targetFile = targetStat?.isFile() ? canonicalTarget : undefined;\n // A targeted file git does not list (an ignored one) is still measured, as an addition.\n const unlistedTarget = targetFile && path.relative(repoRoot, targetFile).split(path.sep).join('/');\n if (unlistedTarget !== undefined && !unlistedTarget.startsWith('../') && !repositoryFiles.has(unlistedTarget)) {\n repositoryFiles.add(unlistedTarget);\n changedFiles.push({ status: 'added', headPath: unlistedTarget });\n }\n const explicitFiles = listExplicitlyTargetedPaths(targetFile, repoRoot, changedFiles);\n // Base blobs are excluded by the attributes of the revision they come from. A changed\n // .gitattributes can re-include files whose bytes did not change, so then every file's base\n // attributes are needed to find them.\n const attributesChanged = changedFiles.some((changed) =>\n [changed.headPath, changed.basePath].some(\n (file) => file !== undefined && path.posix.basename(file) === '.gitattributes'\n )\n );\n const basePaths = changedFiles.flatMap((changed) => (changed.basePath === undefined ? [] : [changed.basePath]));\n const [headAttributesExclusion, baseAttributesExclusion] = await Promise.all([\n loadRepositoryExclusion(repoRoot, repositoryFiles, options.exclude),\n loadRepositoryExclusion(\n repoRoot,\n attributesChanged ? [...new Set([...basePaths, ...repositoryFiles])] : basePaths,\n options.exclude,\n mergeBase\n ),\n ]);\n const headExclusion = keepPaths(headAttributesExclusion, explicitFiles);\n const baseExclusion = keepPaths(baseAttributesExclusion, explicitFiles);\n const scan = await scanListedFiles(\n repoRoot,\n repositoryFiles,\n { ...options, loadExclusion: () => Promise.resolve(headExclusion) },\n explicitFiles\n );\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 if (attributesChanged) {\n changedFiles.push(\n ...(await listReincludedFiles(scannedFiles, changedFiles, repoRoot, headExclusion, baseExclusion))\n );\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, explicitFiles.has(path.join(repoRoot, changedPath))))\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 prepared = await prepareChangedFiles(\n changedFiles,\n {\n repoRoot,\n mergeBase,\n canonicalTarget,\n options,\n scannedFiles,\n baseSymlinkPaths,\n scanErrors: [...errors],\n headExclusion,\n baseExclusion,\n explicitFiles,\n generatedHeadFiles: new Set(scan.generatedFiles),\n },\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 renamedPaths = new Map(\n prepared.flatMap(({ changed }) =>\n changed.status === 'renamed' && changed.basePath !== undefined\n ? [[changed.basePath, changed.headPath] as const]\n : []\n )\n );\n const headPathOf = (basePath: string): string => renamedPaths.get(basePath) ?? basePath;\n const lineSignaturesOf = createLineSignatureIndex(scannedFiles);\n const newlyDuplicatedLines = findNewlyDuplicatedLines(\n prepared.map((file) => toDuplicationChange(file, baseCross, headCross, headPathOf, lineSignaturesOf))\n );\n const inputs = prepared.map((file, index) => toGateInput(file, newlyDuplicatedLines[index] ?? [], 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/** Content with a generated-code marker, to ask whether an exclusion honors the marker for a path. */\nconst markedCode = '/* @generated */';\n\n/**\n * Unchanged files the head scan measured but the merge-base would not have: excluded by its\n * attributes, or generated by content without the `-linguist-generated` override the working tree\n * adds. Their code is newly measured, so it gates as a modification from an unmeasurable base. The\n * head side comes from the scan, which already applied scope, symbolic-link, and exclusion rules.\n */\nasync function listReincludedFiles(\n scannedFiles: readonly ScannedFile[],\n changedFiles: readonly ChangedFile[],\n repoRoot: string,\n headExclusion: Exclusion,\n baseExclusion: Exclusion\n): Promise<ChangedFile[]> {\n const changedPaths = new Set(changedFiles.flatMap((changed) => [changed.headPath, changed.basePath]));\n const reincluded: ChangedFile[] = [];\n for (const { relativePath, file } of scannedFiles) {\n const absolutePath = path.join(repoRoot, relativePath);\n if (changedPaths.has(relativePath)) {\n continue;\n }\n // The same content is generated at one revision only when its override differs, so the\n // content is read just for files the working tree newly marks -linguist-generated.\n const overrideAdded =\n baseExclusion.isGeneratedCode(absolutePath, markedCode) &&\n !headExclusion.isGeneratedCode(absolutePath, markedCode);\n if (\n baseExclusion.isExcludedPath(absolutePath) ||\n (overrideAdded && baseExclusion.isGeneratedCode(absolutePath, await readFile(file.file, 'utf8')))\n ) {\n reincluded.push({ status: 'modified', basePath: relativePath, headPath: relativePath });\n }\n }\n return reincluded;\n}\n\n/**\n * An explicitly targeted file is measured even when excluded, like the ranking command's single\n * file target: the file itself, and its base path when it was renamed.\n */\nfunction listExplicitlyTargetedPaths(\n targetFile: string | undefined,\n repoRoot: string,\n changedFiles: ChangedFile[]\n): Set<string> {\n if (targetFile === undefined) {\n return new Set();\n }\n const paths = new Set([targetFile]);\n for (const changed of changedFiles) {\n if (changed.basePath !== undefined && path.join(repoRoot, changed.headPath) === targetFile) {\n paths.add(path.join(repoRoot, changed.basePath));\n }\n }\n return paths;\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 /** Errors the head scan recorded against changed files. */\n scanErrors: readonly string[];\n headExclusion: Exclusion;\n baseExclusion: Exclusion;\n /** The explicitly targeted file (and its renamed base path), measured despite every exclusion. */\n explicitFiles: ReadonlySet<string>;\n /** Absolute paths the head scan skipped as generated code. */\n generatedHeadFiles: ReadonlySet<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 // Files are prepared concurrently (git reads and base measurements); each records its diagnostics\n // on its own, and they are appended in changed-file order so the report stays deterministic.\n const results = await mapConcurrently(changedFiles, os.availableParallelism() * 2, async (changed) => {\n const fileErrors: string[] = [];\n const fileWarnings: string[] = [];\n const file = await prepareChangedFile(changed, context, headByPath, fileErrors, fileWarnings);\n return { file, fileErrors, fileWarnings };\n });\n const prepared: PreparedFile[] = [];\n for (const { file, fileErrors, fileWarnings } of results) {\n errors.push(...fileErrors);\n warnings.push(...fileWarnings);\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 absoluteHeadPath = path.join(context.repoRoot, changed.headPath);\n const headScannable =\n changed.status !== 'deleted' &&\n isScannedPath(changed.headPath, context.options, context.explicitFiles.has(absoluteHeadPath)) &&\n !context.headExclusion.isExcludedPath(absoluteHeadPath) &&\n !context.generatedHeadFiles.has(absoluteHeadPath) &&\n !(await isSymbolicLink(absoluteHeadPath));\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 absoluteBasePath = changed.basePath === undefined ? '' : path.join(context.repoRoot, changed.basePath);\n const baseScannable =\n changed.basePath !== undefined &&\n isScannedPath(changed.basePath, context.options, context.explicitFiles.has(absoluteBasePath)) &&\n !context.baseExclusion.isExcludedPath(absoluteBasePath) &&\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, context.scanErrors, 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) {\n const outcome = await measureBaseRevision(file, changed.basePath as string, context, errors, warnings);\n // A base that turns out to be generated code gates its head as new code, like any other base\n // outside the scan scope.\n if (outcome === 'failed' || (outcome === 'generated' && !headFile)) {\n return undefined;\n }\n }\n\n try {\n if (headFile) {\n // The duplication gate needs the head lines, so an unreadable head fails like an unmeasured one.\n file.headContent = await readFile(headFile.file, 'utf8');\n collectHeadFunctionTokens(file, file.headContent, context, warnings);\n }\n file.hunks = await diffMeasuredRevisions(file, context);\n } catch (error) {\n errors.push(`${displayFile}: ${getErrorMessage(error)}`);\n return undefined;\n }\n return file;\n}\n\nasync function diffMeasuredRevisions(file: PreparedFile, context: GateContext): Promise<LineHunk[]> {\n const { changed, baseContent, headContent } = file;\n if (baseContent !== undefined && headContent !== undefined) {\n return await listLineHunks(context.repoRoot, context.mergeBase, changed.basePath as string, changed.headPath);\n }\n return [\n {\n baseStart: 1,\n baseCount: baseContent === undefined ? 0 : splitLines(baseContent).length,\n headStart: 1,\n headCount: headContent === undefined ? 0 : splitLines(headContent).length,\n },\n ];\n}\n\n/** Line numbering of the measured metrics (classifyLines splits on every line terminator). */\nfunction splitLines(content: string): string[] {\n return content.split(/\\r\\n|\\n|\\r/u);\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, scanErrors: readonly string[], errors: string[]): void {\n if (!scanErrors.some((error) => error.startsWith(`${headPath}:`))) {\n errors.push(`${headPath}: changed file was not measured`);\n }\n}\n\nfunction collectHeadFunctionTokens(\n file: PreparedFile,\n headContent: string,\n context: GateContext,\n warnings: string[]\n): void {\n try {\n file.headFunctionTokens = collectFunctionTokenSequences(headContent, {\n language: languageOf(file.changed.headPath, context),\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: ${getErrorMessage(error)}`);\n }\n}\n\n/**\n * Measures the merge-base blob into `file`; 'failed' (with an error recorded) only when the\n * metrics themselves cannot be measured, and 'generated' (nothing measured) for generated code.\n * The auxiliary collections (duplication candidates, token sequences) may fail independently of\n * the metrics, so their failure only degrades duplication data and rename re-matching — the\n * 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<'measured' | 'generated' | 'failed'> {\n const measureOptions = {\n language: languageOf(basePath, context),\n duplication: context.options.duplication,\n };\n let baseContent;\n try {\n baseContent = await readFileAtRevision(context.repoRoot, context.mergeBase, basePath);\n if (context.baseExclusion.isGeneratedCode(path.join(context.repoRoot, basePath), baseContent)) {\n return 'generated';\n }\n const measured = await measureWithCrossFileData(baseContent, measureOptions);\n file.baseMetrics = measured.metrics;\n file.baseContent = baseContent;\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): ${getErrorMessage(error)}`);\n return 'failed';\n }\n try {\n file.baseFunctionTokens = collectFunctionTokenSequences(baseContent, measureOptions);\n } catch (error) {\n warnings.push(`${basePath} (at merge-base): function token sequences unavailable: ${getErrorMessage(error)}`);\n }\n return 'measured';\n}\n\n/** The language of a scannable path; an explicitly targeted file skips the test-file name rules. */\nfunction languageOf(relativePath: string, context: GateContext): LanguageName {\n const explicitTarget = context.explicitFiles.has(path.join(context.repoRoot, relativePath));\n return getLanguage(relativePath, context.options, explicitTarget) as LanguageName;\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 toDuplicationChange(\n file: PreparedFile,\n baseCross: CrossFileDuplicationMetrics | undefined,\n headCross: CrossFileDuplicationMetrics | undefined,\n headPathOf: (basePath: string) => string,\n lineSignaturesOf: (file: string) => LineSignatures | undefined\n): DuplicationChange {\n const headOccurrences = collectOccurrences(file.headFile?.metrics, headCross, file.changed.headPath);\n return {\n baseLines: file.baseContent === undefined ? undefined : splitLines(file.baseContent),\n headLines: file.headContent === undefined ? undefined : splitLines(file.headContent),\n baseDuplicatedLines:\n file.baseMetrics === undefined || file.changed.basePath === undefined\n ? new Set()\n : collectDuplicatedLineNumbers(file.baseMetrics, baseCross, file.changed.basePath),\n headDuplicatedLines:\n file.headFile === undefined\n ? new Set()\n : keepRepeatedLines(\n collectDuplicatedLineNumbers(file.headFile.metrics, headCross, file.changed.headPath),\n file.changed.headPath,\n headOccurrences,\n lineSignaturesOf\n ),\n baseOccurrences: collectOccurrences(file.baseMetrics, baseCross, file.changed.basePath, headPathOf),\n headOccurrences,\n hunks: file.hunks ?? [],\n };\n}\n\n/** A file's per-line normalized token signatures and how many lines carry each. */\ninterface LineSignatures {\n byLine: Map<number, string>;\n lineCounts: Map<string, number>;\n}\n\n/** Line signatures of the scanned head files, computed on first use. */\nfunction createLineSignatureIndex(scannedFiles: ScannedFile[]): (file: string) => LineSignatures | undefined {\n const tokensByFile = new Map(\n scannedFiles.map(({ relativePath, file }) => [relativePath, file.duplicationCandidates?.tokens])\n );\n const signaturesByFile = new Map<string, LineSignatures | undefined>();\n return (file) => {\n if (!signaturesByFile.has(file)) {\n const tokens = tokensByFile.get(file);\n signaturesByFile.set(file, tokens && signLines(tokens));\n }\n return signaturesByFile.get(file);\n };\n}\n\n/**\n * Each line's tokens with identifiers numbered by first occurrence, as clone detection anonymizes\n * them.\n */\nfunction signLines(tokens: readonly Token[]): LineSignatures {\n const byLine = new Map<number, string>();\n const identifierIndexesByLine = new Map<number, Map<string, number>>();\n for (const token of tokens) {\n const line = token.startRow + 1;\n let text = token.text;\n if (token.kind === 'id') {\n // Numbered by first occurrence within the line, so `a + a` and `a + b` stay distinct.\n const identifierIndexes = identifierIndexesByLine.get(line) ?? new Map<string, number>();\n identifierIndexesByLine.set(line, identifierIndexes);\n if (!identifierIndexes.has(token.text)) {\n identifierIndexes.set(token.text, identifierIndexes.size);\n }\n text = `\\u0001${identifierIndexes.get(token.text)}`;\n }\n byLine.set(line, `${byLine.get(line) ?? ''}${text}\\u0000`);\n }\n const lineCounts = new Map<string, number>();\n for (const signature of byLine.values()) {\n lineCounts.set(signature, (lineCounts.get(signature) ?? 0) + 1);\n }\n return { byLine, lineCounts };\n}\n\n/**\n * The duplicated lines whose normalized tokens a partner file of their clones (the file itself,\n * on another line, for `''`) repeats on some line. Near-miss coverage also marks lines the copies\n * do not share, such as a new line inserted into an old near-miss clone, which copied nothing.\n */\nfunction keepRepeatedLines(\n lines: ReadonlySet<number>,\n file: string,\n occurrences: readonly CloneOccurrence[],\n lineSignaturesOf: (file: string) => LineSignatures | undefined\n): Set<number> {\n const own = lineSignaturesOf(file);\n if (own === undefined) {\n return new Set(lines);\n }\n const repeats = (signature: string, partner: string): boolean => {\n if (partner === '') {\n return (own.lineCounts.get(signature) ?? 0) > 1;\n }\n const partnerSignatures = lineSignaturesOf(partner);\n return partnerSignatures === undefined || partnerSignatures.lineCounts.has(signature);\n };\n const partnersByLine = indexPartnersByLine(occurrences);\n return new Set(\n [...lines].filter((line) => {\n const signature = own.byLine.get(line);\n return (\n signature === undefined || [...(partnersByLine.get(line) ?? [])].some((partner) => repeats(signature, partner))\n );\n })\n );\n}\n\n/**\n * The file's clone occurrences with their partner files (`''` for the file itself), partner paths\n * renamed by `renamePartner` so base partners compare with head ones.\n */\nfunction collectOccurrences(\n metrics: CodeMetrics | undefined,\n cross: CrossFileDuplicationMetrics | undefined,\n file: string | undefined,\n renamePartner: (partner: string) => string = (partner) => partner\n): CloneOccurrence[] {\n if (metrics === undefined || file === undefined) {\n return [];\n }\n const withinFile = metrics.duplication.duplicateBlockGroups.flatMap((group) =>\n group.map(({ startLine, endLine }) => ({ startLine, endLine, partners: [''] }))\n );\n const crossFile = (cross?.groups ?? []).flatMap((group) => {\n const own = group.occurrences.filter((occurrence) => occurrence.file === file);\n const partners = [\n ...group.files.filter((partner) => partner !== file).map(renamePartner),\n ...(own.length > 1 ? [''] : []),\n ];\n return own.map(({ startLine, endLine }) => ({ startLine, endLine, partners }));\n });\n return [...withinFile, ...crossFile];\n}\n\nfunction toGateInput(\n file: PreparedFile,\n newlyDuplicatedLines: number[],\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 newlyDuplicatedLines,\n duplicationPartners: collectPartners(file, newlyDuplicatedLines, headCross),\n gated: file.gated,\n };\n}\n\n/** Other files sharing a cross-file clone that covers the given lines. */\nfunction collectPartners(\n file: PreparedFile,\n lines: number[],\n headCross: CrossFileDuplicationMetrics | undefined\n): string[] {\n const headPath = file.changed.headPath;\n const covers = (occurrence: CrossFileDuplicateOccurrence): boolean =>\n occurrence.file === headPath && lines.some((line) => line >= occurrence.startLine && line <= occurrence.endLine);\n const partners = new Set<string>();\n for (const group of headCross?.groups ?? []) {\n if (group.occurrences.some(covers)) {\n for (const partner of group.files) {\n if (partner !== headPath) {\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 newlyDuplicatedLines: input.newlyDuplicatedLines,\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,2 +1,2 @@
1
- "use strict";const e={minTokens:40,maxGapTokens:30,minSimilarityPercent:70};function t(t){return{minTokens:n(t?.minTokens,e.minTokens),maxGapTokens:n(t?.maxGapTokens,e.maxGapTokens),minSimilarityPercent:n(t?.minSimilarityPercent,e.minSimilarityPercent)}}function n(e,t){return e===void 0||Number.isNaN(e)?t:Math.min(Math.max(Math.trunc(e),0),4294967295)}function r(e,t){return e*5>=t}function i(e){let t=new Int32Array(e.length+1);for(let[n,r]of e.entries())t[n+1]=(t[n]??0)+(r.literalHash===void 0?0:1);return t}function a(e,t,n){let r=[],i=[],a=[];for(let[t,n]of e.entries())for(let e of n.containers)i.push(t),a.push(e);let c=t=>e[i[t]??0],l=/* @__PURE__ */ new Map,u=a.map((e,n)=>s(c(n)?.tokens??[],e,t));for(let[e,t]of u.entries()){let n=i[e]??0;for(let[r,i]of t.windowKeysByStart.entries())for(let t of i){if(t===void 0)continue;let i=l.get(t);i?(i.count+=1,i.containerIndex!==e&&(i.containerIndex=-1),i.contextIndex!==n&&(i.contextIndex=-1),i.minStart=Math.min(i.minStart,r),i.maxStart=Math.max(i.maxStart,r)):l.set(t,{count:1,containerIndex:e,contextIndex:n,minStart:r,maxStart:r})}}let f=(e,t)=>{if(e===void 0)return!1;let r=l.get(e);return r===void 0||r.count<2?!1:n?r.contextIndex===-1:r.containerIndex===-1||r.maxStart-r.minStart>=t},p=e=>{let t=u[e.containerIndex]?.statementHashes??[],n=t[e.start];for(let r=e.start+1;r<e.start+e.length;r+=1)if(t[r]!==n)return!0;return!1},m=[];for(let[e,t]of u.entries())for(let[n,r]of t.windowKeysByStart.entries())for(let[i,a]of r.entries()){if(!f(a,i)||!p({containerIndex:e,start:n,length:i}))continue;let r=t.windowKeysByStart[n]?.[i+1],o=t.windowKeysByStart[n-1]?.[i+1];f(r,i+1)||f(o,i+1)||m.push({containerIndex:e,start:n,length:i})}let h=new Set(m.map(o)),_=m;for(;_.length>0;){let e=[];for(let t of _){let n=a[t.containerIndex],o=n?.[t.start],s=n?.[t.start+t.length-1],l=c(t.containerIndex);if(!o||!s||!l)continue;let u=`s:${g(l.tokens,l.literalCountPrefix,o.startTokenIndex,s.endTokenIndex)}`;r.push({candidate:d(u,o.startTokenIndex,s.endTokenIndex,o,s),contextIndex:i[t.containerIndex]??0}),e.push(t)}_=[];for(let t of e)for(let e of[t.start,t.start+1]){let n={containerIndex:t.containerIndex,start:e,length:t.length-1},r=u[t.containerIndex]?.windowKeysByStart[e]?.[n.length];!h.has(o(n))&&f(r,n.length)&&p(n)&&(h.add(o(n)),_.push(n))}}return r}function o(e){return`${e.containerIndex}:${e.start}:${e.length}`}function s(e,t,n){let r=t.map(t=>_(e,t.startTokenIndex,t.endTokenIndex)),i=[];for(let e=0;e<t.length;e+=1){let a=[],o=5381,s=0,c=Math.min(t.length,e+100);for(let i=e;i<c;i+=1){let c=t[i],l=r[i];if(!c||l===void 0)break;o=x(o,l),s+=c.endTokenIndex-c.startTokenIndex;let u=i-e+1;a[u]=u>=2&&s>=n?x(o,u):void 0}i.push(a)}return{windowKeysByStart:i,statementHashes:r}}function c(e,t){return l(e)(t)}function l(e){let t=e.length+31>>>5,n=/* @__PURE__ */ new Map;for(let[r,i]of e.entries()){let e=n.get(i);e||(e=new Uint32Array(t),n.set(i,e));let a=r>>>5;e[a]=(e[a]??0)|1<<(r&31)}let r=new Uint32Array(t);return e=>{r.fill(0);for(let i of e){let e=n.get(i),a=1,o=0;for(let n=0;n<t;n+=1){let t=r[n]??0,i=((e?.[n]??0)|t)>>>0,s=(t<<1|a)>>>0;a=t>>>31;let c=i-s-o;o=+(c<0),r[n]=i&~c}}let i=0;for(let e of r)i+=u(e);return i}}function u(e){let t=e-(e>>>1&1431655765);return t=(t&858993459)+(t>>>2&858993459),Math.imul(t+(t>>>4)&252645135,16843009)>>>24&255}function d(e,t,n,r,i){return{fingerprint:e,tokenCount:n-t,startTokenIndex:t,endTokenIndex:n,startIndex:r.startIndex,endIndex:i.endIndex,startLine:r.startLine,endLine:i.endLine}}const f=[],p=[];function m(e){let t=f[e];return t===void 0&&(t=y(`$${e}`),f[e]=t),t}function h(e){let t=p[e];return t===void 0&&(t=b(`$${e}`),p[e]=t),t}function g(e,t,n,i){let[a,o]=v(e,n,i,r((t[i]??0)-(t[n]??0),i-n));return`${a}:${o}:${i-n}`}function _(e,t,n){let[r,i]=v(e,t,n,!1);return r^Math.imul(i,31)}function v(e,t,n,r){let i=/* @__PURE__ */ new Map,a=5381,o=52711;for(let s=t;s<n;s+=1){let t=e[s];if(!t)continue;let n,c;if(t.kind===`id`){let e=i.get(t.text);e===void 0&&(e=i.size,i.set(t.text,e)),n=m(e),c=h(e)}else n=t.textHash,c=t.textHash2;a=Math.imul(a,31)+n|0,o=Math.imul(o,37)^c,r&&t.literalHash!==void 0&&t.literalHash2!==void 0&&(a=Math.imul(a,31)+t.literalHash|0,o=Math.imul(o,37)^t.literalHash2)}return[a,o]}function y(e){let t=5381;for(let n=0;n<e.length;n+=1)t=Math.imul(t,33)^e.charCodeAt(n);return t}function b(e){let t=-2128831035;for(let n=0;n<e.length;n+=1)t=Math.imul(t^e.charCodeAt(n),16777619);return t}function x(e,t){return Math.imul(e,31)+t}function S(e,t,n=()=>!0){if(t<=0||e.length<2)return e;e.sort(T);for(let r=!0;r;){r=!1;let i=C(e,t);for(let a=0;a<e.length&&!r;a+=1)for(let o of i[a]??[]){let i=e[a],s=e[o];if(!i||!s)continue;let c=E(i,s,t,n),l=c??E(s,i,t,n);if(!l)continue;let u=c?l.firstReplaced:l.secondReplaced,d=c?l.secondReplaced:l.firstReplaced;u&&d?(e[a]=l.merged,e.splice(o,1)):d?e[o]=l.merged:u?e[a]=l.merged:e.push(l.merged);for(let e of l.pairedRetained)e.spanCountedElsewhere=!0;e.sort(T),r=!0;break}}return e}function C(e,t){let n=[];for(let[t,r]of e.entries())for(let e of r)n.push({startTokenIndex:e.startTokenIndex,groupIndex:t});n.sort((e,t)=>e.startTokenIndex-t.startTokenIndex);let r=e.map(()=>/* @__PURE__ */ new Set);for(let[i,a]of e.entries())for(let e of a)for(let a=w(n,e.endTokenIndex);a<n.length&&(n[a]?.startTokenIndex??0)<=e.endTokenIndex+t;a+=1){let e=n[a]?.groupIndex??i;e!==i&&r[Math.min(e,i)]?.add(Math.max(e,i))}return r.map(e=>[...e].toSorted((e,t)=>e-t))}function w(e,t){let n=0,r=e.length;for(;n<r;){let i=n+r>>>1;(e[i]?.startTokenIndex??0)<t?n=i+1:r=i}return n}function T(e,t){let n=e[0],r=t[0];return(n?.startTokenIndex??0)-(r?.startTokenIndex??0)||(n?.endTokenIndex??0)-(r?.endTokenIndex??0)}function E(e,t,n,r){let[i,a]=D(e),[o,s]=D(t),c=[],l=0,u=-1;for(let e of o){for(;l<i.length;){let t=i[l];if(t&&t.endTokenIndex+n<e.startTokenIndex)l+=1;else break}let t=i[l];t&&t.endTokenIndex<=e.startTokenIndex&&t.startTokenIndex>=u&&(c.push([t,e]),u=e.endTokenIndex,l+=1)}let d=c.length===a,f=c.length===s;if(c.length<2||!d&&!f)return;let p=d&&!e.some(e=>e.nestedInLargerGroup),m=f&&!t.some(e=>e.nestedInLargerGroup),h=c.map(([e,t])=>({...e,spanCountedElsewhere:void 0,nestedInLargerGroup:void 0,segments:[...e.segments,...t.segments],tokenCount:e.tokenCount+t.tokenCount,endTokenIndex:t.endTokenIndex,endIndex:t.endIndex,endLine:t.endLine}));if(r(h))return{merged:h,firstReplaced:p,secondReplaced:m,pairedRetained:[...p?[]:c.map(([e])=>e),...m?[]:c.map(([,e])=>e)]}}function D(e){let t=[],n=0;for(let r of e)r.nestedInLargerGroup||(n+=1),!r.spanCountedElsewhere&&!r.nestedInLargerGroup&&t.push(r);return[t,n]}function O(e){let t=0,n=0,r=!1;for(let i of e){if(i.spanCountedElsewhere){r=!0;continue}t+=i.segments.length,n=Math.max(n,i.segments.length)}return r?t:t-n}function k(e,t,n,r){for(let i=e.startTokenIndex;i<e.endTokenIndex;i+=1){let e=t[i];for(let t=e?.startRow??0;t<=(e?.endRow??-1);t+=1)(!n||n.has(t+1))&&r.add(t+1)}}exports.buildLiteralCountPrefix=i,exports.collectSegmentLines=k,exports.collectSequenceWindowCandidates=a,exports.countRedundantFragments=O,exports.createLcsLengthCounter=l,exports.defaultDuplicationOptions=e,exports.lcsLength=c,exports.mergeAdjacentGroups=S,exports.resolveDuplicationOptions=t;
1
+ "use strict";let e=require("@willbooster/shared-lib");const t={minTokens:40,maxGapTokens:30,minSimilarityPercent:70};function n(e){return{minTokens:r(e?.minTokens,t.minTokens),maxGapTokens:r(e?.maxGapTokens,t.maxGapTokens),minSimilarityPercent:r(e?.minSimilarityPercent,t.minSimilarityPercent)}}function r(t,n){return t===void 0||Number.isNaN(t)?n:(0,e.clamp)(Math.trunc(t),0,4294967295)}function i(e,t){return e*5>=t}function a(e){let t=new Int32Array(e.length+1);for(let[n,r]of e.entries())t[n+1]=(t[n]??0)+(r.literalHash===void 0?0:1);return t}function o(e,t,n){let r=[],i=[],a=[];for(let[t,n]of e.entries())for(let e of n.containers)i.push(t),a.push(e);let o=t=>e[i[t]??0],l=/* @__PURE__ */ new Map,u=a.map((e,n)=>c(o(n)?.tokens??[],e,t));for(let[e,t]of u.entries()){let n=i[e]??0;for(let[r,i]of t.windowKeysByStart.entries())for(let t of i){if(t===void 0)continue;let i=l.get(t);i?(i.count+=1,i.containerIndex!==e&&(i.containerIndex=-1),i.contextIndex!==n&&(i.contextIndex=-1),i.minStart=Math.min(i.minStart,r),i.maxStart=Math.max(i.maxStart,r)):l.set(t,{count:1,containerIndex:e,contextIndex:n,minStart:r,maxStart:r})}}let d=(e,t)=>{if(e===void 0)return!1;let r=l.get(e);return r===void 0||r.count<2?!1:n?r.contextIndex===-1:r.containerIndex===-1||r.maxStart-r.minStart>=t},p=e=>{let t=u[e.containerIndex]?.statementHashes??[],n=t[e.start];for(let r=e.start+1;r<e.start+e.length;r+=1)if(t[r]!==n)return!0;return!1},m=[];for(let[e,t]of u.entries())for(let[n,r]of t.windowKeysByStart.entries())for(let[i,a]of r.entries()){if(!d(a,i)||!p({containerIndex:e,start:n,length:i}))continue;let r=t.windowKeysByStart[n]?.[i+1],o=t.windowKeysByStart[n-1]?.[i+1];d(r,i+1)||d(o,i+1)||m.push({containerIndex:e,start:n,length:i})}let h=new Set(m.map(s)),g=m;for(;g.length>0;){let e=[];for(let t of g){let n=a[t.containerIndex],s=n?.[t.start],c=n?.[t.start+t.length-1],l=o(t.containerIndex);if(!s||!c||!l)continue;let u=`s:${_(l.tokens,l.literalCountPrefix,s.startTokenIndex,c.endTokenIndex)}`;r.push({candidate:f(u,s.startTokenIndex,c.endTokenIndex,s,c),contextIndex:i[t.containerIndex]??0}),e.push(t)}g=[];for(let t of e)for(let e of[t.start,t.start+1]){let n={containerIndex:t.containerIndex,start:e,length:t.length-1},r=u[t.containerIndex]?.windowKeysByStart[e]?.[n.length];!h.has(s(n))&&d(r,n.length)&&p(n)&&(h.add(s(n)),g.push(n))}}return r}function s(e){return`${e.containerIndex}:${e.start}:${e.length}`}function c(e,t,n){let r=t.map(t=>v(e,t.startTokenIndex,t.endTokenIndex)),i=[];for(let e=0;e<t.length;e+=1){let a=[],o=5381,s=0,c=Math.min(t.length,e+100);for(let i=e;i<c;i+=1){let c=t[i],l=r[i];if(!c||l===void 0)break;o=S(o,l),s+=c.endTokenIndex-c.startTokenIndex;let u=i-e+1;a[u]=u>=2&&s>=n?S(o,u):void 0}i.push(a)}return{windowKeysByStart:i,statementHashes:r}}function l(e,t){return u(e)(t)}function u(e){let t=e.length+31>>>5,n=/* @__PURE__ */ new Map;for(let[r,i]of e.entries()){let e=n.get(i);e||(e=new Uint32Array(t),n.set(i,e));let a=r>>>5;e[a]=(e[a]??0)|1<<(r&31)}let r=new Uint32Array(t);return e=>{r.fill(0);for(let i of e){let e=n.get(i),a=1,o=0;for(let n=0;n<t;n+=1){let t=r[n]??0,i=((e?.[n]??0)|t)>>>0,s=(t<<1|a)>>>0;a=t>>>31;let c=i-s-o;o=+(c<0),r[n]=i&~c}}let i=0;for(let e of r)i+=d(e);return i}}function d(e){let t=e-(e>>>1&1431655765);return t=(t&858993459)+(t>>>2&858993459),Math.imul(t+(t>>>4)&252645135,16843009)>>>24&255}function f(e,t,n,r,i){return{fingerprint:e,tokenCount:n-t,startTokenIndex:t,endTokenIndex:n,startIndex:r.startIndex,endIndex:i.endIndex,startLine:r.startLine,endLine:i.endLine}}const p=[],m=[];function h(e){let t=p[e];return t===void 0&&(t=b(`$${e}`),p[e]=t),t}function g(e){let t=m[e];return t===void 0&&(t=x(`$${e}`),m[e]=t),t}function _(e,t,n,r){let[a,o]=y(e,n,r,i((t[r]??0)-(t[n]??0),r-n));return`${a}:${o}:${r-n}`}function v(e,t,n){let[r,i]=y(e,t,n,!1);return r^Math.imul(i,31)}function y(e,t,n,r){let i=/* @__PURE__ */ new Map,a=5381,o=52711;for(let s=t;s<n;s+=1){let t=e[s];if(!t)continue;let n,c;if(t.kind===`id`){let e=i.get(t.text);e===void 0&&(e=i.size,i.set(t.text,e)),n=h(e),c=g(e)}else n=t.textHash,c=t.textHash2;a=Math.imul(a,31)+n|0,o=Math.imul(o,37)^c,r&&t.literalHash!==void 0&&t.literalHash2!==void 0&&(a=Math.imul(a,31)+t.literalHash|0,o=Math.imul(o,37)^t.literalHash2)}return[a,o]}function b(e){let t=5381;for(let n=0;n<e.length;n+=1)t=Math.imul(t,33)^e.charCodeAt(n);return t}function x(e){let t=-2128831035;for(let n=0;n<e.length;n+=1)t=Math.imul(t^e.charCodeAt(n),16777619);return t}function S(e,t){return Math.imul(e,31)+t}function C(e,t,n=()=>!0){if(t<=0||e.length<2)return e;e.sort(E);for(let r=!0;r;){r=!1;let i=w(e,t);for(let a=0;a<e.length&&!r;a+=1)for(let o of i[a]??[]){let i=e[a],s=e[o];if(!i||!s)continue;let c=D(i,s,t,n),l=c??D(s,i,t,n);if(!l)continue;let u=c?l.firstReplaced:l.secondReplaced,d=c?l.secondReplaced:l.firstReplaced;u&&d?(e[a]=l.merged,e.splice(o,1)):d?e[o]=l.merged:u?e[a]=l.merged:e.push(l.merged);for(let e of l.pairedRetained)e.spanCountedElsewhere=!0;e.sort(E),r=!0;break}}return e}function w(e,t){let n=[];for(let[t,r]of e.entries())for(let e of r)n.push({startTokenIndex:e.startTokenIndex,groupIndex:t});n.sort((e,t)=>e.startTokenIndex-t.startTokenIndex);let r=e.map(()=>/* @__PURE__ */ new Set);for(let[i,a]of e.entries())for(let e of a)for(let a=T(n,e.endTokenIndex);a<n.length&&(n[a]?.startTokenIndex??0)<=e.endTokenIndex+t;a+=1){let e=n[a]?.groupIndex??i;e!==i&&r[Math.min(e,i)]?.add(Math.max(e,i))}return r.map(e=>[...e].toSorted((e,t)=>e-t))}function T(e,t){let n=0,r=e.length;for(;n<r;){let i=n+r>>>1;(e[i]?.startTokenIndex??0)<t?n=i+1:r=i}return n}function E(e,t){let n=e[0],r=t[0];return(n?.startTokenIndex??0)-(r?.startTokenIndex??0)||(n?.endTokenIndex??0)-(r?.endTokenIndex??0)}function D(e,t,n,r){let[i,a]=O(e),[o,s]=O(t),c=[],l=0,u=-1;for(let e of o){for(;l<i.length;){let t=i[l];if(t&&t.endTokenIndex+n<e.startTokenIndex)l+=1;else break}let t=i[l];t&&t.endTokenIndex<=e.startTokenIndex&&t.startTokenIndex>=u&&(c.push([t,e]),u=e.endTokenIndex,l+=1)}let d=c.length===a,f=c.length===s;if(c.length<2||!d&&!f)return;let p=d&&!e.some(e=>e.nestedInLargerGroup),m=f&&!t.some(e=>e.nestedInLargerGroup),h=c.map(([e,t])=>({...e,spanCountedElsewhere:void 0,nestedInLargerGroup:void 0,segments:[...e.segments,...t.segments],tokenCount:e.tokenCount+t.tokenCount,endTokenIndex:t.endTokenIndex,endIndex:t.endIndex,endLine:t.endLine}));if(r(h))return{merged:h,firstReplaced:p,secondReplaced:m,pairedRetained:[...p?[]:c.map(([e])=>e),...m?[]:c.map(([,e])=>e)]}}function O(e){let t=[],n=0;for(let r of e)r.nestedInLargerGroup||(n+=1),!r.spanCountedElsewhere&&!r.nestedInLargerGroup&&t.push(r);return[t,n]}function k(e){let t=0,n=0,r=!1;for(let i of e){if(i.spanCountedElsewhere){r=!0;continue}t+=i.segments.length,n=Math.max(n,i.segments.length)}return r?t:t-n}function A(e,t,n,r){for(let i=e.startTokenIndex;i<e.endTokenIndex;i+=1){let e=t[i];for(let t=e?.startRow??0;t<=(e?.endRow??-1);t+=1)(!n||n.has(t+1))&&r.add(t+1)}}exports.buildLiteralCountPrefix=a,exports.collectSegmentLines=A,exports.collectSequenceWindowCandidates=o,exports.countRedundantFragments=k,exports.createLcsLengthCounter=u,exports.defaultDuplicationOptions=t,exports.lcsLength=l,exports.mergeAdjacentGroups=C,exports.resolveDuplicationOptions=n;
2
2
  //# sourceMappingURL=duplication.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"duplication.cjs","names":[],"sources":["../src/duplication.ts"],"sourcesContent":["import type { DuplicationOptions } from './types.js';\n\n/**\n * Project-level duplication machinery operating on normalized token streams. Tokenization itself\n * (parsing, identifier anonymization, literal normalization) happens in the Rust addon, which\n * serializes each file's Token stream and statement structure; the helpers here match statement\n * windows across files, merge gap-adjacent groups, and count duplicated lines over that data.\n */\n\nexport const defaultDuplicationOptions: Required<DuplicationOptions> = {\n minTokens: 40,\n maxGapTokens: 30,\n minSimilarityPercent: 70,\n};\n\n/**\n * Fills defaults for absent settings, applying the same normalization as the native boundary's\n * clampToU32 — NaN (e.g. `Number(unsetEnvVariable)`) counts as absent, and other values truncate\n * and clamp to [0, u32::MAX] — so the TypeScript half of cross-file matching cannot diverge from\n * the natively collected candidates on such input.\n */\nexport function resolveDuplicationOptions(options?: DuplicationOptions): Required<DuplicationOptions> {\n return {\n minTokens: resolveOption(options?.minTokens, defaultDuplicationOptions.minTokens),\n maxGapTokens: resolveOption(options?.maxGapTokens, defaultDuplicationOptions.maxGapTokens),\n minSimilarityPercent: resolveOption(options?.minSimilarityPercent, defaultDuplicationOptions.minSimilarityPercent),\n };\n}\n\nfunction resolveOption(value: number | undefined, fallback: number): number {\n return value === undefined || Number.isNaN(value)\n ? fallback\n : Math.min(Math.max(Math.trunc(value), 0), 0xFF_FF_FF_FF);\n}\n\n/** Minimum consecutive statements for a statement-sequence duplicate candidate. */\nconst minSequenceStatementCount = 2;\n/**\n * Caps the window length so statement-sequence enumeration stays linear in the statement count.\n * Heterogeneous clones longer than the cap are reported as capped windows (a deliberate\n * conservative undercount trading completeness for bounded discovery cost).\n */\nconst maxSequenceStatementCount = 100;\n\n/**\n * A region whose normalized tokens are at least 20% literal values is data-like (a lookup table, a\n * constant list, a value-mapping switch), not logic: literal values re-enter its fingerprint so\n * tables that merely share their shape stop counting as copy-paste. Compared in integer math\n * (5 * literals >= total) so the TypeScript and native sides cannot disagree on the boundary.\n */\nfunction isLiteralDense(literalCount: number, tokenCount: number): boolean {\n return literalCount * 5 >= tokenCount;\n}\n\nexport interface Token {\n /** Normalization target: identifiers to anonymize, literal kind tags, or the raw token text. */\n kind: 'id' | 'text';\n text: string;\n /**\n * Two INDEPENDENT hashes of `text` (djb2 and FNV-1a), precomputed so fingerprinting nested\n * regions never re-hashes a token. Feeding the same per-token hash to both fingerprint\n * accumulators would collapse the key to 32 effective bits: one djb2 collision between two\n * token texts would then equate whole regions.\n */\n textHash: number;\n textHash2: number;\n /** Hash pair of a value-carrying literal's value, folded into data-like region fingerprints. */\n literalHash?: number;\n literalHash2?: number;\n /**\n * True for verbatim-kept NAMES (member/callee/type names, named grammar leaves): together with\n * value-carrying literals these are the content-bearing tokens the near-miss content gate\n * counts. Keywords, operators, and punctuation come from unnamed nodes and stay false.\n */\n isName?: boolean;\n /** 0-based source rows the token occupies, so line coverage counts only matched-token lines. */\n startRow: number;\n endRow: number;\n}\n\n/** A token span with its source position; plain data so cross-file matching can retain it. */\nexport interface TokenRange {\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/** A contiguous run of matched tokens; gapped (merged) duplicates carry several per occurrence. */\ninterface TokenSegment {\n startTokenIndex: number;\n endTokenIndex: number;\n}\n\ninterface DuplicateCandidate {\n fingerprint: string;\n tokenCount: number;\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\nexport interface CountedOccurrence {\n /** Matched token runs; more than one once gapped groups are merged. */\n segments: TokenSegment[];\n /**\n * Set on occurrences whose span another reported group already counts: a retained group's\n * occurrences that a partial gapped merge also paired into a merged group, and cross-file copies\n * nested inside a larger group's region. Block counting must not count them again.\n */\n spanCountedElsewhere?: boolean;\n /**\n * Set on cross-file copies nested inside a larger group's region (they also set\n * `spanCountedElsewhere`). They never pair in gapped merging, and they do not keep their group's\n * standalone copies from merging, which they are not copies of. They do keep the merged group\n * from taking their group's place, since only the original group reports the nesting.\n */\n nestedInLargerGroup?: boolean;\n /** Sum of segment token counts (the gap tokens are not matched content). */\n tokenCount: number;\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/** A duplicate region found in one file, exported for cross-file matching by fingerprint. */\nexport interface CrossFileDuplicateCandidate {\n /** Content key: equal fingerprints mean equal normalized token sequences (up to hash collision). */\n fingerprint: string;\n tokenCount: number;\n /** Token positions within the owning file, for cross-file gapped (Type-3) merging. */\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/**\n * One file's contribution to cross-file clone detection: catalogued candidates plus the normalized\n * token stream and statement structure, so the project-level pass can match partial statement runs\n * (windows that a single file cannot know repeat elsewhere) and merge gap-adjacent groups.\n */\nexport interface CrossFileDuplicationFileData {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n /**\n * The mutually disjoint blocks (at least `minTokens` long, not literal-dense) compared for\n * cross-file near-miss (Type-3) clones. Optional for backward compatibility; without it, the\n * file takes part in exact and gapped cross-file matching only.\n */\n nearMissBlocks?: TokenRange[];\n /**\n * 1-based lines that are neither blank nor comment-only, so cross-file line coverage counts only\n * code lines (blank rows inside multi-row tokens such as template literals carry no content).\n * Optional for backward compatibility; without it, every matched-token row counts.\n */\n codeLineNumbers?: Set<number>;\n}\n\n/** literalCountPrefix[i] = value-carrying literal tokens in tokens[0..i), for O(1) density checks. */\nexport function buildLiteralCountPrefix(tokens: Token[]): Int32Array {\n const prefix = new Int32Array(tokens.length + 1);\n for (const [index, token] of tokens.entries()) {\n prefix[index + 1] = (prefix[index] ?? 0) + (token.literalHash === undefined ? 0 : 1);\n }\n return prefix;\n}\n\ninterface WindowOccurrences {\n count: number;\n /** -1 once occurrences span more than one container. */\n containerIndex: number;\n /** -1 once occurrences span more than one context (file). */\n contextIndex: number;\n minStart: number;\n maxStart: number;\n}\n\ninterface SequenceWindow {\n containerIndex: number;\n start: number;\n length: number;\n}\n\n/** One file's token stream and statement containers, as a window-matching context. */\nexport interface SequenceWindowContext {\n tokens: Token[];\n literalCountPrefix: Int32Array;\n containers: TokenRange[][];\n}\n\nexport interface ContextualSequenceCandidate {\n candidate: DuplicateCandidate;\n contextIndex: number;\n}\n\n/**\n * Enumerates runs of consecutive sibling statements over one or more contexts (files). Every\n * container statement participates; only the window length is capped, so enumeration stays linear\n * in the statement count. Windows are grouped by a cheap rolling hash of per-statement\n * fingerprints, and only locally maximal repeated windows — those whose one-statement extensions\n * stop repeating — become candidates with an exact (window-consistent) fingerprint. Without the\n * maximality filter a degenerate file of near-identical statements would fingerprint every\n * sub-window of every repeated region. With `requireMultipleContexts` a window only counts as\n * repeated when its occurrences span at least two contexts (CPD-style cross-file matching): a\n * repeat confined to one file is that file's own concern, and emitting it here would flood the\n * project-level selection with unusable single-file groups.\n */\nexport function collectSequenceWindowCandidates(\n contexts: SequenceWindowContext[],\n minTokens: number,\n requireMultipleContexts: boolean\n): ContextualSequenceCandidate[] {\n const candidates: ContextualSequenceCandidate[] = [];\n const contextIndexByContainer: number[] = [];\n const containers: TokenRange[][] = [];\n for (const [contextIndex, context] of contexts.entries()) {\n for (const statements of context.containers) {\n contextIndexByContainer.push(contextIndex);\n containers.push(statements);\n }\n }\n const contextAt = (containerIndex: number): SequenceWindowContext | undefined =>\n contexts[contextIndexByContainer[containerIndex] ?? 0];\n const occurrencesByWindowKey = new Map<number, WindowOccurrences>();\n const containerWindows = containers.map((statements, containerIndex) =>\n enumerateContainerWindows(contextAt(containerIndex)?.tokens ?? [], statements, minTokens)\n );\n for (const [containerIndex, windows] of containerWindows.entries()) {\n const contextIndex = contextIndexByContainer[containerIndex] ?? 0;\n for (const [start, row] of windows.windowKeysByStart.entries()) {\n for (const windowKey of row) {\n if (windowKey === undefined) {\n continue;\n }\n const occurrences = occurrencesByWindowKey.get(windowKey);\n if (occurrences) {\n occurrences.count += 1;\n if (occurrences.containerIndex !== containerIndex) {\n occurrences.containerIndex = -1;\n }\n if (occurrences.contextIndex !== contextIndex) {\n occurrences.contextIndex = -1;\n }\n occurrences.minStart = Math.min(occurrences.minStart, start);\n occurrences.maxStart = Math.max(occurrences.maxStart, start);\n } else {\n occurrencesByWindowKey.set(windowKey, {\n count: 1,\n containerIndex,\n contextIndex,\n minStart: start,\n maxStart: start,\n });\n }\n }\n }\n }\n\n // A window only \"repeats\" when two of its occurrences can coexist without overlapping: sliding\n // matches inside a homogeneous run (start spread smaller than the window length) can never both\n // be counted and must neither qualify a window nor dominate its sub-windows. Cross-context\n // matching instead requires occurrences in two contexts, which coexist by construction.\n const repeats = (windowKey: number | undefined, length: number): boolean => {\n if (windowKey === undefined) {\n return false;\n }\n const occurrences = occurrencesByWindowKey.get(windowKey);\n if (occurrences === undefined || occurrences.count < 2) {\n return false;\n }\n if (requireMultipleContexts) {\n return occurrences.contextIndex === -1;\n }\n return occurrences.containerIndex === -1 || occurrences.maxStart - occurrences.minStart >= length;\n };\n\n // A window whose statements all share one normalized shape (sixteen `let x = 0;` declarations,\n // a constant table) is a homogeneous preamble, not a copy-paste: requiring two distinct\n // per-statement shapes keeps such runs out of duplicate groups and the duplication ratio.\n const hasDistinctStatements = (window: SequenceWindow): boolean => {\n const hashes = containerWindows[window.containerIndex]?.statementHashes ?? [];\n const firstHash = hashes[window.start];\n for (let index = window.start + 1; index < window.start + window.length; index += 1) {\n if (hashes[index] !== firstHash) {\n return true;\n }\n }\n return false;\n };\n\n const maximalWindows: SequenceWindow[] = [];\n for (const [containerIndex, windows] of containerWindows.entries()) {\n for (const [start, row] of windows.windowKeysByStart.entries()) {\n for (const [length, windowKey] of row.entries()) {\n if (!repeats(windowKey, length) || !hasDistinctStatements({ containerIndex, start, length })) {\n continue;\n }\n // Dominated windows are skipped: the one-statement extension also repeats, so a larger\n // candidate covering this window exists.\n const extendedRight = windows.windowKeysByStart[start]?.[length + 1];\n const extendedLeft = windows.windowKeysByStart[start - 1]?.[length + 1];\n if (repeats(extendedRight, length + 1) || repeats(extendedLeft, length + 1)) {\n continue;\n }\n maximalWindows.push({ containerIndex, start, length });\n }\n }\n }\n\n // The rolling hash anonymizes identifiers per statement, so a window can look repeated coarsely\n // while its exact (window-consistent) fingerprints differ, and a longer window's match can\n // dominate sub-windows that other copies still need (three copies where only two extend one\n // statement further). Every emitted window therefore exposes its repeating, unvisited\n // sub-windows; `visited` bounds the worklist and lengths strictly decrease, so it terminates.\n const visited = new Set(maximalWindows.map(windowId));\n let frontier = maximalWindows;\n while (frontier.length > 0) {\n const emitted: SequenceWindow[] = [];\n for (const window of frontier) {\n const statements = containers[window.containerIndex];\n const first = statements?.[window.start];\n const last = statements?.[window.start + window.length - 1];\n const context = contextAt(window.containerIndex);\n if (!first || !last || !context) {\n continue;\n }\n const fingerprint = `s:${fingerprintKey(context.tokens, context.literalCountPrefix, first.startTokenIndex, last.endTokenIndex)}`;\n candidates.push({\n candidate: toCandidate(fingerprint, first.startTokenIndex, last.endTokenIndex, first, last),\n contextIndex: contextIndexByContainer[window.containerIndex] ?? 0,\n });\n emitted.push(window);\n }\n frontier = [];\n for (const window of emitted) {\n for (const start of [window.start, window.start + 1]) {\n const subWindow = { containerIndex: window.containerIndex, start, length: window.length - 1 };\n const subWindowKey = containerWindows[window.containerIndex]?.windowKeysByStart[start]?.[subWindow.length];\n if (\n visited.has(windowId(subWindow)) ||\n !repeats(subWindowKey, subWindow.length) ||\n !hasDistinctStatements(subWindow)\n ) {\n continue;\n }\n visited.add(windowId(subWindow));\n frontier.push(subWindow);\n }\n }\n }\n return candidates;\n}\n\nfunction windowId(window: SequenceWindow): string {\n return `${window.containerIndex}:${window.start}:${window.length}`;\n}\n\ninterface ContainerWindows {\n /** windowKeysByStart[start][length] is the rolling-hash key of the window, or undefined if it is below the size thresholds. */\n windowKeysByStart: (number | undefined)[][];\n /** Per-statement fingerprint hashes, for the distinct-shape requirement on windows. */\n statementHashes: number[];\n}\n\nfunction enumerateContainerWindows(tokens: Token[], statements: TokenRange[], minTokens: number): ContainerWindows {\n const statementHashes = statements.map((statement) =>\n fingerprintHash(tokens, statement.startTokenIndex, statement.endTokenIndex)\n );\n const windowKeysByStart: (number | undefined)[][] = [];\n for (let start = 0; start < statements.length; start += 1) {\n const row: (number | undefined)[] = [];\n let hash = 5381;\n let tokenCount = 0;\n const maxEnd = Math.min(statements.length, start + maxSequenceStatementCount);\n for (let end = start; end < maxEnd; end += 1) {\n const statement = statements[end];\n const statementHash = statementHashes[end];\n if (!statement || statementHash === undefined) {\n break;\n }\n hash = combineHashes(hash, statementHash);\n tokenCount += statement.endTokenIndex - statement.startTokenIndex;\n const statementCount = end - start + 1;\n row[statementCount] =\n statementCount >= minSequenceStatementCount && tokenCount >= minTokens\n ? combineHashes(hash, statementCount)\n : undefined;\n }\n windowKeysByStart.push(row);\n }\n return { windowKeysByStart, statementHashes };\n}\n\n/**\n * Longest-common-subsequence LENGTH of two symbol sequences via the Allison–Dix bit-parallel\n * recurrence (O(|a|/32 · |b|) words): per symbol of `b`, `x = match | v` and\n * `v = x & ~(x - ((v << 1) | 1))` over multi-word bit vectors; the set bits of `v` count the LCS.\n */\nexport function lcsLength(a: Int32Array, b: Int32Array): number {\n return createLcsLengthCounter(a)(b);\n}\n\n/**\n * lcsLength against a fixed `a`, whose per-symbol position masks are built once and reused for\n * every `b` compared with it.\n */\nexport function createLcsLengthCounter(a: Int32Array): (b: Int32Array) => number {\n const wordCount = (a.length + 31) >>> 5;\n const positionMasks = new Map<number, Uint32Array>();\n for (const [index, symbol] of a.entries()) {\n let mask = positionMasks.get(symbol);\n if (!mask) {\n mask = new Uint32Array(wordCount);\n positionMasks.set(symbol, mask);\n }\n const word = index >>> 5;\n // The Uint32Array store wraps the signed int32 bit pattern to unsigned. The `?? 0` guards in\n // this function are required by noUncheckedIndexedAccess (typed-array reads type as\n // `number | undefined`), not redundancy: every index is in bounds.\n mask[word] = (mask[word] ?? 0) | (1 << (index & 31));\n }\n\n const v = new Uint32Array(wordCount);\n return (b) => {\n v.fill(0);\n for (const symbol of b) {\n const matchMask = positionMasks.get(symbol);\n // `(v << 1) | 1` shifts a carry bit across words; subtraction borrows across words.\n let shiftCarry = 1;\n let borrow = 0;\n for (let word = 0; word < wordCount; word += 1) {\n const previous = v[word] ?? 0;\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `>>> 0` reinterprets the signed int32 bit pattern as unsigned so the borrow subtraction below compares magnitudes; Math.trunc would keep it negative.\n const x = ((matchMask?.[word] ?? 0) | previous) >>> 0;\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- same unsigned reinterpretation as `x`.\n const shifted = ((previous << 1) | shiftCarry) >>> 0;\n shiftCarry = previous >>> 31;\n const difference = x - shifted - borrow;\n borrow = difference < 0 ? 1 : 0;\n // The Uint32Array store wraps the signed int32 bit pattern to unsigned.\n v[word] = x & ~difference;\n }\n }\n\n let length = 0;\n for (const word of v) {\n length += popCount(word);\n }\n return length;\n };\n}\n\nfunction popCount(value: number): number {\n let count = value - ((value >>> 1) & 0x55_55_55_55);\n count = (count & 0x33_33_33_33) + ((count >>> 2) & 0x33_33_33_33);\n return (Math.imul((count + (count >>> 4)) & 0x0F_0F_0F_0F, 0x01_01_01_01) >>> 24) & 0xFF;\n}\n\nfunction toCandidate(\n fingerprint: string,\n startTokenIndex: number,\n endTokenIndex: number,\n first: TokenRange,\n last: TokenRange\n): DuplicateCandidate {\n return {\n fingerprint,\n tokenCount: endTokenIndex - startTokenIndex,\n startTokenIndex,\n endTokenIndex,\n startIndex: first.startIndex,\n endIndex: last.endIndex,\n startLine: first.startLine,\n endLine: last.endLine,\n };\n}\n\n/** Caches of hashText/hashText2 over '$0', '$1', ... so anonymized identifiers hash without allocating. */\nconst anonymizedIndexHashes: number[] = [];\nconst anonymizedIndexHashes2: number[] = [];\n\nfunction anonymizedIndexHash(index: number): number {\n let hash = anonymizedIndexHashes[index];\n if (hash === undefined) {\n hash = hashText(`$${index}`);\n anonymizedIndexHashes[index] = hash;\n }\n return hash;\n}\n\nfunction anonymizedIndexHash2(index: number): number {\n let hash = anonymizedIndexHashes2[index];\n if (hash === undefined) {\n hash = hashText2(`$${index}`);\n anonymizedIndexHashes2[index] = hash;\n }\n return hash;\n}\n\n/**\n * Content key of a token range: two independent 32-bit hashes over the normalized token sequence\n * (identifiers anonymized consistently by first-occurrence order) plus the token count. Regions\n * with equal keys are treated as equal content; a collision would need both 32-bit hashes and the\n * length to coincide, which is negligible for a metrics report. The format and arithmetic match\n * fingerprint_key in native/src/duplication.rs exactly, so window candidates fingerprinted here\n * group together with the per-file candidates the addon catalogues.\n */\nfunction fingerprintKey(\n tokens: Token[],\n literalCountPrefix: Int32Array,\n startTokenIndex: number,\n endTokenIndex: number\n): string {\n const literalCount = (literalCountPrefix[endTokenIndex] ?? 0) - (literalCountPrefix[startTokenIndex] ?? 0);\n const literalDense = isLiteralDense(literalCount, endTokenIndex - startTokenIndex);\n const [primary, secondary] = fingerprintHashPair(tokens, startTokenIndex, endTokenIndex, literalDense);\n return `${primary}:${secondary}:${endTokenIndex - startTokenIndex}`;\n}\n\n/**\n * A single 32-bit summary of a range for the coarse rolling-hash phase. Deliberately\n * density-agnostic: density is a property of the final candidate REGION, and folding literal\n * values into per-statement hashes would make a dense statement inside a logic-heavy window\n * (`const weights = [1, 2, 3];`) block the window from ever being enumerated. The coarse phase\n * over-approximates on shape alone; the exact region fingerprint still applies the density rule.\n */\nfunction fingerprintHash(tokens: Token[], startTokenIndex: number, endTokenIndex: number): number {\n const [primary, secondary] = fingerprintHashPair(tokens, startTokenIndex, endTokenIndex, false);\n // XOR already coerces to int32, matching the native side's i32 arithmetic.\n return primary ^ Math.imul(secondary, 31);\n}\n\nfunction fingerprintHashPair(\n tokens: Token[],\n startTokenIndex: number,\n endTokenIndex: number,\n foldLiteralValues: boolean\n): [number, number] {\n const indexByIdentifier = new Map<string, number>();\n let primary = 5381;\n let secondary = 52_711;\n for (let index = startTokenIndex; index < endTokenIndex; index += 1) {\n const token = tokens[index];\n if (!token) {\n continue;\n }\n // Each accumulator consumes its own independent per-token hash: sharing one would collapse\n // the key to 32 effective bits (a single djb2 collision would equate whole regions).\n let part: number;\n let part2: number;\n if (token.kind === 'id') {\n let identifierIndex = indexByIdentifier.get(token.text);\n if (identifierIndex === undefined) {\n identifierIndex = indexByIdentifier.size;\n indexByIdentifier.set(token.text, identifierIndex);\n }\n part = anonymizedIndexHash(identifierIndex);\n part2 = anonymizedIndexHash2(identifierIndex);\n } else {\n part = token.textHash;\n part2 = token.textHash2;\n }\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `| 0` wraps the sum to int32 (Math.trunc does not), which must match the native side's wrapping i32 arithmetic.\n primary = (Math.imul(primary, 31) + part) | 0;\n secondary = Math.imul(secondary, 37) ^ part2;\n if (foldLiteralValues && token.literalHash !== undefined && token.literalHash2 !== undefined) {\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `| 0` wraps the sum to int32 (Math.trunc does not), which must match the native side's wrapping i32 arithmetic.\n primary = (Math.imul(primary, 31) + token.literalHash) | 0;\n secondary = Math.imul(secondary, 37) ^ token.literalHash2;\n }\n }\n return [primary, secondary];\n}\n\n/** djb2-style hash; XOR keeps the value in signed 32-bit range, which is fine for a grouping key. */\nfunction hashText(text: string): number {\n let hash = 5381;\n for (let index = 0; index < text.length; index += 1) {\n // oxlint-disable-next-line unicorn/prefer-code-point -- djb2 hashes UTF-16 code units; codePointAt would hash surrogate pairs twice (full code point, then the lone low surrogate).\n hash = Math.imul(hash, 33) ^ text.charCodeAt(index);\n }\n return hash;\n}\n\n/** FNV-1a over UTF-16 code units: independent of hashText so the two accumulators never share input. */\nfunction hashText2(text: string): number {\n let hash = -2_128_831_035; // 2166136261 as int32 (the FNV-1a offset basis)\n for (let index = 0; index < text.length; index += 1) {\n // oxlint-disable-next-line unicorn/prefer-code-point -- hashes UTF-16 code units like hashText.\n hash = Math.imul(hash ^ text.charCodeAt(index), 16_777_619);\n }\n return hash;\n}\n\nfunction combineHashes(hash: number, value: number): number {\n return Math.imul(hash, 31) + value;\n}\n\n/**\n * Merges duplicate groups separated by a small token gap into one gapped (Type-3) clone group: a\n * copy edited in one spot splits into two exact groups whose occurrences sit side by side in the\n * same order. Occurrences are paired greedily in source order; a merge happens when the pairing\n * fully pairs at least one group with at least two pairs. Equal-cardinality groups whose\n * occurrences all pair merge into one group as before. When cardinalities differ (a fragment also\n * occurs standalone: prefix ×3, suffix ×2), the fully-paired group is subsumed into the merged\n * gapped group while the other group is RETAINED with ALL its occurrences: dropping the leftover\n * would lose duplicated-line coverage, and reporting it alone would make a single-occurrence group\n * (contradicting duplicateBlockGroupCount's \"appears more than once\" meaning). Line coverage\n * unions ranges, so the overlap between the retained exact group and the merged group is harmless.\n * Merging repeats to a fixpoint so a clone edited in several spots still reassembles; it\n * terminates because a merge either removes the input groups it replaces or marks their paired\n * occurrences as counted elsewhere, and only unmarked occurrences of remaining groups pair, so the\n * number of pairable occurrences strictly decreases with every merge. Gap tokens are not matched\n * content: line coverage and sizes count only the matched segments. Generic so cross-file merging\n * can thread file identity through occurrences.\n */\nexport function mergeAdjacentGroups<T extends CountedOccurrence>(\n groups: T[][],\n maxGapTokens: number,\n isReportableGroup: (group: T[]) => boolean = () => true\n): T[][] {\n if (maxGapTokens <= 0 || groups.length < 2) {\n return groups;\n }\n // Deterministic processing order (mirrored by the native side): by first occurrence position.\n groups.sort(compareGroups);\n for (let restart = true; restart;) {\n restart = false;\n const partnersByGroup = collectGapAdjacentPartners(groups, maxGapTokens);\n for (let leftIndex = 0; leftIndex < groups.length && !restart; leftIndex += 1) {\n for (const rightIndex of partnersByGroup[leftIndex] ?? []) {\n const left = groups[leftIndex];\n const right = groups[rightIndex];\n if (!left || !right) {\n continue;\n }\n const forward = mergeGroups(left, right, maxGapTokens, isReportableGroup);\n const result = forward ?? mergeGroups(right, left, maxGapTokens, isReportableGroup);\n if (!result) {\n continue;\n }\n const leftReplaced = forward ? result.firstReplaced : result.secondReplaced;\n const rightReplaced = forward ? result.secondReplaced : result.firstReplaced;\n if (leftReplaced && rightReplaced) {\n groups[leftIndex] = result.merged;\n groups.splice(rightIndex, 1);\n } else if (rightReplaced) {\n groups[rightIndex] = result.merged;\n } else if (leftReplaced) {\n groups[leftIndex] = result.merged;\n } else {\n // Both groups stay (each is only partly paired, or reports nested copies of its own), so\n // the merged group joins them instead of taking a place.\n groups.push(result.merged);\n }\n // A group that stays keeps ALL its occurrences (line coverage must not shrink, and a\n // reported group must keep >= 2 occurrences), so its paired occurrences now also live\n // inside the merged group's occurrences: mark them so duplicateBlockCount counts each\n // token span once, and so the same pair cannot merge again.\n for (const occurrence of result.pairedRetained) {\n occurrence.spanCountedElsewhere = true;\n }\n groups.sort(compareGroups);\n restart = true;\n break;\n }\n }\n }\n return groups;\n}\n\n/**\n * Per group index, the ascending indexes of later groups that mergeGroups can pair with it in\n * either direction: an occurrence of one starts within `maxGapTokens` after an occurrence of the\n * other ends. Every other pair fails mergeGroups, so skipping it leaves the fixpoint unchanged\n * while each restart stops costing a comparison per pair of groups.\n */\nfunction collectGapAdjacentPartners(groups: CountedOccurrence[][], maxGapTokens: number): number[][] {\n const starts: { startTokenIndex: number; groupIndex: number }[] = [];\n for (const [groupIndex, group] of groups.entries()) {\n for (const occurrence of group) {\n starts.push({ startTokenIndex: occurrence.startTokenIndex, groupIndex });\n }\n }\n starts.sort((left, right) => left.startTokenIndex - right.startTokenIndex);\n const partners = groups.map(() => new Set<number>());\n for (const [groupIndex, group] of groups.entries()) {\n for (const occurrence of group) {\n for (\n let index = lowerBoundByStart(starts, occurrence.endTokenIndex);\n index < starts.length && (starts[index]?.startTokenIndex ?? 0) <= occurrence.endTokenIndex + maxGapTokens;\n index += 1\n ) {\n const other = starts[index]?.groupIndex ?? groupIndex;\n if (other !== groupIndex) {\n partners[Math.min(other, groupIndex)]?.add(Math.max(other, groupIndex));\n }\n }\n }\n }\n return partners.map((set) => [...set].toSorted((left, right) => left - right));\n}\n\nfunction lowerBoundByStart(sorted: { startTokenIndex: number }[], target: number): number {\n let low = 0;\n let high = sorted.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((sorted[middle]?.startTokenIndex ?? 0) < target) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low;\n}\n\nfunction compareGroups(left: CountedOccurrence[], right: CountedOccurrence[]): number {\n const leftFirst = left[0];\n const rightFirst = right[0];\n return (\n (leftFirst?.startTokenIndex ?? 0) - (rightFirst?.startTokenIndex ?? 0) ||\n (leftFirst?.endTokenIndex ?? 0) - (rightFirst?.endTokenIndex ?? 0)\n );\n}\n\ninterface MergeResult<T> {\n merged: T[];\n /**\n * Whether the merged group takes the respective input group's place: its standalone occurrences\n * were all paired and it holds no nested copies that only it can report.\n */\n firstReplaced: boolean;\n secondReplaced: boolean;\n /** The occurrences of groups that stay, which the merged group's spans now also cover. */\n pairedRetained: T[];\n}\n\n/**\n * Pairs `second` occurrences with gap-preceding `first` occurrences, greedily in source order:\n * each trailing occurrence takes the earliest unused leading occurrence within the gap, and a\n * pair's leading must start at or after the previous pair's trailing end so merged spans never\n * overlap. A merge needs at least two pairs (a merged group must still mean \"appears more than\n * once\") and must fully consume at least one group; for equal cardinalities this reduces to the\n * strict all-pairs merge, so pre-partial-merge behavior is unchanged there.\n */\nfunction mergeGroups<T extends CountedOccurrence>(\n first: T[],\n second: T[],\n maxGapTokens: number,\n isReportableGroup: (group: T[]) => boolean\n): MergeResult<T> | undefined {\n // Occurrences a previous partial merge already paired into a merged group must not pair again:\n // their spans already live inside that merged group, so re-pairing them would assemble a second,\n // competing merged group instead of letting the existing merged group extend (and would count\n // the same span twice). Consumption is still judged against the FULL group, so a group holding\n // shared occurrences is never subsumed away; nested copies, which the merged span would not\n // cover anyway, are left out of that judgment so they cannot veto a merge of the standalone\n // copies.\n const [leadings, firstLength] = pairableOccurrences(first);\n const [trailings, secondLength] = pairableOccurrences(second);\n const pairs: [T, T][] = [];\n let leadingIndex = 0;\n let previousTrailingEnd = -1;\n for (const trailing of trailings) {\n // Leadings ending too far before this trailing can never pair a later (even farther) one.\n while (leadingIndex < leadings.length) {\n const leading = leadings[leadingIndex];\n if (leading && leading.endTokenIndex + maxGapTokens < trailing.startTokenIndex) {\n leadingIndex += 1;\n } else {\n break;\n }\n }\n const leading = leadings[leadingIndex];\n if (\n leading &&\n leading.endTokenIndex <= trailing.startTokenIndex &&\n leading.startTokenIndex >= previousTrailingEnd\n ) {\n pairs.push([leading, trailing]);\n previousTrailingEnd = trailing.endTokenIndex;\n leadingIndex += 1;\n }\n }\n const firstFullyPaired = pairs.length === firstLength;\n const secondFullyPaired = pairs.length === secondLength;\n if (pairs.length < 2 || (!firstFullyPaired && !secondFullyPaired)) {\n return undefined;\n }\n // A group holding nested copies stays even when all its standalone copies pair: the merged\n // group's content is larger than what those copies matched, so only the original group can\n // report which files share the matched fragment.\n const firstReplaced = firstFullyPaired && !first.some((occurrence) => occurrence.nestedInLargerGroup);\n const secondReplaced = secondFullyPaired && !second.some((occurrence) => occurrence.nestedInLargerGroup);\n const merged = pairs.map(([leading, trailing]) => ({\n ...leading,\n // A merged occurrence is a fresh span combination; it never inherits shared-span marks.\n spanCountedElsewhere: undefined,\n nestedInLargerGroup: undefined,\n segments: [...leading.segments, ...trailing.segments],\n tokenCount: leading.tokenCount + trailing.tokenCount,\n endTokenIndex: trailing.endTokenIndex,\n endIndex: trailing.endIndex,\n endLine: trailing.endLine,\n }));\n // Only occurrences of one file pair (file offsets exceed the gap), so a merged group spans the\n // files its pairs sit in: with nested copies left out of pairing, that can be fewer files than\n // the input groups covered, and a merged group that is no longer reportable must not form.\n if (!isReportableGroup(merged)) {\n return undefined;\n }\n const pairedRetained = [\n ...(firstReplaced ? [] : pairs.map(([leading]) => leading)),\n ...(secondReplaced ? [] : pairs.map(([, trailing]) => trailing)),\n ];\n return { merged, firstReplaced, secondReplaced, pairedRetained };\n}\n\n/** One pass over a group: its pairable occurrences and its non-nested occurrence count. */\nfunction pairableOccurrences<T extends CountedOccurrence>(group: T[]): [T[], number] {\n const pairable: T[] = [];\n let length = 0;\n for (const occurrence of group) {\n if (!occurrence.nestedInLargerGroup) {\n length += 1;\n }\n // Either flag keeps an occurrence out of pairing: its span is already counted elsewhere, or it\n // is a nested copy of content the merged span would not cover.\n if (!occurrence.spanCountedElsewhere && !occurrence.nestedInLargerGroup) {\n pairable.push(occurrence);\n }\n }\n return [pairable, length];\n}\n\n/**\n * Redundant copies one group adds to duplicateBlockCount. Each redundant occurrence contributes\n * one count per matched fragment, so merging a gapped clone's fragments into one group does not\n * halve duplicateBlockCount: an edited two-fragment pair still counts 2, exactly as its unmerged\n * fragments did. Occurrence shapes can differ within one group (a\n * gap-merged exact pair plus an appended whole-block near-miss copy), so every occurrence's\n * fragments are summed and one representative — the largest — is deducted, keeping the count\n * independent of source order. Occurrences a partial gapped merge also paired into a merged group\n * are skipped: their spans are already counted there, and such a retained group deducts no\n * representative of its own — the merged group's representative already stands for the shared\n * content — so no token span contributes to the count twice.\n */\nexport function countRedundantFragments(group: CountedOccurrence[]): number {\n let fragmentCount = 0;\n let maxFragmentCount = 0;\n let hasSharedOccurrence = false;\n for (const occurrence of group) {\n if (occurrence.spanCountedElsewhere) {\n hasSharedOccurrence = true;\n continue;\n }\n fragmentCount += occurrence.segments.length;\n maxFragmentCount = Math.max(maxFragmentCount, occurrence.segments.length);\n }\n return hasSharedOccurrence ? fragmentCount : fragmentCount - maxFragmentCount;\n}\n\n/** Adds the 1-based code lines the segment's matched tokens cover; shared with cross-file coverage. */\nexport function collectSegmentLines(\n segment: { startTokenIndex: number; endTokenIndex: number },\n tokens: Token[],\n codeLineNumbers: Set<number> | undefined,\n duplicatedLines: Set<number>\n): void {\n for (let index = segment.startTokenIndex; index < segment.endTokenIndex; index += 1) {\n const token = tokens[index];\n for (let row = token?.startRow ?? 0; row <= (token?.endRow ?? -1); row += 1) {\n if (!codeLineNumbers || codeLineNumbers.has(row + 1)) {\n duplicatedLines.add(row + 1);\n }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"duplication.cjs","names":["clamp"],"sources":["../src/duplication.ts"],"sourcesContent":["import { clamp } from '@willbooster/shared-lib';\nimport type { DuplicationOptions } from './types.js';\n\n/**\n * Project-level duplication machinery operating on normalized token streams. Tokenization itself\n * (parsing, identifier anonymization, literal normalization) happens in the Rust addon, which\n * serializes each file's Token stream and statement structure; the helpers here match statement\n * windows across files, merge gap-adjacent groups, and count duplicated lines over that data.\n */\n\nexport const defaultDuplicationOptions: Required<DuplicationOptions> = {\n minTokens: 40,\n maxGapTokens: 30,\n minSimilarityPercent: 70,\n};\n\n/**\n * Fills defaults for absent settings, applying the same normalization as the native boundary's\n * clampToU32 — NaN (e.g. `Number(unsetEnvVariable)`) counts as absent, and other values truncate\n * and clamp to [0, u32::MAX] — so the TypeScript half of cross-file matching cannot diverge from\n * the natively collected candidates on such input.\n */\nexport function resolveDuplicationOptions(options?: DuplicationOptions): Required<DuplicationOptions> {\n return {\n minTokens: resolveOption(options?.minTokens, defaultDuplicationOptions.minTokens),\n maxGapTokens: resolveOption(options?.maxGapTokens, defaultDuplicationOptions.maxGapTokens),\n minSimilarityPercent: resolveOption(options?.minSimilarityPercent, defaultDuplicationOptions.minSimilarityPercent),\n };\n}\n\nfunction resolveOption(value: number | undefined, fallback: number): number {\n return value === undefined || Number.isNaN(value) ? fallback : clamp(Math.trunc(value), 0, 0xFF_FF_FF_FF);\n}\n\n/** Minimum consecutive statements for a statement-sequence duplicate candidate. */\nconst minSequenceStatementCount = 2;\n/**\n * Caps the window length so statement-sequence enumeration stays linear in the statement count.\n * Heterogeneous clones longer than the cap are reported as capped windows (a deliberate\n * conservative undercount trading completeness for bounded discovery cost).\n */\nconst maxSequenceStatementCount = 100;\n\n/**\n * A region whose normalized tokens are at least 20% literal values is data-like (a lookup table, a\n * constant list, a value-mapping switch), not logic: literal values re-enter its fingerprint so\n * tables that merely share their shape stop counting as copy-paste. Compared in integer math\n * (5 * literals >= total) so the TypeScript and native sides cannot disagree on the boundary.\n */\nfunction isLiteralDense(literalCount: number, tokenCount: number): boolean {\n return literalCount * 5 >= tokenCount;\n}\n\nexport interface Token {\n /** Normalization target: identifiers to anonymize, literal kind tags, or the raw token text. */\n kind: 'id' | 'text';\n text: string;\n /**\n * Two INDEPENDENT hashes of `text` (djb2 and FNV-1a), precomputed so fingerprinting nested\n * regions never re-hashes a token. Feeding the same per-token hash to both fingerprint\n * accumulators would collapse the key to 32 effective bits: one djb2 collision between two\n * token texts would then equate whole regions.\n */\n textHash: number;\n textHash2: number;\n /** Hash pair of a value-carrying literal's value, folded into data-like region fingerprints. */\n literalHash?: number;\n literalHash2?: number;\n /**\n * True for verbatim-kept NAMES (member/callee/type names, named grammar leaves): together with\n * value-carrying literals these are the content-bearing tokens the near-miss content gate\n * counts. Keywords, operators, and punctuation come from unnamed nodes and stay false.\n */\n isName?: boolean;\n /** 0-based source rows the token occupies, so line coverage counts only matched-token lines. */\n startRow: number;\n endRow: number;\n}\n\n/** A token span with its source position; plain data so cross-file matching can retain it. */\nexport interface TokenRange {\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/** A contiguous run of matched tokens; gapped (merged) duplicates carry several per occurrence. */\ninterface TokenSegment {\n startTokenIndex: number;\n endTokenIndex: number;\n}\n\ninterface DuplicateCandidate {\n fingerprint: string;\n tokenCount: number;\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\nexport interface CountedOccurrence {\n /** Matched token runs; more than one once gapped groups are merged. */\n segments: TokenSegment[];\n /**\n * Set on occurrences whose span another reported group already counts: a retained group's\n * occurrences that a partial gapped merge also paired into a merged group, and cross-file copies\n * nested inside a larger group's region. Block counting must not count them again.\n */\n spanCountedElsewhere?: boolean;\n /**\n * Set on cross-file copies nested inside a larger group's region (they also set\n * `spanCountedElsewhere`). They never pair in gapped merging, and they do not keep their group's\n * standalone copies from merging, which they are not copies of. They do keep the merged group\n * from taking their group's place, since only the original group reports the nesting.\n */\n nestedInLargerGroup?: boolean;\n /** Sum of segment token counts (the gap tokens are not matched content). */\n tokenCount: number;\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/** A duplicate region found in one file, exported for cross-file matching by fingerprint. */\nexport interface CrossFileDuplicateCandidate {\n /** Content key: equal fingerprints mean equal normalized token sequences (up to hash collision). */\n fingerprint: string;\n tokenCount: number;\n /** Token positions within the owning file, for cross-file gapped (Type-3) merging. */\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/**\n * One file's contribution to cross-file clone detection: catalogued candidates plus the normalized\n * token stream and statement structure, so the project-level pass can match partial statement runs\n * (windows that a single file cannot know repeat elsewhere) and merge gap-adjacent groups.\n */\nexport interface CrossFileDuplicationFileData {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n /**\n * The mutually disjoint blocks (at least `minTokens` long, not literal-dense) compared for\n * cross-file near-miss (Type-3) clones. Optional for backward compatibility; without it, the\n * file takes part in exact and gapped cross-file matching only.\n */\n nearMissBlocks?: TokenRange[];\n /**\n * 1-based lines that are neither blank nor comment-only, so cross-file line coverage counts only\n * code lines (blank rows inside multi-row tokens such as template literals carry no content).\n * Optional for backward compatibility; without it, every matched-token row counts.\n */\n codeLineNumbers?: Set<number>;\n}\n\n/** literalCountPrefix[i] = value-carrying literal tokens in tokens[0..i), for O(1) density checks. */\nexport function buildLiteralCountPrefix(tokens: Token[]): Int32Array {\n const prefix = new Int32Array(tokens.length + 1);\n for (const [index, token] of tokens.entries()) {\n prefix[index + 1] = (prefix[index] ?? 0) + (token.literalHash === undefined ? 0 : 1);\n }\n return prefix;\n}\n\ninterface WindowOccurrences {\n count: number;\n /** -1 once occurrences span more than one container. */\n containerIndex: number;\n /** -1 once occurrences span more than one context (file). */\n contextIndex: number;\n minStart: number;\n maxStart: number;\n}\n\ninterface SequenceWindow {\n containerIndex: number;\n start: number;\n length: number;\n}\n\n/** One file's token stream and statement containers, as a window-matching context. */\nexport interface SequenceWindowContext {\n tokens: Token[];\n literalCountPrefix: Int32Array;\n containers: TokenRange[][];\n}\n\nexport interface ContextualSequenceCandidate {\n candidate: DuplicateCandidate;\n contextIndex: number;\n}\n\n/**\n * Enumerates runs of consecutive sibling statements over one or more contexts (files). Every\n * container statement participates; only the window length is capped, so enumeration stays linear\n * in the statement count. Windows are grouped by a cheap rolling hash of per-statement\n * fingerprints, and only locally maximal repeated windows — those whose one-statement extensions\n * stop repeating — become candidates with an exact (window-consistent) fingerprint. Without the\n * maximality filter a degenerate file of near-identical statements would fingerprint every\n * sub-window of every repeated region. With `requireMultipleContexts` a window only counts as\n * repeated when its occurrences span at least two contexts (CPD-style cross-file matching): a\n * repeat confined to one file is that file's own concern, and emitting it here would flood the\n * project-level selection with unusable single-file groups.\n */\nexport function collectSequenceWindowCandidates(\n contexts: SequenceWindowContext[],\n minTokens: number,\n requireMultipleContexts: boolean\n): ContextualSequenceCandidate[] {\n const candidates: ContextualSequenceCandidate[] = [];\n const contextIndexByContainer: number[] = [];\n const containers: TokenRange[][] = [];\n for (const [contextIndex, context] of contexts.entries()) {\n for (const statements of context.containers) {\n contextIndexByContainer.push(contextIndex);\n containers.push(statements);\n }\n }\n const contextAt = (containerIndex: number): SequenceWindowContext | undefined =>\n contexts[contextIndexByContainer[containerIndex] ?? 0];\n const occurrencesByWindowKey = new Map<number, WindowOccurrences>();\n const containerWindows = containers.map((statements, containerIndex) =>\n enumerateContainerWindows(contextAt(containerIndex)?.tokens ?? [], statements, minTokens)\n );\n for (const [containerIndex, windows] of containerWindows.entries()) {\n const contextIndex = contextIndexByContainer[containerIndex] ?? 0;\n for (const [start, row] of windows.windowKeysByStart.entries()) {\n for (const windowKey of row) {\n if (windowKey === undefined) {\n continue;\n }\n const occurrences = occurrencesByWindowKey.get(windowKey);\n if (occurrences) {\n occurrences.count += 1;\n if (occurrences.containerIndex !== containerIndex) {\n occurrences.containerIndex = -1;\n }\n if (occurrences.contextIndex !== contextIndex) {\n occurrences.contextIndex = -1;\n }\n occurrences.minStart = Math.min(occurrences.minStart, start);\n occurrences.maxStart = Math.max(occurrences.maxStart, start);\n } else {\n occurrencesByWindowKey.set(windowKey, {\n count: 1,\n containerIndex,\n contextIndex,\n minStart: start,\n maxStart: start,\n });\n }\n }\n }\n }\n\n // A window only \"repeats\" when two of its occurrences can coexist without overlapping: sliding\n // matches inside a homogeneous run (start spread smaller than the window length) can never both\n // be counted and must neither qualify a window nor dominate its sub-windows. Cross-context\n // matching instead requires occurrences in two contexts, which coexist by construction.\n const repeats = (windowKey: number | undefined, length: number): boolean => {\n if (windowKey === undefined) {\n return false;\n }\n const occurrences = occurrencesByWindowKey.get(windowKey);\n if (occurrences === undefined || occurrences.count < 2) {\n return false;\n }\n if (requireMultipleContexts) {\n return occurrences.contextIndex === -1;\n }\n return occurrences.containerIndex === -1 || occurrences.maxStart - occurrences.minStart >= length;\n };\n\n // A window whose statements all share one normalized shape (sixteen `let x = 0;` declarations,\n // a constant table) is a homogeneous preamble, not a copy-paste: requiring two distinct\n // per-statement shapes keeps such runs out of duplicate groups and the duplication ratio.\n const hasDistinctStatements = (window: SequenceWindow): boolean => {\n const hashes = containerWindows[window.containerIndex]?.statementHashes ?? [];\n const firstHash = hashes[window.start];\n for (let index = window.start + 1; index < window.start + window.length; index += 1) {\n if (hashes[index] !== firstHash) {\n return true;\n }\n }\n return false;\n };\n\n const maximalWindows: SequenceWindow[] = [];\n for (const [containerIndex, windows] of containerWindows.entries()) {\n for (const [start, row] of windows.windowKeysByStart.entries()) {\n for (const [length, windowKey] of row.entries()) {\n if (!repeats(windowKey, length) || !hasDistinctStatements({ containerIndex, start, length })) {\n continue;\n }\n // Dominated windows are skipped: the one-statement extension also repeats, so a larger\n // candidate covering this window exists.\n const extendedRight = windows.windowKeysByStart[start]?.[length + 1];\n const extendedLeft = windows.windowKeysByStart[start - 1]?.[length + 1];\n if (repeats(extendedRight, length + 1) || repeats(extendedLeft, length + 1)) {\n continue;\n }\n maximalWindows.push({ containerIndex, start, length });\n }\n }\n }\n\n // The rolling hash anonymizes identifiers per statement, so a window can look repeated coarsely\n // while its exact (window-consistent) fingerprints differ, and a longer window's match can\n // dominate sub-windows that other copies still need (three copies where only two extend one\n // statement further). Every emitted window therefore exposes its repeating, unvisited\n // sub-windows; `visited` bounds the worklist and lengths strictly decrease, so it terminates.\n const visited = new Set(maximalWindows.map(windowId));\n let frontier = maximalWindows;\n while (frontier.length > 0) {\n const emitted: SequenceWindow[] = [];\n for (const window of frontier) {\n const statements = containers[window.containerIndex];\n const first = statements?.[window.start];\n const last = statements?.[window.start + window.length - 1];\n const context = contextAt(window.containerIndex);\n if (!first || !last || !context) {\n continue;\n }\n const fingerprint = `s:${fingerprintKey(context.tokens, context.literalCountPrefix, first.startTokenIndex, last.endTokenIndex)}`;\n candidates.push({\n candidate: toCandidate(fingerprint, first.startTokenIndex, last.endTokenIndex, first, last),\n contextIndex: contextIndexByContainer[window.containerIndex] ?? 0,\n });\n emitted.push(window);\n }\n frontier = [];\n for (const window of emitted) {\n for (const start of [window.start, window.start + 1]) {\n const subWindow = { containerIndex: window.containerIndex, start, length: window.length - 1 };\n const subWindowKey = containerWindows[window.containerIndex]?.windowKeysByStart[start]?.[subWindow.length];\n if (\n visited.has(windowId(subWindow)) ||\n !repeats(subWindowKey, subWindow.length) ||\n !hasDistinctStatements(subWindow)\n ) {\n continue;\n }\n visited.add(windowId(subWindow));\n frontier.push(subWindow);\n }\n }\n }\n return candidates;\n}\n\nfunction windowId(window: SequenceWindow): string {\n return `${window.containerIndex}:${window.start}:${window.length}`;\n}\n\ninterface ContainerWindows {\n /** windowKeysByStart[start][length] is the rolling-hash key of the window, or undefined if it is below the size thresholds. */\n windowKeysByStart: (number | undefined)[][];\n /** Per-statement fingerprint hashes, for the distinct-shape requirement on windows. */\n statementHashes: number[];\n}\n\nfunction enumerateContainerWindows(tokens: Token[], statements: TokenRange[], minTokens: number): ContainerWindows {\n const statementHashes = statements.map((statement) =>\n fingerprintHash(tokens, statement.startTokenIndex, statement.endTokenIndex)\n );\n const windowKeysByStart: (number | undefined)[][] = [];\n for (let start = 0; start < statements.length; start += 1) {\n const row: (number | undefined)[] = [];\n let hash = 5381;\n let tokenCount = 0;\n const maxEnd = Math.min(statements.length, start + maxSequenceStatementCount);\n for (let end = start; end < maxEnd; end += 1) {\n const statement = statements[end];\n const statementHash = statementHashes[end];\n if (!statement || statementHash === undefined) {\n break;\n }\n hash = combineHashes(hash, statementHash);\n tokenCount += statement.endTokenIndex - statement.startTokenIndex;\n const statementCount = end - start + 1;\n row[statementCount] =\n statementCount >= minSequenceStatementCount && tokenCount >= minTokens\n ? combineHashes(hash, statementCount)\n : undefined;\n }\n windowKeysByStart.push(row);\n }\n return { windowKeysByStart, statementHashes };\n}\n\n/**\n * Longest-common-subsequence LENGTH of two symbol sequences via the Allison–Dix bit-parallel\n * recurrence (O(|a|/32 · |b|) words): per symbol of `b`, `x = match | v` and\n * `v = x & ~(x - ((v << 1) | 1))` over multi-word bit vectors; the set bits of `v` count the LCS.\n */\nexport function lcsLength(a: Int32Array, b: Int32Array): number {\n return createLcsLengthCounter(a)(b);\n}\n\n/**\n * lcsLength against a fixed `a`, whose per-symbol position masks are built once and reused for\n * every `b` compared with it.\n */\nexport function createLcsLengthCounter(a: Int32Array): (b: Int32Array) => number {\n const wordCount = (a.length + 31) >>> 5;\n const positionMasks = new Map<number, Uint32Array>();\n for (const [index, symbol] of a.entries()) {\n let mask = positionMasks.get(symbol);\n if (!mask) {\n mask = new Uint32Array(wordCount);\n positionMasks.set(symbol, mask);\n }\n const word = index >>> 5;\n // The Uint32Array store wraps the signed int32 bit pattern to unsigned. The `?? 0` guards in\n // this function are required by noUncheckedIndexedAccess (typed-array reads type as\n // `number | undefined`), not redundancy: every index is in bounds.\n mask[word] = (mask[word] ?? 0) | (1 << (index & 31));\n }\n\n const v = new Uint32Array(wordCount);\n return (b) => {\n v.fill(0);\n for (const symbol of b) {\n const matchMask = positionMasks.get(symbol);\n // `(v << 1) | 1` shifts a carry bit across words; subtraction borrows across words.\n let shiftCarry = 1;\n let borrow = 0;\n for (let word = 0; word < wordCount; word += 1) {\n const previous = v[word] ?? 0;\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `>>> 0` reinterprets the signed int32 bit pattern as unsigned so the borrow subtraction below compares magnitudes; Math.trunc would keep it negative.\n const x = ((matchMask?.[word] ?? 0) | previous) >>> 0;\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- same unsigned reinterpretation as `x`.\n const shifted = ((previous << 1) | shiftCarry) >>> 0;\n shiftCarry = previous >>> 31;\n const difference = x - shifted - borrow;\n borrow = difference < 0 ? 1 : 0;\n // The Uint32Array store wraps the signed int32 bit pattern to unsigned.\n v[word] = x & ~difference;\n }\n }\n\n let length = 0;\n for (const word of v) {\n length += popCount(word);\n }\n return length;\n };\n}\n\nfunction popCount(value: number): number {\n let count = value - ((value >>> 1) & 0x55_55_55_55);\n count = (count & 0x33_33_33_33) + ((count >>> 2) & 0x33_33_33_33);\n return (Math.imul((count + (count >>> 4)) & 0x0F_0F_0F_0F, 0x01_01_01_01) >>> 24) & 0xFF;\n}\n\nfunction toCandidate(\n fingerprint: string,\n startTokenIndex: number,\n endTokenIndex: number,\n first: TokenRange,\n last: TokenRange\n): DuplicateCandidate {\n return {\n fingerprint,\n tokenCount: endTokenIndex - startTokenIndex,\n startTokenIndex,\n endTokenIndex,\n startIndex: first.startIndex,\n endIndex: last.endIndex,\n startLine: first.startLine,\n endLine: last.endLine,\n };\n}\n\n/** Caches of hashText/hashText2 over '$0', '$1', ... so anonymized identifiers hash without allocating. */\nconst anonymizedIndexHashes: number[] = [];\nconst anonymizedIndexHashes2: number[] = [];\n\nfunction anonymizedIndexHash(index: number): number {\n let hash = anonymizedIndexHashes[index];\n if (hash === undefined) {\n hash = hashText(`$${index}`);\n anonymizedIndexHashes[index] = hash;\n }\n return hash;\n}\n\nfunction anonymizedIndexHash2(index: number): number {\n let hash = anonymizedIndexHashes2[index];\n if (hash === undefined) {\n hash = hashText2(`$${index}`);\n anonymizedIndexHashes2[index] = hash;\n }\n return hash;\n}\n\n/**\n * Content key of a token range: two independent 32-bit hashes over the normalized token sequence\n * (identifiers anonymized consistently by first-occurrence order) plus the token count. Regions\n * with equal keys are treated as equal content; a collision would need both 32-bit hashes and the\n * length to coincide, which is negligible for a metrics report. The format and arithmetic match\n * fingerprint_key in native/src/duplication.rs exactly, so window candidates fingerprinted here\n * group together with the per-file candidates the addon catalogues.\n */\nfunction fingerprintKey(\n tokens: Token[],\n literalCountPrefix: Int32Array,\n startTokenIndex: number,\n endTokenIndex: number\n): string {\n const literalCount = (literalCountPrefix[endTokenIndex] ?? 0) - (literalCountPrefix[startTokenIndex] ?? 0);\n const literalDense = isLiteralDense(literalCount, endTokenIndex - startTokenIndex);\n const [primary, secondary] = fingerprintHashPair(tokens, startTokenIndex, endTokenIndex, literalDense);\n return `${primary}:${secondary}:${endTokenIndex - startTokenIndex}`;\n}\n\n/**\n * A single 32-bit summary of a range for the coarse rolling-hash phase. Deliberately\n * density-agnostic: density is a property of the final candidate REGION, and folding literal\n * values into per-statement hashes would make a dense statement inside a logic-heavy window\n * (`const weights = [1, 2, 3];`) block the window from ever being enumerated. The coarse phase\n * over-approximates on shape alone; the exact region fingerprint still applies the density rule.\n */\nfunction fingerprintHash(tokens: Token[], startTokenIndex: number, endTokenIndex: number): number {\n const [primary, secondary] = fingerprintHashPair(tokens, startTokenIndex, endTokenIndex, false);\n // XOR already coerces to int32, matching the native side's i32 arithmetic.\n return primary ^ Math.imul(secondary, 31);\n}\n\nfunction fingerprintHashPair(\n tokens: Token[],\n startTokenIndex: number,\n endTokenIndex: number,\n foldLiteralValues: boolean\n): [number, number] {\n const indexByIdentifier = new Map<string, number>();\n let primary = 5381;\n let secondary = 52_711;\n for (let index = startTokenIndex; index < endTokenIndex; index += 1) {\n const token = tokens[index];\n if (!token) {\n continue;\n }\n // Each accumulator consumes its own independent per-token hash: sharing one would collapse\n // the key to 32 effective bits (a single djb2 collision would equate whole regions).\n let part: number;\n let part2: number;\n if (token.kind === 'id') {\n let identifierIndex = indexByIdentifier.get(token.text);\n if (identifierIndex === undefined) {\n identifierIndex = indexByIdentifier.size;\n indexByIdentifier.set(token.text, identifierIndex);\n }\n part = anonymizedIndexHash(identifierIndex);\n part2 = anonymizedIndexHash2(identifierIndex);\n } else {\n part = token.textHash;\n part2 = token.textHash2;\n }\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `| 0` wraps the sum to int32 (Math.trunc does not), which must match the native side's wrapping i32 arithmetic.\n primary = (Math.imul(primary, 31) + part) | 0;\n secondary = Math.imul(secondary, 37) ^ part2;\n if (foldLiteralValues && token.literalHash !== undefined && token.literalHash2 !== undefined) {\n // oxlint-disable-next-line unicorn/prefer-math-trunc -- `| 0` wraps the sum to int32 (Math.trunc does not), which must match the native side's wrapping i32 arithmetic.\n primary = (Math.imul(primary, 31) + token.literalHash) | 0;\n secondary = Math.imul(secondary, 37) ^ token.literalHash2;\n }\n }\n return [primary, secondary];\n}\n\n/** djb2-style hash; XOR keeps the value in signed 32-bit range, which is fine for a grouping key. */\nfunction hashText(text: string): number {\n let hash = 5381;\n for (let index = 0; index < text.length; index += 1) {\n // oxlint-disable-next-line unicorn/prefer-code-point -- djb2 hashes UTF-16 code units; codePointAt would hash surrogate pairs twice (full code point, then the lone low surrogate).\n hash = Math.imul(hash, 33) ^ text.charCodeAt(index);\n }\n return hash;\n}\n\n/** FNV-1a over UTF-16 code units: independent of hashText so the two accumulators never share input. */\nfunction hashText2(text: string): number {\n let hash = -2_128_831_035; // 2166136261 as int32 (the FNV-1a offset basis)\n for (let index = 0; index < text.length; index += 1) {\n // oxlint-disable-next-line unicorn/prefer-code-point -- hashes UTF-16 code units like hashText.\n hash = Math.imul(hash ^ text.charCodeAt(index), 16_777_619);\n }\n return hash;\n}\n\nfunction combineHashes(hash: number, value: number): number {\n return Math.imul(hash, 31) + value;\n}\n\n/**\n * Merges duplicate groups separated by a small token gap into one gapped (Type-3) clone group: a\n * copy edited in one spot splits into two exact groups whose occurrences sit side by side in the\n * same order. Occurrences are paired greedily in source order; a merge happens when the pairing\n * fully pairs at least one group with at least two pairs. Equal-cardinality groups whose\n * occurrences all pair merge into one group as before. When cardinalities differ (a fragment also\n * occurs standalone: prefix ×3, suffix ×2), the fully-paired group is subsumed into the merged\n * gapped group while the other group is RETAINED with ALL its occurrences: dropping the leftover\n * would lose duplicated-line coverage, and reporting it alone would make a single-occurrence group\n * (contradicting duplicateBlockGroupCount's \"appears more than once\" meaning). Line coverage\n * unions ranges, so the overlap between the retained exact group and the merged group is harmless.\n * Merging repeats to a fixpoint so a clone edited in several spots still reassembles; it\n * terminates because a merge either removes the input groups it replaces or marks their paired\n * occurrences as counted elsewhere, and only unmarked occurrences of remaining groups pair, so the\n * number of pairable occurrences strictly decreases with every merge. Gap tokens are not matched\n * content: line coverage and sizes count only the matched segments. Generic so cross-file merging\n * can thread file identity through occurrences.\n */\nexport function mergeAdjacentGroups<T extends CountedOccurrence>(\n groups: T[][],\n maxGapTokens: number,\n isReportableGroup: (group: T[]) => boolean = () => true\n): T[][] {\n if (maxGapTokens <= 0 || groups.length < 2) {\n return groups;\n }\n // Deterministic processing order (mirrored by the native side): by first occurrence position.\n groups.sort(compareGroups);\n for (let restart = true; restart;) {\n restart = false;\n const partnersByGroup = collectGapAdjacentPartners(groups, maxGapTokens);\n for (let leftIndex = 0; leftIndex < groups.length && !restart; leftIndex += 1) {\n for (const rightIndex of partnersByGroup[leftIndex] ?? []) {\n const left = groups[leftIndex];\n const right = groups[rightIndex];\n if (!left || !right) {\n continue;\n }\n const forward = mergeGroups(left, right, maxGapTokens, isReportableGroup);\n const result = forward ?? mergeGroups(right, left, maxGapTokens, isReportableGroup);\n if (!result) {\n continue;\n }\n const leftReplaced = forward ? result.firstReplaced : result.secondReplaced;\n const rightReplaced = forward ? result.secondReplaced : result.firstReplaced;\n if (leftReplaced && rightReplaced) {\n groups[leftIndex] = result.merged;\n groups.splice(rightIndex, 1);\n } else if (rightReplaced) {\n groups[rightIndex] = result.merged;\n } else if (leftReplaced) {\n groups[leftIndex] = result.merged;\n } else {\n // Both groups stay (each is only partly paired, or reports nested copies of its own), so\n // the merged group joins them instead of taking a place.\n groups.push(result.merged);\n }\n // A group that stays keeps ALL its occurrences (line coverage must not shrink, and a\n // reported group must keep >= 2 occurrences), so its paired occurrences now also live\n // inside the merged group's occurrences: mark them so duplicateBlockCount counts each\n // token span once, and so the same pair cannot merge again.\n for (const occurrence of result.pairedRetained) {\n occurrence.spanCountedElsewhere = true;\n }\n groups.sort(compareGroups);\n restart = true;\n break;\n }\n }\n }\n return groups;\n}\n\n/**\n * Per group index, the ascending indexes of later groups that mergeGroups can pair with it in\n * either direction: an occurrence of one starts within `maxGapTokens` after an occurrence of the\n * other ends. Every other pair fails mergeGroups, so skipping it leaves the fixpoint unchanged\n * while each restart stops costing a comparison per pair of groups.\n */\nfunction collectGapAdjacentPartners(groups: CountedOccurrence[][], maxGapTokens: number): number[][] {\n const starts: { startTokenIndex: number; groupIndex: number }[] = [];\n for (const [groupIndex, group] of groups.entries()) {\n for (const occurrence of group) {\n starts.push({ startTokenIndex: occurrence.startTokenIndex, groupIndex });\n }\n }\n starts.sort((left, right) => left.startTokenIndex - right.startTokenIndex);\n const partners = groups.map(() => new Set<number>());\n for (const [groupIndex, group] of groups.entries()) {\n for (const occurrence of group) {\n for (\n let index = lowerBoundByStart(starts, occurrence.endTokenIndex);\n index < starts.length && (starts[index]?.startTokenIndex ?? 0) <= occurrence.endTokenIndex + maxGapTokens;\n index += 1\n ) {\n const other = starts[index]?.groupIndex ?? groupIndex;\n if (other !== groupIndex) {\n partners[Math.min(other, groupIndex)]?.add(Math.max(other, groupIndex));\n }\n }\n }\n }\n return partners.map((set) => [...set].toSorted((left, right) => left - right));\n}\n\nfunction lowerBoundByStart(sorted: { startTokenIndex: number }[], target: number): number {\n let low = 0;\n let high = sorted.length;\n while (low < high) {\n const middle = (low + high) >>> 1;\n if ((sorted[middle]?.startTokenIndex ?? 0) < target) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low;\n}\n\nfunction compareGroups(left: CountedOccurrence[], right: CountedOccurrence[]): number {\n const leftFirst = left[0];\n const rightFirst = right[0];\n return (\n (leftFirst?.startTokenIndex ?? 0) - (rightFirst?.startTokenIndex ?? 0) ||\n (leftFirst?.endTokenIndex ?? 0) - (rightFirst?.endTokenIndex ?? 0)\n );\n}\n\ninterface MergeResult<T> {\n merged: T[];\n /**\n * Whether the merged group takes the respective input group's place: its standalone occurrences\n * were all paired and it holds no nested copies that only it can report.\n */\n firstReplaced: boolean;\n secondReplaced: boolean;\n /** The occurrences of groups that stay, which the merged group's spans now also cover. */\n pairedRetained: T[];\n}\n\n/**\n * Pairs `second` occurrences with gap-preceding `first` occurrences, greedily in source order:\n * each trailing occurrence takes the earliest unused leading occurrence within the gap, and a\n * pair's leading must start at or after the previous pair's trailing end so merged spans never\n * overlap. A merge needs at least two pairs (a merged group must still mean \"appears more than\n * once\") and must fully consume at least one group; for equal cardinalities this reduces to the\n * strict all-pairs merge, so pre-partial-merge behavior is unchanged there.\n */\nfunction mergeGroups<T extends CountedOccurrence>(\n first: T[],\n second: T[],\n maxGapTokens: number,\n isReportableGroup: (group: T[]) => boolean\n): MergeResult<T> | undefined {\n // Occurrences a previous partial merge already paired into a merged group must not pair again:\n // their spans already live inside that merged group, so re-pairing them would assemble a second,\n // competing merged group instead of letting the existing merged group extend (and would count\n // the same span twice). Consumption is still judged against the FULL group, so a group holding\n // shared occurrences is never subsumed away; nested copies, which the merged span would not\n // cover anyway, are left out of that judgment so they cannot veto a merge of the standalone\n // copies.\n const [leadings, firstLength] = pairableOccurrences(first);\n const [trailings, secondLength] = pairableOccurrences(second);\n const pairs: [T, T][] = [];\n let leadingIndex = 0;\n let previousTrailingEnd = -1;\n for (const trailing of trailings) {\n // Leadings ending too far before this trailing can never pair a later (even farther) one.\n while (leadingIndex < leadings.length) {\n const leading = leadings[leadingIndex];\n if (leading && leading.endTokenIndex + maxGapTokens < trailing.startTokenIndex) {\n leadingIndex += 1;\n } else {\n break;\n }\n }\n const leading = leadings[leadingIndex];\n if (\n leading &&\n leading.endTokenIndex <= trailing.startTokenIndex &&\n leading.startTokenIndex >= previousTrailingEnd\n ) {\n pairs.push([leading, trailing]);\n previousTrailingEnd = trailing.endTokenIndex;\n leadingIndex += 1;\n }\n }\n const firstFullyPaired = pairs.length === firstLength;\n const secondFullyPaired = pairs.length === secondLength;\n if (pairs.length < 2 || (!firstFullyPaired && !secondFullyPaired)) {\n return undefined;\n }\n // A group holding nested copies stays even when all its standalone copies pair: the merged\n // group's content is larger than what those copies matched, so only the original group can\n // report which files share the matched fragment.\n const firstReplaced = firstFullyPaired && !first.some((occurrence) => occurrence.nestedInLargerGroup);\n const secondReplaced = secondFullyPaired && !second.some((occurrence) => occurrence.nestedInLargerGroup);\n const merged = pairs.map(([leading, trailing]) => ({\n ...leading,\n // A merged occurrence is a fresh span combination; it never inherits shared-span marks.\n spanCountedElsewhere: undefined,\n nestedInLargerGroup: undefined,\n segments: [...leading.segments, ...trailing.segments],\n tokenCount: leading.tokenCount + trailing.tokenCount,\n endTokenIndex: trailing.endTokenIndex,\n endIndex: trailing.endIndex,\n endLine: trailing.endLine,\n }));\n // Only occurrences of one file pair (file offsets exceed the gap), so a merged group spans the\n // files its pairs sit in: with nested copies left out of pairing, that can be fewer files than\n // the input groups covered, and a merged group that is no longer reportable must not form.\n if (!isReportableGroup(merged)) {\n return undefined;\n }\n const pairedRetained = [\n ...(firstReplaced ? [] : pairs.map(([leading]) => leading)),\n ...(secondReplaced ? [] : pairs.map(([, trailing]) => trailing)),\n ];\n return { merged, firstReplaced, secondReplaced, pairedRetained };\n}\n\n/** One pass over a group: its pairable occurrences and its non-nested occurrence count. */\nfunction pairableOccurrences<T extends CountedOccurrence>(group: T[]): [T[], number] {\n const pairable: T[] = [];\n let length = 0;\n for (const occurrence of group) {\n if (!occurrence.nestedInLargerGroup) {\n length += 1;\n }\n // Either flag keeps an occurrence out of pairing: its span is already counted elsewhere, or it\n // is a nested copy of content the merged span would not cover.\n if (!occurrence.spanCountedElsewhere && !occurrence.nestedInLargerGroup) {\n pairable.push(occurrence);\n }\n }\n return [pairable, length];\n}\n\n/**\n * Redundant copies one group adds to duplicateBlockCount. Each redundant occurrence contributes\n * one count per matched fragment, so merging a gapped clone's fragments into one group does not\n * halve duplicateBlockCount: an edited two-fragment pair still counts 2, exactly as its unmerged\n * fragments did. Occurrence shapes can differ within one group (a\n * gap-merged exact pair plus an appended whole-block near-miss copy), so every occurrence's\n * fragments are summed and one representative — the largest — is deducted, keeping the count\n * independent of source order. Occurrences a partial gapped merge also paired into a merged group\n * are skipped: their spans are already counted there, and such a retained group deducts no\n * representative of its own — the merged group's representative already stands for the shared\n * content — so no token span contributes to the count twice.\n */\nexport function countRedundantFragments(group: CountedOccurrence[]): number {\n let fragmentCount = 0;\n let maxFragmentCount = 0;\n let hasSharedOccurrence = false;\n for (const occurrence of group) {\n if (occurrence.spanCountedElsewhere) {\n hasSharedOccurrence = true;\n continue;\n }\n fragmentCount += occurrence.segments.length;\n maxFragmentCount = Math.max(maxFragmentCount, occurrence.segments.length);\n }\n return hasSharedOccurrence ? fragmentCount : fragmentCount - maxFragmentCount;\n}\n\n/** Adds the 1-based code lines the segment's matched tokens cover; shared with cross-file coverage. */\nexport function collectSegmentLines(\n segment: { startTokenIndex: number; endTokenIndex: number },\n tokens: Token[],\n codeLineNumbers: Set<number> | undefined,\n duplicatedLines: Set<number>\n): void {\n for (let index = segment.startTokenIndex; index < segment.endTokenIndex; index += 1) {\n const token = tokens[index];\n for (let row = token?.startRow ?? 0; row <= (token?.endRow ?? -1); row += 1) {\n if (!codeLineNumbers || codeLineNumbers.has(row + 1)) {\n duplicatedLines.add(row + 1);\n }\n }\n }\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e={minTokens:40,maxGapTokens:30,minSimilarityPercent:70};function t(t){return{minTokens:n(t?.minTokens,e.minTokens),maxGapTokens:n(t?.maxGapTokens,e.maxGapTokens),minSimilarityPercent:n(t?.minSimilarityPercent,e.minSimilarityPercent)}}function n(e,t){return e===void 0||Number.isNaN(e)?t:Math.min(Math.max(Math.trunc(e),0),4294967295)}function r(e,t){return e*5>=t}function i(e){let t=new Int32Array(e.length+1);for(let[n,r]of e.entries())t[n+1]=(t[n]??0)+(r.literalHash===void 0?0:1);return t}function a(e,t,n){let r=[],i=[],a=[];for(let[t,n]of e.entries())for(let e of n.containers)i.push(t),a.push(e);let c=t=>e[i[t]??0],l=/* @__PURE__ */ new Map,u=a.map((e,n)=>s(c(n)?.tokens??[],e,t));for(let[e,t]of u.entries()){let n=i[e]??0;for(let[r,i]of t.windowKeysByStart.entries())for(let t of i){if(t===void 0)continue;let i=l.get(t);i?(i.count+=1,i.containerIndex!==e&&(i.containerIndex=-1),i.contextIndex!==n&&(i.contextIndex=-1),i.minStart=Math.min(i.minStart,r),i.maxStart=Math.max(i.maxStart,r)):l.set(t,{count:1,containerIndex:e,contextIndex:n,minStart:r,maxStart:r})}}let f=(e,t)=>{if(e===void 0)return!1;let r=l.get(e);return r===void 0||r.count<2?!1:n?r.contextIndex===-1:r.containerIndex===-1||r.maxStart-r.minStart>=t},p=e=>{let t=u[e.containerIndex]?.statementHashes??[],n=t[e.start];for(let r=e.start+1;r<e.start+e.length;r+=1)if(t[r]!==n)return!0;return!1},m=[];for(let[e,t]of u.entries())for(let[n,r]of t.windowKeysByStart.entries())for(let[i,a]of r.entries()){if(!f(a,i)||!p({containerIndex:e,start:n,length:i}))continue;let r=t.windowKeysByStart[n]?.[i+1],o=t.windowKeysByStart[n-1]?.[i+1];f(r,i+1)||f(o,i+1)||m.push({containerIndex:e,start:n,length:i})}let h=new Set(m.map(o)),_=m;for(;_.length>0;){let e=[];for(let t of _){let n=a[t.containerIndex],o=n?.[t.start],s=n?.[t.start+t.length-1],l=c(t.containerIndex);if(!o||!s||!l)continue;let u=`s:${g(l.tokens,l.literalCountPrefix,o.startTokenIndex,s.endTokenIndex)}`;r.push({candidate:d(u,o.startTokenIndex,s.endTokenIndex,o,s),contextIndex:i[t.containerIndex]??0}),e.push(t)}_=[];for(let t of e)for(let e of[t.start,t.start+1]){let n={containerIndex:t.containerIndex,start:e,length:t.length-1},r=u[t.containerIndex]?.windowKeysByStart[e]?.[n.length];!h.has(o(n))&&f(r,n.length)&&p(n)&&(h.add(o(n)),_.push(n))}}return r}function o(e){return`${e.containerIndex}:${e.start}:${e.length}`}function s(e,t,n){let r=t.map(t=>_(e,t.startTokenIndex,t.endTokenIndex)),i=[];for(let e=0;e<t.length;e+=1){let a=[],o=5381,s=0,c=Math.min(t.length,e+100);for(let i=e;i<c;i+=1){let c=t[i],l=r[i];if(!c||l===void 0)break;o=x(o,l),s+=c.endTokenIndex-c.startTokenIndex;let u=i-e+1;a[u]=u>=2&&s>=n?x(o,u):void 0}i.push(a)}return{windowKeysByStart:i,statementHashes:r}}function c(e,t){return l(e)(t)}function l(e){let t=e.length+31>>>5,n=/* @__PURE__ */ new Map;for(let[r,i]of e.entries()){let e=n.get(i);e||(e=new Uint32Array(t),n.set(i,e));let a=r>>>5;e[a]=(e[a]??0)|1<<(r&31)}let r=new Uint32Array(t);return e=>{r.fill(0);for(let i of e){let e=n.get(i),a=1,o=0;for(let n=0;n<t;n+=1){let t=r[n]??0,i=((e?.[n]??0)|t)>>>0,s=(t<<1|a)>>>0;a=t>>>31;let c=i-s-o;o=+(c<0),r[n]=i&~c}}let i=0;for(let e of r)i+=u(e);return i}}function u(e){let t=e-(e>>>1&1431655765);return t=(t&858993459)+(t>>>2&858993459),Math.imul(t+(t>>>4)&252645135,16843009)>>>24&255}function d(e,t,n,r,i){return{fingerprint:e,tokenCount:n-t,startTokenIndex:t,endTokenIndex:n,startIndex:r.startIndex,endIndex:i.endIndex,startLine:r.startLine,endLine:i.endLine}}const f=[],p=[];function m(e){let t=f[e];return t===void 0&&(t=y(`$${e}`),f[e]=t),t}function h(e){let t=p[e];return t===void 0&&(t=b(`$${e}`),p[e]=t),t}function g(e,t,n,i){let[a,o]=v(e,n,i,r((t[i]??0)-(t[n]??0),i-n));return`${a}:${o}:${i-n}`}function _(e,t,n){let[r,i]=v(e,t,n,!1);return r^Math.imul(i,31)}function v(e,t,n,r){let i=/* @__PURE__ */ new Map,a=5381,o=52711;for(let s=t;s<n;s+=1){let t=e[s];if(!t)continue;let n,c;if(t.kind===`id`){let e=i.get(t.text);e===void 0&&(e=i.size,i.set(t.text,e)),n=m(e),c=h(e)}else n=t.textHash,c=t.textHash2;a=Math.imul(a,31)+n|0,o=Math.imul(o,37)^c,r&&t.literalHash!==void 0&&t.literalHash2!==void 0&&(a=Math.imul(a,31)+t.literalHash|0,o=Math.imul(o,37)^t.literalHash2)}return[a,o]}function y(e){let t=5381;for(let n=0;n<e.length;n+=1)t=Math.imul(t,33)^e.charCodeAt(n);return t}function b(e){let t=-2128831035;for(let n=0;n<e.length;n+=1)t=Math.imul(t^e.charCodeAt(n),16777619);return t}function x(e,t){return Math.imul(e,31)+t}function S(e,t,n=()=>!0){if(t<=0||e.length<2)return e;e.sort(T);for(let r=!0;r;){r=!1;let i=C(e,t);for(let a=0;a<e.length&&!r;a+=1)for(let o of i[a]??[]){let i=e[a],s=e[o];if(!i||!s)continue;let c=E(i,s,t,n),l=c??E(s,i,t,n);if(!l)continue;let u=c?l.firstReplaced:l.secondReplaced,d=c?l.secondReplaced:l.firstReplaced;u&&d?(e[a]=l.merged,e.splice(o,1)):d?e[o]=l.merged:u?e[a]=l.merged:e.push(l.merged);for(let e of l.pairedRetained)e.spanCountedElsewhere=!0;e.sort(T),r=!0;break}}return e}function C(e,t){let n=[];for(let[t,r]of e.entries())for(let e of r)n.push({startTokenIndex:e.startTokenIndex,groupIndex:t});n.sort((e,t)=>e.startTokenIndex-t.startTokenIndex);let r=e.map(()=>/* @__PURE__ */ new Set);for(let[i,a]of e.entries())for(let e of a)for(let a=w(n,e.endTokenIndex);a<n.length&&(n[a]?.startTokenIndex??0)<=e.endTokenIndex+t;a+=1){let e=n[a]?.groupIndex??i;e!==i&&r[Math.min(e,i)]?.add(Math.max(e,i))}return r.map(e=>[...e].toSorted((e,t)=>e-t))}function w(e,t){let n=0,r=e.length;for(;n<r;){let i=n+r>>>1;(e[i]?.startTokenIndex??0)<t?n=i+1:r=i}return n}function T(e,t){let n=e[0],r=t[0];return(n?.startTokenIndex??0)-(r?.startTokenIndex??0)||(n?.endTokenIndex??0)-(r?.endTokenIndex??0)}function E(e,t,n,r){let[i,a]=D(e),[o,s]=D(t),c=[],l=0,u=-1;for(let e of o){for(;l<i.length;){let t=i[l];if(t&&t.endTokenIndex+n<e.startTokenIndex)l+=1;else break}let t=i[l];t&&t.endTokenIndex<=e.startTokenIndex&&t.startTokenIndex>=u&&(c.push([t,e]),u=e.endTokenIndex,l+=1)}let d=c.length===a,f=c.length===s;if(c.length<2||!d&&!f)return;let p=d&&!e.some(e=>e.nestedInLargerGroup),m=f&&!t.some(e=>e.nestedInLargerGroup),h=c.map(([e,t])=>({...e,spanCountedElsewhere:void 0,nestedInLargerGroup:void 0,segments:[...e.segments,...t.segments],tokenCount:e.tokenCount+t.tokenCount,endTokenIndex:t.endTokenIndex,endIndex:t.endIndex,endLine:t.endLine}));if(r(h))return{merged:h,firstReplaced:p,secondReplaced:m,pairedRetained:[...p?[]:c.map(([e])=>e),...m?[]:c.map(([,e])=>e)]}}function D(e){let t=[],n=0;for(let r of e)r.nestedInLargerGroup||(n+=1),!r.spanCountedElsewhere&&!r.nestedInLargerGroup&&t.push(r);return[t,n]}function O(e){let t=0,n=0,r=!1;for(let i of e){if(i.spanCountedElsewhere){r=!0;continue}t+=i.segments.length,n=Math.max(n,i.segments.length)}return r?t:t-n}function k(e,t,n,r){for(let i=e.startTokenIndex;i<e.endTokenIndex;i+=1){let e=t[i];for(let t=e?.startRow??0;t<=(e?.endRow??-1);t+=1)(!n||n.has(t+1))&&r.add(t+1)}}export{i as buildLiteralCountPrefix,k as collectSegmentLines,a as collectSequenceWindowCandidates,O as countRedundantFragments,l as createLcsLengthCounter,e as defaultDuplicationOptions,c as lcsLength,S as mergeAdjacentGroups,t as resolveDuplicationOptions};
1
+ import{clamp as e}from"@willbooster/shared-lib";const t={minTokens:40,maxGapTokens:30,minSimilarityPercent:70};function n(e){return{minTokens:r(e?.minTokens,t.minTokens),maxGapTokens:r(e?.maxGapTokens,t.maxGapTokens),minSimilarityPercent:r(e?.minSimilarityPercent,t.minSimilarityPercent)}}function r(t,n){return t===void 0||Number.isNaN(t)?n:e(Math.trunc(t),0,4294967295)}function i(e,t){return e*5>=t}function a(e){let t=new Int32Array(e.length+1);for(let[n,r]of e.entries())t[n+1]=(t[n]??0)+(r.literalHash===void 0?0:1);return t}function o(e,t,n){let r=[],i=[],a=[];for(let[t,n]of e.entries())for(let e of n.containers)i.push(t),a.push(e);let o=t=>e[i[t]??0],l=/* @__PURE__ */ new Map,u=a.map((e,n)=>c(o(n)?.tokens??[],e,t));for(let[e,t]of u.entries()){let n=i[e]??0;for(let[r,i]of t.windowKeysByStart.entries())for(let t of i){if(t===void 0)continue;let i=l.get(t);i?(i.count+=1,i.containerIndex!==e&&(i.containerIndex=-1),i.contextIndex!==n&&(i.contextIndex=-1),i.minStart=Math.min(i.minStart,r),i.maxStart=Math.max(i.maxStart,r)):l.set(t,{count:1,containerIndex:e,contextIndex:n,minStart:r,maxStart:r})}}let d=(e,t)=>{if(e===void 0)return!1;let r=l.get(e);return r===void 0||r.count<2?!1:n?r.contextIndex===-1:r.containerIndex===-1||r.maxStart-r.minStart>=t},p=e=>{let t=u[e.containerIndex]?.statementHashes??[],n=t[e.start];for(let r=e.start+1;r<e.start+e.length;r+=1)if(t[r]!==n)return!0;return!1},m=[];for(let[e,t]of u.entries())for(let[n,r]of t.windowKeysByStart.entries())for(let[i,a]of r.entries()){if(!d(a,i)||!p({containerIndex:e,start:n,length:i}))continue;let r=t.windowKeysByStart[n]?.[i+1],o=t.windowKeysByStart[n-1]?.[i+1];d(r,i+1)||d(o,i+1)||m.push({containerIndex:e,start:n,length:i})}let h=new Set(m.map(s)),g=m;for(;g.length>0;){let e=[];for(let t of g){let n=a[t.containerIndex],s=n?.[t.start],c=n?.[t.start+t.length-1],l=o(t.containerIndex);if(!s||!c||!l)continue;let u=`s:${_(l.tokens,l.literalCountPrefix,s.startTokenIndex,c.endTokenIndex)}`;r.push({candidate:f(u,s.startTokenIndex,c.endTokenIndex,s,c),contextIndex:i[t.containerIndex]??0}),e.push(t)}g=[];for(let t of e)for(let e of[t.start,t.start+1]){let n={containerIndex:t.containerIndex,start:e,length:t.length-1},r=u[t.containerIndex]?.windowKeysByStart[e]?.[n.length];!h.has(s(n))&&d(r,n.length)&&p(n)&&(h.add(s(n)),g.push(n))}}return r}function s(e){return`${e.containerIndex}:${e.start}:${e.length}`}function c(e,t,n){let r=t.map(t=>v(e,t.startTokenIndex,t.endTokenIndex)),i=[];for(let e=0;e<t.length;e+=1){let a=[],o=5381,s=0,c=Math.min(t.length,e+100);for(let i=e;i<c;i+=1){let c=t[i],l=r[i];if(!c||l===void 0)break;o=S(o,l),s+=c.endTokenIndex-c.startTokenIndex;let u=i-e+1;a[u]=u>=2&&s>=n?S(o,u):void 0}i.push(a)}return{windowKeysByStart:i,statementHashes:r}}function l(e,t){return u(e)(t)}function u(e){let t=e.length+31>>>5,n=/* @__PURE__ */ new Map;for(let[r,i]of e.entries()){let e=n.get(i);e||(e=new Uint32Array(t),n.set(i,e));let a=r>>>5;e[a]=(e[a]??0)|1<<(r&31)}let r=new Uint32Array(t);return e=>{r.fill(0);for(let i of e){let e=n.get(i),a=1,o=0;for(let n=0;n<t;n+=1){let t=r[n]??0,i=((e?.[n]??0)|t)>>>0,s=(t<<1|a)>>>0;a=t>>>31;let c=i-s-o;o=+(c<0),r[n]=i&~c}}let i=0;for(let e of r)i+=d(e);return i}}function d(e){let t=e-(e>>>1&1431655765);return t=(t&858993459)+(t>>>2&858993459),Math.imul(t+(t>>>4)&252645135,16843009)>>>24&255}function f(e,t,n,r,i){return{fingerprint:e,tokenCount:n-t,startTokenIndex:t,endTokenIndex:n,startIndex:r.startIndex,endIndex:i.endIndex,startLine:r.startLine,endLine:i.endLine}}const p=[],m=[];function h(e){let t=p[e];return t===void 0&&(t=b(`$${e}`),p[e]=t),t}function g(e){let t=m[e];return t===void 0&&(t=x(`$${e}`),m[e]=t),t}function _(e,t,n,r){let[a,o]=y(e,n,r,i((t[r]??0)-(t[n]??0),r-n));return`${a}:${o}:${r-n}`}function v(e,t,n){let[r,i]=y(e,t,n,!1);return r^Math.imul(i,31)}function y(e,t,n,r){let i=/* @__PURE__ */ new Map,a=5381,o=52711;for(let s=t;s<n;s+=1){let t=e[s];if(!t)continue;let n,c;if(t.kind===`id`){let e=i.get(t.text);e===void 0&&(e=i.size,i.set(t.text,e)),n=h(e),c=g(e)}else n=t.textHash,c=t.textHash2;a=Math.imul(a,31)+n|0,o=Math.imul(o,37)^c,r&&t.literalHash!==void 0&&t.literalHash2!==void 0&&(a=Math.imul(a,31)+t.literalHash|0,o=Math.imul(o,37)^t.literalHash2)}return[a,o]}function b(e){let t=5381;for(let n=0;n<e.length;n+=1)t=Math.imul(t,33)^e.charCodeAt(n);return t}function x(e){let t=-2128831035;for(let n=0;n<e.length;n+=1)t=Math.imul(t^e.charCodeAt(n),16777619);return t}function S(e,t){return Math.imul(e,31)+t}function C(e,t,n=()=>!0){if(t<=0||e.length<2)return e;e.sort(E);for(let r=!0;r;){r=!1;let i=w(e,t);for(let a=0;a<e.length&&!r;a+=1)for(let o of i[a]??[]){let i=e[a],s=e[o];if(!i||!s)continue;let c=D(i,s,t,n),l=c??D(s,i,t,n);if(!l)continue;let u=c?l.firstReplaced:l.secondReplaced,d=c?l.secondReplaced:l.firstReplaced;u&&d?(e[a]=l.merged,e.splice(o,1)):d?e[o]=l.merged:u?e[a]=l.merged:e.push(l.merged);for(let e of l.pairedRetained)e.spanCountedElsewhere=!0;e.sort(E),r=!0;break}}return e}function w(e,t){let n=[];for(let[t,r]of e.entries())for(let e of r)n.push({startTokenIndex:e.startTokenIndex,groupIndex:t});n.sort((e,t)=>e.startTokenIndex-t.startTokenIndex);let r=e.map(()=>/* @__PURE__ */ new Set);for(let[i,a]of e.entries())for(let e of a)for(let a=T(n,e.endTokenIndex);a<n.length&&(n[a]?.startTokenIndex??0)<=e.endTokenIndex+t;a+=1){let e=n[a]?.groupIndex??i;e!==i&&r[Math.min(e,i)]?.add(Math.max(e,i))}return r.map(e=>[...e].toSorted((e,t)=>e-t))}function T(e,t){let n=0,r=e.length;for(;n<r;){let i=n+r>>>1;(e[i]?.startTokenIndex??0)<t?n=i+1:r=i}return n}function E(e,t){let n=e[0],r=t[0];return(n?.startTokenIndex??0)-(r?.startTokenIndex??0)||(n?.endTokenIndex??0)-(r?.endTokenIndex??0)}function D(e,t,n,r){let[i,a]=O(e),[o,s]=O(t),c=[],l=0,u=-1;for(let e of o){for(;l<i.length;){let t=i[l];if(t&&t.endTokenIndex+n<e.startTokenIndex)l+=1;else break}let t=i[l];t&&t.endTokenIndex<=e.startTokenIndex&&t.startTokenIndex>=u&&(c.push([t,e]),u=e.endTokenIndex,l+=1)}let d=c.length===a,f=c.length===s;if(c.length<2||!d&&!f)return;let p=d&&!e.some(e=>e.nestedInLargerGroup),m=f&&!t.some(e=>e.nestedInLargerGroup),h=c.map(([e,t])=>({...e,spanCountedElsewhere:void 0,nestedInLargerGroup:void 0,segments:[...e.segments,...t.segments],tokenCount:e.tokenCount+t.tokenCount,endTokenIndex:t.endTokenIndex,endIndex:t.endIndex,endLine:t.endLine}));if(r(h))return{merged:h,firstReplaced:p,secondReplaced:m,pairedRetained:[...p?[]:c.map(([e])=>e),...m?[]:c.map(([,e])=>e)]}}function O(e){let t=[],n=0;for(let r of e)r.nestedInLargerGroup||(n+=1),!r.spanCountedElsewhere&&!r.nestedInLargerGroup&&t.push(r);return[t,n]}function k(e){let t=0,n=0,r=!1;for(let i of e){if(i.spanCountedElsewhere){r=!0;continue}t+=i.segments.length,n=Math.max(n,i.segments.length)}return r?t:t-n}function A(e,t,n,r){for(let i=e.startTokenIndex;i<e.endTokenIndex;i+=1){let e=t[i];for(let t=e?.startRow??0;t<=(e?.endRow??-1);t+=1)(!n||n.has(t+1))&&r.add(t+1)}}export{a as buildLiteralCountPrefix,A as collectSegmentLines,o as collectSequenceWindowCandidates,k as countRedundantFragments,u as createLcsLengthCounter,t as defaultDuplicationOptions,l as lcsLength,C as mergeAdjacentGroups,n as resolveDuplicationOptions};
2
2
  //# sourceMappingURL=duplication.js.map