Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

escomplex-plugin-metrics-module

Package Overview
Dependencies
Maintainers
1
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

escomplex-plugin-metrics-module - npm Package Compare versions

Comparing version 0.0.1 to 0.0.2

491

dist/PluginMetricsModule.js

@@ -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;
}
}
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc