escomplex-plugin-metrics-module
Advanced tools
Comparing version 0.0.1 to 0.0.2
@@ -11,4 +11,8 @@ 'use strict'; | ||
var _safeName = require('typhonjs-escomplex-commons/dist/traits/safeName.js'); | ||
var _MethodReport = require('typhonjs-escomplex-commons/dist/module/report/MethodReport.js'); | ||
var _MethodReport2 = _interopRequireDefault(_MethodReport); | ||
var _safeName = require('typhonjs-escomplex-commons/dist/module/traits/safeName.js'); | ||
var _safeName2 = _interopRequireDefault(_safeName); | ||
@@ -21,3 +25,3 @@ | ||
/** | ||
* Provides an typhonjs-escomplex-module / ESComplexModule plugin which gathers and calculates all default metrics. | ||
* Provides a typhonjs-escomplex-module / ESComplexModule plugin which gathers and calculates all default metrics. | ||
* | ||
@@ -63,12 +67,51 @@ * @see https://www.npmjs.com/package/typhonjs-escomplex-commons | ||
value: function onEnterNode(ev) { | ||
var syntax = this.syntaxes[ev.data.node.type]; | ||
var report = ev.data.report; | ||
var node = ev.data.node; | ||
var parent = ev.data.parent; | ||
var syntax = this.syntaxes[node.type]; | ||
if (syntax !== null && (typeof syntax === 'undefined' ? 'undefined' : _typeof(syntax)) === 'object') { | ||
this.processNode(ev.data.node, ev.data.parent, syntax); | ||
// Process node. | ||
// Process LLOC. | ||
report.incrementLogicalSloc(syntax.lloc.valueOf(node, parent)); | ||
// Process Cyclomatic. | ||
report.incrementCyclomatic(syntax.cyclomatic.valueOf(node, parent)); | ||
// Process operators HalsteadArray. | ||
syntax.operators.process(report, node, parent); | ||
// Process operands HalsteadArray. | ||
syntax.operands.process(report, node, parent); | ||
// Process any dependencies. | ||
report.addDependencies(syntax.dependencies.valueOf(node, parent)); | ||
// Handle creating new scope if applicable. | ||
if (syntax.newScope) { | ||
this.createScope(ev.data.node, ev.data.parent); | ||
var newScope = syntax.newScope.valueOf(node, parent); | ||
switch (newScope) { | ||
case 'class': | ||
report.createScope(newScope, (0, _safeName2.default)(node.id), node.loc.start.line, node.loc.end.line); | ||
break; | ||
case 'method': | ||
{ | ||
// ESTree has a parent node which defines the method name with a child FunctionExpression / | ||
// FunctionDeclaration. Babylon AST only has ClassMethod with a child `key` providing the method name. | ||
var name = parent && parent.type === 'MethodDefinition' ? (0, _safeName2.default)(parent.key) : (0, _safeName2.default)(node.id || node.key); | ||
var paramCount = node.params.length; | ||
report.createScope(newScope, name, node.loc.start.line, node.loc.end.line, paramCount); | ||
break; | ||
} | ||
} | ||
} | ||
ev.data.ignoreKeys = syntax.ignoreKeys; | ||
ev.data.ignoreKeys = syntax.ignoreKeys.valueOf(node, parent); | ||
} | ||
@@ -87,6 +130,18 @@ } | ||
value: function onExitNode(ev) { | ||
var syntax = this.syntaxes[ev.data.node.type]; | ||
var report = ev.data.report; | ||
var node = ev.data.node; | ||
var parent = ev.data.parent; | ||
var syntax = this.syntaxes[node.type]; | ||
if (syntax !== null && (typeof syntax === 'undefined' ? 'undefined' : _typeof(syntax)) === 'object' && syntax.newScope) { | ||
this.popScope(); | ||
var newScope = syntax.newScope.valueOf(node, parent); | ||
switch (newScope) { | ||
case 'class': | ||
report.popScope(newScope); | ||
break; | ||
case 'method': | ||
report.popScope(newScope); | ||
break; | ||
} | ||
} | ||
@@ -97,2 +152,4 @@ } | ||
* Performs final calculations based on collected report data. | ||
* | ||
* @param {object} ev - escomplex plugin event data. | ||
*/ | ||
@@ -102,4 +159,4 @@ | ||
key: 'onModuleEnd', | ||
value: function onModuleEnd() { | ||
this.calculateMetrics(); | ||
value: function onModuleEnd(ev) { | ||
this._calculateMetrics(ev.data.report); | ||
} | ||
@@ -127,31 +184,2 @@ | ||
this.syntaxes = ev.data.syntaxes; | ||
/** | ||
* Stores the current report being processed. | ||
* @type {object} | ||
*/ | ||
this.currentReport = undefined; | ||
/** | ||
* Used in tracking dependencies. | ||
* @type {boolean} | ||
*/ | ||
this.clearDependencies = true; | ||
/** | ||
* Stores the current report scope stack. | ||
* @type {Array} | ||
*/ | ||
this.scopeStack = []; | ||
/** | ||
* Stores the global report being processed by ESComplexModule. | ||
* @type {object} | ||
*/ | ||
this.report = ev.data.report; | ||
// Creates the default report | ||
this.report.aggregate = this.createFunctionReport(undefined, ev.data.ast.loc, 0); | ||
this.report.functions = []; | ||
this.report.dependencies = []; | ||
} | ||
@@ -166,9 +194,11 @@ | ||
* | ||
* @param {object} data - | ||
* @param {MethodReport} report - A MethodReport to perform calculations on. | ||
* | ||
* @private | ||
*/ | ||
}, { | ||
key: 'calculateCyclomaticDensity', | ||
value: function calculateCyclomaticDensity(data) { | ||
data.cyclomaticDensity = data.cyclomatic / data.sloc.logical * 100; | ||
key: '_calculateCyclomaticDensity', | ||
value: function _calculateCyclomaticDensity(report) { | ||
report.cyclomaticDensity = report.cyclomatic / report.sloc.logical * 100; | ||
} | ||
@@ -182,21 +212,24 @@ | ||
* | ||
* @param {object} data - | ||
* @param {HalsteadData} halstead - A HalsteadData instance to perform calculations on. | ||
* | ||
* @see https://en.wikipedia.org/wiki/Halstead_complexity_measures | ||
* | ||
* @private | ||
*/ | ||
}, { | ||
key: 'calculateHalsteadMetrics', | ||
value: function calculateHalsteadMetrics(data) { | ||
data.length = data.operators.total + data.operands.total; | ||
key: '_calculateHalsteadMetrics', | ||
value: function _calculateHalsteadMetrics(halstead) { | ||
halstead.length = halstead.operators.total + halstead.operands.total; | ||
if (data.length === 0) { | ||
data.vocabulary = data.difficulty = data.volume = data.effort = data.bugs = data.time = 0; | ||
/* istanbul ignore if */ | ||
if (halstead.length === 0) { | ||
halstead.reset(); | ||
} else { | ||
data.vocabulary = data.operators.distinct + data.operands.distinct; | ||
data.difficulty = data.operators.distinct / 2 * (data.operands.distinct === 0 ? 1 : data.operands.total / data.operands.distinct); | ||
data.volume = data.length * (Math.log(data.vocabulary) / Math.log(2)); | ||
data.effort = data.difficulty * data.volume; | ||
data.bugs = data.volume / 3000; | ||
data.time = data.effort / 18; | ||
halstead.vocabulary = halstead.operators.distinct + halstead.operands.distinct; | ||
halstead.difficulty = halstead.operators.distinct / 2 * (halstead.operands.distinct === 0 ? 1 : halstead.operands.total / halstead.operands.distinct); | ||
halstead.volume = halstead.length * (Math.log(halstead.vocabulary) / Math.log(2)); | ||
halstead.effort = halstead.difficulty * halstead.volume; | ||
halstead.bugs = halstead.volume / 3000; | ||
halstead.time = halstead.effort / 18; | ||
} | ||
@@ -218,10 +251,14 @@ } | ||
* | ||
* @param {number} averageEffort - | ||
* @param {number} averageCyclomatic - | ||
* @param {number} averageLoc - | ||
* @param {ClassReport|ModuleReport} report - A ClassReport or ModuleReport to perform calculations on. | ||
* @param {number} averageCyclomatic - Average cyclomatic metric across a ClassReport / ModuleReport. | ||
* @param {number} averageEffort - Average Halstead effort across a ClassReport / ModuleReport. | ||
* @param {number} averageLoc - Average SLOC metric across a ClassReport / ModuleReport. | ||
* | ||
* @private | ||
*/ | ||
}, { | ||
key: 'calculateMaintainabilityIndex', | ||
value: function calculateMaintainabilityIndex(averageEffort, averageCyclomatic, averageLoc) { | ||
key: '_calculateMaintainabilityIndex', | ||
value: function _calculateMaintainabilityIndex(report, averageCyclomatic, averageEffort, averageLoc) { | ||
/* istanbul ignore if */ | ||
if (averageCyclomatic === 0) { | ||
@@ -231,10 +268,12 @@ throw new Error('Encountered function with cyclomatic complexity zero!'); | ||
this.report.maintainability = 171 - 3.42 * Math.log(averageEffort) - 0.23 * Math.log(averageCyclomatic) - 16.2 * Math.log(averageLoc); | ||
report.maintainability = 171 - 3.42 * Math.log(averageEffort) - 0.23 * Math.log(averageCyclomatic) - 16.2 * Math.log(averageLoc); | ||
if (this.report.maintainability > 171) { | ||
this.report.maintainability = 171; | ||
/* istanbul ignore if */ | ||
if (report.maintainability > 171) { | ||
report.maintainability = 171; | ||
} | ||
/* istanbul ignore if */ | ||
if (this.settings.newmi) { | ||
this.report.maintainability = Math.max(0, this.report.maintainability * 100 / 171); | ||
report.maintainability = Math.max(0, report.maintainability * 100 / 171); | ||
} | ||
@@ -244,297 +283,93 @@ } | ||
/** | ||
* Coordinates calculating all metrics. | ||
* Coordinates calculating all metrics. All module and class methods are traversed. If there are no module or class | ||
* methods respectively the aggregate MethodReport is used for calculations. | ||
* | ||
* @param {ModuleReport} report - The ModuleReport being processed. | ||
* | ||
* @private | ||
*/ | ||
}, { | ||
key: 'calculateMetrics', | ||
value: function calculateMetrics() { | ||
key: '_calculateMetrics', | ||
value: function _calculateMetrics(report) { | ||
var _this = this; | ||
var count = this.report.functions.length; | ||
var moduleMethodCount = report.methods.length; | ||
var indices = { | ||
loc: 0, | ||
cyclomatic: 1, | ||
effort: 2, | ||
params: 3 | ||
}; | ||
// Retrieve the maintainability sums array and indices object hash. | ||
var sums = [0, 0, 0, 0]; | ||
var _MethodReport$getMain = _MethodReport2.default.getMaintainabilityMetrics(); | ||
this.report.functions.forEach(function (functionReport) { | ||
_this.calculateCyclomaticDensity(functionReport); | ||
_this.calculateHalsteadMetrics(functionReport.halstead); | ||
_this.sumMaintainabilityMetrics(sums, indices, functionReport); | ||
}); | ||
var sums = _MethodReport$getMain.sums; | ||
var indices = _MethodReport$getMain.indices; | ||
this.calculateCyclomaticDensity(this.report.aggregate); | ||
this.calculateHalsteadMetrics(this.report.aggregate.halstead); | ||
// Handle module methods. | ||
if (count === 0) { | ||
// Sane handling of modules that contain no functions. | ||
this.sumMaintainabilityMetrics(sums, indices, this.report.aggregate); | ||
count = 1; | ||
} | ||
report.methods.forEach(function (methodReport) { | ||
_this._calculateCyclomaticDensity(methodReport); | ||
_this._calculateHalsteadMetrics(methodReport.halstead); | ||
var averages = sums.map(function (sum) { | ||
return sum / count; | ||
methodReport.sumMetrics(sums, indices); | ||
}); | ||
this.calculateMaintainabilityIndex(averages[indices.effort], averages[indices.cyclomatic], averages[indices.loc]); | ||
// Handle module class reports. | ||
report.classes.forEach(function (classReport) { | ||
var _MethodReport$getMain2 = _MethodReport2.default.getMaintainabilityMetrics(); | ||
Object.keys(indices).forEach(function (index) { | ||
_this.report[index] = averages[indices[index]]; | ||
}); | ||
} | ||
var classSums = _MethodReport$getMain2.sums; | ||
/** | ||
* Creates a new function report. | ||
* | ||
* @param {string} name - Name of the function. | ||
* @param {number} lines - Number of lines for the function. | ||
* @param {number} params - Number of parameters for function. | ||
* | ||
* @returns {object} | ||
*/ | ||
}, { | ||
key: 'createFunctionReport', | ||
value: function createFunctionReport(name, lines, params) { | ||
var result = { | ||
name: name, | ||
sloc: { | ||
logical: 0 | ||
}, | ||
cyclomatic: 1, | ||
halstead: this.createInitialHalsteadState(), | ||
params: params | ||
}; | ||
var classMethodCount = classReport.methods.length; | ||
moduleMethodCount += classMethodCount; | ||
if ((typeof lines === 'undefined' ? 'undefined' : _typeof(lines)) === 'object') { | ||
result.line = lines.start.line; | ||
result.sloc.physical = lines.end.line - lines.start.line + 1; | ||
} | ||
// Process all class methods. | ||
classReport.methods.forEach(function (methodReport) { | ||
_this._calculateCyclomaticDensity(methodReport); | ||
_this._calculateHalsteadMetrics(methodReport.halstead); | ||
return result; | ||
} | ||
methodReport.sumMetrics(classSums, indices); | ||
methodReport.sumMetrics(sums, indices); | ||
}); | ||
/** | ||
* Creates an object hash representing Halstead state. | ||
* | ||
* @returns {{operators: {distinct: number, total: number, identifiers: Array}, operands: {distinct: number, total: number, identifiers: Array}}} | ||
*/ | ||
_this._calculateCyclomaticDensity(classReport.methodReport); | ||
_this._calculateHalsteadMetrics(classReport.methodReport.halstead); | ||
}, { | ||
key: 'createInitialHalsteadState', | ||
value: function createInitialHalsteadState() { | ||
return { | ||
operators: { distinct: 0, total: 0, identifiers: [] }, | ||
operands: { distinct: 0, total: 0, identifiers: [] } | ||
}; | ||
} | ||
/** | ||
* Creates a report scope when a class or function is entered. | ||
* | ||
* @param {object} node - Current AST node. | ||
* @param {object} parent - Parent AST node. | ||
*/ | ||
}, { | ||
key: 'createScope', | ||
value: function createScope(node, parent) { | ||
// ESTree has a parent node which defines the method name with a child FunctionExpression / FunctionDeclaration. | ||
// Babylon AST only has ClassMethod with a child `key` providing the method name. | ||
var name = parent && parent.type === 'MethodDefinition' ? (0, _safeName2.default)(parent.key) : (0, _safeName2.default)(node.id || node.key); | ||
this.currentReport = this.createFunctionReport(name, node.loc, node.params.length); | ||
this.report.functions.push(this.currentReport); | ||
this.report.aggregate.params += node.params.length; | ||
this.scopeStack.push(this.currentReport); | ||
} | ||
}, { | ||
key: 'halsteadItemEncountered', | ||
value: function halsteadItemEncountered(currentReport, metric, identifier) { | ||
if (currentReport) { | ||
this.incrementHalsteadItems(currentReport, metric, identifier); | ||
} | ||
this.incrementHalsteadItems(this.report.aggregate, metric, identifier); | ||
} | ||
}, { | ||
key: 'incrementCounter', | ||
value: function incrementCounter(node, syntax, name, incrementFn, currentReport) { | ||
var amount = syntax[name]; | ||
if (typeof amount === 'number') { | ||
incrementFn.call(this, currentReport, amount); | ||
} else if (typeof amount === 'function') { | ||
incrementFn.call(this, currentReport, amount(node)); | ||
} | ||
} | ||
}, { | ||
key: 'incrementCyclomatic', | ||
value: function incrementCyclomatic(currentReport, amount) { | ||
this.report.aggregate.cyclomatic += amount; | ||
if (currentReport) { | ||
currentReport.cyclomatic += amount; | ||
} | ||
} | ||
}, { | ||
key: 'incrementDistinctHalsteadItems', | ||
value: function incrementDistinctHalsteadItems(baseReport, metric, identifier) { | ||
if (Object.prototype.hasOwnProperty(identifier)) { | ||
// Avoid clashes with built-in property names. | ||
this.incrementDistinctHalsteadItems(baseReport, metric, '_' + identifier); | ||
} else if (this.isHalsteadMetricDistinct(baseReport, metric, identifier)) { | ||
this.recordDistinctHalsteadMetric(baseReport, metric, identifier); | ||
this.incrementHalsteadMetric(baseReport, metric, 'distinct'); | ||
} | ||
} | ||
}, { | ||
key: 'incrementHalsteadItems', | ||
value: function incrementHalsteadItems(baseReport, metric, identifier) { | ||
this.incrementDistinctHalsteadItems(baseReport, metric, identifier); | ||
this.incrementTotalHalsteadItems(baseReport, metric); | ||
} | ||
}, { | ||
key: 'incrementHalsteadMetric', | ||
value: function incrementHalsteadMetric(baseReport, metric, type) { | ||
if (baseReport) { | ||
baseReport.halstead[metric][type] += 1; | ||
} | ||
} | ||
}, { | ||
key: 'incrementLogicalSloc', | ||
value: function incrementLogicalSloc(currentReport, amount) { | ||
this.report.aggregate.sloc.logical += amount; | ||
if (currentReport) { | ||
currentReport.sloc.logical += amount; | ||
} | ||
} | ||
}, { | ||
key: 'incrementTotalHalsteadItems', | ||
value: function incrementTotalHalsteadItems(baseReport, metric) { | ||
this.incrementHalsteadMetric(baseReport, metric, 'total'); | ||
} | ||
}, { | ||
key: 'isHalsteadMetricDistinct', | ||
value: function isHalsteadMetricDistinct(baseReport, metric, identifier) { | ||
return baseReport.halstead[metric].identifiers.indexOf(identifier) === -1; | ||
} | ||
}, { | ||
key: 'popScope', | ||
value: function popScope() { | ||
this.scopeStack.pop(); | ||
if (this.scopeStack.length > 0) { | ||
this.currentReport = this.scopeStack[this.scopeStack.length - 1]; | ||
} else { | ||
this.currentReport = undefined; | ||
} | ||
} | ||
}, { | ||
key: 'processCyclomatic', | ||
value: function processCyclomatic(node, syntax, currentReport) { | ||
this.incrementCounter(node, syntax, 'cyclomatic', this.incrementCyclomatic, currentReport); | ||
} | ||
}, { | ||
key: 'processDependencies', | ||
value: function processDependencies(node, syntax, clearDependencies) { | ||
var dependencies = void 0; | ||
if (typeof syntax.dependencies === 'function') { | ||
dependencies = syntax.dependencies(node, clearDependencies); | ||
if ((typeof dependencies === 'undefined' ? 'undefined' : _typeof(dependencies)) === 'object' || Array.isArray(dependencies)) { | ||
this.report.dependencies = this.report.dependencies.concat(dependencies); | ||
// If there are no class methods use the class aggregate MethodReport. | ||
if (classMethodCount === 0) { | ||
// Sane handling of classes that contain no methods. | ||
classReport.methodReport.sumMetrics(classSums, indices); | ||
classMethodCount = 1; | ||
} | ||
return true; | ||
} | ||
var classAverages = classSums.map(function (sum) { | ||
return sum / classMethodCount; | ||
}); | ||
return false; | ||
} | ||
}, { | ||
key: 'processHalsteadMetric', | ||
value: function processHalsteadMetric(node, parent, syntax, metric, currentReport) { | ||
var _this2 = this; | ||
_this._calculateMaintainabilityIndex(classReport, classAverages[indices.cyclomatic], classAverages[indices.effort], classAverages[indices.loc]); | ||
if (Array.isArray(syntax[metric])) { | ||
syntax[metric].forEach(function (s) { | ||
var identifier = void 0; | ||
Object.keys(indices).forEach(function (index) { | ||
classReport[index] = classAverages[indices[index]]; | ||
}); | ||
}); | ||
if (typeof s.identifier === 'function') { | ||
identifier = s.identifier(node, parent); | ||
} else { | ||
identifier = s.identifier; | ||
} | ||
this._calculateCyclomaticDensity(report.methodReport); | ||
this._calculateHalsteadMetrics(report.methodReport.halstead); | ||
if (typeof identifier !== 'undefined' && (typeof s.filter !== 'function' || s.filter(node) === true)) { | ||
// Handle the case when a node / syntax returns an array of identifiers. | ||
if (Array.isArray(identifier)) { | ||
identifier.forEach(function (element) { | ||
_this2.halsteadItemEncountered(currentReport, metric, element); | ||
}); | ||
} else { | ||
_this2.halsteadItemEncountered(currentReport, metric, identifier); | ||
} | ||
} | ||
}); | ||
// If there are no module methods use the module aggregate MethodReport. | ||
if (moduleMethodCount === 0) { | ||
// Sane handling of modules that contain no methods. | ||
report.methodReport.sumMetrics(sums, indices); | ||
moduleMethodCount = 1; | ||
} | ||
} | ||
}, { | ||
key: 'processLloc', | ||
value: function processLloc(node, syntax, currentReport) { | ||
this.incrementCounter(node, syntax, 'lloc', this.incrementLogicalSloc, currentReport); | ||
} | ||
/** | ||
* Controls processing an AST node. | ||
* | ||
* @param {object} node - Current AST node. | ||
* @param {object} parent - Parent AST node. | ||
* @param {object} syntax - Syntax trait associated with the give node type. | ||
*/ | ||
var moduleAverages = sums.map(function (sum) { | ||
return sum / moduleMethodCount; | ||
}); | ||
}, { | ||
key: 'processNode', | ||
value: function processNode(node, parent, syntax) { | ||
this.processLloc(node, syntax, this.currentReport); | ||
this.processCyclomatic(node, syntax, this.currentReport); | ||
this.processOperators(node, parent, syntax, this.currentReport); | ||
this.processOperands(node, parent, syntax, this.currentReport); | ||
this._calculateMaintainabilityIndex(report, moduleAverages[indices.cyclomatic], moduleAverages[indices.effort], moduleAverages[indices.loc]); | ||
if (this.processDependencies(node, syntax, this.clearDependencies)) { | ||
// HACK: This will fail with async or if other syntax than CallExpression introduces dependencies. | ||
// TODO: Come up with a less crude approach. | ||
this.clearDependencies = false; | ||
} | ||
Object.keys(indices).forEach(function (index) { | ||
report[index] = moduleAverages[indices[index]]; | ||
}); | ||
} | ||
}, { | ||
key: 'processOperands', | ||
value: function processOperands(node, parent, syntax, currentReport) { | ||
this.processHalsteadMetric(node, parent, syntax, 'operands', currentReport); | ||
} | ||
}, { | ||
key: 'processOperators', | ||
value: function processOperators(node, parent, syntax, currentReport) { | ||
this.processHalsteadMetric(node, parent, syntax, 'operators', currentReport); | ||
} | ||
}, { | ||
key: 'recordDistinctHalsteadMetric', | ||
value: function recordDistinctHalsteadMetric(baseReport, metric, identifier) { | ||
baseReport.halstead[metric].identifiers.push(identifier); | ||
} | ||
}, { | ||
key: 'sumMaintainabilityMetrics', | ||
value: function sumMaintainabilityMetrics(sums, indices, data) { | ||
sums[indices.loc] += data.sloc.logical; | ||
sums[indices.cyclomatic] += data.cyclomatic; | ||
sums[indices.effort] += data.halstead.effort; | ||
sums[indices.params] += data.params; | ||
} | ||
}]); | ||
@@ -541,0 +376,0 @@ |
{ | ||
"name": "escomplex-plugin-metrics-module", | ||
"version": "0.0.1", | ||
"version": "0.0.2", | ||
"homepage": "https://github.com/typhonjs-node-escomplex/escomplex-plugin-metrics-module/", | ||
@@ -23,6 +23,6 @@ "description": "Provides the core module metric / report generation plugin for typhonjs-escomplex module processing.", | ||
"dependencies": { | ||
"typhonjs-escomplex-commons": "^0.0.1" | ||
"typhonjs-escomplex-commons": "^0.0.5" | ||
}, | ||
"devDependencies": { | ||
"escomplex-plugin-syntax-babylon": "^0.0.1", | ||
"escomplex-plugin-syntax-babylon": "^0.0.2", | ||
"typhonjs-ast-walker": "^0.1.0", | ||
@@ -29,0 +29,0 @@ "typhonjs-config-eslint": "^0.4.0", |
'use strict'; | ||
import safeName from 'typhonjs-escomplex-commons/src/traits/safeName.js'; | ||
import MethodReport from 'typhonjs-escomplex-commons/src/module/report/MethodReport.js'; | ||
import safeName from 'typhonjs-escomplex-commons/src/module/traits/safeName.js'; | ||
/** | ||
* Provides an typhonjs-escomplex-module / ESComplexModule plugin which gathers and calculates all default metrics. | ||
* Provides a typhonjs-escomplex-module / ESComplexModule plugin which gathers and calculates all default metrics. | ||
* | ||
@@ -39,11 +41,55 @@ * @see https://www.npmjs.com/package/typhonjs-escomplex-commons | ||
{ | ||
const syntax = this.syntaxes[ev.data.node.type]; | ||
const report = ev.data.report; | ||
const node = ev.data.node; | ||
const parent = ev.data.parent; | ||
const syntax = this.syntaxes[node.type]; | ||
if (syntax !== null && typeof syntax === 'object') | ||
{ | ||
this.processNode(ev.data.node, ev.data.parent, syntax); | ||
// Process node. | ||
if (syntax.newScope) { this.createScope(ev.data.node, ev.data.parent); } | ||
// Process LLOC. | ||
report.incrementLogicalSloc(syntax.lloc.valueOf(node, parent)); | ||
ev.data.ignoreKeys = syntax.ignoreKeys; | ||
// Process Cyclomatic. | ||
report.incrementCyclomatic(syntax.cyclomatic.valueOf(node, parent)); | ||
// Process operators HalsteadArray. | ||
syntax.operators.process(report, node, parent); | ||
// Process operands HalsteadArray. | ||
syntax.operands.process(report, node, parent); | ||
// Process any dependencies. | ||
report.addDependencies(syntax.dependencies.valueOf(node, parent)); | ||
// Handle creating new scope if applicable. | ||
if (syntax.newScope) | ||
{ | ||
const newScope = syntax.newScope.valueOf(node, parent); | ||
switch (newScope) | ||
{ | ||
case 'class': | ||
report.createScope(newScope, safeName(node.id), node.loc.start.line, node.loc.end.line); | ||
break; | ||
case 'method': | ||
{ | ||
// ESTree has a parent node which defines the method name with a child FunctionExpression / | ||
// FunctionDeclaration. Babylon AST only has ClassMethod with a child `key` providing the method name. | ||
const name = parent && parent.type === 'MethodDefinition' ? safeName(parent.key) : | ||
safeName(node.id || node.key); | ||
const paramCount = node.params.length; | ||
report.createScope(newScope, name, node.loc.start.line, node.loc.end.line, paramCount); | ||
break; | ||
} | ||
} | ||
} | ||
ev.data.ignoreKeys = syntax.ignoreKeys.valueOf(node, parent); | ||
} | ||
@@ -60,5 +106,21 @@ } | ||
{ | ||
const syntax = this.syntaxes[ev.data.node.type]; | ||
const report = ev.data.report; | ||
const node = ev.data.node; | ||
const parent = ev.data.parent; | ||
const syntax = this.syntaxes[node.type]; | ||
if (syntax !== null && typeof syntax === 'object' && syntax.newScope) { this.popScope(); } | ||
if (syntax !== null && typeof syntax === 'object' && syntax.newScope) | ||
{ | ||
const newScope = syntax.newScope.valueOf(node, parent); | ||
switch (newScope) | ||
{ | ||
case 'class': | ||
report.popScope(newScope); | ||
break; | ||
case 'method': | ||
report.popScope(newScope); | ||
break; | ||
} | ||
} | ||
} | ||
@@ -68,6 +130,8 @@ | ||
* Performs final calculations based on collected report data. | ||
* | ||
* @param {object} ev - escomplex plugin event data. | ||
*/ | ||
onModuleEnd() | ||
onModuleEnd(ev) | ||
{ | ||
this.calculateMetrics(); | ||
this._calculateMetrics(ev.data.report); | ||
} | ||
@@ -93,31 +157,2 @@ | ||
this.syntaxes = ev.data.syntaxes; | ||
/** | ||
* Stores the current report being processed. | ||
* @type {object} | ||
*/ | ||
this.currentReport = undefined; | ||
/** | ||
* Used in tracking dependencies. | ||
* @type {boolean} | ||
*/ | ||
this.clearDependencies = true; | ||
/** | ||
* Stores the current report scope stack. | ||
* @type {Array} | ||
*/ | ||
this.scopeStack = []; | ||
/** | ||
* Stores the global report being processed by ESComplexModule. | ||
* @type {object} | ||
*/ | ||
this.report = ev.data.report; | ||
// Creates the default report | ||
this.report.aggregate = this.createFunctionReport(undefined, ev.data.ast.loc, 0); | ||
this.report.functions = []; | ||
this.report.dependencies = []; | ||
} | ||
@@ -132,7 +167,9 @@ | ||
* | ||
* @param {object} data - | ||
* @param {MethodReport} report - A MethodReport to perform calculations on. | ||
* | ||
* @private | ||
*/ | ||
calculateCyclomaticDensity(data) | ||
_calculateCyclomaticDensity(report) | ||
{ | ||
data.cyclomaticDensity = (data.cyclomatic / data.sloc.logical) * 100; | ||
report.cyclomaticDensity = (report.cyclomatic / report.sloc.logical) * 100; | ||
} | ||
@@ -146,23 +183,26 @@ | ||
* | ||
* @param {object} data - | ||
* @param {HalsteadData} halstead - A HalsteadData instance to perform calculations on. | ||
* | ||
* @see https://en.wikipedia.org/wiki/Halstead_complexity_measures | ||
* | ||
* @private | ||
*/ | ||
calculateHalsteadMetrics(data) | ||
_calculateHalsteadMetrics(halstead) | ||
{ | ||
data.length = data.operators.total + data.operands.total; | ||
halstead.length = halstead.operators.total + halstead.operands.total; | ||
if (data.length === 0) | ||
/* istanbul ignore if */ | ||
if (halstead.length === 0) | ||
{ | ||
data.vocabulary = data.difficulty = data.volume = data.effort = data.bugs = data.time = 0; | ||
halstead.reset(); | ||
} | ||
else | ||
{ | ||
data.vocabulary = data.operators.distinct + data.operands.distinct; | ||
data.difficulty = (data.operators.distinct / 2) | ||
* (data.operands.distinct === 0 ? 1 : data.operands.total / data.operands.distinct); | ||
data.volume = data.length * (Math.log(data.vocabulary) / Math.log(2)); | ||
data.effort = data.difficulty * data.volume; | ||
data.bugs = data.volume / 3000; | ||
data.time = data.effort / 18; | ||
halstead.vocabulary = halstead.operators.distinct + halstead.operands.distinct; | ||
halstead.difficulty = (halstead.operators.distinct / 2) | ||
* (halstead.operands.distinct === 0 ? 1 : halstead.operands.total / halstead.operands.distinct); | ||
halstead.volume = halstead.length * (Math.log(halstead.vocabulary) / Math.log(2)); | ||
halstead.effort = halstead.difficulty * halstead.volume; | ||
halstead.bugs = halstead.volume / 3000; | ||
halstead.time = halstead.effort / 18; | ||
} | ||
@@ -184,11 +224,15 @@ } | ||
* | ||
* @param {number} averageEffort - | ||
* @param {number} averageCyclomatic - | ||
* @param {number} averageLoc - | ||
* @param {ClassReport|ModuleReport} report - A ClassReport or ModuleReport to perform calculations on. | ||
* @param {number} averageCyclomatic - Average cyclomatic metric across a ClassReport / ModuleReport. | ||
* @param {number} averageEffort - Average Halstead effort across a ClassReport / ModuleReport. | ||
* @param {number} averageLoc - Average SLOC metric across a ClassReport / ModuleReport. | ||
* | ||
* @private | ||
*/ | ||
calculateMaintainabilityIndex(averageEffort, averageCyclomatic, averageLoc) | ||
_calculateMaintainabilityIndex(report, averageCyclomatic, averageEffort, averageLoc) | ||
{ | ||
/* istanbul ignore if */ | ||
if (averageCyclomatic === 0) { throw new Error('Encountered function with cyclomatic complexity zero!'); } | ||
this.report.maintainability = | ||
report.maintainability = | ||
171 | ||
@@ -199,309 +243,88 @@ - (3.42 * Math.log(averageEffort)) | ||
if (this.report.maintainability > 171) { this.report.maintainability = 171; } | ||
/* istanbul ignore if */ | ||
if (report.maintainability > 171) { report.maintainability = 171; } | ||
if (this.settings.newmi) { this.report.maintainability = Math.max(0, (this.report.maintainability * 100) / 171); } | ||
/* istanbul ignore if */ | ||
if (this.settings.newmi) { report.maintainability = Math.max(0, (report.maintainability * 100) / 171); } | ||
} | ||
/** | ||
* Coordinates calculating all metrics. | ||
*/ | ||
calculateMetrics() | ||
{ | ||
let count = this.report.functions.length; | ||
const indices = | ||
{ | ||
loc: 0, | ||
cyclomatic: 1, | ||
effort: 2, | ||
params: 3 | ||
}; | ||
const sums = [0, 0, 0, 0]; | ||
this.report.functions.forEach((functionReport) => | ||
{ | ||
this.calculateCyclomaticDensity(functionReport); | ||
this.calculateHalsteadMetrics(functionReport.halstead); | ||
this.sumMaintainabilityMetrics(sums, indices, functionReport); | ||
}); | ||
this.calculateCyclomaticDensity(this.report.aggregate); | ||
this.calculateHalsteadMetrics(this.report.aggregate.halstead); | ||
if (count === 0) | ||
{ | ||
// Sane handling of modules that contain no functions. | ||
this.sumMaintainabilityMetrics(sums, indices, this.report.aggregate); | ||
count = 1; | ||
} | ||
const averages = sums.map((sum) => { return sum / count; }); | ||
this.calculateMaintainabilityIndex(averages[indices.effort], averages[indices.cyclomatic], averages[indices.loc]); | ||
Object.keys(indices).forEach((index) => { this.report[index] = averages[indices[index]]; }); | ||
} | ||
/** | ||
* Creates a new function report. | ||
* Coordinates calculating all metrics. All module and class methods are traversed. If there are no module or class | ||
* methods respectively the aggregate MethodReport is used for calculations. | ||
* | ||
* @param {string} name - Name of the function. | ||
* @param {number} lines - Number of lines for the function. | ||
* @param {number} params - Number of parameters for function. | ||
* @param {ModuleReport} report - The ModuleReport being processed. | ||
* | ||
* @returns {object} | ||
* @private | ||
*/ | ||
createFunctionReport(name, lines, params) | ||
_calculateMetrics(report) | ||
{ | ||
const result = { | ||
name, | ||
sloc: { | ||
logical: 0 | ||
}, | ||
cyclomatic: 1, | ||
halstead: this.createInitialHalsteadState(), | ||
params | ||
}; | ||
let moduleMethodCount = report.methods.length; | ||
if (typeof lines === 'object') | ||
{ | ||
result.line = lines.start.line; | ||
result.sloc.physical = lines.end.line - lines.start.line + 1; | ||
} | ||
// Retrieve the maintainability sums array and indices object hash. | ||
const { sums, indices } = MethodReport.getMaintainabilityMetrics(); | ||
return result; | ||
} | ||
/** | ||
* Creates an object hash representing Halstead state. | ||
* | ||
* @returns {{operators: {distinct: number, total: number, identifiers: Array}, operands: {distinct: number, total: number, identifiers: Array}}} | ||
*/ | ||
createInitialHalsteadState() | ||
{ | ||
return { | ||
operators: { distinct: 0, total: 0, identifiers: [] }, | ||
operands: { distinct: 0, total: 0, identifiers: [] } | ||
}; | ||
} | ||
/** | ||
* Creates a report scope when a class or function is entered. | ||
* | ||
* @param {object} node - Current AST node. | ||
* @param {object} parent - Parent AST node. | ||
*/ | ||
createScope(node, parent) | ||
{ | ||
// ESTree has a parent node which defines the method name with a child FunctionExpression / FunctionDeclaration. | ||
// Babylon AST only has ClassMethod with a child `key` providing the method name. | ||
const name = parent && parent.type === 'MethodDefinition' ? safeName(parent.key) : safeName(node.id || node.key); | ||
this.currentReport = this.createFunctionReport(name, node.loc, node.params.length); | ||
this.report.functions.push(this.currentReport); | ||
this.report.aggregate.params += node.params.length; | ||
this.scopeStack.push(this.currentReport); | ||
} | ||
halsteadItemEncountered(currentReport, metric, identifier) | ||
{ | ||
if (currentReport) { this.incrementHalsteadItems(currentReport, metric, identifier); } | ||
this.incrementHalsteadItems(this.report.aggregate, metric, identifier); | ||
} | ||
incrementCounter(node, syntax, name, incrementFn, currentReport) | ||
{ | ||
const amount = syntax[name]; | ||
if (typeof amount === 'number') | ||
// Handle module methods. | ||
report.methods.forEach((methodReport) => | ||
{ | ||
incrementFn.call(this, currentReport, amount); | ||
} | ||
else if (typeof amount === 'function') | ||
{ | ||
incrementFn.call(this, currentReport, amount(node)); | ||
} | ||
} | ||
this._calculateCyclomaticDensity(methodReport); | ||
this._calculateHalsteadMetrics(methodReport.halstead); | ||
incrementCyclomatic(currentReport, amount) | ||
{ | ||
this.report.aggregate.cyclomatic += amount; | ||
methodReport.sumMetrics(sums, indices); | ||
}); | ||
if (currentReport) | ||
// Handle module class reports. | ||
report.classes.forEach((classReport) => | ||
{ | ||
currentReport.cyclomatic += amount; | ||
} | ||
} | ||
const { sums: classSums } = MethodReport.getMaintainabilityMetrics(); | ||
incrementDistinctHalsteadItems(baseReport, metric, identifier) | ||
{ | ||
if (Object.prototype.hasOwnProperty(identifier)) | ||
{ | ||
// Avoid clashes with built-in property names. | ||
this.incrementDistinctHalsteadItems(baseReport, metric, `_${identifier}`); | ||
} | ||
else if (this.isHalsteadMetricDistinct(baseReport, metric, identifier)) | ||
{ | ||
this.recordDistinctHalsteadMetric(baseReport, metric, identifier); | ||
this.incrementHalsteadMetric(baseReport, metric, 'distinct'); | ||
} | ||
} | ||
let classMethodCount = classReport.methods.length; | ||
moduleMethodCount += classMethodCount; | ||
incrementHalsteadItems(baseReport, metric, identifier) | ||
{ | ||
this.incrementDistinctHalsteadItems(baseReport, metric, identifier); | ||
this.incrementTotalHalsteadItems(baseReport, metric); | ||
} | ||
// Process all class methods. | ||
classReport.methods.forEach((methodReport) => | ||
{ | ||
this._calculateCyclomaticDensity(methodReport); | ||
this._calculateHalsteadMetrics(methodReport.halstead); | ||
incrementHalsteadMetric(baseReport, metric, type) | ||
{ | ||
if (baseReport) | ||
{ | ||
baseReport.halstead[metric][type] += 1; | ||
} | ||
} | ||
methodReport.sumMetrics(classSums, indices); | ||
methodReport.sumMetrics(sums, indices); | ||
}); | ||
incrementLogicalSloc(currentReport, amount) | ||
{ | ||
this.report.aggregate.sloc.logical += amount; | ||
this._calculateCyclomaticDensity(classReport.methodReport); | ||
this._calculateHalsteadMetrics(classReport.methodReport.halstead); | ||
if (currentReport) | ||
{ | ||
currentReport.sloc.logical += amount; | ||
} | ||
} | ||
incrementTotalHalsteadItems(baseReport, metric) | ||
{ | ||
this.incrementHalsteadMetric(baseReport, metric, 'total'); | ||
} | ||
isHalsteadMetricDistinct(baseReport, metric, identifier) | ||
{ | ||
return baseReport.halstead[metric].identifiers.indexOf(identifier) === -1; | ||
} | ||
popScope() | ||
{ | ||
this.scopeStack.pop(); | ||
if (this.scopeStack.length > 0) | ||
{ | ||
this.currentReport = this.scopeStack[this.scopeStack.length - 1]; | ||
} | ||
else | ||
{ | ||
this.currentReport = undefined; | ||
} | ||
} | ||
processCyclomatic(node, syntax, currentReport) | ||
{ | ||
this.incrementCounter(node, syntax, 'cyclomatic', this.incrementCyclomatic, currentReport); | ||
} | ||
processDependencies(node, syntax, clearDependencies) | ||
{ | ||
let dependencies; | ||
if (typeof syntax.dependencies === 'function') | ||
{ | ||
dependencies = syntax.dependencies(node, clearDependencies); | ||
if (typeof dependencies === 'object' || Array.isArray(dependencies)) | ||
// If there are no class methods use the class aggregate MethodReport. | ||
if (classMethodCount === 0) | ||
{ | ||
this.report.dependencies = this.report.dependencies.concat(dependencies); | ||
// Sane handling of classes that contain no methods. | ||
classReport.methodReport.sumMetrics(classSums, indices); | ||
classMethodCount = 1; | ||
} | ||
return true; | ||
} | ||
const classAverages = classSums.map((sum) => { return sum / classMethodCount; }); | ||
return false; | ||
} | ||
this._calculateMaintainabilityIndex(classReport, classAverages[indices.cyclomatic], | ||
classAverages[indices.effort], classAverages[indices.loc]); | ||
processHalsteadMetric(node, parent, syntax, metric, currentReport) | ||
{ | ||
if (Array.isArray(syntax[metric])) | ||
{ | ||
syntax[metric].forEach((s) => | ||
{ | ||
let identifier; | ||
Object.keys(indices).forEach((index) => { classReport[index] = classAverages[indices[index]]; }); | ||
}); | ||
if (typeof s.identifier === 'function') | ||
{ | ||
identifier = s.identifier(node, parent); | ||
} | ||
else | ||
{ | ||
identifier = s.identifier; | ||
} | ||
this._calculateCyclomaticDensity(report.methodReport); | ||
this._calculateHalsteadMetrics(report.methodReport.halstead); | ||
if (typeof identifier !== 'undefined' && (typeof s.filter !== 'function' || s.filter(node) === true)) | ||
{ | ||
// Handle the case when a node / syntax returns an array of identifiers. | ||
if (Array.isArray(identifier)) | ||
{ | ||
identifier.forEach((element) => { this.halsteadItemEncountered(currentReport, metric, element); }); | ||
} | ||
else | ||
{ | ||
this.halsteadItemEncountered(currentReport, metric, identifier); | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
processLloc(node, syntax, currentReport) | ||
{ | ||
this.incrementCounter(node, syntax, 'lloc', this.incrementLogicalSloc, currentReport); | ||
} | ||
/** | ||
* Controls processing an AST node. | ||
* | ||
* @param {object} node - Current AST node. | ||
* @param {object} parent - Parent AST node. | ||
* @param {object} syntax - Syntax trait associated with the give node type. | ||
*/ | ||
processNode(node, parent, syntax) | ||
{ | ||
this.processLloc(node, syntax, this.currentReport); | ||
this.processCyclomatic(node, syntax, this.currentReport); | ||
this.processOperators(node, parent, syntax, this.currentReport); | ||
this.processOperands(node, parent, syntax, this.currentReport); | ||
if (this.processDependencies(node, syntax, this.clearDependencies)) | ||
// If there are no module methods use the module aggregate MethodReport. | ||
if (moduleMethodCount === 0) | ||
{ | ||
// HACK: This will fail with async or if other syntax than CallExpression introduces dependencies. | ||
// TODO: Come up with a less crude approach. | ||
this.clearDependencies = false; | ||
// Sane handling of modules that contain no methods. | ||
report.methodReport.sumMetrics(sums, indices); | ||
moduleMethodCount = 1; | ||
} | ||
} | ||
processOperands(node, parent, syntax, currentReport) | ||
{ | ||
this.processHalsteadMetric(node, parent, syntax, 'operands', currentReport); | ||
} | ||
const moduleAverages = sums.map((sum) => { return sum / moduleMethodCount; }); | ||
processOperators(node, parent, syntax, currentReport) | ||
{ | ||
this.processHalsteadMetric(node, parent, syntax, 'operators', currentReport); | ||
} | ||
this._calculateMaintainabilityIndex(report, moduleAverages[indices.cyclomatic], | ||
moduleAverages[indices.effort], moduleAverages[indices.loc]); | ||
recordDistinctHalsteadMetric(baseReport, metric, identifier) | ||
{ | ||
baseReport.halstead[metric].identifiers.push(identifier); | ||
Object.keys(indices).forEach((index) => { report[index] = moduleAverages[indices[index]]; }); | ||
} | ||
sumMaintainabilityMetrics(sums, indices, data) | ||
{ | ||
sums[indices.loc] += data.sloc.logical; | ||
sums[indices.cyclomatic] += data.cyclomatic; | ||
sums[indices.effort] += data.halstead.effort; | ||
sums[indices.params] += data.params; | ||
} | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
47943
563
1
+ Addedtyphonjs-escomplex-commons@0.0.5(transitive)
- Removedtyphonjs-escomplex-commons@0.0.1(transitive)