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 +5 -3
- package/dist/metrics.cjs +1 -1
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.js +1 -1
- package/dist/metrics.js.map +1 -1
- package/dist/nativeMetrics.cjs +1 -1
- package/dist/nativeMetrics.cjs.map +1 -1
- package/dist/nativeMetrics.js +1 -1
- package/dist/nativeMetrics.js.map +1 -1
- package/dist/types.d.ts +16 -7
- package/native/src/complexity.rs +109 -21
- package/native/src/functions.rs +9 -7
- package/native/src/languages.rs +17 -16
- package/native/src/lib.rs +1 -2
- package/native/src/measure.rs +83 -2
- package/native/src/types.rs +1 -0
- package/package.json +8 -8
- package/native/src/cyclomatic.rs +0 -79
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),
|
|
177
|
-
|
|
178
|
-
|
|
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
|
package/dist/metrics.cjs.map
CHANGED
|
@@ -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
|
package/dist/metrics.js.map
CHANGED
|
@@ -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"}
|
package/dist/nativeMetrics.cjs
CHANGED
|
@@ -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!==
|
|
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 =
|
|
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"}
|
package/dist/nativeMetrics.js
CHANGED
|
@@ -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!==
|
|
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 =
|
|
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
|
|
69
|
-
* (branch, loop,
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* and
|
|
73
|
-
* decision
|
|
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;
|
package/native/src/complexity.rs
CHANGED
|
@@ -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
|
-
) ->
|
|
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
|
|
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
|
-
|
|
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
|
-
&& !
|
|
216
|
+
&& !is_pathless_switch_branch(current, self.code);
|
|
205
217
|
let is_case_clause = current.is_named() && CASE_CLAUSE_NODE_TYPES.contains(¤t.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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
&& !
|
|
391
|
+
&& !is_pathless_switch_branch(current, code);
|
|
384
392
|
let is_case_clause = current.is_named() && CASE_CLAUSE_NODE_TYPES.contains(¤t.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
|
-
///
|
|
637
|
-
|
|
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
|
package/native/src/functions.rs
CHANGED
|
@@ -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
|
-
///
|
|
7
|
-
///
|
|
8
|
-
///
|
|
9
|
-
///
|
|
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
|
|
39
|
-
|
|
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");
|
package/native/src/languages.rs
CHANGED
|
@@ -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
|
|
11
|
-
/// the
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
+
6
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
/// Measures code metrics for the given source, returning the NativeMetrics payload as JSON.
|
package/native/src/measure.rs
CHANGED
|
@@ -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
|
|
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 =
|
|
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,
|
package/native/src/types.rs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-gauge",
|
|
3
|
-
"version": "4.
|
|
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.
|
|
93
|
-
"code-gauge-linux-arm64-gnu": "4.
|
|
94
|
-
"code-gauge-linux-x64-musl": "4.
|
|
95
|
-
"code-gauge-linux-arm64-musl": "4.
|
|
96
|
-
"code-gauge-darwin-x64": "4.
|
|
97
|
-
"code-gauge-darwin-arm64": "4.
|
|
98
|
-
"code-gauge-win32-x64-msvc": "4.
|
|
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
|
}
|
package/native/src/cyclomatic.rs
DELETED
|
@@ -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
|
-
}
|