escomplex
Version:
Software complexity analysis of JavaScript-family abstract syntax trees.
335 lines (268 loc) • 9.47 kB
JavaScript
/*globals exports, require */
;
var check = require('check-types'), report;
exports.analyse = analyse;
function analyse (ast, walker, options) {
// TODO: Asynchronise
var settings, currentReport, clearDependencies = true, scopeStack = [];
check.assert.object(ast, 'Invalid syntax tree');
check.assert.object(walker, 'Invalid walker');
check.assert.function(walker.walk, 'Invalid walker.walk method');
if (check.object(options)) {
settings = options;
} else {
settings = getDefaultSettings();
}
// TODO: loc is moz-specific, move to walker?
report = createReport(ast.loc);
walker.walk(ast, settings, {
processNode: processNode,
createScope: createScope,
popScope: popScope
});
calculateMetrics(settings);
return report;
function processNode (node, syntax) {
processLloc(node, syntax, currentReport);
processCyclomatic(node, syntax, currentReport);
processOperators(node, syntax, currentReport);
processOperands(node, syntax, currentReport);
if (processDependencies(node, syntax, clearDependencies)) {
// HACK: This will fail with async or if other syntax than CallExpression introduces dependencies.
// TODO: Come up with a less crude approach.
clearDependencies = false;
}
}
function createScope (name, loc, parameterCount) {
currentReport = createFunctionReport(name, loc, parameterCount);
report.functions.push(currentReport);
report.aggregate.params += parameterCount;
scopeStack.push(currentReport);
}
function popScope () {
scopeStack.pop();
if (scopeStack.length > 0) {
currentReport = scopeStack[scopeStack.length - 1];
} else {
currentReport = undefined;
}
}
}
function getDefaultSettings () {
return {
logicalor: true,
switchcase: true,
forin: false,
trycatch: false,
newmi: false
};
}
function createReport (lines) {
return {
aggregate: createFunctionReport(undefined, lines, 0),
functions: [],
dependencies: []
};
}
function createFunctionReport (name, lines, params) {
var result = {
name: name,
sloc: {
logical: 0
},
cyclomatic: 1,
halstead: createInitialHalsteadState(),
params: params
};
if (check.object(lines)) {
result.line = lines.start.line;
result.sloc.physical = lines.end.line - lines.start.line + 1;
}
return result;
}
function createInitialHalsteadState () {
return {
operators: createInitialHalsteadItemState(),
operands: createInitialHalsteadItemState()
};
}
function createInitialHalsteadItemState () {
return {
distinct: 0,
total: 0,
identifiers: []
};
}
function processLloc (node, syntax, currentReport) {
incrementCounter(node, syntax, 'lloc', incrementLogicalSloc, currentReport);
}
function incrementCounter (node, syntax, name, incrementFn, currentReport) {
var amount = syntax[name];
if (check.number(amount)) {
incrementFn(currentReport, amount);
} else if (check.function(amount)) {
incrementFn(currentReport, amount(node));
}
}
function incrementLogicalSloc (currentReport, amount) {
report.aggregate.sloc.logical += amount;
if (currentReport) {
currentReport.sloc.logical += amount;
}
}
function processCyclomatic (node, syntax, currentReport) {
incrementCounter(node, syntax, 'cyclomatic', incrementCyclomatic, currentReport);
}
function incrementCyclomatic (currentReport, amount) {
report.aggregate.cyclomatic += amount;
if (currentReport) {
currentReport.cyclomatic += amount;
}
}
function processOperators (node, syntax, currentReport) {
processHalsteadMetric(node, syntax, 'operators', currentReport);
}
function processOperands (node, syntax, currentReport) {
processHalsteadMetric(node, syntax, 'operands', currentReport);
}
function processHalsteadMetric (node, syntax, metric, currentReport) {
if (check.array(syntax[metric])) {
syntax[metric].forEach(function (s) {
var identifier;
if (check.function(s.identifier)) {
identifier = s.identifier(node);
} else {
identifier = s.identifier;
}
if (check.function(s.filter) === false || s.filter(node) === true) {
halsteadItemEncountered(currentReport, metric, identifier);
}
});
}
}
function halsteadItemEncountered (currentReport, metric, identifier) {
if (currentReport) {
incrementHalsteadItems(currentReport, metric, identifier);
}
incrementHalsteadItems(report.aggregate, metric, identifier);
}
function incrementHalsteadItems (baseReport, metric, identifier) {
incrementDistinctHalsteadItems(baseReport, metric, identifier);
incrementTotalHalsteadItems(baseReport, metric);
}
function incrementDistinctHalsteadItems (baseReport, metric, identifier) {
if (Object.prototype.hasOwnProperty(identifier)) {
// Avoid clashes with built-in property names.
incrementDistinctHalsteadItems(baseReport, metric, '_' + identifier);
} else if (isHalsteadMetricDistinct(baseReport, metric, identifier)) {
recordDistinctHalsteadMetric(baseReport, metric, identifier);
incrementHalsteadMetric(baseReport, metric, 'distinct');
}
}
function isHalsteadMetricDistinct (baseReport, metric, identifier) {
return baseReport.halstead[metric].identifiers.indexOf(identifier) === -1;
}
function recordDistinctHalsteadMetric (baseReport, metric, identifier) {
baseReport.halstead[metric].identifiers.push(identifier);
}
function incrementHalsteadMetric (baseReport, metric, type) {
if (baseReport) {
baseReport.halstead[metric][type] += 1;
}
}
function incrementTotalHalsteadItems (baseReport, metric) {
incrementHalsteadMetric(baseReport, metric, 'total');
}
function processDependencies (node, syntax, clearDependencies) {
var dependencies;
if (check.function(syntax.dependencies)) {
dependencies = syntax.dependencies(node, clearDependencies);
if (check.object(dependencies) || check.array(dependencies)) {
report.dependencies = report.dependencies.concat(dependencies);
}
return true;
}
return false;
}
function calculateMetrics (settings) {
var count, indices, sums, averages;
count = report.functions.length;
indices = {
loc: 0,
cyclomatic: 1,
effort: 2,
params: 3
};
sums = [ 0, 0, 0, 0 ];
report.functions.forEach(function (functionReport) {
calculateCyclomaticDensity(functionReport);
calculateHalsteadMetrics(functionReport.halstead);
sumMaintainabilityMetrics(sums, indices, functionReport);
});
calculateCyclomaticDensity(report.aggregate);
calculateHalsteadMetrics(report.aggregate.halstead);
if (count === 0) {
// Sane handling of modules that contain no functions.
sumMaintainabilityMetrics(sums, indices, report.aggregate);
count = 1;
}
averages = sums.map(function (sum) { return sum / count; });
calculateMaintainabilityIndex(
averages[indices.effort],
averages[indices.cyclomatic],
averages[indices.loc],
settings
);
Object.keys(indices).forEach(function (index) {
report[index] = averages[indices[index]];
});
}
function calculateCyclomaticDensity (data) {
data.cyclomaticDensity = (data.cyclomatic / data.sloc.logical) * 100;
}
function calculateHalsteadMetrics (data) {
data.length = data.operators.total + data.operands.total;
if (data.length === 0) {
nilHalsteadMetrics(data);
} 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;
}
}
function nilHalsteadMetrics (data) {
data.vocabulary =
data.difficulty =
data.volume =
data.effort =
data.bugs =
data.time =
0;
}
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;
}
function calculateMaintainabilityIndex (averageEffort, averageCyclomatic, averageLoc, settings) {
if (averageCyclomatic === 0) {
throw new Error('Encountered function with cyclomatic complexity zero!');
}
report.maintainability =
171 -
(3.42 * Math.log(averageEffort)) -
(0.23 * Math.log(averageCyclomatic)) -
(16.2 * Math.log(averageLoc));
if (report.maintainability > 171) {
report.maintainability = 171;
}
if (settings.newmi) {
report.maintainability = Math.max(0, (report.maintainability * 100) / 171);
}
}