code-gauge 4.6.0 → 4.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +25 -0
  2. package/THIRD-PARTY-NOTICES.txt +8911 -0
  3. package/dist/cliConfig.cjs +1 -1
  4. package/dist/cliConfig.cjs.map +1 -1
  5. package/dist/cliConfig.js +1 -1
  6. package/dist/diffCommand.cjs +3 -3
  7. package/dist/diffCommand.cjs.map +1 -1
  8. package/dist/diffCommand.js +3 -3
  9. package/dist/diffCommand.js.map +1 -1
  10. package/dist/languages.cjs +1 -1
  11. package/dist/languages.cjs.map +1 -1
  12. package/dist/languages.js +1 -1
  13. package/dist/languages.js.map +1 -1
  14. package/dist/metrics.cjs +1 -1
  15. package/dist/metrics.cjs.map +1 -1
  16. package/dist/metrics.d.ts +8 -3
  17. package/dist/metrics.js +1 -1
  18. package/dist/metrics.js.map +1 -1
  19. package/dist/nativeMetrics.cjs +3 -3
  20. package/dist/nativeMetrics.cjs.map +1 -1
  21. package/dist/nativeMetrics.d.ts +30 -0
  22. package/dist/nativeMetrics.js +3 -3
  23. package/dist/nativeMetrics.js.map +1 -1
  24. package/dist/scan.cjs +1 -1
  25. package/dist/scan.cjs.map +1 -1
  26. package/dist/scan.d.ts +2 -2
  27. package/dist/scan.js +1 -1
  28. package/dist/scan.js.map +1 -1
  29. package/dist/wasmBinding.cjs +2 -0
  30. package/dist/wasmBinding.cjs.map +1 -0
  31. package/dist/wasmBinding.d.ts +9 -0
  32. package/dist/wasmBinding.js +2 -0
  33. package/dist/wasmBinding.js.map +1 -0
  34. package/dist/worker.cjs +2 -0
  35. package/dist/worker.cjs.map +1 -0
  36. package/dist/worker.d.ts +1 -0
  37. package/dist/worker.js +2 -0
  38. package/dist/worker.js.map +1 -0
  39. package/native/Cargo.lock +1 -0
  40. package/native/Cargo.toml +6 -2
  41. package/native/build.rs +4 -1
  42. package/native/code-gauge.wasm +0 -0
  43. package/native/src/complexity.rs +134 -230
  44. package/native/src/dep_degree.rs +42 -39
  45. package/native/src/duplication.rs +65 -63
  46. package/native/src/functions.rs +169 -152
  47. package/native/src/languages.rs +20 -0
  48. package/native/src/lib.rs +41 -42
  49. package/native/src/measure.rs +109 -123
  50. package/native/src/napi.rs +104 -0
  51. package/native/src/ncss.rs +79 -84
  52. package/native/src/near_miss.rs +7 -7
  53. package/native/src/tree_index.rs +110 -0
  54. package/native/src/util.rs +17 -12
  55. package/native/src/wasm.rs +128 -0
  56. package/native/src/worker_pool.rs +53 -0
  57. package/package.json +16 -11
package/dist/scan.js CHANGED
@@ -1,2 +1,2 @@
1
- import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{detectLanguage as t}from"./languages.js";import{NativeAddonError as n}from"./nativeMetrics.js";import{measureCode as r,measureCodeWithCrossFileData as i}from"./metrics.js";import a from"node:path";import{lstat as o,readFile as s,readdir as c,realpath as l,stat as u}from"node:fs/promises";import d from"node:os";const f=new Set([`.agents`,`.claude`,`.cursor`,`.git`,`.next`,`.playwright-cli`,`.tox`,`.tmp`,`.turbo`,`.venv`,`.yarn`,`__fixtures__`,`__generated__`,`__pycache__`,`coverage`,`dist`,`fixtures`,`generated`,`node_modules`,`obj`,`target`,`test-fixtures`,`vendor`,`venv`]),p=new Set([`__tests__`,`test`,`tests`,`spec`]),m=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-](?:test|spec))\.[^.]+$/iu,h=/Tests?\.(?:java|kt|cs)$/u;function g(e){return e===`~`?d.homedir():e.startsWith(`~/`)?a.join(d.homedir(),e.slice(2)):a.resolve(e)}async function _(e){try{return(await u(e)).isDirectory()?e:a.dirname(e)}catch{return a.dirname(e)}}async function v(e,t){let n=[],r=[],i=[],o=e;try{o=await l(e)}catch{}let s=a.dirname(o),c;try{c=await u(o)}catch(e){let t=`${F(o,s)}: ${R(e)}`;return{displayRoot:s,files:n,errors:[t],warnings:i,fatalError:t}}if(c.isFile()){let e=a.dirname(o),s=P(o,t,!0);if(!s){let t=`${F(o,e)}: unsupported file type`;return{displayRoot:e,files:n,errors:[t],warnings:i,fatalError:t}}let c=x(t,n,r,i,e);try{await D(o,s,`single-file`,c,o)}catch(t){return b(t,e,n,r,i)}return{displayRoot:e,files:n,errors:r,warnings:i}}try{await w(o,x(t,n,r,i,o))}catch(e){return b(e,o,n,r,i)}return{displayRoot:o,files:n,errors:r,warnings:i}}async function y(e,t,n){let r=[],i=[],s=[],c=x(n,r,i,s,e);for(let l of t){let t=N(l,n)?P(l,n):void 0;if(!t)continue;let u=a.join(e,l);if(!(await o(u).catch(()=>{}))?.isSymbolicLink())try{await D(u,t,`directory`,c)}catch(t){return b(t,e,r,i,s)}}return{displayRoot:e,files:r,errors:i,warnings:s}}function b(e,t,r,i,a){if(!(e instanceof n))throw e;let o=R(e);return{displayRoot:t,files:r,errors:[...i,o],warnings:a,fatalError:o}}function x(e,t,n,r,i){return{options:e,files:t,errors:n,warnings:r,visitedDirectories:new Set,visitedFiles:new Set,rootDirectory:i}}async function S(e,t,n){try{return await e()}catch(e){n.errors.push(`${F(t,n.rootDirectory)}: ${R(e)}`);return}}async function C(e,t){let n=await S(()=>l(e),e,t);return n!==void 0&&M(n,t.rootDirectory)?n:void 0}async function w(e,t){let n=await C(e,t);if(n===void 0||t.visitedDirectories.has(n))return;t.visitedDirectories.add(n);let r=await S(()=>c(e,{withFileTypes:!0}),e,t);if(r!==void 0)for(let n of r){let r=a.join(e,n.name);if(n.isSymbolicLink()){await T(n.name,r,t);continue}if(n.isDirectory()){if(j(n.name,t.options))continue;await w(r,t);continue}n.isFile()&&await E(r,t)}}async function T(e,t,n){let r=await C(t,n);if(r===void 0)return;let i=await S(()=>u(t),t,n);if(i!==void 0){if(i.isDirectory()){if(j(e,n.options)||j(a.basename(r),n.options))return;await w(t,n);return}i.isFile()&&await E(t,n,r,r)}}async function E(e,t,n=e,r){let i=P(n,t.options);i&&await D(e,i,`directory`,t,r)}async function D(e,t,i,a,o){try{let n=o??await l(e);if(a.visitedFiles.has(n))return;a.visitedFiles.add(n);let c=await s(e,`utf8`),u={language:t,duplication:a.options.duplication};if(i===`single-file`){a.files.push({file:e,metrics:r(c,u)});return}let{metrics:d,crossFileData:f,crossFileError:p}=O(c,u);p!==void 0&&a.warnings.push(`${F(e,a.rootDirectory)}: cross-file duplication candidates unavailable: ${p}`),a.files.push({file:e,metrics:d,duplicationCandidates:f})}catch(t){if(t instanceof n)throw t;a.errors.push(`${F(e,a.rootDirectory)}: ${R(t)}`)}}function O(e,t){try{return i(e,t)}catch(i){if(i instanceof n)throw i;return{metrics:r(e,t),crossFileError:R(i)}}}function k(t,n){if(t.fatalError||t.files.length<2)return;let r=t.files.flatMap(({file:e,duplicationCandidates:n})=>n?[{file:F(e,t.displayRoot),...n}]:[]);r.length<2||(t.crossFileDuplication=e(r,n.duplication))}function A(e,t,n){let r=new Set(e?.duplication.duplicateLineNumbers),i=t&&Object.hasOwn(t.duplicateLineNumbersByFile,n)?t.duplicateLineNumbersByFile[n]??[]:[];for(let e of i)r.add(e);return r}function j(e,t){return f.has(e)?!0:!t.includeTests&&p.has(e)}function M(e,t){let n=a.relative(t,e);return n===``||n!==`..`&&!n.startsWith(`..${a.sep}`)&&!a.isAbsolute(n)}function N(e,t){let n=e.split(`/`);for(let e of n.slice(0,-1))if(f.has(e)||!t.includeTests&&p.has(e))return!1;return P(e,t)!==void 0}function P(e,n,r=!1){let i=e.toLowerCase();if(!(!r&&(i.endsWith(`.d.ts`)||i.endsWith(`.d.mts`)||i.endsWith(`.d.cts`)||i.endsWith(`.min.js`)||i.endsWith(`.pnp.cjs`)))&&(r||n.includeTests||!m.test(a.basename(e))&&!h.test(a.basename(e))))return t(e)}function F(e,t){return a.relative(t,e)||a.basename(e)}function I(e){process.stdout.write(e)}function L(e){process.stderr.write(e)}function R(e){return e instanceof Error?e.message:String(e)}export{k as addCrossFileDuplication,A as collectDuplicatedLineNumbers,_ as configSearchDirectory,R as formatError,F as formatPath,P as getLanguage,N as isScannedPath,O as measureWithCrossFileData,g as resolveTarget,y as scanListedFiles,v as scanTarget,L as writeStderr,I as writeStdout};
1
+ import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{detectLanguage as t}from"./languages.js";import{NativeAddonError as n}from"./nativeMetrics.js";import{measureCode as r,measureCodeWithCrossFileDataAsync as i}from"./metrics.js";import{lstat as a,readFile as o,readdir as s,realpath as c,stat as l}from"node:fs/promises";import u from"node:path";import d from"node:os";const f=new Set([`.agents`,`.claude`,`.cursor`,`.git`,`.next`,`.playwright-cli`,`.tox`,`.tmp`,`.turbo`,`.venv`,`.yarn`,`__fixtures__`,`__generated__`,`__pycache__`,`coverage`,`dist`,`fixtures`,`generated`,`node_modules`,`obj`,`target`,`test-fixtures`,`vendor`,`venv`]),p=new Set([`__tests__`,`test`,`tests`,`spec`]),m=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-](?:test|spec))\.[^.]+$/iu,h=/Tests?\.(?:java|kt|cs)$/u;function g(e){return e===`~`?d.homedir():e.startsWith(`~/`)?u.join(d.homedir(),e.slice(2)):u.resolve(e)}async function _(e){try{return(await l(e)).isDirectory()?e:u.dirname(e)}catch{return u.dirname(e)}}const v=d.availableParallelism()*2;async function y(e,t){let n=e;try{n=await c(e)}catch{}let r=u.dirname(n),i;try{i=await l(n)}catch(e){let t=`${R(n,r)}: ${V(e)}`;return{displayRoot:r,files:[],errors:[t],warnings:[],fatalError:t}}if(i.isFile()){let e=u.dirname(n),r=L(n,t,!0);if(!r){let t=`${R(n,e)}: unsupported file type`;return{displayRoot:e,files:[],errors:[t],warnings:[],fatalError:t}}let i=S(t,e);return await k(n,r,`single-file`,i,n),x(i,e)}let a=S(t,n);return await E(n,a),x(a,n)}async function b(e,t,n){let r=S(n,e);for(let i of t){if(r.fatalSeen)break;let t=I(i,n)?L(i,n):void 0;if(!t)continue;let o=u.join(e,i);(await a(o).catch(()=>{}))?.isSymbolicLink()||await k(o,t,`directory`,r)}return x(r,e)}async function x(e,t){let n=[],r=[],i=[];for(let a of e.outcomes){let e=await a;if(`fatal`in e){let a=V(e.fatal);return{displayRoot:t,files:n,errors:[...r,a],warnings:i,fatalError:a}}if(`error`in e){r.push(e.error);continue}n.push(e.file),e.warning!==void 0&&i.push(e.warning)}return{displayRoot:t,files:n,errors:r,warnings:i}}function S(e,t){return{options:e,outcomes:[],inFlight:new Set,fatalSeen:!1,visitedDirectories:new Set,visitedFiles:new Set,rootDirectory:t}}function C(e,t,n){e.outcomes.push({error:`${R(t,e.rootDirectory)}: ${V(n)}`})}async function w(e,t,n){try{return await e()}catch(e){C(n,t,e);return}}async function T(e,t){let n=await w(()=>c(e),e,t);return n!==void 0&&F(n,t.rootDirectory)?n:void 0}async function E(e,t){let n=await T(e,t);if(n===void 0||t.visitedDirectories.has(n))return;t.visitedDirectories.add(n);let r=await w(()=>s(e,{withFileTypes:!0}),e,t);if(r!==void 0)for(let n of r){if(t.fatalSeen)return;let r=u.join(e,n.name);if(n.isSymbolicLink()){await D(n.name,r,t);continue}if(n.isDirectory()){if(P(n.name,t.options))continue;await E(r,t);continue}n.isFile()&&await O(r,t)}}async function D(e,t,n){let r=await T(t,n);if(r===void 0)return;let i=await w(()=>l(t),t,n);if(i!==void 0){if(i.isDirectory()){if(P(e,n.options)||P(u.basename(r),n.options))return;await E(t,n);return}i.isFile()&&await O(t,n,r,r)}}async function O(e,t,n=e,r){let i=L(n,t.options);i&&await k(e,i,`directory`,t,r)}async function k(e,t,n,r,i){if(r.fatalSeen)return;let a;try{a=i??await c(e)}catch(t){C(r,e,t);return}if(r.visitedFiles.has(a))return;for(r.visitedFiles.add(a);r.inFlight.size>=v;)await Promise.race(r.inFlight);let o=A(e,t,n,r);r.inFlight.add(o),o.then(()=>r.inFlight.delete(o)),r.outcomes.push(o)}async function A(e,t,i,a){try{let n=await o(e,`utf8`),s={language:t,duplication:a.options.duplication};if(i===`single-file`)return{file:{file:e,metrics:r(n,s)}};let{metrics:c,crossFileData:l,crossFileError:u}=await j(n,s);return{file:{file:e,metrics:c,duplicationCandidates:l},warning:u===void 0?void 0:`${R(e,a.rootDirectory)}: cross-file duplication candidates unavailable: ${u}`}}catch(t){return t instanceof n?(a.fatalSeen=!0,{fatal:t}):{error:`${R(e,a.rootDirectory)}: ${V(t)}`}}}async function j(e,t){try{return await i(e,t)}catch(i){if(i instanceof n)throw i;return{metrics:r(e,t),crossFileError:V(i)}}}function M(t,n){if(t.fatalError||t.files.length<2)return;let r=t.files.flatMap(({file:e,duplicationCandidates:n})=>n?[{file:R(e,t.displayRoot),...n}]:[]);r.length<2||(t.crossFileDuplication=e(r,n.duplication))}function N(e,t,n){let r=new Set(e?.duplication.duplicateLineNumbers),i=t&&Object.hasOwn(t.duplicateLineNumbersByFile,n)?t.duplicateLineNumbersByFile[n]??[]:[];for(let e of i)r.add(e);return r}function P(e,t){return f.has(e)?!0:!t.includeTests&&p.has(e)}function F(e,t){let n=u.relative(t,e);return n===``||n!==`..`&&!n.startsWith(`..${u.sep}`)&&!u.isAbsolute(n)}function I(e,t){let n=e.split(`/`);for(let e of n.slice(0,-1))if(f.has(e)||!t.includeTests&&p.has(e))return!1;return L(e,t)!==void 0}function L(e,n,r=!1){let i=e.toLowerCase();if(!(!r&&(i.endsWith(`.d.ts`)||i.endsWith(`.d.mts`)||i.endsWith(`.d.cts`)||i.endsWith(`.min.js`)||i.endsWith(`.pnp.cjs`)))&&(r||n.includeTests||!m.test(u.basename(e))&&!h.test(u.basename(e))))return t(e)}function R(e,t){return u.relative(t,e)||u.basename(e)}function z(e){process.stdout.write(e)}function B(e){process.stderr.write(e)}function V(e){return e instanceof Error?e.message:String(e)}export{M as addCrossFileDuplication,N as collectDuplicatedLineNumbers,_ as configSearchDirectory,V as formatError,R as formatPath,L as getLanguage,I as isScannedPath,j as measureWithCrossFileData,g as resolveTarget,b as scanListedFiles,y as scanTarget,B as writeStderr,z as writeStdout};
2
2
  //# sourceMappingURL=scan.js.map
package/dist/scan.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"scan.js","names":[],"sources":["../src/scan.ts"],"sourcesContent":["import { lstat, readdir, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData } from './duplication.js';\nimport { detectLanguage } from './languages.js';\nimport { measureCode, measureCodeWithCrossFileData } from './metrics.js';\nimport { NativeAddonError } from './nativeMetrics.js';\nimport type { CodeMetrics, DuplicationOptions, LanguageName, MeasureOptions } from './types.js';\n\n/** The scan settings shared by every command (a structural subset of each command's options). */\nexport interface ScanOptions {\n duplication: Required<DuplicationOptions>;\n includeTests: boolean;\n}\n\nexport interface FileMetrics {\n file: string;\n metrics: CodeMetrics;\n /** Cross-file duplicate candidates and token/statement data, collected only for directory scans. */\n duplicationCandidates?: CrossFileDuplicationFileData;\n}\n\nexport interface ScanResult {\n crossFileDuplication?: CrossFileDuplicationMetrics;\n displayRoot: string;\n errors: string[];\n /** Non-fatal degradations (e.g. cross-file candidates unavailable); the file is still measured. */\n warnings: string[];\n fatalError?: string;\n files: FileMetrics[];\n}\n\nconst ignoredDirectoryNames = new Set([\n '.agents',\n '.claude',\n '.cursor',\n '.git',\n '.next',\n '.playwright-cli',\n '.tox',\n '.tmp',\n '.turbo',\n '.venv',\n '.yarn',\n '__fixtures__',\n '__generated__',\n '__pycache__',\n 'coverage',\n 'dist',\n 'fixtures',\n 'generated',\n 'node_modules',\n // .NET SDK intermediate output (generated sources such as `*.GlobalUsings.g.cs`).\n 'obj',\n 'target',\n 'test-fixtures',\n 'vendor',\n 'venv',\n]);\n\nconst testDirectoryNames = new Set(['__tests__', 'test', 'tests', 'spec']);\nconst testFilePattern = /(?:^test(?:[_-].*)?|\\.(?:spec|test)|[_-](?:test|spec))\\.[^.]+$/iu;\n// JUnit (Java/Kotlin) and xUnit/NUnit (C#) tests use case-sensitive `Test`/`Tests` class-name\n// suffixes; case-insensitive matching would catch production files like `contest.java`.\nconst suffixTestFilePattern = /Tests?\\.(?:java|kt|cs)$/u;\n\nexport function resolveTarget(target: string): string {\n if (target === '~') {\n return os.homedir();\n }\n\n if (target.startsWith('~/')) {\n return path.join(os.homedir(), target.slice(2));\n }\n\n return path.resolve(target);\n}\n\n/** Returns the directory from which the config file search should start (the target itself if it is a directory). */\nexport async function configSearchDirectory(target: string): Promise<string> {\n try {\n const targetStat = await stat(target);\n return targetStat.isDirectory() ? target : path.dirname(target);\n } catch {\n return path.dirname(target);\n }\n}\n\n/** Shared state of one scan, threaded through the directory walk instead of positional plumbing. */\ninterface ScanContext {\n options: ScanOptions;\n files: FileMetrics[];\n errors: string[];\n warnings: string[];\n visitedDirectories: Set<string>;\n visitedFiles: Set<string>;\n /** Scan root: paths are displayed relative to it, and symbolic links may not escape it. */\n rootDirectory: string;\n}\n\nexport async function scanTarget(target: string, options: ScanOptions): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n let canonicalTarget = target;\n try {\n canonicalTarget = await realpath(target);\n } catch {\n // stat below reports missing targets with the original path.\n }\n\n const fallbackDisplayRoot = path.dirname(canonicalTarget);\n let targetStat;\n\n try {\n targetStat = await stat(canonicalTarget);\n } catch (error) {\n const fatalError = `${formatPath(canonicalTarget, fallbackDisplayRoot)}: ${formatError(error)}`;\n return { displayRoot: fallbackDisplayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n if (targetStat.isFile()) {\n const displayRoot = path.dirname(canonicalTarget);\n const language = getLanguage(canonicalTarget, options, true);\n if (!language) {\n const fatalError = `${formatPath(canonicalTarget, displayRoot)}: unsupported file type`;\n return { displayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n const context = makeScanContext(options, files, errors, warnings, displayRoot);\n try {\n await measureFile(canonicalTarget, language, 'single-file', context, canonicalTarget);\n } catch (error) {\n return toFatalResult(error, displayRoot, files, errors, warnings);\n }\n return { displayRoot, files, errors, warnings };\n }\n\n try {\n await scanDirectory(canonicalTarget, makeScanContext(options, files, errors, warnings, canonicalTarget));\n } catch (error) {\n return toFatalResult(error, canonicalTarget, files, errors, warnings);\n }\n return { displayRoot: canonicalTarget, files, errors, warnings };\n}\n\n/**\n * Measures an explicit list of repository-relative files (the diff gate's git-visible allowlist)\n * instead of walking the directory tree, so ignored artifact directories are never parsed. Paths\n * outside the scan scope (ignored/test directories, unsupported or test file names) are skipped\n * with the same rules as the walk.\n */\nexport async function scanListedFiles(\n rootDirectory: string,\n relativePaths: Iterable<string>,\n options: ScanOptions\n): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n const context = makeScanContext(options, files, errors, warnings, rootDirectory);\n for (const relativePath of relativePaths) {\n const language = isScannedPath(relativePath, options) ? getLanguage(relativePath, options) : undefined;\n if (!language) {\n continue;\n }\n const absolutePath = path.join(rootDirectory, relativePath);\n // Symbolic links are not source files: git stores only their target string, so measuring\n // through them would diverge from what any revision of the repository actually contains.\n const stats = await lstat(absolutePath).catch(() => {});\n if (stats?.isSymbolicLink()) {\n continue;\n }\n try {\n await measureFile(absolutePath, language, 'directory', context);\n } catch (error) {\n return toFatalResult(error, rootDirectory, files, errors, warnings);\n }\n }\n return { displayRoot: rootDirectory, files, errors, warnings };\n}\n\n/** A run-wide failure (a missing native addon) as a fatal result; anything else keeps throwing. */\nfunction toFatalResult(\n error: unknown,\n displayRoot: string,\n files: FileMetrics[],\n errors: string[],\n warnings: string[]\n): ScanResult {\n if (!(error instanceof NativeAddonError)) {\n throw error;\n }\n const fatalError = formatError(error);\n // Errors the walk accumulated before the fatal failure stay reported alongside it.\n return { displayRoot, files, errors: [...errors, fatalError], warnings, fatalError };\n}\n\nfunction makeScanContext(\n options: ScanOptions,\n files: FileMetrics[],\n errors: string[],\n warnings: string[],\n rootDirectory: string\n): ScanContext {\n return { options, files, errors, warnings, visitedDirectories: new Set(), visitedFiles: new Set(), rootDirectory };\n}\n\n/** Runs a filesystem operation, recording a scan error and returning undefined when it fails. */\nasync function tryFileSystem<T>(\n operation: () => Promise<T>,\n target: string,\n context: ScanContext\n): Promise<T | undefined> {\n try {\n return await operation();\n } catch (error) {\n context.errors.push(`${formatPath(target, context.rootDirectory)}: ${formatError(error)}`);\n return undefined;\n }\n}\n\n/** Resolves the path (recording errors); undefined when that fails or the result escapes the root. */\nasync function resolveWithinRoot(target: string, context: ScanContext): Promise<string | undefined> {\n const resolved = await tryFileSystem(() => realpath(target), target, context);\n return resolved !== undefined && isWithinDirectory(resolved, context.rootDirectory) ? resolved : undefined;\n}\n\nasync function scanDirectory(directory: string, context: ScanContext): Promise<void> {\n const resolvedDirectory = await resolveWithinRoot(directory, context);\n if (resolvedDirectory === undefined || context.visitedDirectories.has(resolvedDirectory)) {\n return;\n }\n context.visitedDirectories.add(resolvedDirectory);\n\n const entries = await tryFileSystem(() => readdir(directory, { withFileTypes: true }), directory, context);\n if (entries === undefined) {\n return;\n }\n\n for (const entry of entries) {\n const entryPath = path.join(directory, entry.name);\n if (entry.isSymbolicLink()) {\n await scanSymbolicLink(entry.name, entryPath, context);\n continue;\n }\n\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name, context.options)) {\n continue;\n }\n await scanDirectory(entryPath, context);\n continue;\n }\n\n if (entry.isFile()) {\n await measureScannableFile(entryPath, context);\n }\n }\n}\n\nasync function scanSymbolicLink(name: string, entryPath: string, context: ScanContext): Promise<void> {\n const resolvedPath = await resolveWithinRoot(entryPath, context);\n if (resolvedPath === undefined) {\n return;\n }\n\n const entryStat = await tryFileSystem(() => stat(entryPath), entryPath, context);\n if (entryStat === undefined) {\n return;\n }\n\n if (entryStat.isDirectory()) {\n if (\n shouldSkipDirectory(name, context.options) ||\n shouldSkipDirectory(path.basename(resolvedPath), context.options)\n ) {\n return;\n }\n await scanDirectory(entryPath, context);\n return;\n }\n\n if (entryStat.isFile()) {\n await measureScannableFile(entryPath, context, resolvedPath, resolvedPath);\n }\n}\n\nasync function measureScannableFile(\n file: string,\n context: ScanContext,\n languageFile = file,\n realFile?: string\n): Promise<void> {\n const language = getLanguage(languageFile, context.options);\n if (language) {\n await measureFile(file, language, 'directory', context, realFile);\n }\n}\n\nasync function measureFile(\n file: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext,\n realFile?: string\n): Promise<void> {\n try {\n const resolvedFile = realFile ?? (await realpath(file));\n if (context.visitedFiles.has(resolvedFile)) {\n return;\n }\n context.visitedFiles.add(resolvedFile);\n\n const code = await readFile(file, 'utf8');\n const measureOptions = { language, duplication: context.options.duplication };\n // Only directory scans compare files against each other; a single-file target has no peers.\n if (mode === 'single-file') {\n context.files.push({ file, metrics: measureCode(code, measureOptions) });\n return;\n }\n const { metrics, crossFileData, crossFileError } = measureWithCrossFileData(code, measureOptions);\n if (crossFileError !== undefined) {\n // A warning, not an error: the file's metrics are complete, only its participation in\n // cross-file matching is lost, so it is not \"skipped\" and must not fail --fail-on-error.\n context.warnings.push(\n `${formatPath(file, context.rootDirectory)}: cross-file duplication candidates unavailable: ${crossFileError}`\n );\n }\n context.files.push({ file, metrics, duplicationCandidates: crossFileData });\n } catch (error) {\n // A missing native addon fails every file identically: propagate it once as a fatal scan\n // error instead of recording one \"skipped\" entry per file behind a successful exit code.\n if (error instanceof NativeAddonError) {\n throw error;\n }\n context.errors.push(`${formatPath(file, context.rootDirectory)}: ${formatError(error)}`);\n }\n}\n\n/**\n * Measures a file together with its cross-file contribution from one parse. The contribution is\n * auxiliary: if collecting it fails where plain measurement succeeds (e.g. a payload too large to\n * cross the addon boundary), the metrics are still returned with the failure message, which\n * callers report as a warning rather than an error.\n */\nexport function measureWithCrossFileData(\n code: string,\n measureOptions: MeasureOptions\n): { metrics: CodeMetrics; crossFileData?: CrossFileDuplicationFileData; crossFileError?: string } {\n try {\n return measureCodeWithCrossFileData(code, measureOptions);\n } catch (error) {\n if (error instanceof NativeAddonError) {\n throw error;\n }\n return { metrics: measureCode(code, measureOptions), crossFileError: formatError(error) };\n }\n}\n\n/** Runs after the scan so every measured file's candidates participate. */\nexport function addCrossFileDuplication(result: ScanResult, options: ScanOptions): void {\n if (result.fatalError || result.files.length < 2) {\n return;\n }\n const sourceFiles = result.files.flatMap(({ file, duplicationCandidates }) =>\n duplicationCandidates ? [{ file: formatPath(file, result.displayRoot), ...duplicationCandidates }] : []\n );\n if (sourceFiles.length < 2) {\n return;\n }\n result.crossFileDuplication = measureCrossFileDuplication(sourceFiles, options.duplication);\n}\n\n/**\n * Distinct 1-based code lines covered by within-file or cross-file duplicated content. Both\n * sources expose the exact lines carrying matched tokens (never block bounding ranges, which\n * would over-count comment/blank lines and the unmatched gap of a merged clone), so the union is\n * a subset of the file's code lines and a ratio derived over code lines can never exceed 1.\n */\nexport function collectDuplicatedLineNumbers(\n metrics: CodeMetrics | undefined,\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n formattedFile: string\n): Set<number> {\n const lines = new Set(metrics?.duplication.duplicateLineNumbers);\n // Object.hasOwn: a file named like an Object.prototype member must not read an inherited value.\n const crossFileLines =\n crossFileDuplication && Object.hasOwn(crossFileDuplication.duplicateLineNumbersByFile, formattedFile)\n ? (crossFileDuplication.duplicateLineNumbersByFile[formattedFile] ?? [])\n : [];\n for (const line of crossFileLines) {\n lines.add(line);\n }\n return lines;\n}\n\nfunction shouldSkipDirectory(name: string, options: ScanOptions): boolean {\n if (ignoredDirectoryNames.has(name)) {\n return true;\n }\n\n if (options.includeTests) {\n return false;\n }\n\n return testDirectoryNames.has(name);\n}\n\nfunction isWithinDirectory(candidate: string, directory: string): boolean {\n const relative = path.relative(directory, candidate);\n return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));\n}\n\n/**\n * Whether a repository-relative path would be scanned: no ignored or excluded-test directory\n * segment and a supported, non-test file name. The diff gate uses this for base-revision\n * eligibility, so code renamed into scan scope gates as new code instead of ratcheting against\n * a blob the scanner would never have measured.\n */\nexport function isScannedPath(relativePath: string, options: ScanOptions): boolean {\n const segments = relativePath.split('/');\n for (const segment of segments.slice(0, -1)) {\n if (ignoredDirectoryNames.has(segment) || (!options.includeTests && testDirectoryNames.has(segment))) {\n return false;\n }\n }\n return getLanguage(relativePath, options) !== undefined;\n}\n\nexport function getLanguage(file: string, options: ScanOptions, explicitTarget = false): LanguageName | undefined {\n const lowerFile = file.toLowerCase();\n if (\n !explicitTarget &&\n (lowerFile.endsWith('.d.ts') ||\n lowerFile.endsWith('.d.mts') ||\n lowerFile.endsWith('.d.cts') ||\n lowerFile.endsWith('.min.js') ||\n lowerFile.endsWith('.pnp.cjs'))\n ) {\n return undefined;\n }\n\n if (\n !explicitTarget &&\n !options.includeTests &&\n (testFilePattern.test(path.basename(file)) || suffixTestFilePattern.test(path.basename(file)))\n ) {\n return undefined;\n }\n\n return detectLanguage(file);\n}\n\nexport function formatPath(file: string, base: string): string {\n return path.relative(base, file) || path.basename(file);\n}\n\nexport function writeStdout(message: string): void {\n process.stdout.write(message);\n}\n\nexport function writeStderr(message: string): void {\n process.stderr.write(message);\n}\n\nexport function formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"scan.js","names":[],"sources":["../src/scan.ts"],"sourcesContent":["import { lstat, readdir, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData } from './duplication.js';\nimport { detectLanguage } from './languages.js';\nimport { measureCode, measureCodeWithCrossFileDataAsync } from './metrics.js';\nimport { NativeAddonError } from './nativeMetrics.js';\nimport type { CodeMetrics, DuplicationOptions, LanguageName, MeasureOptions } from './types.js';\n\n/** The scan settings shared by every command (a structural subset of each command's options). */\nexport interface ScanOptions {\n duplication: Required<DuplicationOptions>;\n includeTests: boolean;\n}\n\nexport interface FileMetrics {\n file: string;\n metrics: CodeMetrics;\n /** Cross-file duplicate candidates and token/statement data, collected only for directory scans. */\n duplicationCandidates?: CrossFileDuplicationFileData;\n}\n\nexport interface ScanResult {\n crossFileDuplication?: CrossFileDuplicationMetrics;\n displayRoot: string;\n errors: string[];\n /** Non-fatal degradations (e.g. cross-file candidates unavailable); the file is still measured. */\n warnings: string[];\n fatalError?: string;\n files: FileMetrics[];\n}\n\nconst ignoredDirectoryNames = new Set([\n '.agents',\n '.claude',\n '.cursor',\n '.git',\n '.next',\n '.playwright-cli',\n '.tox',\n '.tmp',\n '.turbo',\n '.venv',\n '.yarn',\n '__fixtures__',\n '__generated__',\n '__pycache__',\n 'coverage',\n 'dist',\n 'fixtures',\n 'generated',\n 'node_modules',\n // .NET SDK intermediate output (generated sources such as `*.GlobalUsings.g.cs`).\n 'obj',\n 'target',\n 'test-fixtures',\n 'vendor',\n 'venv',\n]);\n\nconst testDirectoryNames = new Set(['__tests__', 'test', 'tests', 'spec']);\nconst testFilePattern = /(?:^test(?:[_-].*)?|\\.(?:spec|test)|[_-](?:test|spec))\\.[^.]+$/iu;\n// JUnit (Java/Kotlin) and xUnit/NUnit (C#) tests use case-sensitive `Test`/`Tests` class-name\n// suffixes; case-insensitive matching would catch production files like `contest.java`.\nconst suffixTestFilePattern = /Tests?\\.(?:java|kt|cs)$/u;\n\nexport function resolveTarget(target: string): string {\n if (target === '~') {\n return os.homedir();\n }\n\n if (target.startsWith('~/')) {\n return path.join(os.homedir(), target.slice(2));\n }\n\n return path.resolve(target);\n}\n\n/** Returns the directory from which the config file search should start (the target itself if it is a directory). */\nexport async function configSearchDirectory(target: string): Promise<string> {\n try {\n const targetStat = await stat(target);\n return targetStat.isDirectory() ? target : path.dirname(target);\n } catch {\n return path.dirname(target);\n }\n}\n\n/** Shared state of one scan, threaded through the directory walk instead of positional plumbing. */\ninterface ScanContext {\n options: ScanOptions;\n /**\n * Outcomes in walk order. Files are measured concurrently, so a measurement is recorded as a\n * promise here and applied in this order once the walk ends, keeping results deterministic.\n */\n outcomes: (ScanOutcome | Promise<ScanOutcome>)[];\n /** Measurements in flight, bounded so file contents and payloads do not pile up during the walk. */\n inFlight: Set<Promise<ScanOutcome>>;\n /** Set once a measurement fails fatally; the walk then starts no further work. */\n fatalSeen: boolean;\n visitedDirectories: Set<string>;\n visitedFiles: Set<string>;\n /** Scan root: paths are displayed relative to it, and symbolic links may not escape it. */\n rootDirectory: string;\n}\n\n/**\n * A missing native addon fails every file identically, so it ends the scan as one fatal error\n * instead of one \"skipped\" entry per file behind a successful exit code.\n */\ntype ScanOutcome = { file: FileMetrics; warning?: string } | { error: string } | { fatal: NativeAddonError };\n\n// Twice the addon's worker count keeps its pool busy while finished payloads are parsed.\nconst maxMeasurementsInFlight = os.availableParallelism() * 2;\n\nexport async function scanTarget(target: string, options: ScanOptions): Promise<ScanResult> {\n let canonicalTarget = target;\n try {\n canonicalTarget = await realpath(target);\n } catch {\n // stat below reports missing targets with the original path.\n }\n\n const fallbackDisplayRoot = path.dirname(canonicalTarget);\n let targetStat;\n\n try {\n targetStat = await stat(canonicalTarget);\n } catch (error) {\n const fatalError = `${formatPath(canonicalTarget, fallbackDisplayRoot)}: ${formatError(error)}`;\n return { displayRoot: fallbackDisplayRoot, files: [], errors: [fatalError], warnings: [], fatalError };\n }\n\n if (targetStat.isFile()) {\n const displayRoot = path.dirname(canonicalTarget);\n const language = getLanguage(canonicalTarget, options, true);\n if (!language) {\n const fatalError = `${formatPath(canonicalTarget, displayRoot)}: unsupported file type`;\n return { displayRoot, files: [], errors: [fatalError], warnings: [], fatalError };\n }\n\n const context = makeScanContext(options, displayRoot);\n await measureFile(canonicalTarget, language, 'single-file', context, canonicalTarget);\n return settleScan(context, displayRoot);\n }\n\n const context = makeScanContext(options, canonicalTarget);\n await scanDirectory(canonicalTarget, context);\n return settleScan(context, canonicalTarget);\n}\n\n/**\n * Measures an explicit list of repository-relative files (the diff gate's git-visible allowlist)\n * instead of walking the directory tree, so ignored artifact directories are never parsed. Paths\n * outside the scan scope (ignored/test directories, unsupported or test file names) are skipped\n * with the same rules as the walk.\n */\nexport async function scanListedFiles(\n rootDirectory: string,\n relativePaths: Iterable<string>,\n options: ScanOptions\n): Promise<ScanResult> {\n const context = makeScanContext(options, rootDirectory);\n for (const relativePath of relativePaths) {\n if (context.fatalSeen) {\n break;\n }\n const language = isScannedPath(relativePath, options) ? getLanguage(relativePath, options) : undefined;\n if (!language) {\n continue;\n }\n const absolutePath = path.join(rootDirectory, relativePath);\n // Symbolic links are not source files: git stores only their target string, so measuring\n // through them would diverge from what any revision of the repository actually contains.\n const stats = await lstat(absolutePath).catch(() => {});\n if (stats?.isSymbolicLink()) {\n continue;\n }\n await measureFile(absolutePath, language, 'directory', context);\n }\n return settleScan(context, rootDirectory);\n}\n\n/** Applies the scan's outcomes in walk order once every measurement has settled. */\nasync function settleScan(context: ScanContext, displayRoot: string): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n for (const pending of context.outcomes) {\n const outcome = await pending;\n if ('fatal' in outcome) {\n const fatalError = formatError(outcome.fatal);\n // Errors the walk recorded before the fatal failure stay reported alongside it.\n return { displayRoot, files, errors: [...errors, fatalError], warnings, fatalError };\n }\n if ('error' in outcome) {\n errors.push(outcome.error);\n continue;\n }\n files.push(outcome.file);\n if (outcome.warning !== undefined) {\n warnings.push(outcome.warning);\n }\n }\n return { displayRoot, files, errors, warnings };\n}\n\nfunction makeScanContext(options: ScanOptions, rootDirectory: string): ScanContext {\n return {\n options,\n outcomes: [],\n inFlight: new Set(),\n fatalSeen: false,\n visitedDirectories: new Set(),\n visitedFiles: new Set(),\n rootDirectory,\n };\n}\n\nfunction recordError(context: ScanContext, target: string, error: unknown): void {\n context.outcomes.push({ error: `${formatPath(target, context.rootDirectory)}: ${formatError(error)}` });\n}\n\n/** Runs a filesystem operation, recording a scan error and returning undefined when it fails. */\nasync function tryFileSystem<T>(\n operation: () => Promise<T>,\n target: string,\n context: ScanContext\n): Promise<T | undefined> {\n try {\n return await operation();\n } catch (error) {\n recordError(context, target, error);\n return undefined;\n }\n}\n\n/** Resolves the path (recording errors); undefined when that fails or the result escapes the root. */\nasync function resolveWithinRoot(target: string, context: ScanContext): Promise<string | undefined> {\n const resolved = await tryFileSystem(() => realpath(target), target, context);\n return resolved !== undefined && isWithinDirectory(resolved, context.rootDirectory) ? resolved : undefined;\n}\n\nasync function scanDirectory(directory: string, context: ScanContext): Promise<void> {\n const resolvedDirectory = await resolveWithinRoot(directory, context);\n if (resolvedDirectory === undefined || context.visitedDirectories.has(resolvedDirectory)) {\n return;\n }\n context.visitedDirectories.add(resolvedDirectory);\n\n const entries = await tryFileSystem(() => readdir(directory, { withFileTypes: true }), directory, context);\n if (entries === undefined) {\n return;\n }\n\n for (const entry of entries) {\n if (context.fatalSeen) {\n return;\n }\n const entryPath = path.join(directory, entry.name);\n if (entry.isSymbolicLink()) {\n await scanSymbolicLink(entry.name, entryPath, context);\n continue;\n }\n\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name, context.options)) {\n continue;\n }\n await scanDirectory(entryPath, context);\n continue;\n }\n\n if (entry.isFile()) {\n await measureScannableFile(entryPath, context);\n }\n }\n}\n\nasync function scanSymbolicLink(name: string, entryPath: string, context: ScanContext): Promise<void> {\n const resolvedPath = await resolveWithinRoot(entryPath, context);\n if (resolvedPath === undefined) {\n return;\n }\n\n const entryStat = await tryFileSystem(() => stat(entryPath), entryPath, context);\n if (entryStat === undefined) {\n return;\n }\n\n if (entryStat.isDirectory()) {\n if (\n shouldSkipDirectory(name, context.options) ||\n shouldSkipDirectory(path.basename(resolvedPath), context.options)\n ) {\n return;\n }\n await scanDirectory(entryPath, context);\n return;\n }\n\n if (entryStat.isFile()) {\n await measureScannableFile(entryPath, context, resolvedPath, resolvedPath);\n }\n}\n\nasync function measureScannableFile(\n file: string,\n context: ScanContext,\n languageFile = file,\n realFile?: string\n): Promise<void> {\n const language = getLanguage(languageFile, context.options);\n if (language) {\n await measureFile(file, language, 'directory', context, realFile);\n }\n}\n\n/**\n * Resolves and deduplicates the file in walk order, then starts measuring it concurrently; its\n * outcome is recorded in walk order (see ScanContext.outcomes).\n */\nasync function measureFile(\n file: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext,\n realFile?: string\n): Promise<void> {\n if (context.fatalSeen) {\n return;\n }\n let resolvedFile;\n try {\n resolvedFile = realFile ?? (await realpath(file));\n } catch (error) {\n recordError(context, file, error);\n return;\n }\n if (context.visitedFiles.has(resolvedFile)) {\n return;\n }\n context.visitedFiles.add(resolvedFile);\n\n // Any settled measurement frees its slot (removed by the callback below, which runs before the\n // race resumes), so one slow file never idles the pool.\n while (context.inFlight.size >= maxMeasurementsInFlight) {\n await Promise.race(context.inFlight);\n }\n const outcome = readAndMeasureFile(file, language, mode, context);\n context.inFlight.add(outcome);\n void outcome.then(() => context.inFlight.delete(outcome));\n context.outcomes.push(outcome);\n}\n\nasync function readAndMeasureFile(\n file: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext\n): Promise<ScanOutcome> {\n try {\n const code = await readFile(file, 'utf8');\n const measureOptions = { language, duplication: context.options.duplication };\n // Only directory scans compare files against each other; a single-file target has no peers.\n if (mode === 'single-file') {\n return { file: { file, metrics: measureCode(code, measureOptions) } };\n }\n const { metrics, crossFileData, crossFileError } = await measureWithCrossFileData(code, measureOptions);\n return {\n file: { file, metrics, duplicationCandidates: crossFileData },\n // A warning, not an error: the file's metrics are complete, only its participation in\n // cross-file matching is lost, so it is not \"skipped\" and must not fail --fail-on-error.\n warning:\n crossFileError === undefined\n ? undefined\n : `${formatPath(file, context.rootDirectory)}: cross-file duplication candidates unavailable: ${crossFileError}`,\n };\n } catch (error) {\n if (error instanceof NativeAddonError) {\n context.fatalSeen = true;\n return { fatal: error };\n }\n return { error: `${formatPath(file, context.rootDirectory)}: ${formatError(error)}` };\n }\n}\n\n/**\n * Measures a file together with its cross-file contribution from one parse. The contribution is\n * auxiliary: if collecting it fails where plain measurement succeeds (e.g. a payload too large to\n * cross the addon boundary), the metrics are still returned with the failure message, which\n * callers report as a warning rather than an error.\n */\nexport async function measureWithCrossFileData(\n code: string,\n measureOptions: MeasureOptions\n): Promise<{ metrics: CodeMetrics; crossFileData?: CrossFileDuplicationFileData; crossFileError?: string }> {\n try {\n return await measureCodeWithCrossFileDataAsync(code, measureOptions);\n } catch (error) {\n if (error instanceof NativeAddonError) {\n throw error;\n }\n return { metrics: measureCode(code, measureOptions), crossFileError: formatError(error) };\n }\n}\n\n/** Runs after the scan so every measured file's candidates participate. */\nexport function addCrossFileDuplication(result: ScanResult, options: ScanOptions): void {\n if (result.fatalError || result.files.length < 2) {\n return;\n }\n const sourceFiles = result.files.flatMap(({ file, duplicationCandidates }) =>\n duplicationCandidates ? [{ file: formatPath(file, result.displayRoot), ...duplicationCandidates }] : []\n );\n if (sourceFiles.length < 2) {\n return;\n }\n result.crossFileDuplication = measureCrossFileDuplication(sourceFiles, options.duplication);\n}\n\n/**\n * Distinct 1-based code lines covered by within-file or cross-file duplicated content. Both\n * sources expose the exact lines carrying matched tokens (never block bounding ranges, which\n * would over-count comment/blank lines and the unmatched gap of a merged clone), so the union is\n * a subset of the file's code lines and a ratio derived over code lines can never exceed 1.\n */\nexport function collectDuplicatedLineNumbers(\n metrics: CodeMetrics | undefined,\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n formattedFile: string\n): Set<number> {\n const lines = new Set(metrics?.duplication.duplicateLineNumbers);\n // Object.hasOwn: a file named like an Object.prototype member must not read an inherited value.\n const crossFileLines =\n crossFileDuplication && Object.hasOwn(crossFileDuplication.duplicateLineNumbersByFile, formattedFile)\n ? (crossFileDuplication.duplicateLineNumbersByFile[formattedFile] ?? [])\n : [];\n for (const line of crossFileLines) {\n lines.add(line);\n }\n return lines;\n}\n\nfunction shouldSkipDirectory(name: string, options: ScanOptions): boolean {\n if (ignoredDirectoryNames.has(name)) {\n return true;\n }\n\n if (options.includeTests) {\n return false;\n }\n\n return testDirectoryNames.has(name);\n}\n\nfunction isWithinDirectory(candidate: string, directory: string): boolean {\n const relative = path.relative(directory, candidate);\n return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));\n}\n\n/**\n * Whether a repository-relative path would be scanned: no ignored or excluded-test directory\n * segment and a supported, non-test file name. The diff gate uses this for base-revision\n * eligibility, so code renamed into scan scope gates as new code instead of ratcheting against\n * a blob the scanner would never have measured.\n */\nexport function isScannedPath(relativePath: string, options: ScanOptions): boolean {\n const segments = relativePath.split('/');\n for (const segment of segments.slice(0, -1)) {\n if (ignoredDirectoryNames.has(segment) || (!options.includeTests && testDirectoryNames.has(segment))) {\n return false;\n }\n }\n return getLanguage(relativePath, options) !== undefined;\n}\n\nexport function getLanguage(file: string, options: ScanOptions, explicitTarget = false): LanguageName | undefined {\n const lowerFile = file.toLowerCase();\n if (\n !explicitTarget &&\n (lowerFile.endsWith('.d.ts') ||\n lowerFile.endsWith('.d.mts') ||\n lowerFile.endsWith('.d.cts') ||\n lowerFile.endsWith('.min.js') ||\n lowerFile.endsWith('.pnp.cjs'))\n ) {\n return undefined;\n }\n\n if (\n !explicitTarget &&\n !options.includeTests &&\n (testFilePattern.test(path.basename(file)) || suffixTestFilePattern.test(path.basename(file)))\n ) {\n return undefined;\n }\n\n return detectLanguage(file);\n}\n\nexport function formatPath(file: string, base: string): string {\n return path.relative(base, file) || path.basename(file);\n}\n\nexport function writeStdout(message: string): void {\n process.stdout.write(message);\n}\n\nexport function writeStderr(message: string): void {\n process.stderr.write(message);\n}\n\nexport function formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ "use strict";const e=require("./nativeMetrics.cjs"),t=new TextEncoder,n=new TextDecoder;function r(e){let t,r,s=a=>{if(!t){if(r)throw r;try{t=i(e)}catch(e){throw r=e,e}}let{exports:o,stderr:s}=t;s.length=0;let c;try{c=a(o)}catch(e){throw t=void 0,Error(`The code-gauge WebAssembly module crashed: ${s.join(``).trim()||String(e)}`,{cause:e})}let l=n.decode(new Uint8Array(o.memory.buffer,o.result_ptr(),o.result_len()));if(c!==0)throw Error(l);return l},c=(e,t,n,r,i,c,l)=>s(s=>s.measure_code(...a(s,e),...a(s,t),Number(n),o(r),o(i),o(c),Number(l)));return{measureCodeNative:c,measureCodeNativeAsync:async(...e)=>c(...e),collectCrossFileDataNative:(e,t,n)=>s(r=>r.collect_cross_file_data(...a(r,e),...a(r,t),o(n))),collectFunctionTokenSequencesNative:(e,t)=>s(n=>n.collect_function_token_sequences(...a(n,e),...a(n,t)))}}function i(t){let n=[],r,i=new WebAssembly.Instance(t,{wasi_snapshot_preview1:c(()=>r,n)}).exports;r=i.memory;let a=i.payload_version();if(a!==8)throw new e.NativeAddonError(`The code-gauge WebAssembly module has payload version ${a}, but 8 is expected; reinstall code-gauge, or in a code-gauge repository checkout, rebuild it with \`bun run build-wasm\``);return{exports:i,stderr:n}}function a(e,n){let r=t.encode(n),i=e.alloc(r.length);return new Uint8Array(e.memory.buffer,i,r.length).set(r),[i,r.length]}function o(e){return e??-1}const s=65536;function c(e,t){let r=()=>new DataView(e().buffer),i=()=>8;return{environ_get:()=>0,environ_sizes_get:(e,t)=>(r().setUint32(e,0,!0),r().setUint32(t,0,!0),0),clock_time_get:(e,t,n)=>(r().setBigUint64(n,BigInt(Date.now())*1000000n,!0),0),random_get:(t,n)=>{for(let r=0;r<n;r+=s)crypto.getRandomValues(new Uint8Array(e().buffer,t+r,Math.min(s,n-r)));return 0},fd_write:(i,a,o,s)=>{let c=r(),l=0;for(let r=0;r<o;r++){let o=c.getUint32(a+r*8,!0),s=c.getUint32(a+r*8+4,!0);i===2&&t.push(n.decode(new Uint8Array(e().buffer,o,s))),l+=s}return c.setUint32(s,l,!0),0},fd_close:i,fd_fdstat_get:i,fd_fdstat_set_flags:i,fd_read:i,fd_seek:i,proc_exit:e=>{throw Error(`The code-gauge WebAssembly module exited with code ${e}`)}}}exports.createWasmBinding=r;
2
+ //# sourceMappingURL=wasmBinding.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wasmBinding.cjs","names":["NativeAddonError"],"sources":["../src/wasmBinding.ts"],"sourcesContent":["import { expectedPayloadVersion, NativeAddonError, type NativeBinding } from './nativeMetrics.js';\n\n/** The C ABI exported by native/src/wasm.rs. */\ninterface WasmExports {\n memory: WebAssembly.Memory;\n payload_version(): number;\n alloc(length: number): number;\n result_ptr(): number;\n result_len(): number;\n measure_code(\n codePtr: number,\n codeLength: number,\n languagePtr: number,\n languageLength: number,\n includeSyntaxTree: number,\n minTokens: number,\n maxGapTokens: number,\n minSimilarityPercent: number,\n includeCrossFileData: number\n ): number;\n collect_cross_file_data(\n codePtr: number,\n codeLength: number,\n languagePtr: number,\n languageLength: number,\n minTokens: number\n ): number;\n collect_function_token_sequences(\n codePtr: number,\n codeLength: number,\n languagePtr: number,\n languageLength: number\n ): number;\n}\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\n/**\n * Wraps the WebAssembly build of the native addon (native/src/wasm.rs) as a NativeBinding. The\n * module is instantiated synchronously on first use and again after a trap (a panic or stack\n * overflow), because a trap leaves the instance's memory and stack pointer in an undefined state.\n * An instantiation failure (e.g., a payload version mismatch) is memoized like the N-API loader's,\n * since instantiating the same module again would fail again.\n */\nexport function createWasmBinding(module: WebAssembly.Module): NativeBinding {\n let instance: { exports: WasmExports; stderr: string[] } | undefined;\n let instantiationFailure: unknown;\n\n const call = (invoke: (exports: WasmExports) => number): string => {\n if (!instance) {\n if (instantiationFailure) {\n throw instantiationFailure;\n }\n try {\n instance = instantiate(module);\n } catch (error) {\n instantiationFailure = error;\n throw error;\n }\n }\n const { exports, stderr } = instance;\n stderr.length = 0;\n let status: number;\n try {\n status = invoke(exports);\n } catch (error) {\n instance = undefined;\n throw new Error(`The code-gauge WebAssembly module crashed: ${stderr.join('').trim() || String(error)}`, {\n cause: error,\n });\n }\n const result = decoder.decode(new Uint8Array(exports.memory.buffer, exports.result_ptr(), exports.result_len()));\n if (status !== 0) {\n throw new Error(result);\n }\n return result;\n };\n\n const measureCodeNative: NativeBinding['measureCodeNative'] = (\n code,\n language,\n includeSyntaxTree,\n minTokens,\n maxGapTokens,\n minSimilarityPercent,\n includeCrossFileData\n ) =>\n call((exports) =>\n exports.measure_code(\n ...passString(exports, code),\n ...passString(exports, language),\n Number(includeSyntaxTree),\n toOptionalU32(minTokens),\n toOptionalU32(maxGapTokens),\n toOptionalU32(minSimilarityPercent),\n Number(includeCrossFileData)\n )\n );\n\n return {\n measureCodeNative,\n // The WebAssembly build has no threads, so the asynchronous form measures in place.\n measureCodeNativeAsync: async (...args) => measureCodeNative(...args),\n collectCrossFileDataNative: (code, language, minTokens) =>\n call((exports) =>\n exports.collect_cross_file_data(\n ...passString(exports, code),\n ...passString(exports, language),\n toOptionalU32(minTokens)\n )\n ),\n collectFunctionTokenSequencesNative: (code, language) =>\n call((exports) =>\n exports.collect_function_token_sequences(...passString(exports, code), ...passString(exports, language))\n ),\n };\n}\n\nfunction instantiate(module: WebAssembly.Module): { exports: WasmExports; stderr: string[] } {\n const stderr: string[] = [];\n let memory: WebAssembly.Memory | undefined;\n const instance = new WebAssembly.Instance(module, {\n wasi_snapshot_preview1: createWasiImports(() => memory as WebAssembly.Memory, stderr),\n });\n const exports = instance.exports as unknown as WasmExports;\n memory = exports.memory;\n const version = exports.payload_version();\n if (version !== expectedPayloadVersion) {\n throw new NativeAddonError(\n `The code-gauge WebAssembly module has payload version ${version}, but ${expectedPayloadVersion} is ` +\n 'expected; reinstall code-gauge, or in a code-gauge repository checkout, rebuild it with `bun run build-wasm`'\n );\n }\n return { exports, stderr };\n}\n\n/** Copies a string into a buffer whose ownership passes to the called export. */\nfunction passString(exports: WasmExports, text: string): [number, number] {\n // TextEncoder replaces lone surrogates with U+FFFD, like toWellFormed() does for the N-API addon.\n const bytes = encoder.encode(text);\n const pointer = exports.alloc(bytes.length);\n new Uint8Array(exports.memory.buffer, pointer, bytes.length).set(bytes);\n return [pointer, bytes.length];\n}\n\n/** native/src/wasm.rs reads a negative value as an absent setting. */\nfunction toOptionalU32(value: number | undefined): number {\n return value ?? -1;\n}\n\nconst WASI_ERRNO_SUCCESS = 0;\nconst WASI_ERRNO_BADF = 8;\n// crypto.getRandomValues() rejects requests larger than this.\nconst MAX_RANDOM_BYTES = 65_536;\n\n/**\n * The WASI preview 1 functions the module imports. The metrics code performs no I/O, so file\n * descriptors are unavailable except for writes, whose stderr output (e.g., a panic message) is\n * kept for the error raised when the module traps.\n */\nfunction createWasiImports(\n getMemory: () => WebAssembly.Memory,\n stderr: string[]\n): Record<string, (...args: never[]) => number> {\n const view = (): DataView => new DataView(getMemory().buffer);\n const unavailable = (): number => WASI_ERRNO_BADF;\n return {\n environ_get: () => WASI_ERRNO_SUCCESS,\n environ_sizes_get: (countPointer: number, sizePointer: number) => {\n view().setUint32(countPointer, 0, true);\n view().setUint32(sizePointer, 0, true);\n return WASI_ERRNO_SUCCESS;\n },\n clock_time_get: (_clockId: number, _precision: bigint, timePointer: number) => {\n view().setBigUint64(timePointer, BigInt(Date.now()) * 1_000_000n, true);\n return WASI_ERRNO_SUCCESS;\n },\n random_get: (pointer: number, length: number) => {\n for (let offset = 0; offset < length; offset += MAX_RANDOM_BYTES) {\n crypto.getRandomValues(\n new Uint8Array(getMemory().buffer, pointer + offset, Math.min(MAX_RANDOM_BYTES, length - offset))\n );\n }\n return WASI_ERRNO_SUCCESS;\n },\n fd_write: (fd: number, iovsPointer: number, iovsLength: number, writtenPointer: number) => {\n const memoryView = view();\n let written = 0;\n for (let index = 0; index < iovsLength; index++) {\n const pointer = memoryView.getUint32(iovsPointer + index * 8, true);\n const length = memoryView.getUint32(iovsPointer + index * 8 + 4, true);\n if (fd === 2) {\n stderr.push(decoder.decode(new Uint8Array(getMemory().buffer, pointer, length)));\n }\n written += length;\n }\n memoryView.setUint32(writtenPointer, written, true);\n return WASI_ERRNO_SUCCESS;\n },\n fd_close: unavailable,\n fd_fdstat_get: unavailable,\n fd_fdstat_set_flags: unavailable,\n fd_read: unavailable,\n fd_seek: unavailable,\n proc_exit: (code: number) => {\n throw new Error(`The code-gauge WebAssembly module exited with code ${code}`);\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import { type NativeBinding } from './nativeMetrics.js';
2
+ /**
3
+ * Wraps the WebAssembly build of the native addon (native/src/wasm.rs) as a NativeBinding. The
4
+ * module is instantiated synchronously on first use and again after a trap (a panic or stack
5
+ * overflow), because a trap leaves the instance's memory and stack pointer in an undefined state.
6
+ * An instantiation failure (e.g., a payload version mismatch) is memoized like the N-API loader's,
7
+ * since instantiating the same module again would fail again.
8
+ */
9
+ export declare function createWasmBinding(module: WebAssembly.Module): NativeBinding;
@@ -0,0 +1,2 @@
1
+ import{NativeAddonError as e}from"./nativeMetrics.js";const t=new TextEncoder,n=new TextDecoder;function r(e){let t,r,s=a=>{if(!t){if(r)throw r;try{t=i(e)}catch(e){throw r=e,e}}let{exports:o,stderr:s}=t;s.length=0;let c;try{c=a(o)}catch(e){throw t=void 0,Error(`The code-gauge WebAssembly module crashed: ${s.join(``).trim()||String(e)}`,{cause:e})}let l=n.decode(new Uint8Array(o.memory.buffer,o.result_ptr(),o.result_len()));if(c!==0)throw Error(l);return l},c=(e,t,n,r,i,c,l)=>s(s=>s.measure_code(...a(s,e),...a(s,t),Number(n),o(r),o(i),o(c),Number(l)));return{measureCodeNative:c,measureCodeNativeAsync:async(...e)=>c(...e),collectCrossFileDataNative:(e,t,n)=>s(r=>r.collect_cross_file_data(...a(r,e),...a(r,t),o(n))),collectFunctionTokenSequencesNative:(e,t)=>s(n=>n.collect_function_token_sequences(...a(n,e),...a(n,t)))}}function i(t){let n=[],r,i=new WebAssembly.Instance(t,{wasi_snapshot_preview1:c(()=>r,n)}).exports;r=i.memory;let a=i.payload_version();if(a!==8)throw new e(`The code-gauge WebAssembly module has payload version ${a}, but 8 is expected; reinstall code-gauge, or in a code-gauge repository checkout, rebuild it with \`bun run build-wasm\``);return{exports:i,stderr:n}}function a(e,n){let r=t.encode(n),i=e.alloc(r.length);return new Uint8Array(e.memory.buffer,i,r.length).set(r),[i,r.length]}function o(e){return e??-1}const s=65536;function c(e,t){let r=()=>new DataView(e().buffer),i=()=>8;return{environ_get:()=>0,environ_sizes_get:(e,t)=>(r().setUint32(e,0,!0),r().setUint32(t,0,!0),0),clock_time_get:(e,t,n)=>(r().setBigUint64(n,BigInt(Date.now())*1000000n,!0),0),random_get:(t,n)=>{for(let r=0;r<n;r+=s)crypto.getRandomValues(new Uint8Array(e().buffer,t+r,Math.min(s,n-r)));return 0},fd_write:(i,a,o,s)=>{let c=r(),l=0;for(let r=0;r<o;r++){let o=c.getUint32(a+r*8,!0),s=c.getUint32(a+r*8+4,!0);i===2&&t.push(n.decode(new Uint8Array(e().buffer,o,s))),l+=s}return c.setUint32(s,l,!0),0},fd_close:i,fd_fdstat_get:i,fd_fdstat_set_flags:i,fd_read:i,fd_seek:i,proc_exit:e=>{throw Error(`The code-gauge WebAssembly module exited with code ${e}`)}}}export{r as createWasmBinding};
2
+ //# sourceMappingURL=wasmBinding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wasmBinding.js","names":[],"sources":["../src/wasmBinding.ts"],"sourcesContent":["import { expectedPayloadVersion, NativeAddonError, type NativeBinding } from './nativeMetrics.js';\n\n/** The C ABI exported by native/src/wasm.rs. */\ninterface WasmExports {\n memory: WebAssembly.Memory;\n payload_version(): number;\n alloc(length: number): number;\n result_ptr(): number;\n result_len(): number;\n measure_code(\n codePtr: number,\n codeLength: number,\n languagePtr: number,\n languageLength: number,\n includeSyntaxTree: number,\n minTokens: number,\n maxGapTokens: number,\n minSimilarityPercent: number,\n includeCrossFileData: number\n ): number;\n collect_cross_file_data(\n codePtr: number,\n codeLength: number,\n languagePtr: number,\n languageLength: number,\n minTokens: number\n ): number;\n collect_function_token_sequences(\n codePtr: number,\n codeLength: number,\n languagePtr: number,\n languageLength: number\n ): number;\n}\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\n/**\n * Wraps the WebAssembly build of the native addon (native/src/wasm.rs) as a NativeBinding. The\n * module is instantiated synchronously on first use and again after a trap (a panic or stack\n * overflow), because a trap leaves the instance's memory and stack pointer in an undefined state.\n * An instantiation failure (e.g., a payload version mismatch) is memoized like the N-API loader's,\n * since instantiating the same module again would fail again.\n */\nexport function createWasmBinding(module: WebAssembly.Module): NativeBinding {\n let instance: { exports: WasmExports; stderr: string[] } | undefined;\n let instantiationFailure: unknown;\n\n const call = (invoke: (exports: WasmExports) => number): string => {\n if (!instance) {\n if (instantiationFailure) {\n throw instantiationFailure;\n }\n try {\n instance = instantiate(module);\n } catch (error) {\n instantiationFailure = error;\n throw error;\n }\n }\n const { exports, stderr } = instance;\n stderr.length = 0;\n let status: number;\n try {\n status = invoke(exports);\n } catch (error) {\n instance = undefined;\n throw new Error(`The code-gauge WebAssembly module crashed: ${stderr.join('').trim() || String(error)}`, {\n cause: error,\n });\n }\n const result = decoder.decode(new Uint8Array(exports.memory.buffer, exports.result_ptr(), exports.result_len()));\n if (status !== 0) {\n throw new Error(result);\n }\n return result;\n };\n\n const measureCodeNative: NativeBinding['measureCodeNative'] = (\n code,\n language,\n includeSyntaxTree,\n minTokens,\n maxGapTokens,\n minSimilarityPercent,\n includeCrossFileData\n ) =>\n call((exports) =>\n exports.measure_code(\n ...passString(exports, code),\n ...passString(exports, language),\n Number(includeSyntaxTree),\n toOptionalU32(minTokens),\n toOptionalU32(maxGapTokens),\n toOptionalU32(minSimilarityPercent),\n Number(includeCrossFileData)\n )\n );\n\n return {\n measureCodeNative,\n // The WebAssembly build has no threads, so the asynchronous form measures in place.\n measureCodeNativeAsync: async (...args) => measureCodeNative(...args),\n collectCrossFileDataNative: (code, language, minTokens) =>\n call((exports) =>\n exports.collect_cross_file_data(\n ...passString(exports, code),\n ...passString(exports, language),\n toOptionalU32(minTokens)\n )\n ),\n collectFunctionTokenSequencesNative: (code, language) =>\n call((exports) =>\n exports.collect_function_token_sequences(...passString(exports, code), ...passString(exports, language))\n ),\n };\n}\n\nfunction instantiate(module: WebAssembly.Module): { exports: WasmExports; stderr: string[] } {\n const stderr: string[] = [];\n let memory: WebAssembly.Memory | undefined;\n const instance = new WebAssembly.Instance(module, {\n wasi_snapshot_preview1: createWasiImports(() => memory as WebAssembly.Memory, stderr),\n });\n const exports = instance.exports as unknown as WasmExports;\n memory = exports.memory;\n const version = exports.payload_version();\n if (version !== expectedPayloadVersion) {\n throw new NativeAddonError(\n `The code-gauge WebAssembly module has payload version ${version}, but ${expectedPayloadVersion} is ` +\n 'expected; reinstall code-gauge, or in a code-gauge repository checkout, rebuild it with `bun run build-wasm`'\n );\n }\n return { exports, stderr };\n}\n\n/** Copies a string into a buffer whose ownership passes to the called export. */\nfunction passString(exports: WasmExports, text: string): [number, number] {\n // TextEncoder replaces lone surrogates with U+FFFD, like toWellFormed() does for the N-API addon.\n const bytes = encoder.encode(text);\n const pointer = exports.alloc(bytes.length);\n new Uint8Array(exports.memory.buffer, pointer, bytes.length).set(bytes);\n return [pointer, bytes.length];\n}\n\n/** native/src/wasm.rs reads a negative value as an absent setting. */\nfunction toOptionalU32(value: number | undefined): number {\n return value ?? -1;\n}\n\nconst WASI_ERRNO_SUCCESS = 0;\nconst WASI_ERRNO_BADF = 8;\n// crypto.getRandomValues() rejects requests larger than this.\nconst MAX_RANDOM_BYTES = 65_536;\n\n/**\n * The WASI preview 1 functions the module imports. The metrics code performs no I/O, so file\n * descriptors are unavailable except for writes, whose stderr output (e.g., a panic message) is\n * kept for the error raised when the module traps.\n */\nfunction createWasiImports(\n getMemory: () => WebAssembly.Memory,\n stderr: string[]\n): Record<string, (...args: never[]) => number> {\n const view = (): DataView => new DataView(getMemory().buffer);\n const unavailable = (): number => WASI_ERRNO_BADF;\n return {\n environ_get: () => WASI_ERRNO_SUCCESS,\n environ_sizes_get: (countPointer: number, sizePointer: number) => {\n view().setUint32(countPointer, 0, true);\n view().setUint32(sizePointer, 0, true);\n return WASI_ERRNO_SUCCESS;\n },\n clock_time_get: (_clockId: number, _precision: bigint, timePointer: number) => {\n view().setBigUint64(timePointer, BigInt(Date.now()) * 1_000_000n, true);\n return WASI_ERRNO_SUCCESS;\n },\n random_get: (pointer: number, length: number) => {\n for (let offset = 0; offset < length; offset += MAX_RANDOM_BYTES) {\n crypto.getRandomValues(\n new Uint8Array(getMemory().buffer, pointer + offset, Math.min(MAX_RANDOM_BYTES, length - offset))\n );\n }\n return WASI_ERRNO_SUCCESS;\n },\n fd_write: (fd: number, iovsPointer: number, iovsLength: number, writtenPointer: number) => {\n const memoryView = view();\n let written = 0;\n for (let index = 0; index < iovsLength; index++) {\n const pointer = memoryView.getUint32(iovsPointer + index * 8, true);\n const length = memoryView.getUint32(iovsPointer + index * 8 + 4, true);\n if (fd === 2) {\n stderr.push(decoder.decode(new Uint8Array(getMemory().buffer, pointer, length)));\n }\n written += length;\n }\n memoryView.setUint32(writtenPointer, written, true);\n return WASI_ERRNO_SUCCESS;\n },\n fd_close: unavailable,\n fd_fdstat_get: unavailable,\n fd_fdstat_set_flags: unavailable,\n fd_read: unavailable,\n fd_seek: unavailable,\n proc_exit: (code: number) => {\n throw new Error(`The code-gauge WebAssembly module exited with code ${code}`);\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./crossFileDuplication.cjs"),n=require("./languages.cjs"),r=require("./nativeMetrics.cjs"),i=require("./metrics.cjs"),a=require("./regressionGate.cjs");require("./index.cjs");const o=require("./wasmBinding.cjs");let s=require("../native/code-gauge.wasm");s=e.__toESM(s,1),r.setNativeBinding(o.createWasmBinding(s.default)),exports.TreeMeasurer=i.TreeMeasurer,exports.collectCrossFileDuplicationFileData=i.collectCrossFileDuplicationFileData,exports.collectDuplicationCandidates=i.collectDuplicationCandidates,exports.collectFunctionTokenSequences=i.collectFunctionTokenSequences,exports.defaultGateOptions=a.defaultGateOptions,exports.defaultLanguages=n.defaultLanguages,exports.defaultMeasurer=i.defaultMeasurer,exports.detectLanguage=n.detectLanguage,exports.evaluateRegressionGate=a.evaluateRegressionGate,exports.measureCode=i.measureCode,exports.measureCrossFileDuplication=t.measureCrossFileDuplication,exports.supportedLanguages=n.supportedLanguages;
2
+ //# sourceMappingURL=worker.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.cjs","names":["setNativeBinding","createWasmBinding","wasmModule"],"sources":["../src/worker.ts"],"sourcesContent":["// The entry point for Cloudflare Workers (the `workerd` export condition), which cannot load N-API\n// addons: the same API runs on the WebAssembly build of the native addon.\nimport wasmModule from '../native/code-gauge.wasm';\nimport { setNativeBinding } from './nativeMetrics.js';\nimport { createWasmBinding } from './wasmBinding.js';\n\nsetNativeBinding(createWasmBinding(wasmModule));\n\nexport * from './index.js';\n"],"mappings":"+ZAMAA,EAAAA,iBAAiBC,EAAAA,kBAAkBC,EAAAA,OAAU,CAAC"}
@@ -0,0 +1 @@
1
+ export * from './index.js';
package/dist/worker.js ADDED
@@ -0,0 +1,2 @@
1
+ import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{defaultLanguages as t,detectLanguage as n,supportedLanguages as r}from"./languages.js";import{setNativeBinding as i}from"./nativeMetrics.js";import{TreeMeasurer as a,collectCrossFileDuplicationFileData as o,collectDuplicationCandidates as s,collectFunctionTokenSequences as c,defaultMeasurer as l,measureCode as u}from"./metrics.js";import{defaultGateOptions as d,evaluateRegressionGate as f}from"./regressionGate.js";import"./index.js";import{createWasmBinding as p}from"./wasmBinding.js";import m from"../native/code-gauge.wasm";i(p(m));export{a as TreeMeasurer,o as collectCrossFileDuplicationFileData,s as collectDuplicationCandidates,c as collectFunctionTokenSequences,d as defaultGateOptions,t as defaultLanguages,l as defaultMeasurer,n as detectLanguage,f as evaluateRegressionGate,u as measureCode,e as measureCrossFileDuplication,r as supportedLanguages};
2
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","names":[],"sources":["../src/worker.ts"],"sourcesContent":["// The entry point for Cloudflare Workers (the `workerd` export condition), which cannot load N-API\n// addons: the same API runs on the WebAssembly build of the native addon.\nimport wasmModule from '../native/code-gauge.wasm';\nimport { setNativeBinding } from './nativeMetrics.js';\nimport { createWasmBinding } from './wasmBinding.js';\n\nsetNativeBinding(createWasmBinding(wasmModule));\n\nexport * from './index.js';\n"],"mappings":"kmBAMA,EAAiB,EAAkB,CAAU,CAAC"}
package/native/Cargo.lock CHANGED
@@ -38,6 +38,7 @@ dependencies = [
38
38
  "napi-build",
39
39
  "napi-derive",
40
40
  "regex",
41
+ "rustc-hash",
41
42
  "serde",
42
43
  "serde_json",
43
44
  "tree-sitter",
package/native/Cargo.toml CHANGED
@@ -9,12 +9,11 @@ publish = false
9
9
  crate-type = ["cdylib"]
10
10
 
11
11
  [dependencies]
12
- napi = { version = "3", default-features = false }
13
- napi-derive = "3"
14
12
  serde = { version = "1", features = ["derive"] }
15
13
  serde_json = "1"
16
14
  indexmap = "2"
17
15
  regex = "1"
16
+ rustc-hash = "2"
18
17
 
19
18
  # Grammar crates are pinned exactly: every metric (and the golden-based test suite) is calibrated
20
19
  # against these grammar versions, so upgrades are deliberate, test-verified changes. The versions
@@ -39,6 +38,11 @@ tree-sitter-ruby = "=0.21.0"
39
38
  tree-sitter-rust = "=0.21.2"
40
39
  tree-sitter-typescript = "=0.21.2"
41
40
 
41
+ # The WebAssembly build (src/wasm.rs) exports a plain C ABI instead of N-API.
42
+ [target.'cfg(not(target_family = "wasm"))'.dependencies]
43
+ napi = { version = "3", default-features = false, features = ["napi4"] }
44
+ napi-derive = "3"
45
+
42
46
  [build-dependencies]
43
47
  napi-build = "2"
44
48
 
package/native/build.rs CHANGED
@@ -1,3 +1,6 @@
1
1
  fn main() {
2
- napi_build::setup();
2
+ let target_family = std::env::var("CARGO_CFG_TARGET_FAMILY").unwrap_or_default();
3
+ if !target_family.split(',').any(|family| family == "wasm") {
4
+ napi_build::setup();
5
+ }
3
6
  }
Binary file