code-gauge 6.0.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -8
- package/dist/nativeMetrics.cjs +1 -1
- package/dist/nativeMetrics.cjs.map +1 -1
- package/dist/nativeMetrics.js +1 -1
- package/dist/nativeMetrics.js.map +1 -1
- package/package.json +8 -11
- package/scripts/installNative.mjs +0 -96
package/README.md
CHANGED
|
@@ -375,14 +375,10 @@ supporting arbitrary grammars would mean shipping incomplete metrics for them.
|
|
|
375
375
|
|
|
376
376
|
Parsing and every metric pass run in a Rust addon (tree-sitter); the thin TypeScript layer handles
|
|
377
377
|
the CLI, cross-file matching, and the Halstead float derivations. Releases include prebuilt addons
|
|
378
|
-
for Linux x64/arm64 (glibc and musl), macOS x64/arm64, and Windows x64
|
|
379
|
-
|
|
380
|
-
[Rust toolchain](https://rustup.rs)
|
|
381
|
-
|
|
382
|
-
If a prebuilt addon is unavailable, package managers that block dependency install scripts by
|
|
383
|
-
default (recent npm versions, and Bun unless `code-gauge` is listed in `trustedDependencies`) skip
|
|
384
|
-
the fallback build. Either approve `code-gauge`'s install script, or build the addon manually
|
|
385
|
-
inside the installed package (the runtime error message points here too):
|
|
378
|
+
for Linux x64/arm64 (glibc and musl), macOS x64/arm64, and Windows x64, and a release that
|
|
379
|
+
includes them runs no install script. On other platforms, build the addon from the bundled sources inside the installed
|
|
380
|
+
package, which requires a [Rust toolchain](https://rustup.rs) (the runtime error message points
|
|
381
|
+
here too):
|
|
386
382
|
|
|
387
383
|
```sh
|
|
388
384
|
node node_modules/code-gauge/scripts/buildNative.mjs
|
package/dist/nativeMetrics.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
"use strict";let e=require("@willbooster/shared-lib");function t(e,t,n,i,a=!1){return JSON.parse(f().measureCodeNative(...r(e,t,n,i,a)))}function n(e,t,n,i,a=!1){return f().measureCodeNativeAsync(...r(e,t,n,i,a)).then(e=>JSON.parse(e))}function r(e,t,n,r,i){return[o(e),t,n,s(r?.minTokens),s(r?.maxGapTokens),s(r?.minSimilarityPercent),i]}function i(e,t,n){return JSON.parse(f().collectCrossFileDataNative(o(e),t,s(n)))}function a(e,t){return JSON.parse(f().collectFunctionTokenSequencesNative(o(e),t)).map(e=>Int32Array.from(e))}function o(e){return e.isWellFormed()?e:e.toWellFormed()}function s(t){return t===void 0||Number.isNaN(t)?void 0:(0,e.clamp)(Math.trunc(t),0,4294967295)}var c=class extends Error{};let l,u;function d(e){l=e}function f(){if(l)return l;if(u)throw u;let t=process.getBuiltinModule(`node:module`).createRequire(require("url").pathToFileURL(__filename).href),n=[`code-gauge-${p()}`,`../native/code-gauge.node`],r=[];for(let i of n){let n;try{n=t(i)}catch(t){r.push(` ${i}: ${(0,e.getErrorMessage)(t).split(`
|
|
2
|
-
`)[0]}`);continue}let a=n.payloadVersion?.();if(a!==8){r.push(` ${i}: payload version ${a??`unknown`} does not match the expected 8; rebuild the addon with \`bun run build-native\``);continue}return l=n,n}throw u=new c(`The code-gauge native addon is not available for ${p()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain)
|
|
2
|
+
`)[0]}`);continue}let a=n.payloadVersion?.();if(a!==8){r.push(` ${i}: payload version ${a??`unknown`} does not match the expected 8; rebuild the addon with \`bun run build-native\``);continue}return l=n,n}throw u=new c(`The code-gauge native addon is not available for ${p()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain).\n${r.join(`
|
|
3
3
|
`)}`),u}function p(){let e=`${process.platform}-${process.arch}`;return process.platform===`win32`?`${e}-msvc`:process.platform===`linux`?(process.report?.getReport())?.header?.glibcVersionRuntime?`${e}-gnu`:`${e}-musl`:e}exports.NativeAddonError=c,exports.collectCrossFileDataNative=i,exports.collectFunctionTokenSequencesNative=a,exports.measureCodeNative=t,exports.measureCodeNativeAsync=n,exports.setNativeBinding=d;
|
|
4
4
|
//# sourceMappingURL=nativeMetrics.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nativeMetrics.cjs","names":["clamp","getErrorMessage"],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { clamp, getErrorMessage } from '@willbooster/shared-lib';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ntype NativeMeasureArguments = [\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens: number | undefined,\n maxGapTokens: number | undefined,\n minSimilarityPercent: number | undefined,\n includeCrossFileData: boolean,\n];\n\nexport interface NativeBinding {\n measureCodeNative(...args: NativeMeasureArguments): string;\n measureCodeNativeAsync(...args: NativeMeasureArguments): Promise<string>;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields, or lack newer binding functions, instead of failing with a clear rebuild message.\n */\nexport const expectedPayloadVersion = 8;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n ) as NativeMetricsPayload;\n}\n\n/**\n * measureCodeNative on the addon's worker threads, so several files can be measured in parallel.\n * A missing addon still throws synchronously, like measureCodeNative.\n */\nexport function measureCodeNativeAsync(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): Promise<NativeMetricsPayload> {\n return loadBinding()\n .measureCodeNativeAsync(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n .then((json) => JSON.parse(json) as NativeMetricsPayload);\n}\n\n/** The binding arguments shared by the sync and async measurements, so both measure alike. */\nfunction toNativeMeasureArguments(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication: DuplicationOptions | undefined,\n includeCrossFileData: boolean\n): NativeMeasureArguments {\n return [\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData,\n ];\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value) ? undefined : clamp(Math.trunc(value), 0, 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\n/** Replaces the N-API addon, e.g. with the WebAssembly build on runtimes without N-API. */\nexport function setNativeBinding(binding: NativeBinding): void {\n cachedBinding = binding;\n}\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n // node:module is loaded lazily so that runtimes without it can bundle this module.\n const requireNative = process.getBuiltinModule('node:module').createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${getErrorMessage(error).split('\\n')[0]}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n 'toolchain); when installing with npm, also allow install scripts for code-gauge so its ' +\n `postinstall build can run.\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"sDAmEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CACF,CACF,CAMA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACQ,CAC/B,OAAO,EAAY,CAAC,CACjB,uBACC,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CAAC,CACA,KAAM,GAAS,KAAK,MAAM,CAAI,CAAyB,CAC5D,CAGA,SAAS,EACP,EACA,EACA,EACA,EACA,EACwB,CACxB,MAAO,CACL,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAAI,IAAA,IAAA,EAAYA,EAAAA,MAAAA,CAAM,KAAK,MAAM,CAAK,EAAG,EAAG,UAAa,CAC3G,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAGJ,SAAgB,EAAiB,EAA8B,CAC7D,EAAgB,CAClB,CAEA,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAIR,IAAM,EAAgB,QAAQ,iBAAiB,aAAa,CAAC,CAAC,cAAA,QAAA,KAAA,CAAA,CAAA,cAAA,UAAA,CAAA,CAAA,IAA6B,EACrF,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,KAAA,EAAIC,EAAAA,gBAAAA,CAAgB,CAAK,CAAC,CAAC,MAAM;CAAI,CAAC,CAAC,IAAI,EACxE,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAA,EAAoC,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAOA,KANA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,2NAGrC,EAAS,KAAK;CAAI,GACrD,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
|
|
1
|
+
{"version":3,"file":"nativeMetrics.cjs","names":["clamp","getErrorMessage"],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { clamp, getErrorMessage } from '@willbooster/shared-lib';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ntype NativeMeasureArguments = [\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens: number | undefined,\n maxGapTokens: number | undefined,\n minSimilarityPercent: number | undefined,\n includeCrossFileData: boolean,\n];\n\nexport interface NativeBinding {\n measureCodeNative(...args: NativeMeasureArguments): string;\n measureCodeNativeAsync(...args: NativeMeasureArguments): Promise<string>;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields, or lack newer binding functions, instead of failing with a clear rebuild message.\n */\nexport const expectedPayloadVersion = 8;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n ) as NativeMetricsPayload;\n}\n\n/**\n * measureCodeNative on the addon's worker threads, so several files can be measured in parallel.\n * A missing addon still throws synchronously, like measureCodeNative.\n */\nexport function measureCodeNativeAsync(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): Promise<NativeMetricsPayload> {\n return loadBinding()\n .measureCodeNativeAsync(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n .then((json) => JSON.parse(json) as NativeMetricsPayload);\n}\n\n/** The binding arguments shared by the sync and async measurements, so both measure alike. */\nfunction toNativeMeasureArguments(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication: DuplicationOptions | undefined,\n includeCrossFileData: boolean\n): NativeMeasureArguments {\n return [\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData,\n ];\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value) ? undefined : clamp(Math.trunc(value), 0, 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\n/** Replaces the N-API addon, e.g. with the WebAssembly build on runtimes without N-API. */\nexport function setNativeBinding(binding: NativeBinding): void {\n cachedBinding = binding;\n}\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n // node:module is loaded lazily so that runtimes without it can bundle this module.\n const requireNative = process.getBuiltinModule('node:module').createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${getErrorMessage(error).split('\\n')[0]}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n `toolchain).\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"sDAmEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CACF,CACF,CAMA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACQ,CAC/B,OAAO,EAAY,CAAC,CACjB,uBACC,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CAAC,CACA,KAAM,GAAS,KAAK,MAAM,CAAI,CAAyB,CAC5D,CAGA,SAAS,EACP,EACA,EACA,EACA,EACA,EACwB,CACxB,MAAO,CACL,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAAI,IAAA,IAAA,EAAYA,EAAAA,MAAAA,CAAM,KAAK,MAAM,CAAK,EAAG,EAAG,UAAa,CAC3G,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAGJ,SAAgB,EAAiB,EAA8B,CAC7D,EAAgB,CAClB,CAEA,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAIR,IAAM,EAAgB,QAAQ,iBAAiB,aAAa,CAAC,CAAC,cAAA,QAAA,KAAA,CAAA,CAAA,cAAA,UAAA,CAAA,CAAA,IAA6B,EACrF,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,KAAA,EAAIC,EAAAA,gBAAAA,CAAgB,CAAK,CAAC,CAAC,MAAM;CAAI,CAAC,CAAC,IAAI,EACxE,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAA,EAAoC,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAMA,KALA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,qHAEpD,EAAS,KAAK;CAAI,GACtC,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
|
package/dist/nativeMetrics.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import{clamp as e,getErrorMessage as t}from"@willbooster/shared-lib";function n(e,t,n,r,a=!1){return JSON.parse(p().measureCodeNative(...i(e,t,n,r,a)))}function r(e,t,n,r,a=!1){return p().measureCodeNativeAsync(...i(e,t,n,r,a)).then(e=>JSON.parse(e))}function i(e,t,n,r,i){return[s(e),t,n,c(r?.minTokens),c(r?.maxGapTokens),c(r?.minSimilarityPercent),i]}function a(e,t,n){return JSON.parse(p().collectCrossFileDataNative(s(e),t,c(n)))}function o(e,t){return JSON.parse(p().collectFunctionTokenSequencesNative(s(e),t)).map(e=>Int32Array.from(e))}function s(e){return e.isWellFormed()?e:e.toWellFormed()}function c(t){return t===void 0||Number.isNaN(t)?void 0:e(Math.trunc(t),0,4294967295)}var l=class extends Error{};let u,d;function f(e){u=e}function p(){if(u)return u;if(d)throw d;let e=process.getBuiltinModule(`node:module`).createRequire(import.meta.url),n=[`code-gauge-${m()}`,`../native/code-gauge.node`],r=[];for(let i of n){let n;try{n=e(i)}catch(e){r.push(` ${i}: ${t(e).split(`
|
|
2
|
-
`)[0]}`);continue}let a=n.payloadVersion?.();if(a!==8){r.push(` ${i}: payload version ${a??`unknown`} does not match the expected 8; rebuild the addon with \`bun run build-native\``);continue}return u=n,n}throw d=new l(`The code-gauge native addon is not available for ${m()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain)
|
|
2
|
+
`)[0]}`);continue}let a=n.payloadVersion?.();if(a!==8){r.push(` ${i}: payload version ${a??`unknown`} does not match the expected 8; rebuild the addon with \`bun run build-native\``);continue}return u=n,n}throw d=new l(`The code-gauge native addon is not available for ${m()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain).\n${r.join(`
|
|
3
3
|
`)}`),d}function m(){let e=`${process.platform}-${process.arch}`;return process.platform===`win32`?`${e}-msvc`:process.platform===`linux`?(process.report?.getReport())?.header?.glibcVersionRuntime?`${e}-gnu`:`${e}-musl`:e}export{l as NativeAddonError,a as collectCrossFileDataNative,o as collectFunctionTokenSequencesNative,n as measureCodeNative,r as measureCodeNativeAsync,f as setNativeBinding};
|
|
4
4
|
//# sourceMappingURL=nativeMetrics.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nativeMetrics.js","names":[],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { clamp, getErrorMessage } from '@willbooster/shared-lib';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ntype NativeMeasureArguments = [\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens: number | undefined,\n maxGapTokens: number | undefined,\n minSimilarityPercent: number | undefined,\n includeCrossFileData: boolean,\n];\n\nexport interface NativeBinding {\n measureCodeNative(...args: NativeMeasureArguments): string;\n measureCodeNativeAsync(...args: NativeMeasureArguments): Promise<string>;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields, or lack newer binding functions, instead of failing with a clear rebuild message.\n */\nexport const expectedPayloadVersion = 8;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n ) as NativeMetricsPayload;\n}\n\n/**\n * measureCodeNative on the addon's worker threads, so several files can be measured in parallel.\n * A missing addon still throws synchronously, like measureCodeNative.\n */\nexport function measureCodeNativeAsync(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): Promise<NativeMetricsPayload> {\n return loadBinding()\n .measureCodeNativeAsync(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n .then((json) => JSON.parse(json) as NativeMetricsPayload);\n}\n\n/** The binding arguments shared by the sync and async measurements, so both measure alike. */\nfunction toNativeMeasureArguments(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication: DuplicationOptions | undefined,\n includeCrossFileData: boolean\n): NativeMeasureArguments {\n return [\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData,\n ];\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value) ? undefined : clamp(Math.trunc(value), 0, 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\n/** Replaces the N-API addon, e.g. with the WebAssembly build on runtimes without N-API. */\nexport function setNativeBinding(binding: NativeBinding): void {\n cachedBinding = binding;\n}\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n // node:module is loaded lazily so that runtimes without it can bundle this module.\n const requireNative = process.getBuiltinModule('node:module').createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${getErrorMessage(error).split('\\n')[0]}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n 'toolchain); when installing with npm, also allow install scripts for code-gauge so its ' +\n `postinstall build can run.\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"qEAmEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CACF,CACF,CAMA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACQ,CAC/B,OAAO,EAAY,CAAC,CACjB,uBACC,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CAAC,CACA,KAAM,GAAS,KAAK,MAAM,CAAI,CAAyB,CAC5D,CAGA,SAAS,EACP,EACA,EACA,EACA,EACA,EACwB,CACxB,MAAO,CACL,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAAI,IAAA,GAAY,EAAM,KAAK,MAAM,CAAK,EAAG,EAAG,UAAa,CAC3G,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAGJ,SAAgB,EAAiB,EAA8B,CAC7D,EAAgB,CAClB,CAEA,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAIR,IAAM,EAAgB,QAAQ,iBAAiB,aAAa,CAAC,CAAC,cAAc,YAAY,GAAG,EACrF,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,IAAI,EAAgB,CAAK,CAAC,CAAC,MAAM;CAAI,CAAC,CAAC,IAAI,EACxE,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAA,EAAoC,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAOA,KANA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,2NAGrC,EAAS,KAAK;CAAI,GACrD,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
|
|
1
|
+
{"version":3,"file":"nativeMetrics.js","names":[],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { clamp, getErrorMessage } from '@willbooster/shared-lib';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ntype NativeMeasureArguments = [\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens: number | undefined,\n maxGapTokens: number | undefined,\n minSimilarityPercent: number | undefined,\n includeCrossFileData: boolean,\n];\n\nexport interface NativeBinding {\n measureCodeNative(...args: NativeMeasureArguments): string;\n measureCodeNativeAsync(...args: NativeMeasureArguments): Promise<string>;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields, or lack newer binding functions, instead of failing with a clear rebuild message.\n */\nexport const expectedPayloadVersion = 8;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n ) as NativeMetricsPayload;\n}\n\n/**\n * measureCodeNative on the addon's worker threads, so several files can be measured in parallel.\n * A missing addon still throws synchronously, like measureCodeNative.\n */\nexport function measureCodeNativeAsync(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): Promise<NativeMetricsPayload> {\n return loadBinding()\n .measureCodeNativeAsync(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n .then((json) => JSON.parse(json) as NativeMetricsPayload);\n}\n\n/** The binding arguments shared by the sync and async measurements, so both measure alike. */\nfunction toNativeMeasureArguments(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication: DuplicationOptions | undefined,\n includeCrossFileData: boolean\n): NativeMeasureArguments {\n return [\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData,\n ];\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value) ? undefined : clamp(Math.trunc(value), 0, 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\n/** Replaces the N-API addon, e.g. with the WebAssembly build on runtimes without N-API. */\nexport function setNativeBinding(binding: NativeBinding): void {\n cachedBinding = binding;\n}\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n // node:module is loaded lazily so that runtimes without it can bundle this module.\n const requireNative = process.getBuiltinModule('node:module').createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${getErrorMessage(error).split('\\n')[0]}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n `toolchain).\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"qEAmEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CACF,CACF,CAMA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACQ,CAC/B,OAAO,EAAY,CAAC,CACjB,uBACC,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CAAC,CACA,KAAM,GAAS,KAAK,MAAM,CAAI,CAAyB,CAC5D,CAGA,SAAS,EACP,EACA,EACA,EACA,EACA,EACwB,CACxB,MAAO,CACL,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAAI,IAAA,GAAY,EAAM,KAAK,MAAM,CAAK,EAAG,EAAG,UAAa,CAC3G,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAGJ,SAAgB,EAAiB,EAA8B,CAC7D,EAAgB,CAClB,CAEA,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAIR,IAAM,EAAgB,QAAQ,iBAAiB,aAAa,CAAC,CAAC,cAAc,YAAY,GAAG,EACrF,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,IAAI,EAAgB,CAAK,CAAC,CAAC,MAAM;CAAI,CAAC,CAAC,IAAI,EACxE,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAA,EAAoC,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAMA,KALA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,qHAEpD,EAAS,KAAK;CAAI,GACtC,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-gauge",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "Measure code metrics with tree-sitter.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -41,7 +41,6 @@
|
|
|
41
41
|
"native/code-gauge.wasm",
|
|
42
42
|
"native/src/",
|
|
43
43
|
"scripts/buildNative.mjs",
|
|
44
|
-
"scripts/installNative.mjs",
|
|
45
44
|
"THIRD-PARTY-NOTICES.txt"
|
|
46
45
|
],
|
|
47
46
|
"scripts": {
|
|
@@ -51,10 +50,8 @@
|
|
|
51
50
|
"build-wasm": "node scripts/buildWasm.mjs",
|
|
52
51
|
"cleanup": "bun wb lint --fix --format",
|
|
53
52
|
"format": "bun wb lint --format",
|
|
54
|
-
"postinstall": "node scripts/installNative.mjs",
|
|
55
53
|
"lint": "bun wb lint",
|
|
56
54
|
"lint-fix": "bun wb lint --fix",
|
|
57
|
-
"prepare": "(bun lefthook install || true) && (test ! -f .lefthook/normalize-bun-lockfile.sh || bash .lefthook/normalize-bun-lockfile.sh)",
|
|
58
55
|
"test": "bun wb test",
|
|
59
56
|
"test/ci": "bun wb test-on-ci",
|
|
60
57
|
"typecheck": "bun wb typecheck",
|
|
@@ -95,12 +92,12 @@
|
|
|
95
92
|
"registry": "https://registry.npmjs.org/"
|
|
96
93
|
},
|
|
97
94
|
"optionalDependencies": {
|
|
98
|
-
"code-gauge-linux-x64-gnu": "
|
|
99
|
-
"code-gauge-linux-arm64-gnu": "
|
|
100
|
-
"code-gauge-linux-x64-musl": "
|
|
101
|
-
"code-gauge-linux-arm64-musl": "
|
|
102
|
-
"code-gauge-darwin-x64": "
|
|
103
|
-
"code-gauge-darwin-arm64": "
|
|
104
|
-
"code-gauge-win32-x64-msvc": "
|
|
95
|
+
"code-gauge-linux-x64-gnu": "7.0.0",
|
|
96
|
+
"code-gauge-linux-arm64-gnu": "7.0.0",
|
|
97
|
+
"code-gauge-linux-x64-musl": "7.0.0",
|
|
98
|
+
"code-gauge-linux-arm64-musl": "7.0.0",
|
|
99
|
+
"code-gauge-darwin-x64": "7.0.0",
|
|
100
|
+
"code-gauge-darwin-arm64": "7.0.0",
|
|
101
|
+
"code-gauge-win32-x64-msvc": "7.0.0"
|
|
105
102
|
}
|
|
106
103
|
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Postinstall hook ensuring the native Rust addon is available: a prebuilt platform package or an
|
|
3
|
-
// already-built native/code-gauge.node is kept when it serves the payload version this checkout
|
|
4
|
-
// expects (a stale addon surviving a `git pull` must not be kept — the runtime loader would
|
|
5
|
-
// reject it), otherwise the addon is built from the bundled sources when a Rust toolchain is
|
|
6
|
-
// available. Never fails the install — the whole body is guarded and the process always exits 0
|
|
7
|
-
// (npm runs lifecycle scripts through cmd.exe on Windows, where a `|| true` suffix would not
|
|
8
|
-
// work) — and the runtime loader raises a descriptive error on first use if no addon could be
|
|
9
|
-
// provided.
|
|
10
|
-
|
|
11
|
-
import { execFileSync } from 'node:child_process';
|
|
12
|
-
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
13
|
-
import path from 'node:path';
|
|
14
|
-
import { fileURLToPath } from 'node:url';
|
|
15
|
-
|
|
16
|
-
const packageRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
-
|
|
18
|
-
// No explicit process.exit: nothing here keeps the event loop alive, so the process ends with
|
|
19
|
-
// code 0 on its own once the synchronous work returns — and exit() could truncate the warning's
|
|
20
|
-
// asynchronous stderr write (stderr is a pipe under npm, which is asynchronous on Windows).
|
|
21
|
-
try {
|
|
22
|
-
installNativeAddon();
|
|
23
|
-
} catch (error) {
|
|
24
|
-
warnBuildFailure(error);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function installNativeAddon() {
|
|
28
|
-
// The authoritative payload version lives in the bundled Rust source; a parse failure yields
|
|
29
|
-
// undefined, which no addon reports, so validation then always falls through to a fresh build.
|
|
30
|
-
const expectedPayloadVersion = /pub fn payload_version\(\) -> u32 \{\s*(\d+)/.exec(
|
|
31
|
-
readFileSync(path.join(packageRoot, 'native', 'src', 'lib.rs'), 'utf8')
|
|
32
|
-
)?.[1];
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
servesExpectedPayload(`code-gauge-${platformTriplet()}`, expectedPayloadVersion) ||
|
|
36
|
-
servesExpectedPayload(path.join(packageRoot, 'native', 'code-gauge.node'), expectedPayloadVersion)
|
|
37
|
-
) {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
execFileSync(process.execPath, [path.join(packageRoot, 'scripts', 'buildNative.mjs')], { stdio: 'inherit' });
|
|
43
|
-
} catch (error) {
|
|
44
|
-
warnBuildFailure(error);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
// In an installed package the cargo build tree (hundreds of MB) buys nothing once the addon is
|
|
48
|
-
// copied out; a repository checkout (recognized by its sources) keeps it as the build cache.
|
|
49
|
-
// Guarded separately: a cleanup failure (e.g. a held Windows file handle) leaves a usable addon
|
|
50
|
-
// behind and must not be reported as a build failure.
|
|
51
|
-
if (!existsSync(path.join(packageRoot, 'src'))) {
|
|
52
|
-
try {
|
|
53
|
-
rmSync(path.join(packageRoot, 'native', 'target'), { recursive: true, force: true });
|
|
54
|
-
} catch {
|
|
55
|
-
// The leftover build tree only costs disk space.
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// The platform-package suffix in the napi-rs naming convention: Linux is qualified by libc ABI
|
|
61
|
-
// (a glibc-linked addon cannot load on Alpine/musl) and Windows by toolchain ABI. Must match
|
|
62
|
-
// platformTriplet in src/nativeMetrics.ts.
|
|
63
|
-
function platformTriplet() {
|
|
64
|
-
const base = `${process.platform}-${process.arch}`;
|
|
65
|
-
if (process.platform === 'win32') {
|
|
66
|
-
return `${base}-msvc`;
|
|
67
|
-
}
|
|
68
|
-
if (process.platform !== 'linux') {
|
|
69
|
-
return base;
|
|
70
|
-
}
|
|
71
|
-
return process.report?.getReport()?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Probed in a child process: loading the addon here would keep its library mapped for this
|
|
75
|
-
// process's lifetime, and overwriting a mapped DLL is refused on Windows — exactly what the
|
|
76
|
-
// stale-addon rebuild below must be able to do.
|
|
77
|
-
function servesExpectedPayload(specifier, expectedPayloadVersion) {
|
|
78
|
-
try {
|
|
79
|
-
const reported = execFileSync(
|
|
80
|
-
process.execPath,
|
|
81
|
-
['-p', `String(require(${JSON.stringify(specifier)}).payloadVersion?.())`],
|
|
82
|
-
// stderr is discarded: a missing platform package is the normal case, not a diagnostic.
|
|
83
|
-
{ cwd: packageRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
|
|
84
|
-
).trim();
|
|
85
|
-
return expectedPayloadVersion !== undefined && reported === expectedPayloadVersion;
|
|
86
|
-
} catch {
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function warnBuildFailure(error) {
|
|
92
|
-
console.warn(
|
|
93
|
-
`code-gauge: could not build the native addon (${error instanceof Error ? error.message : String(error)}). ` +
|
|
94
|
-
'Install a Rust toolchain and run `node scripts/buildNative.mjs` in the package directory to enable it.'
|
|
95
|
-
);
|
|
96
|
-
}
|