code-gauge 4.3.0 → 4.4.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 CHANGED
@@ -173,9 +173,11 @@ The `duplication` section tunes how clones are detected:
173
173
  import declarations count, and statement-shaped content is counted uniformly in expression
174
174
  positions too
175
175
  - Per-function and file-level nesting depth
176
- - Per-function cyclomatic complexity (McCabe, own body only), matching PMD's
177
- `CyclomaticComplexity` rule for Java; an API-only compatibility metric that ranking and the
178
- regression gate ignore
176
+ - Per-function cyclomatic complexity (McCabe, own body only), counted as NIST SP 500-235 defines it:
177
+ every decision and short-circuit operator adds one, one per case-labelled statement, plus the
178
+ file total over McCabe's components (every function, every initializer block, decisions outside
179
+ functions, and the module body of a file that runs top-level code); an API-only metric that
180
+ ranking and the regression gate ignore
179
181
  - Per-function parameter counts and locations (name, node type, line span)
180
182
  - Within-file duplication: copy-pasted blocks matched on normalized tokens (identifiers anonymized
181
183
  consistently, literals by kind, and literal-dense data tables excluded unless their values also
package/dist/metrics.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";const e=require("./languages.cjs"),t=require("./nativeMetrics.cjs");var n=class{registry=e.createLanguageRegistry();getSupportedLanguages(){return[...new Set([...this.registry.values()].map(e=>e.name))]}measure(e,n){let i=this.resolveLanguage(n.language),a=n.includeSyntaxTree??!1;return r(t.measureCodeNative(e,i.name,a,n.duplication),a)}collectDuplicationCandidates(e,t){return this.collectCrossFileDuplicationFileData(e,t).candidates}collectCrossFileDuplicationFileData(e,n){let r=this.resolveLanguage(n.language),i=t.collectCrossFileDataNative(e,r.name,n.duplication?.minTokens);return{candidates:i.candidates,tokens:i.tokens,containerStatements:i.containerStatements,codeLineNumbers:new Set(i.codeLineNumbers)}}collectFunctionTokenSequences(e,n){let r=this.resolveLanguage(n.language);return t.collectFunctionTokenSequencesNative(e,r.name)}resolveLanguage(e){let t=this.registry.get(e);if(!t)throw Error(`Unsupported language: ${e}`);return t}};function r(e,t){return{language:e.language,bytes:e.bytes,lines:e.lines,functions:e.functions.map(e=>({name:e.name,nodeType:e.nodeType,startLine:e.startLine,startColumn:e.startColumn,endLine:e.endLine,endColumn:e.endColumn,cyclomaticComplexity:e.cyclomaticComplexity,cognitiveComplexity:e.cognitiveComplexity,nestingDepth:e.nestingDepth,ncss:e.ncss,parameterCount:e.parameterCount,halstead:i(e.halsteadCounts),depDegree:e.depDegree})),cognitiveComplexity:e.cognitiveComplexity,maxCognitiveComplexity:e.maxCognitiveComplexity,nestingDepth:e.nestingDepth,ncssCount:e.ncssCount,duplication:e.duplication,halstead:i(e.halsteadCounts),syntaxTree:t?e.syntaxTree:void 0}}function i(e){let{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i}=e,a=t+n,o=r+i,s=a===0?0:o*Math.log2(a);return{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i,vocabulary:a,length:o,volume:s,effort:(n===0?0:t/2*(i/n))*s}}const a=new n;function o(e,t){return a.measure(e,t)}function s(e,t){return a.collectDuplicationCandidates(e,t)}function c(e,t){return a.collectFunctionTokenSequences(e,t)}function l(e,t){return a.collectCrossFileDuplicationFileData(e,t)}exports.TreeMeasurer=n,exports.collectCrossFileDuplicationFileData=l,exports.collectDuplicationCandidates=s,exports.collectFunctionTokenSequences=c,exports.defaultMeasurer=a,exports.measureCode=o;
1
+ "use strict";const e=require("./languages.cjs"),t=require("./nativeMetrics.cjs");var n=class{registry=e.createLanguageRegistry();getSupportedLanguages(){return[...new Set([...this.registry.values()].map(e=>e.name))]}measure(e,n){let i=this.resolveLanguage(n.language),a=n.includeSyntaxTree??!1;return r(t.measureCodeNative(e,i.name,a,n.duplication),a)}collectDuplicationCandidates(e,t){return this.collectCrossFileDuplicationFileData(e,t).candidates}collectCrossFileDuplicationFileData(e,n){let r=this.resolveLanguage(n.language),i=t.collectCrossFileDataNative(e,r.name,n.duplication?.minTokens);return{candidates:i.candidates,tokens:i.tokens,containerStatements:i.containerStatements,codeLineNumbers:new Set(i.codeLineNumbers)}}collectFunctionTokenSequences(e,n){let r=this.resolveLanguage(n.language);return t.collectFunctionTokenSequencesNative(e,r.name)}resolveLanguage(e){let t=this.registry.get(e);if(!t)throw Error(`Unsupported language: ${e}`);return t}};function r(e,t){return{language:e.language,bytes:e.bytes,lines:e.lines,functions:e.functions.map(e=>({name:e.name,nodeType:e.nodeType,startLine:e.startLine,startColumn:e.startColumn,endLine:e.endLine,endColumn:e.endColumn,cyclomaticComplexity:e.cyclomaticComplexity,cognitiveComplexity:e.cognitiveComplexity,nestingDepth:e.nestingDepth,ncss:e.ncss,parameterCount:e.parameterCount,halstead:i(e.halsteadCounts),depDegree:e.depDegree})),cyclomaticComplexity:e.cyclomaticComplexity,cognitiveComplexity:e.cognitiveComplexity,maxCognitiveComplexity:e.maxCognitiveComplexity,nestingDepth:e.nestingDepth,ncssCount:e.ncssCount,duplication:e.duplication,halstead:i(e.halsteadCounts),syntaxTree:t?e.syntaxTree:void 0}}function i(e){let{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i}=e,a=t+n,o=r+i,s=a===0?0:o*Math.log2(a);return{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i,vocabulary:a,length:o,volume:s,effort:(n===0?0:t/2*(i/n))*s}}const a=new n;function o(e,t){return a.measure(e,t)}function s(e,t){return a.collectDuplicationCandidates(e,t)}function c(e,t){return a.collectFunctionTokenSequences(e,t)}function l(e,t){return a.collectCrossFileDuplicationFileData(e,t)}exports.TreeMeasurer=n,exports.collectCrossFileDuplicationFileData=l,exports.collectDuplicationCandidates=s,exports.collectFunctionTokenSequences=c,exports.defaultMeasurer=a,exports.measureCode=o;
2
2
  //# sourceMappingURL=metrics.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.cjs","names":["createLanguageRegistry","measureCodeNative","collectCrossFileDataNative","collectFunctionTokenSequencesNative"],"sources":["../src/metrics.ts"],"sourcesContent":["import type { CrossFileDuplicateCandidate, CrossFileDuplicationFileData } from './duplication.js';\nimport { createLanguageRegistry } from './languages.js';\nimport {\n collectCrossFileDataNative,\n collectFunctionTokenSequencesNative,\n measureCodeNative,\n type NativeHalsteadCounts,\n type NativeMetricsPayload,\n} from './nativeMetrics.js';\nimport type { CodeMetrics, HalsteadMetrics, LanguageDefinition, LanguageName, MeasureOptions } from './types.js';\n\n/**\n * Measures code metrics for the built-in languages. Parsing and metric passes run in the bundled\n * Rust addon (tree-sitter); this class resolves language aliases, crosses the N-API boundary, and\n * derives the Halstead float metrics from the natively measured counts.\n */\nexport class TreeMeasurer {\n private readonly registry = createLanguageRegistry();\n\n getSupportedLanguages(): LanguageName[] {\n return [...new Set([...this.registry.values()].map((language) => language.name))];\n }\n\n measure(code: string, options: MeasureOptions): CodeMetrics {\n const language = this.resolveLanguage(options.language);\n const includeSyntaxTree = options.includeSyntaxTree ?? false;\n const payload = measureCodeNative(code, language.name, includeSyntaxTree, options.duplication);\n return assembleNativeMetrics(payload, includeSyntaxTree);\n }\n\n /** Collects one file's duplicate candidates for cross-file clone detection. */\n collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return this.collectCrossFileDuplicationFileData(code, options).candidates;\n }\n\n /**\n * Collects one file's duplicate candidates, normalized tokens, and statement structure for\n * cross-file clone detection with measureCrossFileDuplication.\n */\n collectCrossFileDuplicationFileData(code: string, options: MeasureOptions): CrossFileDuplicationFileData {\n const language = this.resolveLanguage(options.language);\n const payload = collectCrossFileDataNative(code, language.name, options.duplication?.minTokens);\n return {\n candidates: payload.candidates,\n tokens: payload.tokens,\n containerStatements: payload.containerStatements,\n // Lets cross-file line coverage count only code lines, like within-file coverage.\n codeLineNumbers: new Set(payload.codeLineNumbers),\n };\n }\n\n /**\n * Normalized token hash sequences of every function, index-parallel to the functions array of\n * measure(): identifiers are anonymized by first occurrence within the function, literals by\n * kind, and keywords/operators kept verbatim, so the regression gate can re-match renamed or\n * moved functions across two revisions by token-LCS similarity.\n */\n collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n const language = this.resolveLanguage(options.language);\n return collectFunctionTokenSequencesNative(code, language.name);\n }\n\n private resolveLanguage(name: LanguageName): LanguageDefinition {\n const language = this.registry.get(name);\n if (!language) {\n throw new Error(`Unsupported language: ${name}`);\n }\n return language;\n }\n}\n\n/**\n * Completes a native measurement into CodeMetrics. The object is rebuilt field by field (rather\n * than spread from the parsed JSON) so the result has a stable shape, including\n * explicitly-undefined optional keys.\n */\nfunction assembleNativeMetrics(payload: NativeMetricsPayload, includeSyntaxTree: boolean): CodeMetrics {\n return {\n language: payload.language,\n bytes: payload.bytes,\n lines: payload.lines,\n functions: payload.functions.map((fn) => ({\n name: fn.name,\n nodeType: fn.nodeType,\n startLine: fn.startLine,\n startColumn: fn.startColumn,\n endLine: fn.endLine,\n endColumn: fn.endColumn,\n cyclomaticComplexity: fn.cyclomaticComplexity,\n cognitiveComplexity: fn.cognitiveComplexity,\n nestingDepth: fn.nestingDepth,\n ncss: fn.ncss,\n parameterCount: fn.parameterCount,\n halstead: deriveHalsteadMetrics(fn.halsteadCounts),\n depDegree: fn.depDegree,\n })),\n cognitiveComplexity: payload.cognitiveComplexity,\n maxCognitiveComplexity: payload.maxCognitiveComplexity,\n nestingDepth: payload.nestingDepth,\n ncssCount: payload.ncssCount,\n duplication: payload.duplication,\n halstead: deriveHalsteadMetrics(payload.halsteadCounts),\n syntaxTree: includeSyntaxTree ? payload.syntaxTree : undefined,\n };\n}\n\nfunction deriveHalsteadMetrics(counts: NativeHalsteadCounts): HalsteadMetrics {\n const { distinctOperators, distinctOperands, totalOperators, totalOperands } = counts;\n const vocabulary = distinctOperators + distinctOperands;\n const length = totalOperators + totalOperands;\n const volume = vocabulary === 0 ? 0 : length * Math.log2(vocabulary);\n const difficulty = distinctOperands === 0 ? 0 : (distinctOperators / 2) * (totalOperands / distinctOperands);\n const effort = difficulty * volume;\n\n return {\n distinctOperators,\n distinctOperands,\n totalOperators,\n totalOperands,\n vocabulary,\n length,\n volume,\n effort,\n };\n}\n\nexport const defaultMeasurer = new TreeMeasurer();\n\nexport function measureCode(code: string, options: MeasureOptions): CodeMetrics {\n return defaultMeasurer.measure(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return defaultMeasurer.collectDuplicationCandidates(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n return defaultMeasurer.collectFunctionTokenSequences(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectCrossFileDuplicationFileData(\n code: string,\n options: MeasureOptions\n): CrossFileDuplicationFileData {\n return defaultMeasurer.collectCrossFileDuplicationFileData(code, options);\n}\n"],"mappings":"iFAgBA,IAAa,EAAb,KAA0B,CACxB,SAA4BA,EAAAA,uBAAuB,EAEnD,uBAAwC,CACtC,MAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,OAAO,CAAC,CAAC,CAAC,IAAK,GAAa,EAAS,IAAI,CAAC,CAAC,CAClF,CAEA,QAAQ,EAAc,EAAsC,CAC1D,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EAChD,EAAoB,EAAQ,mBAAqB,GAEvD,OAAO,EADSC,EAAAA,kBAAkB,EAAM,EAAS,KAAM,EAAmB,EAAQ,WAC/C,EAAG,CAAiB,CACzD,CAGA,6BAA6B,EAAc,EAAwD,CACjG,OAAO,KAAK,oCAAoC,EAAM,CAAO,CAAC,CAAC,UACjE,CAMA,oCAAoC,EAAc,EAAuD,CACvG,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EAChD,EAAUC,EAAAA,2BAA2B,EAAM,EAAS,KAAM,EAAQ,aAAa,SAAS,EAC9F,MAAO,CACL,WAAY,EAAQ,WACpB,OAAQ,EAAQ,OAChB,oBAAqB,EAAQ,oBAE7B,gBAAiB,IAAI,IAAI,EAAQ,eAAe,CAClD,CACF,CAQA,8BAA8B,EAAc,EAAuC,CACjF,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EACtD,OAAOC,EAAAA,oCAAoC,EAAM,EAAS,IAAI,CAChE,CAEA,gBAAwB,EAAwC,CAC9D,IAAM,EAAW,KAAK,SAAS,IAAI,CAAI,EACvC,GAAI,CAAC,EACH,MAAU,MAAM,yBAAyB,GAAM,EAEjD,OAAO,CACT,CACF,EAOA,SAAS,EAAsB,EAA+B,EAAyC,CACrG,MAAO,CACL,SAAU,EAAQ,SAClB,MAAO,EAAQ,MACf,MAAO,EAAQ,MACf,UAAW,EAAQ,UAAU,IAAK,IAAQ,CACxC,KAAM,EAAG,KACT,SAAU,EAAG,SACb,UAAW,EAAG,UACd,YAAa,EAAG,YAChB,QAAS,EAAG,QACZ,UAAW,EAAG,UACd,qBAAsB,EAAG,qBACzB,oBAAqB,EAAG,oBACxB,aAAc,EAAG,aACjB,KAAM,EAAG,KACT,eAAgB,EAAG,eACnB,SAAU,EAAsB,EAAG,cAAc,EACjD,UAAW,EAAG,SAChB,EAAE,EACF,oBAAqB,EAAQ,oBAC7B,uBAAwB,EAAQ,uBAChC,aAAc,EAAQ,aACtB,UAAW,EAAQ,UACnB,YAAa,EAAQ,YACrB,SAAU,EAAsB,EAAQ,cAAc,EACtD,WAAY,EAAoB,EAAQ,WAAa,IAAA,EACvD,CACF,CAEA,SAAS,EAAsB,EAA+C,CAC5E,GAAM,CAAE,oBAAmB,mBAAkB,iBAAgB,iBAAkB,EACzE,EAAa,EAAoB,EACjC,EAAS,EAAiB,EAC1B,EAAS,IAAe,EAAI,EAAI,EAAS,KAAK,KAAK,CAAU,EAInE,MAAO,CACL,oBACA,mBACA,iBACA,gBACA,aACA,SACA,SACA,QAXiB,IAAqB,EAAI,EAAK,EAAoB,GAAM,EAAgB,IAC/D,CAW5B,CACF,CAEA,MAAa,EAAkB,IAAI,EAEnC,SAAgB,EAAY,EAAc,EAAsC,CAC9E,OAAO,EAAgB,QAAQ,EAAM,CAAO,CAC9C,CAGA,SAAgB,EAA6B,EAAc,EAAwD,CACjH,OAAO,EAAgB,6BAA6B,EAAM,CAAO,CACnE,CAGA,SAAgB,EAA8B,EAAc,EAAuC,CACjG,OAAO,EAAgB,8BAA8B,EAAM,CAAO,CACpE,CAGA,SAAgB,EACd,EACA,EAC8B,CAC9B,OAAO,EAAgB,oCAAoC,EAAM,CAAO,CAC1E"}
1
+ {"version":3,"file":"metrics.cjs","names":["createLanguageRegistry","measureCodeNative","collectCrossFileDataNative","collectFunctionTokenSequencesNative"],"sources":["../src/metrics.ts"],"sourcesContent":["import type { CrossFileDuplicateCandidate, CrossFileDuplicationFileData } from './duplication.js';\nimport { createLanguageRegistry } from './languages.js';\nimport {\n collectCrossFileDataNative,\n collectFunctionTokenSequencesNative,\n measureCodeNative,\n type NativeHalsteadCounts,\n type NativeMetricsPayload,\n} from './nativeMetrics.js';\nimport type { CodeMetrics, HalsteadMetrics, LanguageDefinition, LanguageName, MeasureOptions } from './types.js';\n\n/**\n * Measures code metrics for the built-in languages. Parsing and metric passes run in the bundled\n * Rust addon (tree-sitter); this class resolves language aliases, crosses the N-API boundary, and\n * derives the Halstead float metrics from the natively measured counts.\n */\nexport class TreeMeasurer {\n private readonly registry = createLanguageRegistry();\n\n getSupportedLanguages(): LanguageName[] {\n return [...new Set([...this.registry.values()].map((language) => language.name))];\n }\n\n measure(code: string, options: MeasureOptions): CodeMetrics {\n const language = this.resolveLanguage(options.language);\n const includeSyntaxTree = options.includeSyntaxTree ?? false;\n const payload = measureCodeNative(code, language.name, includeSyntaxTree, options.duplication);\n return assembleNativeMetrics(payload, includeSyntaxTree);\n }\n\n /** Collects one file's duplicate candidates for cross-file clone detection. */\n collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return this.collectCrossFileDuplicationFileData(code, options).candidates;\n }\n\n /**\n * Collects one file's duplicate candidates, normalized tokens, and statement structure for\n * cross-file clone detection with measureCrossFileDuplication.\n */\n collectCrossFileDuplicationFileData(code: string, options: MeasureOptions): CrossFileDuplicationFileData {\n const language = this.resolveLanguage(options.language);\n const payload = collectCrossFileDataNative(code, language.name, options.duplication?.minTokens);\n return {\n candidates: payload.candidates,\n tokens: payload.tokens,\n containerStatements: payload.containerStatements,\n // Lets cross-file line coverage count only code lines, like within-file coverage.\n codeLineNumbers: new Set(payload.codeLineNumbers),\n };\n }\n\n /**\n * Normalized token hash sequences of every function, index-parallel to the functions array of\n * measure(): identifiers are anonymized by first occurrence within the function, literals by\n * kind, and keywords/operators kept verbatim, so the regression gate can re-match renamed or\n * moved functions across two revisions by token-LCS similarity.\n */\n collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n const language = this.resolveLanguage(options.language);\n return collectFunctionTokenSequencesNative(code, language.name);\n }\n\n private resolveLanguage(name: LanguageName): LanguageDefinition {\n const language = this.registry.get(name);\n if (!language) {\n throw new Error(`Unsupported language: ${name}`);\n }\n return language;\n }\n}\n\n/**\n * Completes a native measurement into CodeMetrics. The object is rebuilt field by field (rather\n * than spread from the parsed JSON) so the result has a stable shape, including\n * explicitly-undefined optional keys.\n */\nfunction assembleNativeMetrics(payload: NativeMetricsPayload, includeSyntaxTree: boolean): CodeMetrics {\n return {\n language: payload.language,\n bytes: payload.bytes,\n lines: payload.lines,\n functions: payload.functions.map((fn) => ({\n name: fn.name,\n nodeType: fn.nodeType,\n startLine: fn.startLine,\n startColumn: fn.startColumn,\n endLine: fn.endLine,\n endColumn: fn.endColumn,\n cyclomaticComplexity: fn.cyclomaticComplexity,\n cognitiveComplexity: fn.cognitiveComplexity,\n nestingDepth: fn.nestingDepth,\n ncss: fn.ncss,\n parameterCount: fn.parameterCount,\n halstead: deriveHalsteadMetrics(fn.halsteadCounts),\n depDegree: fn.depDegree,\n })),\n cyclomaticComplexity: payload.cyclomaticComplexity,\n cognitiveComplexity: payload.cognitiveComplexity,\n maxCognitiveComplexity: payload.maxCognitiveComplexity,\n nestingDepth: payload.nestingDepth,\n ncssCount: payload.ncssCount,\n duplication: payload.duplication,\n halstead: deriveHalsteadMetrics(payload.halsteadCounts),\n syntaxTree: includeSyntaxTree ? payload.syntaxTree : undefined,\n };\n}\n\nfunction deriveHalsteadMetrics(counts: NativeHalsteadCounts): HalsteadMetrics {\n const { distinctOperators, distinctOperands, totalOperators, totalOperands } = counts;\n const vocabulary = distinctOperators + distinctOperands;\n const length = totalOperators + totalOperands;\n const volume = vocabulary === 0 ? 0 : length * Math.log2(vocabulary);\n const difficulty = distinctOperands === 0 ? 0 : (distinctOperators / 2) * (totalOperands / distinctOperands);\n const effort = difficulty * volume;\n\n return {\n distinctOperators,\n distinctOperands,\n totalOperators,\n totalOperands,\n vocabulary,\n length,\n volume,\n effort,\n };\n}\n\nexport const defaultMeasurer = new TreeMeasurer();\n\nexport function measureCode(code: string, options: MeasureOptions): CodeMetrics {\n return defaultMeasurer.measure(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return defaultMeasurer.collectDuplicationCandidates(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n return defaultMeasurer.collectFunctionTokenSequences(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectCrossFileDuplicationFileData(\n code: string,\n options: MeasureOptions\n): CrossFileDuplicationFileData {\n return defaultMeasurer.collectCrossFileDuplicationFileData(code, options);\n}\n"],"mappings":"iFAgBA,IAAa,EAAb,KAA0B,CACxB,SAA4BA,EAAAA,uBAAuB,EAEnD,uBAAwC,CACtC,MAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,OAAO,CAAC,CAAC,CAAC,IAAK,GAAa,EAAS,IAAI,CAAC,CAAC,CAClF,CAEA,QAAQ,EAAc,EAAsC,CAC1D,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EAChD,EAAoB,EAAQ,mBAAqB,GAEvD,OAAO,EADSC,EAAAA,kBAAkB,EAAM,EAAS,KAAM,EAAmB,EAAQ,WAC/C,EAAG,CAAiB,CACzD,CAGA,6BAA6B,EAAc,EAAwD,CACjG,OAAO,KAAK,oCAAoC,EAAM,CAAO,CAAC,CAAC,UACjE,CAMA,oCAAoC,EAAc,EAAuD,CACvG,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EAChD,EAAUC,EAAAA,2BAA2B,EAAM,EAAS,KAAM,EAAQ,aAAa,SAAS,EAC9F,MAAO,CACL,WAAY,EAAQ,WACpB,OAAQ,EAAQ,OAChB,oBAAqB,EAAQ,oBAE7B,gBAAiB,IAAI,IAAI,EAAQ,eAAe,CAClD,CACF,CAQA,8BAA8B,EAAc,EAAuC,CACjF,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EACtD,OAAOC,EAAAA,oCAAoC,EAAM,EAAS,IAAI,CAChE,CAEA,gBAAwB,EAAwC,CAC9D,IAAM,EAAW,KAAK,SAAS,IAAI,CAAI,EACvC,GAAI,CAAC,EACH,MAAU,MAAM,yBAAyB,GAAM,EAEjD,OAAO,CACT,CACF,EAOA,SAAS,EAAsB,EAA+B,EAAyC,CACrG,MAAO,CACL,SAAU,EAAQ,SAClB,MAAO,EAAQ,MACf,MAAO,EAAQ,MACf,UAAW,EAAQ,UAAU,IAAK,IAAQ,CACxC,KAAM,EAAG,KACT,SAAU,EAAG,SACb,UAAW,EAAG,UACd,YAAa,EAAG,YAChB,QAAS,EAAG,QACZ,UAAW,EAAG,UACd,qBAAsB,EAAG,qBACzB,oBAAqB,EAAG,oBACxB,aAAc,EAAG,aACjB,KAAM,EAAG,KACT,eAAgB,EAAG,eACnB,SAAU,EAAsB,EAAG,cAAc,EACjD,UAAW,EAAG,SAChB,EAAE,EACF,qBAAsB,EAAQ,qBAC9B,oBAAqB,EAAQ,oBAC7B,uBAAwB,EAAQ,uBAChC,aAAc,EAAQ,aACtB,UAAW,EAAQ,UACnB,YAAa,EAAQ,YACrB,SAAU,EAAsB,EAAQ,cAAc,EACtD,WAAY,EAAoB,EAAQ,WAAa,IAAA,EACvD,CACF,CAEA,SAAS,EAAsB,EAA+C,CAC5E,GAAM,CAAE,oBAAmB,mBAAkB,iBAAgB,iBAAkB,EACzE,EAAa,EAAoB,EACjC,EAAS,EAAiB,EAC1B,EAAS,IAAe,EAAI,EAAI,EAAS,KAAK,KAAK,CAAU,EAInE,MAAO,CACL,oBACA,mBACA,iBACA,gBACA,aACA,SACA,SACA,QAXiB,IAAqB,EAAI,EAAK,EAAoB,GAAM,EAAgB,IAC/D,CAW5B,CACF,CAEA,MAAa,EAAkB,IAAI,EAEnC,SAAgB,EAAY,EAAc,EAAsC,CAC9E,OAAO,EAAgB,QAAQ,EAAM,CAAO,CAC9C,CAGA,SAAgB,EAA6B,EAAc,EAAwD,CACjH,OAAO,EAAgB,6BAA6B,EAAM,CAAO,CACnE,CAGA,SAAgB,EAA8B,EAAc,EAAuC,CACjG,OAAO,EAAgB,8BAA8B,EAAM,CAAO,CACpE,CAGA,SAAgB,EACd,EACA,EAC8B,CAC9B,OAAO,EAAgB,oCAAoC,EAAM,CAAO,CAC1E"}
package/dist/metrics.js CHANGED
@@ -1,2 +1,2 @@
1
- import{createLanguageRegistry as e}from"./languages.js";import{collectCrossFileDataNative as t,collectFunctionTokenSequencesNative as n,measureCodeNative as r}from"./nativeMetrics.js";var i=class{registry=e();getSupportedLanguages(){return[...new Set([...this.registry.values()].map(e=>e.name))]}measure(e,t){let n=this.resolveLanguage(t.language),i=t.includeSyntaxTree??!1;return a(r(e,n.name,i,t.duplication),i)}collectDuplicationCandidates(e,t){return this.collectCrossFileDuplicationFileData(e,t).candidates}collectCrossFileDuplicationFileData(e,n){let r=this.resolveLanguage(n.language),i=t(e,r.name,n.duplication?.minTokens);return{candidates:i.candidates,tokens:i.tokens,containerStatements:i.containerStatements,codeLineNumbers:new Set(i.codeLineNumbers)}}collectFunctionTokenSequences(e,t){let r=this.resolveLanguage(t.language);return n(e,r.name)}resolveLanguage(e){let t=this.registry.get(e);if(!t)throw Error(`Unsupported language: ${e}`);return t}};function a(e,t){return{language:e.language,bytes:e.bytes,lines:e.lines,functions:e.functions.map(e=>({name:e.name,nodeType:e.nodeType,startLine:e.startLine,startColumn:e.startColumn,endLine:e.endLine,endColumn:e.endColumn,cyclomaticComplexity:e.cyclomaticComplexity,cognitiveComplexity:e.cognitiveComplexity,nestingDepth:e.nestingDepth,ncss:e.ncss,parameterCount:e.parameterCount,halstead:o(e.halsteadCounts),depDegree:e.depDegree})),cognitiveComplexity:e.cognitiveComplexity,maxCognitiveComplexity:e.maxCognitiveComplexity,nestingDepth:e.nestingDepth,ncssCount:e.ncssCount,duplication:e.duplication,halstead:o(e.halsteadCounts),syntaxTree:t?e.syntaxTree:void 0}}function o(e){let{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i}=e,a=t+n,o=r+i,s=a===0?0:o*Math.log2(a);return{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i,vocabulary:a,length:o,volume:s,effort:(n===0?0:t/2*(i/n))*s}}const s=new i;function c(e,t){return s.measure(e,t)}function l(e,t){return s.collectDuplicationCandidates(e,t)}function u(e,t){return s.collectFunctionTokenSequences(e,t)}function d(e,t){return s.collectCrossFileDuplicationFileData(e,t)}export{i as TreeMeasurer,d as collectCrossFileDuplicationFileData,l as collectDuplicationCandidates,u as collectFunctionTokenSequences,s as defaultMeasurer,c as measureCode};
1
+ import{createLanguageRegistry as e}from"./languages.js";import{collectCrossFileDataNative as t,collectFunctionTokenSequencesNative as n,measureCodeNative as r}from"./nativeMetrics.js";var i=class{registry=e();getSupportedLanguages(){return[...new Set([...this.registry.values()].map(e=>e.name))]}measure(e,t){let n=this.resolveLanguage(t.language),i=t.includeSyntaxTree??!1;return a(r(e,n.name,i,t.duplication),i)}collectDuplicationCandidates(e,t){return this.collectCrossFileDuplicationFileData(e,t).candidates}collectCrossFileDuplicationFileData(e,n){let r=this.resolveLanguage(n.language),i=t(e,r.name,n.duplication?.minTokens);return{candidates:i.candidates,tokens:i.tokens,containerStatements:i.containerStatements,codeLineNumbers:new Set(i.codeLineNumbers)}}collectFunctionTokenSequences(e,t){let r=this.resolveLanguage(t.language);return n(e,r.name)}resolveLanguage(e){let t=this.registry.get(e);if(!t)throw Error(`Unsupported language: ${e}`);return t}};function a(e,t){return{language:e.language,bytes:e.bytes,lines:e.lines,functions:e.functions.map(e=>({name:e.name,nodeType:e.nodeType,startLine:e.startLine,startColumn:e.startColumn,endLine:e.endLine,endColumn:e.endColumn,cyclomaticComplexity:e.cyclomaticComplexity,cognitiveComplexity:e.cognitiveComplexity,nestingDepth:e.nestingDepth,ncss:e.ncss,parameterCount:e.parameterCount,halstead:o(e.halsteadCounts),depDegree:e.depDegree})),cyclomaticComplexity:e.cyclomaticComplexity,cognitiveComplexity:e.cognitiveComplexity,maxCognitiveComplexity:e.maxCognitiveComplexity,nestingDepth:e.nestingDepth,ncssCount:e.ncssCount,duplication:e.duplication,halstead:o(e.halsteadCounts),syntaxTree:t?e.syntaxTree:void 0}}function o(e){let{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i}=e,a=t+n,o=r+i,s=a===0?0:o*Math.log2(a);return{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i,vocabulary:a,length:o,volume:s,effort:(n===0?0:t/2*(i/n))*s}}const s=new i;function c(e,t){return s.measure(e,t)}function l(e,t){return s.collectDuplicationCandidates(e,t)}function u(e,t){return s.collectFunctionTokenSequences(e,t)}function d(e,t){return s.collectCrossFileDuplicationFileData(e,t)}export{i as TreeMeasurer,d as collectCrossFileDuplicationFileData,l as collectDuplicationCandidates,u as collectFunctionTokenSequences,s as defaultMeasurer,c as measureCode};
2
2
  //# sourceMappingURL=metrics.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.js","names":[],"sources":["../src/metrics.ts"],"sourcesContent":["import type { CrossFileDuplicateCandidate, CrossFileDuplicationFileData } from './duplication.js';\nimport { createLanguageRegistry } from './languages.js';\nimport {\n collectCrossFileDataNative,\n collectFunctionTokenSequencesNative,\n measureCodeNative,\n type NativeHalsteadCounts,\n type NativeMetricsPayload,\n} from './nativeMetrics.js';\nimport type { CodeMetrics, HalsteadMetrics, LanguageDefinition, LanguageName, MeasureOptions } from './types.js';\n\n/**\n * Measures code metrics for the built-in languages. Parsing and metric passes run in the bundled\n * Rust addon (tree-sitter); this class resolves language aliases, crosses the N-API boundary, and\n * derives the Halstead float metrics from the natively measured counts.\n */\nexport class TreeMeasurer {\n private readonly registry = createLanguageRegistry();\n\n getSupportedLanguages(): LanguageName[] {\n return [...new Set([...this.registry.values()].map((language) => language.name))];\n }\n\n measure(code: string, options: MeasureOptions): CodeMetrics {\n const language = this.resolveLanguage(options.language);\n const includeSyntaxTree = options.includeSyntaxTree ?? false;\n const payload = measureCodeNative(code, language.name, includeSyntaxTree, options.duplication);\n return assembleNativeMetrics(payload, includeSyntaxTree);\n }\n\n /** Collects one file's duplicate candidates for cross-file clone detection. */\n collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return this.collectCrossFileDuplicationFileData(code, options).candidates;\n }\n\n /**\n * Collects one file's duplicate candidates, normalized tokens, and statement structure for\n * cross-file clone detection with measureCrossFileDuplication.\n */\n collectCrossFileDuplicationFileData(code: string, options: MeasureOptions): CrossFileDuplicationFileData {\n const language = this.resolveLanguage(options.language);\n const payload = collectCrossFileDataNative(code, language.name, options.duplication?.minTokens);\n return {\n candidates: payload.candidates,\n tokens: payload.tokens,\n containerStatements: payload.containerStatements,\n // Lets cross-file line coverage count only code lines, like within-file coverage.\n codeLineNumbers: new Set(payload.codeLineNumbers),\n };\n }\n\n /**\n * Normalized token hash sequences of every function, index-parallel to the functions array of\n * measure(): identifiers are anonymized by first occurrence within the function, literals by\n * kind, and keywords/operators kept verbatim, so the regression gate can re-match renamed or\n * moved functions across two revisions by token-LCS similarity.\n */\n collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n const language = this.resolveLanguage(options.language);\n return collectFunctionTokenSequencesNative(code, language.name);\n }\n\n private resolveLanguage(name: LanguageName): LanguageDefinition {\n const language = this.registry.get(name);\n if (!language) {\n throw new Error(`Unsupported language: ${name}`);\n }\n return language;\n }\n}\n\n/**\n * Completes a native measurement into CodeMetrics. The object is rebuilt field by field (rather\n * than spread from the parsed JSON) so the result has a stable shape, including\n * explicitly-undefined optional keys.\n */\nfunction assembleNativeMetrics(payload: NativeMetricsPayload, includeSyntaxTree: boolean): CodeMetrics {\n return {\n language: payload.language,\n bytes: payload.bytes,\n lines: payload.lines,\n functions: payload.functions.map((fn) => ({\n name: fn.name,\n nodeType: fn.nodeType,\n startLine: fn.startLine,\n startColumn: fn.startColumn,\n endLine: fn.endLine,\n endColumn: fn.endColumn,\n cyclomaticComplexity: fn.cyclomaticComplexity,\n cognitiveComplexity: fn.cognitiveComplexity,\n nestingDepth: fn.nestingDepth,\n ncss: fn.ncss,\n parameterCount: fn.parameterCount,\n halstead: deriveHalsteadMetrics(fn.halsteadCounts),\n depDegree: fn.depDegree,\n })),\n cognitiveComplexity: payload.cognitiveComplexity,\n maxCognitiveComplexity: payload.maxCognitiveComplexity,\n nestingDepth: payload.nestingDepth,\n ncssCount: payload.ncssCount,\n duplication: payload.duplication,\n halstead: deriveHalsteadMetrics(payload.halsteadCounts),\n syntaxTree: includeSyntaxTree ? payload.syntaxTree : undefined,\n };\n}\n\nfunction deriveHalsteadMetrics(counts: NativeHalsteadCounts): HalsteadMetrics {\n const { distinctOperators, distinctOperands, totalOperators, totalOperands } = counts;\n const vocabulary = distinctOperators + distinctOperands;\n const length = totalOperators + totalOperands;\n const volume = vocabulary === 0 ? 0 : length * Math.log2(vocabulary);\n const difficulty = distinctOperands === 0 ? 0 : (distinctOperators / 2) * (totalOperands / distinctOperands);\n const effort = difficulty * volume;\n\n return {\n distinctOperators,\n distinctOperands,\n totalOperators,\n totalOperands,\n vocabulary,\n length,\n volume,\n effort,\n };\n}\n\nexport const defaultMeasurer = new TreeMeasurer();\n\nexport function measureCode(code: string, options: MeasureOptions): CodeMetrics {\n return defaultMeasurer.measure(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return defaultMeasurer.collectDuplicationCandidates(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n return defaultMeasurer.collectFunctionTokenSequences(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectCrossFileDuplicationFileData(\n code: string,\n options: MeasureOptions\n): CrossFileDuplicationFileData {\n return defaultMeasurer.collectCrossFileDuplicationFileData(code, options);\n}\n"],"mappings":"wLAgBA,IAAa,EAAb,KAA0B,CACxB,SAA4B,EAAuB,EAEnD,uBAAwC,CACtC,MAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,OAAO,CAAC,CAAC,CAAC,IAAK,GAAa,EAAS,IAAI,CAAC,CAAC,CAClF,CAEA,QAAQ,EAAc,EAAsC,CAC1D,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EAChD,EAAoB,EAAQ,mBAAqB,GAEvD,OAAO,EADS,EAAkB,EAAM,EAAS,KAAM,EAAmB,EAAQ,WAC/C,EAAG,CAAiB,CACzD,CAGA,6BAA6B,EAAc,EAAwD,CACjG,OAAO,KAAK,oCAAoC,EAAM,CAAO,CAAC,CAAC,UACjE,CAMA,oCAAoC,EAAc,EAAuD,CACvG,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EAChD,EAAU,EAA2B,EAAM,EAAS,KAAM,EAAQ,aAAa,SAAS,EAC9F,MAAO,CACL,WAAY,EAAQ,WACpB,OAAQ,EAAQ,OAChB,oBAAqB,EAAQ,oBAE7B,gBAAiB,IAAI,IAAI,EAAQ,eAAe,CAClD,CACF,CAQA,8BAA8B,EAAc,EAAuC,CACjF,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EACtD,OAAO,EAAoC,EAAM,EAAS,IAAI,CAChE,CAEA,gBAAwB,EAAwC,CAC9D,IAAM,EAAW,KAAK,SAAS,IAAI,CAAI,EACvC,GAAI,CAAC,EACH,MAAU,MAAM,yBAAyB,GAAM,EAEjD,OAAO,CACT,CACF,EAOA,SAAS,EAAsB,EAA+B,EAAyC,CACrG,MAAO,CACL,SAAU,EAAQ,SAClB,MAAO,EAAQ,MACf,MAAO,EAAQ,MACf,UAAW,EAAQ,UAAU,IAAK,IAAQ,CACxC,KAAM,EAAG,KACT,SAAU,EAAG,SACb,UAAW,EAAG,UACd,YAAa,EAAG,YAChB,QAAS,EAAG,QACZ,UAAW,EAAG,UACd,qBAAsB,EAAG,qBACzB,oBAAqB,EAAG,oBACxB,aAAc,EAAG,aACjB,KAAM,EAAG,KACT,eAAgB,EAAG,eACnB,SAAU,EAAsB,EAAG,cAAc,EACjD,UAAW,EAAG,SAChB,EAAE,EACF,oBAAqB,EAAQ,oBAC7B,uBAAwB,EAAQ,uBAChC,aAAc,EAAQ,aACtB,UAAW,EAAQ,UACnB,YAAa,EAAQ,YACrB,SAAU,EAAsB,EAAQ,cAAc,EACtD,WAAY,EAAoB,EAAQ,WAAa,IAAA,EACvD,CACF,CAEA,SAAS,EAAsB,EAA+C,CAC5E,GAAM,CAAE,oBAAmB,mBAAkB,iBAAgB,iBAAkB,EACzE,EAAa,EAAoB,EACjC,EAAS,EAAiB,EAC1B,EAAS,IAAe,EAAI,EAAI,EAAS,KAAK,KAAK,CAAU,EAInE,MAAO,CACL,oBACA,mBACA,iBACA,gBACA,aACA,SACA,SACA,QAXiB,IAAqB,EAAI,EAAK,EAAoB,GAAM,EAAgB,IAC/D,CAW5B,CACF,CAEA,MAAa,EAAkB,IAAI,EAEnC,SAAgB,EAAY,EAAc,EAAsC,CAC9E,OAAO,EAAgB,QAAQ,EAAM,CAAO,CAC9C,CAGA,SAAgB,EAA6B,EAAc,EAAwD,CACjH,OAAO,EAAgB,6BAA6B,EAAM,CAAO,CACnE,CAGA,SAAgB,EAA8B,EAAc,EAAuC,CACjG,OAAO,EAAgB,8BAA8B,EAAM,CAAO,CACpE,CAGA,SAAgB,EACd,EACA,EAC8B,CAC9B,OAAO,EAAgB,oCAAoC,EAAM,CAAO,CAC1E"}
1
+ {"version":3,"file":"metrics.js","names":[],"sources":["../src/metrics.ts"],"sourcesContent":["import type { CrossFileDuplicateCandidate, CrossFileDuplicationFileData } from './duplication.js';\nimport { createLanguageRegistry } from './languages.js';\nimport {\n collectCrossFileDataNative,\n collectFunctionTokenSequencesNative,\n measureCodeNative,\n type NativeHalsteadCounts,\n type NativeMetricsPayload,\n} from './nativeMetrics.js';\nimport type { CodeMetrics, HalsteadMetrics, LanguageDefinition, LanguageName, MeasureOptions } from './types.js';\n\n/**\n * Measures code metrics for the built-in languages. Parsing and metric passes run in the bundled\n * Rust addon (tree-sitter); this class resolves language aliases, crosses the N-API boundary, and\n * derives the Halstead float metrics from the natively measured counts.\n */\nexport class TreeMeasurer {\n private readonly registry = createLanguageRegistry();\n\n getSupportedLanguages(): LanguageName[] {\n return [...new Set([...this.registry.values()].map((language) => language.name))];\n }\n\n measure(code: string, options: MeasureOptions): CodeMetrics {\n const language = this.resolveLanguage(options.language);\n const includeSyntaxTree = options.includeSyntaxTree ?? false;\n const payload = measureCodeNative(code, language.name, includeSyntaxTree, options.duplication);\n return assembleNativeMetrics(payload, includeSyntaxTree);\n }\n\n /** Collects one file's duplicate candidates for cross-file clone detection. */\n collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return this.collectCrossFileDuplicationFileData(code, options).candidates;\n }\n\n /**\n * Collects one file's duplicate candidates, normalized tokens, and statement structure for\n * cross-file clone detection with measureCrossFileDuplication.\n */\n collectCrossFileDuplicationFileData(code: string, options: MeasureOptions): CrossFileDuplicationFileData {\n const language = this.resolveLanguage(options.language);\n const payload = collectCrossFileDataNative(code, language.name, options.duplication?.minTokens);\n return {\n candidates: payload.candidates,\n tokens: payload.tokens,\n containerStatements: payload.containerStatements,\n // Lets cross-file line coverage count only code lines, like within-file coverage.\n codeLineNumbers: new Set(payload.codeLineNumbers),\n };\n }\n\n /**\n * Normalized token hash sequences of every function, index-parallel to the functions array of\n * measure(): identifiers are anonymized by first occurrence within the function, literals by\n * kind, and keywords/operators kept verbatim, so the regression gate can re-match renamed or\n * moved functions across two revisions by token-LCS similarity.\n */\n collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n const language = this.resolveLanguage(options.language);\n return collectFunctionTokenSequencesNative(code, language.name);\n }\n\n private resolveLanguage(name: LanguageName): LanguageDefinition {\n const language = this.registry.get(name);\n if (!language) {\n throw new Error(`Unsupported language: ${name}`);\n }\n return language;\n }\n}\n\n/**\n * Completes a native measurement into CodeMetrics. The object is rebuilt field by field (rather\n * than spread from the parsed JSON) so the result has a stable shape, including\n * explicitly-undefined optional keys.\n */\nfunction assembleNativeMetrics(payload: NativeMetricsPayload, includeSyntaxTree: boolean): CodeMetrics {\n return {\n language: payload.language,\n bytes: payload.bytes,\n lines: payload.lines,\n functions: payload.functions.map((fn) => ({\n name: fn.name,\n nodeType: fn.nodeType,\n startLine: fn.startLine,\n startColumn: fn.startColumn,\n endLine: fn.endLine,\n endColumn: fn.endColumn,\n cyclomaticComplexity: fn.cyclomaticComplexity,\n cognitiveComplexity: fn.cognitiveComplexity,\n nestingDepth: fn.nestingDepth,\n ncss: fn.ncss,\n parameterCount: fn.parameterCount,\n halstead: deriveHalsteadMetrics(fn.halsteadCounts),\n depDegree: fn.depDegree,\n })),\n cyclomaticComplexity: payload.cyclomaticComplexity,\n cognitiveComplexity: payload.cognitiveComplexity,\n maxCognitiveComplexity: payload.maxCognitiveComplexity,\n nestingDepth: payload.nestingDepth,\n ncssCount: payload.ncssCount,\n duplication: payload.duplication,\n halstead: deriveHalsteadMetrics(payload.halsteadCounts),\n syntaxTree: includeSyntaxTree ? payload.syntaxTree : undefined,\n };\n}\n\nfunction deriveHalsteadMetrics(counts: NativeHalsteadCounts): HalsteadMetrics {\n const { distinctOperators, distinctOperands, totalOperators, totalOperands } = counts;\n const vocabulary = distinctOperators + distinctOperands;\n const length = totalOperators + totalOperands;\n const volume = vocabulary === 0 ? 0 : length * Math.log2(vocabulary);\n const difficulty = distinctOperands === 0 ? 0 : (distinctOperators / 2) * (totalOperands / distinctOperands);\n const effort = difficulty * volume;\n\n return {\n distinctOperators,\n distinctOperands,\n totalOperators,\n totalOperands,\n vocabulary,\n length,\n volume,\n effort,\n };\n}\n\nexport const defaultMeasurer = new TreeMeasurer();\n\nexport function measureCode(code: string, options: MeasureOptions): CodeMetrics {\n return defaultMeasurer.measure(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return defaultMeasurer.collectDuplicationCandidates(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n return defaultMeasurer.collectFunctionTokenSequences(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectCrossFileDuplicationFileData(\n code: string,\n options: MeasureOptions\n): CrossFileDuplicationFileData {\n return defaultMeasurer.collectCrossFileDuplicationFileData(code, options);\n}\n"],"mappings":"wLAgBA,IAAa,EAAb,KAA0B,CACxB,SAA4B,EAAuB,EAEnD,uBAAwC,CACtC,MAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,OAAO,CAAC,CAAC,CAAC,IAAK,GAAa,EAAS,IAAI,CAAC,CAAC,CAClF,CAEA,QAAQ,EAAc,EAAsC,CAC1D,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EAChD,EAAoB,EAAQ,mBAAqB,GAEvD,OAAO,EADS,EAAkB,EAAM,EAAS,KAAM,EAAmB,EAAQ,WAC/C,EAAG,CAAiB,CACzD,CAGA,6BAA6B,EAAc,EAAwD,CACjG,OAAO,KAAK,oCAAoC,EAAM,CAAO,CAAC,CAAC,UACjE,CAMA,oCAAoC,EAAc,EAAuD,CACvG,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EAChD,EAAU,EAA2B,EAAM,EAAS,KAAM,EAAQ,aAAa,SAAS,EAC9F,MAAO,CACL,WAAY,EAAQ,WACpB,OAAQ,EAAQ,OAChB,oBAAqB,EAAQ,oBAE7B,gBAAiB,IAAI,IAAI,EAAQ,eAAe,CAClD,CACF,CAQA,8BAA8B,EAAc,EAAuC,CACjF,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EACtD,OAAO,EAAoC,EAAM,EAAS,IAAI,CAChE,CAEA,gBAAwB,EAAwC,CAC9D,IAAM,EAAW,KAAK,SAAS,IAAI,CAAI,EACvC,GAAI,CAAC,EACH,MAAU,MAAM,yBAAyB,GAAM,EAEjD,OAAO,CACT,CACF,EAOA,SAAS,EAAsB,EAA+B,EAAyC,CACrG,MAAO,CACL,SAAU,EAAQ,SAClB,MAAO,EAAQ,MACf,MAAO,EAAQ,MACf,UAAW,EAAQ,UAAU,IAAK,IAAQ,CACxC,KAAM,EAAG,KACT,SAAU,EAAG,SACb,UAAW,EAAG,UACd,YAAa,EAAG,YAChB,QAAS,EAAG,QACZ,UAAW,EAAG,UACd,qBAAsB,EAAG,qBACzB,oBAAqB,EAAG,oBACxB,aAAc,EAAG,aACjB,KAAM,EAAG,KACT,eAAgB,EAAG,eACnB,SAAU,EAAsB,EAAG,cAAc,EACjD,UAAW,EAAG,SAChB,EAAE,EACF,qBAAsB,EAAQ,qBAC9B,oBAAqB,EAAQ,oBAC7B,uBAAwB,EAAQ,uBAChC,aAAc,EAAQ,aACtB,UAAW,EAAQ,UACnB,YAAa,EAAQ,YACrB,SAAU,EAAsB,EAAQ,cAAc,EACtD,WAAY,EAAoB,EAAQ,WAAa,IAAA,EACvD,CACF,CAEA,SAAS,EAAsB,EAA+C,CAC5E,GAAM,CAAE,oBAAmB,mBAAkB,iBAAgB,iBAAkB,EACzE,EAAa,EAAoB,EACjC,EAAS,EAAiB,EAC1B,EAAS,IAAe,EAAI,EAAI,EAAS,KAAK,KAAK,CAAU,EAInE,MAAO,CACL,oBACA,mBACA,iBACA,gBACA,aACA,SACA,SACA,QAXiB,IAAqB,EAAI,EAAK,EAAoB,GAAM,EAAgB,IAC/D,CAW5B,CACF,CAEA,MAAa,EAAkB,IAAI,EAEnC,SAAgB,EAAY,EAAc,EAAsC,CAC9E,OAAO,EAAgB,QAAQ,EAAM,CAAO,CAC9C,CAGA,SAAgB,EAA6B,EAAc,EAAwD,CACjH,OAAO,EAAgB,6BAA6B,EAAM,CAAO,CACnE,CAGA,SAAgB,EAA8B,EAAc,EAAuC,CACjG,OAAO,EAAgB,8BAA8B,EAAM,CAAO,CACpE,CAGA,SAAgB,EACd,EACA,EAC8B,CAC9B,OAAO,EAAgB,oCAAoC,EAAM,CAAO,CAC1E"}
@@ -1,4 +1,4 @@
1
1
  "use strict";let e=require("node:module");function t(e,t,n,r){return JSON.parse(l().measureCodeNative(i(e),t,n,a(r?.minTokens),a(r?.maxGapTokens),a(r?.minSimilarityPercent)))}function n(e,t,n){return JSON.parse(l().collectCrossFileDataNative(i(e),t,a(n)))}function r(e,t){return JSON.parse(l().collectFunctionTokenSequencesNative(i(e),t)).map(e=>Int32Array.from(e))}function i(e){return e.isWellFormed()?e:e.toWellFormed()}function a(e){return e===void 0||Number.isNaN(e)?void 0:Math.min(Math.max(Math.trunc(e),0),4294967295)}var o=class extends Error{};let s,c;function l(){if(s)return s;if(c)throw c;let t=(0,e.createRequire)(require("url").pathToFileURL(__filename).href),n=[`code-gauge-${u()}`,`../native/code-gauge.node`],r=[];for(let e of n){let n;try{n=t(e)}catch(t){r.push(` ${e}: ${t instanceof Error?t.message.split(`
2
- `)[0]:String(t)}`);continue}let i=n.payloadVersion?.();if(i!==5){r.push(` ${e}: payload version ${i??`unknown`} does not match the expected 5; rebuild the addon with \`bun run build-native\``);continue}return s=n,n}throw c=new o(`The code-gauge native addon is not available for ${u()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain); when installing with npm, also allow install scripts for code-gauge so its postinstall build can run.\n${r.join(`
2
+ `)[0]:String(t)}`);continue}let i=n.payloadVersion?.();if(i!==6){r.push(` ${e}: payload version ${i??`unknown`} does not match the expected 6; rebuild the addon with \`bun run build-native\``);continue}return s=n,n}throw c=new o(`The code-gauge native addon is not available for ${u()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain); when installing with npm, also allow install scripts for code-gauge so its postinstall build can run.\n${r.join(`
3
3
  `)}`),c}function u(){let e=`${process.platform}-${process.arch}`;return process.platform===`win32`?`${e}-msvc`:process.platform===`linux`?(process.report?.getReport())?.header?.glibcVersionRuntime?`${e}-gnu`:`${e}-musl`:e}exports.NativeAddonError=o,exports.collectCrossFileDataNative=n,exports.collectFunctionTokenSequencesNative=r,exports.measureCodeNative=t;
4
4
  //# sourceMappingURL=nativeMetrics.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"nativeMetrics.cjs","names":["createRequire"],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n}\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 /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ninterface NativeBinding {\n measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens?: number,\n maxGapTokens?: number,\n minSimilarityPercent?: number\n ): string;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields instead of failing with a clear rebuild message.\n */\nconst expectedPayloadVersion = 5;\n\n/** Measures one file via the native addon, returning the raw payload for assembly in metrics.ts. */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent)\n )\n ) as NativeMetricsPayload;\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value)\n ? undefined\n : Math.min(Math.max(Math.trunc(value), 0), 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n const requireNative = createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${error instanceof Error ? error.message.split('\\n')[0] : String(error)}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n 'toolchain); when installing with npm, also allow install scripts for code-gauge so its ' +\n `postinstall build can run.\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"0CA0DA,SAAgB,EACd,EACA,EACA,EACA,EACsB,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,CAC9C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAC5C,IAAA,GACA,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,CAAK,EAAG,CAAC,EAAG,UAAa,CAC5D,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAEJ,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAGR,IAAM,GAAA,EAAgBA,EAAAA,cAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,IAA6B,EAC7C,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,IAAI,aAAiB,MAAQ,EAAM,QAAQ,MAAM;CAAI,CAAC,CAAC,GAAK,OAAO,CAAK,GAAG,EACxG,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAY,EAAwB,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAOA,KANA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,2NAGrC,EAAS,KAAK;CAAI,GACrD,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
1
+ {"version":3,"file":"nativeMetrics.cjs","names":["createRequire"],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n}\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 /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ninterface NativeBinding {\n measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens?: number,\n maxGapTokens?: number,\n minSimilarityPercent?: number\n ): string;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields instead of failing with a clear rebuild message.\n */\nconst expectedPayloadVersion = 6;\n\n/** Measures one file via the native addon, returning the raw payload for assembly in metrics.ts. */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent)\n )\n ) as NativeMetricsPayload;\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value)\n ? undefined\n : Math.min(Math.max(Math.trunc(value), 0), 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n const requireNative = createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${error instanceof Error ? error.message.split('\\n')[0] : String(error)}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n 'toolchain); when installing with npm, also allow install scripts for code-gauge so its ' +\n `postinstall build can run.\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"0CA0DA,SAAgB,EACd,EACA,EACA,EACA,EACsB,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,CAC9C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAC5C,IAAA,GACA,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,CAAK,EAAG,CAAC,EAAG,UAAa,CAC5D,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAEJ,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAGR,IAAM,GAAA,EAAgBA,EAAAA,cAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,IAA6B,EAC7C,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,IAAI,aAAiB,MAAQ,EAAM,QAAQ,MAAM;CAAI,CAAC,CAAC,GAAK,OAAO,CAAK,GAAG,EACxG,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAY,EAAwB,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAOA,KANA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,2NAGrC,EAAS,KAAK;CAAI,GACrD,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
@@ -1,4 +1,4 @@
1
1
  import{createRequire as e}from"node:module";function t(e,t,n,r){return JSON.parse(l().measureCodeNative(i(e),t,n,a(r?.minTokens),a(r?.maxGapTokens),a(r?.minSimilarityPercent)))}function n(e,t,n){return JSON.parse(l().collectCrossFileDataNative(i(e),t,a(n)))}function r(e,t){return JSON.parse(l().collectFunctionTokenSequencesNative(i(e),t)).map(e=>Int32Array.from(e))}function i(e){return e.isWellFormed()?e:e.toWellFormed()}function a(e){return e===void 0||Number.isNaN(e)?void 0:Math.min(Math.max(Math.trunc(e),0),4294967295)}var o=class extends Error{};let s,c;function l(){if(s)return s;if(c)throw c;let t=e(import.meta.url),n=[`code-gauge-${u()}`,`../native/code-gauge.node`],r=[];for(let e of n){let n;try{n=t(e)}catch(t){r.push(` ${e}: ${t instanceof Error?t.message.split(`
2
- `)[0]:String(t)}`);continue}let i=n.payloadVersion?.();if(i!==5){r.push(` ${e}: payload version ${i??`unknown`} does not match the expected 5; rebuild the addon with \`bun run build-native\``);continue}return s=n,n}throw c=new o(`The code-gauge native addon is not available for ${u()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain); when installing with npm, also allow install scripts for code-gauge so its postinstall build can run.\n${r.join(`
2
+ `)[0]:String(t)}`);continue}let i=n.payloadVersion?.();if(i!==6){r.push(` ${e}: payload version ${i??`unknown`} does not match the expected 6; rebuild the addon with \`bun run build-native\``);continue}return s=n,n}throw c=new o(`The code-gauge native addon is not available for ${u()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain); when installing with npm, also allow install scripts for code-gauge so its postinstall build can run.\n${r.join(`
3
3
  `)}`),c}function u(){let e=`${process.platform}-${process.arch}`;return process.platform===`win32`?`${e}-msvc`:process.platform===`linux`?(process.report?.getReport())?.header?.glibcVersionRuntime?`${e}-gnu`:`${e}-musl`:e}export{o as NativeAddonError,n as collectCrossFileDataNative,r as collectFunctionTokenSequencesNative,t as measureCodeNative};
4
4
  //# sourceMappingURL=nativeMetrics.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"nativeMetrics.js","names":[],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n}\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 /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ninterface NativeBinding {\n measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens?: number,\n maxGapTokens?: number,\n minSimilarityPercent?: number\n ): string;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields instead of failing with a clear rebuild message.\n */\nconst expectedPayloadVersion = 5;\n\n/** Measures one file via the native addon, returning the raw payload for assembly in metrics.ts. */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent)\n )\n ) as NativeMetricsPayload;\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value)\n ? undefined\n : Math.min(Math.max(Math.trunc(value), 0), 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n const requireNative = createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${error instanceof Error ? error.message.split('\\n')[0] : String(error)}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n 'toolchain); when installing with npm, also allow install scripts for code-gauge so its ' +\n `postinstall build can run.\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"4CA0DA,SAAgB,EACd,EACA,EACA,EACA,EACsB,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,CAC9C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAC5C,IAAA,GACA,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,CAAK,EAAG,CAAC,EAAG,UAAa,CAC5D,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAEJ,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAGR,IAAM,EAAgB,EAAc,YAAY,GAAG,EAC7C,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,IAAI,aAAiB,MAAQ,EAAM,QAAQ,MAAM;CAAI,CAAC,CAAC,GAAK,OAAO,CAAK,GAAG,EACxG,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAY,EAAwB,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAOA,KANA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,2NAGrC,EAAS,KAAK;CAAI,GACrD,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
1
+ {"version":3,"file":"nativeMetrics.js","names":[],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n}\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 /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ninterface NativeBinding {\n measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens?: number,\n maxGapTokens?: number,\n minSimilarityPercent?: number\n ): string;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields instead of failing with a clear rebuild message.\n */\nconst expectedPayloadVersion = 6;\n\n/** Measures one file via the native addon, returning the raw payload for assembly in metrics.ts. */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent)\n )\n ) as NativeMetricsPayload;\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value)\n ? undefined\n : Math.min(Math.max(Math.trunc(value), 0), 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n const requireNative = createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${error instanceof Error ? error.message.split('\\n')[0] : String(error)}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n 'toolchain); when installing with npm, also allow install scripts for code-gauge so its ' +\n `postinstall build can run.\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"4CA0DA,SAAgB,EACd,EACA,EACA,EACA,EACsB,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,CAC9C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAC5C,IAAA,GACA,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,CAAK,EAAG,CAAC,EAAG,UAAa,CAC5D,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAEJ,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAGR,IAAM,EAAgB,EAAc,YAAY,GAAG,EAC7C,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,IAAI,aAAiB,MAAQ,EAAM,QAAQ,MAAM;CAAI,CAAC,CAAC,GAAK,OAAO,CAAK,GAAG,EACxG,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAY,EAAwB,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAOA,KANA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,2NAGrC,EAAS,KAAK;CAAI,GACrD,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
package/dist/types.d.ts CHANGED
@@ -65,13 +65,12 @@ export interface FunctionMetrics {
65
65
  /** Exclusive end column, so functions can be tested for containment in each other. */
66
66
  endColumn: number;
67
67
  /**
68
- * McCabe cyclomatic complexity of the function's own body: 1 plus one per decision point
69
- * (branch, loop, non-default case label, catch, ternary, boolean operator, pattern guard). Java
70
- * follows PMD's `CyclomaticComplexity` instead, which also counts `throw` but counts `&&`/`||`
71
- * only inside branch, loop, ternary, and switch conditions and adds nothing for pattern labels
72
- * and guards. Nested function bodies are excluded, so summing over functions counts each
73
- * decision point once. Not used by ranking or the regression gate; exposed for consumers that
74
- * report PMD-compatible metrics.
68
+ * McCabe cyclomatic complexity of the function's own body, counted from source as NIST SP 500-235
69
+ * §4 prescribes: 1 plus one per decision (branch, loop, catch, ternary, pattern guard), per
70
+ * short-circuit operator (`&&`, `||`, `and`, `or`) wherever it appears, and per case-labelled
71
+ * statement or match arm (stacked labels share one; default and catch-all arms add none).
72
+ * Full-evaluation operators and `throw` add nothing. Nested function bodies are excluded, so
73
+ * summing over functions counts each decision once. Not used by ranking or the regression gate.
75
74
  */
76
75
  cyclomaticComplexity: number;
77
76
  cognitiveComplexity: number;
@@ -141,6 +140,16 @@ export interface CodeMetrics {
141
140
  bytes: number;
142
141
  lines: LineMetrics;
143
142
  functions: FunctionMetrics[];
143
+ /**
144
+ * The file's cyclomatic complexity as McCabe's v = e - n + 2p over its components: the sum of
145
+ * every function's value, plus 1 per initializer block (Java static/instance initializers, Kotlin
146
+ * `init`, JavaScript/TypeScript class `static` blocks), plus the decisions outside functions,
147
+ * plus 1 for the module body when
148
+ * the file runs top-level code (always for JavaScript, TypeScript, Python, and Ruby; for C# and
149
+ * Kotlin when the file has top-level statements, except a Kotlin file with parse errors, whose top
150
+ * level the grammar cannot be trusted to have read correctly).
151
+ */
152
+ cyclomaticComplexity: number;
144
153
  cognitiveComplexity: number;
145
154
  maxCognitiveComplexity: number;
146
155
  nestingDepth: number;
@@ -12,7 +12,6 @@ pub struct ComplexityResult {
12
12
  pub struct LanguageSets {
13
13
  pub function_nodes: HashSet<&'static str>,
14
14
  pub decision_nodes: HashSet<&'static str>,
15
- pub pmd_cyclomatic: bool,
16
15
  pub nesting_nodes: HashSet<&'static str>,
17
16
  pub ncss_nodes: HashSet<&'static str>,
18
17
  pub ncss_containers: HashSet<&'static str>,
@@ -23,7 +22,6 @@ impl LanguageSets {
23
22
  LanguageSets {
24
23
  function_nodes: language.function_node_types.iter().copied().collect(),
25
24
  decision_nodes: language.decision_node_types.iter().copied().collect(),
26
- pmd_cyclomatic: language.pmd_cyclomatic,
27
25
  nesting_nodes: language.nesting_node_types.iter().copied().collect(),
28
26
  ncss_nodes: language.ncss_node_types.iter().copied().collect(),
29
27
  ncss_containers: language.ncss_container_node_types.iter().copied().collect(),
@@ -138,6 +136,16 @@ struct FunctionBodyPass<'sets, 'code, 'source> {
138
136
  code: &'code Source<'source>,
139
137
  frames: Vec<FunctionBodyFrame>,
140
138
  results: HashMap<usize, FunctionBodyMetrics>,
139
+ /// Cyclomatic decisions inside class bodies nested in functions, which no function owns.
140
+ nested_class_decisions: u64,
141
+ }
142
+
143
+ /// Per-function body metrics, plus the cyclomatic decisions no function body owns.
144
+ pub struct BodyMetrics {
145
+ pub by_function: HashMap<usize, FunctionBodyMetrics>,
146
+ /// Cyclomatic decisions outside every function body (top-level statements, field initializers,
147
+ /// including those of classes nested in functions).
148
+ pub top_level_decisions: u64,
141
149
  }
142
150
 
143
151
  /// Per-function complexity and NCSS for every function boundary, in one post-order pass so each
@@ -152,16 +160,20 @@ pub fn measure_function_body_metrics(
152
160
  root: Node<'_>,
153
161
  sets: &LanguageSets,
154
162
  code: &Source<'_>,
155
- ) -> HashMap<usize, FunctionBodyMetrics> {
163
+ ) -> BodyMetrics {
156
164
  let mut pass = FunctionBodyPass {
157
165
  sets,
158
166
  code,
159
- // frames[0] is a sentinel for top-level code; its accumulation is discarded.
167
+ // frames[0] is a sentinel for top-level code; only its cyclomatic decisions are kept.
160
168
  frames: vec![FunctionBodyFrame::new(0, 0)],
161
169
  results: HashMap::new(),
170
+ nested_class_decisions: 0,
162
171
  };
163
172
  pass.visit(root, 0, 0, false, false, false);
164
- pass.results
173
+ BodyMetrics {
174
+ by_function: pass.results,
175
+ top_level_decisions: pass.frames[0].cyclomatic_complexity - 1 + pass.nested_class_decisions,
176
+ }
165
177
  }
166
178
 
167
179
  impl FunctionBodyPass<'_, '_, '_> {
@@ -201,7 +213,7 @@ impl FunctionBodyPass<'_, '_, '_> {
201
213
  // `if` keyword token), so only named nodes count as decisions.
202
214
  let is_decision = current.is_named()
203
215
  && self.sets.decision_nodes.contains(current.kind())
204
- && !is_default_switch_branch(current);
216
+ && !is_pathless_switch_branch(current, self.code);
205
217
  let is_case_clause = current.is_named() && CASE_CLAUSE_NODE_TYPES.contains(&current.kind());
206
218
  // Ruby's `case ... else` arm is an `else` node; like every other language's default branch
207
219
  // it nests its contents inside the switch (it cannot go in the Ruby nesting set because
@@ -216,18 +228,14 @@ impl FunctionBodyPass<'_, '_, '_> {
216
228
  // surcharge (Sonar cognitive-complexity semantics).
217
229
  let is_continuation = is_decision && is_flat_chain_continuation(current);
218
230
 
219
- if counts_for_own_body {
220
- self.top_frame().cyclomatic_complexity += if self.sets.pmd_cyclomatic {
221
- crate::cyclomatic::pmd_cyclomatic_increment(current, self.code)
222
- } else {
223
- // Each boolean operator and pattern guard is one more execution path; `else` adds
224
- // none.
225
- u64::from(
226
- is_decision
227
- || is_boolean_operator(current, self.code)
228
- || is_pattern_guard(current),
229
- )
230
- };
231
+ // Each branch, short-circuit operator, and pattern guard adds one path (McCabe; NIST SP
232
+ // 500-235 §4); `else` adds none.
233
+ if is_decision || is_boolean_operator(current, self.code) || is_pattern_guard(current) {
234
+ if counts_for_own_body {
235
+ self.top_frame().cyclomatic_complexity += 1;
236
+ } else if !opens_frame {
237
+ self.nested_class_decisions += 1;
238
+ }
231
239
  }
232
240
  if is_decision && !is_case_clause {
233
241
  if is_continuation {
@@ -380,7 +388,7 @@ pub fn measure_complexity(
380
388
  // `if` keyword token), so only named nodes count as decisions.
381
389
  let is_decision = current.is_named()
382
390
  && sets.decision_nodes.contains(current.kind())
383
- && !is_default_switch_branch(current);
391
+ && !is_pathless_switch_branch(current, code);
384
392
  let is_case_clause = current.is_named() && CASE_CLAUSE_NODE_TYPES.contains(&current.kind());
385
393
  // Ruby's `case ... else` arm is an `else` node; like every other language's default branch
386
394
  // it nests its contents inside the switch (it cannot go in the Ruby nesting set because
@@ -633,13 +641,93 @@ fn is_flat_chain_continuation(node: Node<'_>) -> bool {
633
641
  .is_some_and(|alternative| alternative.id() == node.id())
634
642
  }
635
643
 
636
- /// Default branches of switch-like constructs add no decision.
637
- fn is_default_switch_branch(node: Node<'_>) -> bool {
644
+ /// Switch branches that add no path. NIST SP 500-235 counts one path per case-labelled statement:
645
+ /// a label-only case shares the statement of the case below it, and that statement adds a path
646
+ /// unless one of its stacked labels is an unguarded default or catch-all (`case 3: default: g();`
647
+ /// and `default: case 3: g();` are the default outcome) or all of them are catch-alls. Guards are
648
+ /// charged separately, so a guarded catch-all never absorbs a case stacked with it.
649
+ fn is_pathless_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
650
+ if is_label_only_case(node) {
651
+ return true;
652
+ }
653
+ let mut stacked = vec![node];
654
+ let mut previous = node.prev_named_sibling();
655
+ while let Some(sibling) = previous {
656
+ if !crate::ncss::COMMENT_NODE_TYPES.contains(&sibling.kind()) {
657
+ if !is_label_only_case(sibling) {
658
+ break;
659
+ }
660
+ stacked.push(sibling);
661
+ }
662
+ previous = sibling.prev_named_sibling();
663
+ }
664
+ stacked
665
+ .iter()
666
+ .any(|label| is_default_switch_branch(*label, code) && !has_pattern_guard(*label))
667
+ || stacked
668
+ .iter()
669
+ .all(|label| is_default_switch_branch(*label, code))
670
+ }
671
+
672
+ /// A guard directly on the arm (C# `when`, Python `if`, Ruby `if`/`unless`) or inside its label or
673
+ /// pattern (Java `when`, Rust `if`).
674
+ fn has_pattern_guard(node: Node<'_>) -> bool {
675
+ crate::util::named_children(node).into_iter().any(|child| {
676
+ is_pattern_guard(child)
677
+ || crate::util::named_children(child)
678
+ .into_iter()
679
+ .any(is_pattern_guard)
680
+ })
681
+ }
682
+
683
+ /// A C/C++/JS/Java/C# case whose labels share the next case's statements; every grammar parses each
684
+ /// stacked label as its own case node.
685
+ fn is_label_only_case(node: Node<'_>) -> bool {
686
+ let children = non_comment_children(node);
687
+ match node.kind() {
688
+ "case_statement" => {
689
+ let value = node.child_by_field_name("value").map(|value| value.id());
690
+ children.iter().all(|child| Some(child.id()) == value)
691
+ }
692
+ "switch_case" | "switch_default" => node.child_by_field_name("body").is_none(),
693
+ "switch_block_statement_group" => {
694
+ children.iter().all(|child| child.kind() == "switch_label")
695
+ }
696
+ // A C# label is a pattern (`case 1:` parses as a constant pattern) plus an optional `when`
697
+ // guard; anything else, a `#if` block included, is the section's body.
698
+ "switch_section" => children.iter().all(|child| {
699
+ child.kind().ends_with("pattern")
700
+ || child.kind() == "discard"
701
+ || child.kind() == "when_clause"
702
+ }),
703
+ _ => false,
704
+ }
705
+ }
706
+
707
+ fn is_default_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
638
708
  let kind = node.kind();
709
+ if kind == "switch_default" {
710
+ return true;
711
+ }
639
712
  if kind == "case_statement" {
640
713
  return node.child_by_field_name("value").is_none();
641
714
  }
642
715
 
716
+ // Java `default:` groups and `default ->` rules: a label with no expression or pattern, or a
717
+ // `case null, default` label, whose `default` the grammar parses as an identifier.
718
+ if kind == "switch_block_statement_group" || kind == "switch_rule" {
719
+ return non_comment_children(node)
720
+ .into_iter()
721
+ .filter(|child| child.kind() == "switch_label")
722
+ .any(|label| {
723
+ let parts = non_comment_children(label);
724
+ parts.is_empty()
725
+ || parts.iter().any(|part| {
726
+ part.kind() == "identifier" && node_text(*part, code) == "default"
727
+ })
728
+ });
729
+ }
730
+
643
731
  // C# `default:` sections and catch-all (`_`, `var x`) labels and arms, and Kotlin `else ->`
644
732
  // entries. A guarded catch-all (`_ when cond =>`) is still a default arm: only its guard
645
733
  // branches, which is_pattern_guard charges, like Python's `case _ if cond:` and Rust's
@@ -3,15 +3,16 @@ use tree_sitter::Node;
3
3
 
4
4
  use crate::util::{all_children, find_children_by_field_name, named_children, node_text, Source};
5
5
 
6
- /// C++ `function_definition` also covers pure-virtual/`= default`/`= delete` members; those have no
7
- /// `body` and are signatures, not implementations, matching how TypeScript method signatures are
8
- /// excluded. Java `method_declaration` is NOT here: PMD reports abstract/interface methods as
9
- /// methods (NCSS 1), so bodyless Java methods stay in the function list (as do C#'s and Kotlin's).
10
- /// C# auto-property accessors (`{ get; set; }`) and Kotlin visibility-only accessors (`private
6
+ /// Declarations without a body have no control flow, so they are signatures, not functions: C++
7
+ /// pure-virtual/`= default`/`= delete` members, abstract/interface/extern methods (Java, C#,
8
+ /// Kotlin), Go assembly-backed function declarations, and Rust trait method signatures, matching
9
+ /// how TypeScript method signatures are excluded. C# auto-property accessors (`{ get; set; }`) and Kotlin visibility-only accessors (`private
11
10
  /// set`) hold no code, so they need a body too; a C# property or indexer is a function only in its
12
11
  /// expression-bodied form (`int X => ...`), otherwise its accessors are the functions.
13
12
  const BODY_REQUIRED_FUNCTION_TYPES: &[&str] = &[
14
13
  "function_definition",
14
+ "function_declaration",
15
+ "method_declaration",
15
16
  "constructor_declaration",
16
17
  "compact_constructor_declaration",
17
18
  "function_signature_item",
@@ -35,8 +36,9 @@ pub fn is_implemented_function(node: Node<'_>) -> bool {
35
36
  .is_some_and(|value| value.kind() == "arrow_expression_clause");
36
37
  }
37
38
 
38
- // The Kotlin grammar has no fields; an implemented accessor holds a `function_body` child.
39
- if node.kind() == "getter" || node.kind() == "setter" {
39
+ // The Kotlin grammar has no fields; an implemented function or accessor holds a
40
+ // `function_body` child.
41
+ if node.kind() == "getter" || node.kind() == "setter" || node.kind() == "function_declaration" {
40
42
  return named_children(node)
41
43
  .iter()
42
44
  .any(|child| child.kind() == "function_body");
@@ -7,9 +7,10 @@ pub struct LanguageDefinition {
7
7
  grammar_id: GrammarId,
8
8
  pub function_node_types: &'static [&'static str],
9
9
  pub decision_node_types: &'static [&'static str],
10
- /// Whether cyclomatic complexity follows PMD's Java CycloVisitor (see cyclomatic.rs) instead of
11
- /// the generic one-path-per-decision rule.
12
- pub pmd_cyclomatic: bool,
10
+ /// Whether every file's top-level code runs as a module body (a component of its own in the
11
+ /// file's cyclomatic complexity); C# and Kotlin decide per file by the presence of top-level
12
+ /// statements (never for a Kotlin file with parse errors).
13
+ pub executes_top_level: bool,
13
14
  pub nesting_node_types: &'static [&'static str],
14
15
  pub ncss_node_types: &'static [&'static str],
15
16
  pub ncss_container_node_types: &'static [&'static str],
@@ -659,7 +660,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
659
660
  grammar_id: GrammarId::JavaScript,
660
661
  function_node_types: COMMON_FUNCTION_NODES,
661
662
  decision_node_types: COMMON_DECISION_NODES,
662
- pmd_cyclomatic: false,
663
+ executes_top_level: true,
663
664
  nesting_node_types: COMMON_NESTING_NODES,
664
665
  ncss_node_types: JS_NCSS_NODES,
665
666
  ncss_container_node_types: &[],
@@ -670,7 +671,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
670
671
  grammar_id: GrammarId::JavaScript,
671
672
  function_node_types: COMMON_FUNCTION_NODES,
672
673
  decision_node_types: COMMON_DECISION_NODES,
673
- pmd_cyclomatic: false,
674
+ executes_top_level: true,
674
675
  nesting_node_types: COMMON_NESTING_NODES,
675
676
  ncss_node_types: JS_NCSS_NODES,
676
677
  ncss_container_node_types: &[],
@@ -681,7 +682,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
681
682
  grammar_id: GrammarId::TypeScript,
682
683
  function_node_types: COMMON_FUNCTION_NODES,
683
684
  decision_node_types: COMMON_DECISION_NODES,
684
- pmd_cyclomatic: false,
685
+ executes_top_level: true,
685
686
  nesting_node_types: COMMON_NESTING_NODES,
686
687
  ncss_node_types: JS_NCSS_NODES,
687
688
  ncss_container_node_types: &[],
@@ -692,7 +693,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
692
693
  grammar_id: GrammarId::Tsx,
693
694
  function_node_types: COMMON_FUNCTION_NODES,
694
695
  decision_node_types: COMMON_DECISION_NODES,
695
- pmd_cyclomatic: false,
696
+ executes_top_level: true,
696
697
  nesting_node_types: COMMON_NESTING_NODES,
697
698
  ncss_node_types: JS_NCSS_NODES,
698
699
  ncss_container_node_types: &[],
@@ -703,7 +704,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
703
704
  grammar_id: GrammarId::Python,
704
705
  function_node_types: COMMON_FUNCTION_NODES,
705
706
  decision_node_types: COMMON_DECISION_NODES,
706
- pmd_cyclomatic: false,
707
+ executes_top_level: true,
707
708
  nesting_node_types: COMMON_NESTING_NODES,
708
709
  ncss_node_types: PYTHON_NCSS_NODES,
709
710
  ncss_container_node_types: &[],
@@ -714,7 +715,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
714
715
  grammar_id: GrammarId::Go,
715
716
  function_node_types: COMMON_FUNCTION_NODES,
716
717
  decision_node_types: GO_DECISION_NODES,
717
- pmd_cyclomatic: false,
718
+ executes_top_level: false,
718
719
  nesting_node_types: GO_NESTING_NODES,
719
720
  ncss_node_types: GO_NCSS_NODES,
720
721
  ncss_container_node_types: &[],
@@ -725,7 +726,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
725
726
  grammar_id: GrammarId::Rust,
726
727
  function_node_types: COMMON_FUNCTION_NODES,
727
728
  decision_node_types: COMMON_DECISION_NODES,
728
- pmd_cyclomatic: false,
729
+ executes_top_level: false,
729
730
  nesting_node_types: COMMON_NESTING_NODES,
730
731
  ncss_node_types: RUST_NCSS_NODES,
731
732
  ncss_container_node_types: RUST_NCSS_CONTAINERS,
@@ -736,7 +737,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
736
737
  grammar_id: GrammarId::Java,
737
738
  function_node_types: JAVA_FUNCTION_NODES,
738
739
  decision_node_types: JAVA_DECISION_NODES,
739
- pmd_cyclomatic: true,
740
+ executes_top_level: false,
740
741
  nesting_node_types: JAVA_DECISION_NODES,
741
742
  ncss_node_types: JAVA_NCSS_NODES,
742
743
  ncss_container_node_types: &[],
@@ -747,7 +748,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
747
748
  grammar_id: GrammarId::Ruby,
748
749
  function_node_types: RUBY_FUNCTION_NODES,
749
750
  decision_node_types: RUBY_DECISION_NODES,
750
- pmd_cyclomatic: false,
751
+ executes_top_level: true,
751
752
  nesting_node_types: RUBY_DECISION_NODES,
752
753
  ncss_node_types: RUBY_NCSS_NODES,
753
754
  ncss_container_node_types: RUBY_NCSS_CONTAINERS,
@@ -758,7 +759,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
758
759
  grammar_id: GrammarId::C,
759
760
  function_node_types: C_FUNCTION_NODES,
760
761
  decision_node_types: C_DECISION_NODES,
761
- pmd_cyclomatic: false,
762
+ executes_top_level: false,
762
763
  nesting_node_types: C_DECISION_NODES,
763
764
  ncss_node_types: C_NCSS_NODES,
764
765
  ncss_container_node_types: &[],
@@ -769,7 +770,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
769
770
  grammar_id: GrammarId::Cpp,
770
771
  function_node_types: C_FUNCTION_NODES,
771
772
  decision_node_types: CPP_DECISION_NODES,
772
- pmd_cyclomatic: false,
773
+ executes_top_level: false,
773
774
  nesting_node_types: CPP_DECISION_NODES,
774
775
  ncss_node_types: CPP_NCSS_NODES,
775
776
  ncss_container_node_types: &[],
@@ -780,7 +781,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
780
781
  grammar_id: GrammarId::CSharp,
781
782
  function_node_types: CSHARP_FUNCTION_NODES,
782
783
  decision_node_types: CSHARP_DECISION_NODES,
783
- pmd_cyclomatic: false,
784
+ executes_top_level: false,
784
785
  nesting_node_types: CSHARP_DECISION_NODES,
785
786
  ncss_node_types: CSHARP_NCSS_NODES,
786
787
  ncss_container_node_types: &[],
@@ -791,7 +792,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
791
792
  grammar_id: GrammarId::Kotlin,
792
793
  function_node_types: KOTLIN_FUNCTION_NODES,
793
794
  decision_node_types: KOTLIN_DECISION_NODES,
794
- pmd_cyclomatic: false,
795
+ executes_top_level: false,
795
796
  nesting_node_types: KOTLIN_DECISION_NODES,
796
797
  ncss_node_types: KOTLIN_NCSS_NODES,
797
798
  ncss_container_node_types: KOTLIN_NCSS_CONTAINERS,
package/native/src/lib.rs CHANGED
@@ -6,7 +6,6 @@ use napi_derive::napi;
6
6
  use crate::duplication::DuplicationSettings;
7
7
 
8
8
  mod complexity;
9
- mod cyclomatic;
10
9
  mod dep_degree;
11
10
  mod duplication;
12
11
  mod functions;
@@ -22,7 +21,7 @@ mod util;
22
21
  /// together with `expectedPayloadVersion` in src/nativeMetrics.ts.
23
22
  #[napi]
24
23
  pub fn payload_version() -> u32 {
25
- 5
24
+ 6
26
25
  }
27
26
 
28
27
  /// Measures code metrics for the given source, returning the NativeMetrics payload as JSON.
@@ -38,11 +38,12 @@ pub fn measure(
38
38
  .filter(|node| !is_lambda_body_block(*node) && is_implemented_function(*node))
39
39
  .collect();
40
40
 
41
- let body_metrics_by_node_id = measure_function_body_metrics(root, &sets, code);
41
+ let body_metrics = measure_function_body_metrics(root, &sets, code);
42
42
  let function_metrics: Vec<FunctionMetrics> = functions
43
43
  .iter()
44
44
  .map(|node| {
45
- let body_metrics = body_metrics_by_node_id
45
+ let body_metrics = body_metrics
46
+ .by_function
46
47
  .get(&node.id())
47
48
  .expect("every collected function node opens a frame in the body-metrics pass");
48
49
  FunctionMetrics {
@@ -76,6 +77,16 @@ pub fn measure(
76
77
  language: language.name.to_string(),
77
78
  bytes: code.code.len(),
78
79
  lines,
80
+ // McCabe's v = e - n + 2p over the file's components: every function, every initializer
81
+ // block, and the module body when the file runs top-level code; decisions outside functions
82
+ // belong to the file.
83
+ cyclomatic_complexity: function_metrics
84
+ .iter()
85
+ .map(|function| function.cyclomatic_complexity)
86
+ .sum::<u64>()
87
+ + body_metrics.top_level_decisions
88
+ + u64::from(language.executes_top_level || has_top_level_statements(root, language))
89
+ + count_initializer_blocks(root),
79
90
  cognitive_complexity: global_complexity.cognitive_complexity,
80
91
  max_cognitive_complexity: function_metrics
81
92
  .iter()
@@ -95,6 +106,76 @@ pub fn measure(
95
106
  })
96
107
  }
97
108
 
109
+ /// Initializer blocks run code of their own, so each is a component like a function: Java static
110
+ /// and instance initializers, Kotlin `init` blocks, and JavaScript/TypeScript class `static`
111
+ /// blocks. Their decisions already count as decisions outside functions.
112
+ fn count_initializer_blocks(root: Node<'_>) -> u64 {
113
+ let initializer_types: HashSet<&'static str> = [
114
+ "static_initializer",
115
+ "anonymous_initializer",
116
+ "class_static_block",
117
+ "block",
118
+ ]
119
+ .into_iter()
120
+ .collect();
121
+ collect_nodes(root, &initializer_types)
122
+ .into_iter()
123
+ .filter(|node| {
124
+ // A bare block is an initializer only as a direct member of a Java class or enum body.
125
+ node.kind() != "block"
126
+ || node.parent().is_some_and(|parent| {
127
+ matches!(parent.kind(), "class_body" | "enum_body_declarations")
128
+ })
129
+ })
130
+ .count() as u64
131
+ }
132
+
133
+ /// A C# top-level statement: a `global_statement`, or a statement inside a top-level `#if` block,
134
+ /// which the grammar does not wrap in `global_statement`; preprocessor blocks are transparent.
135
+ fn is_csharp_top_level_statement(node: Node<'_>) -> bool {
136
+ if node.kind().starts_with("preproc_") {
137
+ return named_children(node)
138
+ .into_iter()
139
+ .any(is_csharp_top_level_statement);
140
+ }
141
+ node.kind() == "global_statement"
142
+ || node.kind() == "block"
143
+ || node.kind().ends_with("_statement")
144
+ }
145
+
146
+ /// Whether a C# or Kotlin file runs top-level code: C# top-level statements, or a Kotlin script's
147
+ /// statements beside its declarations.
148
+ fn has_top_level_statements(root: Node<'_>, language: &LanguageDefinition) -> bool {
149
+ const KOTLIN_DECLARATIONS: &[&str] = &[
150
+ "package_header",
151
+ "import_list",
152
+ "class_declaration",
153
+ "object_declaration",
154
+ "function_declaration",
155
+ "property_declaration",
156
+ "type_alias",
157
+ "shebang_line",
158
+ "file_annotation",
159
+ "getter",
160
+ "setter",
161
+ ];
162
+ let children = named_children(root);
163
+ match language.name {
164
+ "csharp" => children.into_iter().any(is_csharp_top_level_statement),
165
+ // The grammar mis-parses some valid declarations (non-empty companion objects, `fun
166
+ // interface`) and leaves recovery residue at the top level, so a file with parse errors is
167
+ // never taken for a script.
168
+ "kotlin" => {
169
+ !root.has_error()
170
+ && children.iter().any(|child| {
171
+ !KOTLIN_DECLARATIONS.contains(&child.kind())
172
+ && !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind())
173
+ })
174
+ }
175
+ _ => false,
176
+ }
177
+ }
178
+
98
179
  /// Collects one file's cross-file clone-detection contribution; see CrossFileFileData.
99
180
  pub fn collect_cross_file_data(
100
181
  code: &str,
@@ -11,6 +11,7 @@ pub struct NativeMetrics {
11
11
  pub bytes: usize,
12
12
  pub lines: LineMetrics,
13
13
  pub functions: Vec<FunctionMetrics>,
14
+ pub cyclomatic_complexity: u64,
14
15
  pub cognitive_complexity: u64,
15
16
  pub max_cognitive_complexity: u64,
16
17
  pub nesting_depth: u64,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-gauge",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "Measure code metrics with tree-sitter.",
5
5
  "keywords": [
6
6
  "cli",
@@ -89,12 +89,12 @@
89
89
  "registry": "https://registry.npmjs.org/"
90
90
  },
91
91
  "optionalDependencies": {
92
- "code-gauge-linux-x64-gnu": "4.3.0",
93
- "code-gauge-linux-arm64-gnu": "4.3.0",
94
- "code-gauge-linux-x64-musl": "4.3.0",
95
- "code-gauge-linux-arm64-musl": "4.3.0",
96
- "code-gauge-darwin-x64": "4.3.0",
97
- "code-gauge-darwin-arm64": "4.3.0",
98
- "code-gauge-win32-x64-msvc": "4.3.0"
92
+ "code-gauge-linux-x64-gnu": "4.4.0",
93
+ "code-gauge-linux-arm64-gnu": "4.4.0",
94
+ "code-gauge-linux-x64-musl": "4.4.0",
95
+ "code-gauge-linux-arm64-musl": "4.4.0",
96
+ "code-gauge-darwin-x64": "4.4.0",
97
+ "code-gauge-darwin-arm64": "4.4.0",
98
+ "code-gauge-win32-x64-msvc": "4.4.0"
99
99
  }
100
100
  }
@@ -1,79 +0,0 @@
1
- use tree_sitter::Node;
2
-
3
- use crate::util::{named_children, node_text, Source};
4
-
5
- /// Cyclomatic paths a Java node adds to its function, following PMD 7.26.0's CycloVisitor with
6
- /// its default options: branches and loops add 1 plus the boolean paths of their condition,
7
- /// `throw`/`catch`/enhanced `for` add 1, and a switch adds the boolean paths of its tested
8
- /// expression plus the expression alternatives of each non-default label. `&&`/`||` outside these
9
- /// conditions (initializers, returns, arguments) and pattern labels and guards add nothing.
10
- pub fn pmd_cyclomatic_increment(node: Node<'_>, code: &Source<'_>) -> u64 {
11
- match node.kind() {
12
- "if_statement" | "while_statement" | "do_statement" | "for_statement"
13
- | "ternary_expression" => {
14
- 1 + node
15
- .child_by_field_name("condition")
16
- .map_or(0, |condition| {
17
- boolean_expression_complexity(condition, code)
18
- })
19
- }
20
- "enhanced_for_statement" | "catch_clause" | "throw_statement" => 1,
21
- "switch_expression" => node
22
- .child_by_field_name("condition")
23
- .map_or(0, |condition| {
24
- boolean_expression_complexity(condition, code)
25
- }),
26
- "switch_block_statement_group" | "switch_rule" => named_children(node)
27
- .into_iter()
28
- .filter(|child| child.kind() == "switch_label")
29
- .flat_map(named_children)
30
- .filter(|child| {
31
- child.kind() != "guard"
32
- && child.kind() != "pattern"
33
- && !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind())
34
- })
35
- .count() as u64,
36
- _ => 0,
37
- }
38
- }
39
-
40
- /// PMD's booleanExpressionComplexity: a conditional expression costs 2 plus its parts; any other
41
- /// expression costs its `&&`/`||` operators. PMD has no parenthesis nodes, so they are unwrapped.
42
- fn boolean_expression_complexity(expression: Node<'_>, code: &Source<'_>) -> u64 {
43
- let mut expression = expression;
44
- while expression.kind() == "parenthesized_expression" {
45
- let Some(inner) = named_children(expression)
46
- .into_iter()
47
- .find(|child| !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind()))
48
- else {
49
- return 0;
50
- };
51
- expression = inner;
52
- }
53
- if expression.kind() == "ternary_expression" {
54
- return 2 + ["condition", "consequence", "alternative"]
55
- .iter()
56
- .filter_map(|field| expression.child_by_field_name(field))
57
- .map(|part| boolean_expression_complexity(part, code))
58
- .sum::<u64>();
59
- }
60
- count_conditional_operators(expression, code)
61
- }
62
-
63
- /// `&&`/`||` binaries in the subtree, not descending into lambdas or class bodies, which PMD treats
64
- /// as find boundaries.
65
- fn count_conditional_operators(node: Node<'_>, code: &Source<'_>) -> u64 {
66
- if node.kind() == "lambda_expression" || node.kind() == "class_body" {
67
- return 0;
68
- }
69
- let own = u64::from(
70
- node.kind() == "binary_expression"
71
- && node
72
- .child_by_field_name("operator")
73
- .is_some_and(|operator| matches!(node_text(operator, code), "&&" | "||")),
74
- );
75
- own + named_children(node)
76
- .into_iter()
77
- .map(|child| count_conditional_operators(child, code))
78
- .sum::<u64>()
79
- }