complexity-report
Advanced tools
Comparing version 0.4.3 to 0.5.0
@@ -27,3 +27,3 @@ { | ||
"funcscope": false, | ||
"globalstrict": false, | ||
"globalstrict": true, | ||
"iterator": false, | ||
@@ -44,3 +44,3 @@ "lastsemic": false, | ||
"validthis": false, | ||
"browser": false, | ||
@@ -47,0 +47,0 @@ "couch": false, |
@@ -10,4 +10,4 @@ /*jshint nomen:false */ | ||
commands = { | ||
test: './node_modules/.bin/mocha --ui tdd --reporter spec --colors --slow 50 ./test/complexityReport.js', | ||
lint: './node_modules/.bin/jshint ./src/complexityReport.js --config config/jshint.json', | ||
test: './node_modules/.bin/mocha --ui tdd --reporter spec --colors ./test/complexityReport.js', | ||
lint: './node_modules/.bin/jshint ./src --config config/jshint.json', | ||
prepare: 'npm install' | ||
@@ -17,3 +17,3 @@ }; | ||
desc('Run the unit tests.'); | ||
task('test', [ 'prepare' ], function () { | ||
task('test', function () { | ||
runTask(test, 'Testing...'); | ||
@@ -25,3 +25,3 @@ }, { | ||
desc('Lint the source code.'); | ||
task('lint', [ 'prepare' ], function () { | ||
task('lint', function () { | ||
runTask(lint, 'Linting...'); | ||
@@ -28,0 +28,0 @@ }, { |
{ | ||
"name": "complexity-report", | ||
"version": "0.4.3", | ||
"version": "0.5.0", | ||
"author": "Phil Booth <pmbooth@gmail.com>", | ||
@@ -5,0 +5,0 @@ "description": "A tool for reporting code complexity metrics in JavaScript projects.", |
@@ -16,4 +16,5 @@ # complexityReport.js | ||
to aid its usefulness in automated environments / CI. | ||
There are also some options | ||
for controlling how metrics are calculated. | ||
There are also options | ||
for controlling how metrics are calculated | ||
and the format of the report output. | ||
@@ -23,2 +24,8 @@ The metrics are calculated by walking syntax trees | ||
For people who are only interested in analysing small amounts of code | ||
and don't want to download the tool, | ||
there is also a web front-end available: | ||
* [JSComplexity.org][jscomplexity] | ||
## Installation | ||
@@ -97,3 +104,3 @@ | ||
defaults to `true`. | ||
* `switchcase`: Boolean indicating whether `switch` statements | ||
* `switchcase`: Boolean indicating whether `switch` statements | ||
should be considered a source of cyclomatic complexity, | ||
@@ -116,2 +123,10 @@ defaults to `true`. | ||
## Related projects | ||
* [Gleb Bahmutov][gleb]'s [js-complexity-viz] | ||
is a wrapper for complexityReport.js, | ||
which uses the Google Visualization API | ||
to render complexity charts | ||
for all JavaScript files in a directory tree. | ||
## Development | ||
@@ -161,5 +176,5 @@ | ||
### Unit tests | ||
### Tests | ||
The unit tests are in `test/complexityReport.js`. You can run them with the | ||
The tests are in `test/complexityReport.js`. You can run them with the | ||
command `npm test` or `jake test`. | ||
@@ -170,2 +185,5 @@ | ||
[esprima]: http://esprima.org/ | ||
[jscomplexity]: http://jscomplexity.org/ | ||
[gleb]: https://github.com/bahmutov | ||
[js-complexity-viz]: https://github.com/bahmutov/js-complexity-viz | ||
[tracker]: https://github.com/philbooth/complexityReport.js/issues | ||
@@ -172,0 +190,0 @@ [node]: http://nodejs.org/ |
274
src/cli.js
#!/usr/bin/env node | ||
/*globals require, process */ | ||
/*globals require, process, console */ | ||
(function () { | ||
'use strict'; | ||
'use strict'; | ||
var reports = [], | ||
var reports = [], | ||
cli = require('commander'), | ||
fs = require('fs'), | ||
cr = require('./complexityReport'), | ||
check = require('check-types'), | ||
options, | ||
formatter, | ||
cli = require('commander'), | ||
fs = require('fs'), | ||
cr = require('./complexityReport'), | ||
check = require('check-types'), | ||
options, | ||
formatter, | ||
state = { | ||
reading: true, | ||
unread: 0, | ||
tooComplex: false | ||
}; | ||
state = { | ||
reading: true, | ||
unread: 0, | ||
tooComplex: false | ||
}; | ||
parseCommandLine(); | ||
readSourceFiles(); | ||
parseCommandLine(); | ||
readSourceFiles(); | ||
function parseCommandLine () { | ||
cli. | ||
usage('[options] <file...>'). | ||
option( | ||
'-o, --output <file>', | ||
'specify an output file for the report' | ||
). | ||
option( | ||
'-f, --format <format>', | ||
'specify the output format of the report' | ||
). | ||
option( | ||
'-t, --threshold <complexity>', | ||
'specifify the per-function complexity threshold', | ||
function (value) { | ||
return parseInt(value, 10); | ||
} | ||
). | ||
option( | ||
'-l, --logicalor', | ||
'disregard operator || as source of cyclomatic complexity' | ||
). | ||
option( | ||
'-s, --switchcase', | ||
'disregard switch statements as source of cyclomatic complexity' | ||
). | ||
option( | ||
'-i, --forin', | ||
'treat for...in statements as source of cyclomatic complexity' | ||
). | ||
option( | ||
'-c, --trycatch', | ||
'treat catch clauses as source of cyclomatic complexity' | ||
); | ||
function parseCommandLine () { | ||
cli. | ||
usage('[options] <file...>'). | ||
option( | ||
'-o, --output <file>', | ||
'specify an output file for the report' | ||
). | ||
option( | ||
'-f, --format <format>', | ||
'specify the output format of the report' | ||
). | ||
option( | ||
'-t, --threshold <complexity>', | ||
'specifify the per-function complexity threshold', | ||
function (value) { | ||
return parseInt(value, 10); | ||
} | ||
). | ||
option( | ||
'-s, --silent', | ||
'don\'t write any output to the console' | ||
). | ||
option( | ||
'-l, --logicalor', | ||
'disregard operator || as source of cyclomatic complexity' | ||
). | ||
option( | ||
'-w, --switchcase', | ||
'disregard switch statements as source of cyclomatic complexity' | ||
). | ||
option( | ||
'-i, --forin', | ||
'treat for...in statements as source of cyclomatic complexity' | ||
). | ||
option( | ||
'-c, --trycatch', | ||
'treat catch clauses as source of cyclomatic complexity' | ||
); | ||
cli.parse(process.argv); | ||
cli.parse(process.argv); | ||
options = { | ||
logicalor: !cli.logicalor, | ||
switchcase: !cli.switchcase, | ||
forin: cli.forin || false, | ||
trycatch: cli.trycatch || false | ||
}; | ||
options = { | ||
logicalor: !cli.logicalor, | ||
switchcase: !cli.switchcase, | ||
forin: cli.forin || false, | ||
trycatch: cli.trycatch || false | ||
}; | ||
if (check.isUnemptyString(cli.format) === false) { | ||
cli.format = 'plain'; | ||
} | ||
formatter = require('./formats/' + cli.format); | ||
if (check.isUnemptyString(cli.format) === false) { | ||
cli.format = 'plain'; | ||
} | ||
formatter = require('./formats/' + cli.format); | ||
} | ||
function readSourceFiles () { | ||
var i; | ||
function readSourceFiles () { | ||
var i; | ||
for (i = 0; i < cli.args.length; i += 1) { | ||
state.unread += 1; | ||
readSourceFile(cli.args[i]); | ||
} | ||
state.reading = false; | ||
for (i = 0; i < cli.args.length; i += 1) { | ||
state.unread += 1; | ||
readSourceFile(cli.args[i]); | ||
} | ||
function readSourceFile (path) { | ||
fs.readFile(path, 'utf8', function (err, source) { | ||
if (err) { | ||
error('readSourceFile', err); | ||
} | ||
state.reading = false; | ||
} | ||
if (beginsWithShebang(source)) { | ||
source = commentFirstLine(source); | ||
} | ||
function readSourceFile (path) { | ||
fs.readFile(path, 'utf8', function (err, source) { | ||
if (err) { | ||
error('readSourceFile', err); | ||
} | ||
getReport(path, source); | ||
if (beginsWithShebang(source)) { | ||
source = commentFirstLine(source); | ||
} | ||
finish(); | ||
}); | ||
} | ||
getReport(path, source); | ||
function error (functionName, err) { | ||
fail('Fatal error [' + functionName + ']: ' + err.message); | ||
} | ||
finish(); | ||
}); | ||
} | ||
function fail (message) { | ||
console.log(message); | ||
process.exit(1); | ||
} | ||
function error (functionName, err) { | ||
fail('Fatal error [' + functionName + ']: ' + err.message); | ||
} | ||
function beginsWithShebang (source) { | ||
return source[0] === '#' && source[1] === '!'; | ||
} | ||
function fail (message) { | ||
console.log(message); | ||
process.exit(1); | ||
} | ||
function commentFirstLine (source) { | ||
return '//' + source; | ||
} | ||
function beginsWithShebang (source) { | ||
return source[0] === '#' && source[1] === '!'; | ||
} | ||
function getReport (path, source) { | ||
var report = cr.run(source, options); | ||
function commentFirstLine (source) { | ||
return '//' + source; | ||
} | ||
if ( | ||
state.tooComplex === false && | ||
check.isNumber(cli.threshold) && | ||
isTooComplex(report) | ||
) { | ||
state.tooComplex = true; | ||
} | ||
function getReport (path, source) { | ||
var report = cr.run(source, options); | ||
report.module = path; | ||
reports.push(report); | ||
if ( | ||
state.tooComplex === false && | ||
check.isNumber(cli.threshold) && | ||
isTooComplex(report) | ||
) { | ||
state.tooComplex = true; | ||
} | ||
function isTooComplex (report) { | ||
var i; | ||
report.module = path; | ||
for (i = 0; i < report.functions.length; i += 1) { | ||
if (report.functions[i].complexity.cyclomatic > cli.threshold) { | ||
return true; | ||
} | ||
} | ||
reports.push(report); | ||
} | ||
return false; | ||
} | ||
function isTooComplex (report) { | ||
var i; | ||
function finish () { | ||
state.unread -= 1; | ||
if (state.reading === false && state.unread === 0) { | ||
writeReport(); | ||
for (i = 0; i < report.functions.length; i += 1) { | ||
if (report.functions[i].complexity.cyclomatic > cli.threshold) { | ||
return true; | ||
} | ||
} | ||
function writeReport () { | ||
var formatted = formatter.format(reports); | ||
return false; | ||
} | ||
if (check.isUnemptyString(cli.output)) { | ||
fs.writeFile(cli.output, formatted, 'utf8', function (err) { | ||
if (err) { | ||
error('writeReport', err); | ||
} | ||
}); | ||
} else { | ||
console.log(formatted); | ||
function finish () { | ||
state.unread -= 1; | ||
if (state.reading === false && state.unread === 0) { | ||
if (!cli.silent) { | ||
writeReport(); | ||
} | ||
exit(); | ||
} | ||
function exit () { | ||
// TODO: Use Q | ||
if (state.tooComplex) { | ||
@@ -179,3 +163,17 @@ fail('Warning: Complexity threshold breached!'); | ||
} | ||
}()); | ||
} | ||
function writeReport () { | ||
var formatted = formatter.format(reports); | ||
if (check.isUnemptyString(cli.output)) { | ||
fs.writeFile(cli.output, formatted, 'utf8', function (err) { | ||
if (err) { | ||
error('writeReport', err); | ||
} | ||
}); | ||
} else { | ||
console.log(formatted); | ||
} | ||
} | ||
@@ -5,3 +5,2 @@ /** | ||
/*jshint globalstrict:true */ | ||
/*globals exports, require */ | ||
@@ -11,3 +10,3 @@ | ||
var check, esprima, syntaxDefinitions, report; | ||
var check, esprima, syntaxDefinitions, safeName, syntaxes, report; | ||
@@ -18,2 +17,4 @@ exports.run = run; | ||
esprima = require('esprima'); | ||
safeName = require('./safeName'); | ||
syntaxDefinitions = require('./syntax'); | ||
@@ -31,2 +32,4 @@ /** | ||
function run (source, options) { | ||
// TODO: Asynchronize. | ||
var settings, ast; | ||
@@ -46,3 +49,3 @@ | ||
syntaxDefinitions = getSyntaxDefinitions(settings); | ||
syntaxes = syntaxDefinitions.get(settings); | ||
report = createReport(ast.loc); | ||
@@ -66,232 +69,2 @@ | ||
function getSyntaxDefinitions (settings) { | ||
// TODO: Refactor to traits. | ||
return { | ||
Literal: { | ||
operands: [ { | ||
identifier: function (node) { | ||
if (check.isString(node.value)) { | ||
// Avoid conflicts between string literals and identifiers. | ||
return '"' + node.value + '"'; | ||
} | ||
return node.value; | ||
} | ||
} ] | ||
}, | ||
Identifier: { | ||
operands: [ { identifier: function (node) { return node.name; } } ] | ||
}, | ||
BlockStatement: { | ||
children: [ 'body' ] | ||
}, | ||
ObjectExpression: { | ||
operators: [ { identifier: '{}' } ], | ||
operands: [ { identifier: safeName } ], | ||
children: [ 'properties' ] | ||
}, | ||
Property: { | ||
lloc: 1, | ||
operators: [ { identifier: ':' } ], | ||
children: [ 'key', 'value' ], | ||
getAssignedName: function (node) { return safeName(node.key); } | ||
}, | ||
ThisExpression: { | ||
operands: [ { identifier: 'this' } ] | ||
}, | ||
ArrayExpression: { | ||
operators: [ { identifier: '[]' } ], | ||
operands: [ { identifier: safeName } ], | ||
children: [ 'elements' ] | ||
}, | ||
MemberExpression: { | ||
lloc: function (node) { | ||
return [ | ||
'ObjectExpression', | ||
'ArrayExpression', | ||
'FunctionExpression' | ||
].indexOf(node.object.type) === -1 ? 0 : 1; | ||
}, | ||
operators: [ { identifier: '.' } ], | ||
children: [ 'object', 'property' ] | ||
}, | ||
CallExpression: getFunctionCallSyntaxDefinition('()'), | ||
NewExpression: getFunctionCallSyntaxDefinition('new'), | ||
ExpressionStatement: { | ||
lloc: 1, | ||
children: [ 'expression' ] | ||
}, | ||
VariableDeclaration: { | ||
operators: [ { identifier: function (node) { return node.kind; } } ], | ||
children: [ 'declarations' ] | ||
}, | ||
VariableDeclarator: { | ||
lloc: 1, | ||
operators: [ { | ||
identifier: '=', | ||
optional: function (node) { | ||
return !!node.init; | ||
} | ||
} ], | ||
children: [ 'id', 'init' ], | ||
getAssignedName: function (node) { return safeName(node.id); } | ||
}, | ||
AssignmentExpression: { | ||
operators: [ { identifier: function (node) { return node.operator; } } ], | ||
children: [ 'left', 'right' ], | ||
getAssignedName: function (node) { | ||
if (node.left.type === 'MemberExpression') { | ||
return safeName(node.left.object) + '.' + node.left.property.name; | ||
} | ||
return safeName(node.left.id); | ||
} | ||
}, | ||
UnaryExpression: { | ||
operators: [ { | ||
identifier: function (node) { | ||
return node.operator + ' (' + (node.prefix ? 'pre' : 'post') + 'fix)'; | ||
} | ||
} ], | ||
children: [ 'argument' ] | ||
}, | ||
UpdateExpression: { | ||
operators: [ { | ||
identifier: function (node) { | ||
return node.operator + ' (' + (node.prefix ? 'pre' : 'post') + 'fix)'; | ||
} | ||
} ], | ||
children: [ 'argument' ] | ||
}, | ||
BinaryExpression: { | ||
operators: [ { identifier: function (node) { return node.operator; } } ], | ||
children: [ 'left', 'right' ] | ||
}, | ||
LogicalExpression: { | ||
complexity: function (node) { return settings.logicalor && node.operator === '||' ? 1 : 0; }, | ||
operators: [ { identifier: function (node) { return node.operator; } } ], | ||
children: [ 'left', 'right' ] | ||
}, | ||
SequenceExpression: { | ||
children: [ 'expressions' ] | ||
}, | ||
IfStatement: { | ||
lloc: function (node) { return node.alternate ? 2 : 1; }, | ||
complexity: 1, | ||
operators: [ { | ||
identifier: 'if' | ||
}, { | ||
identifier: 'else', | ||
optional: function (node) { return !!node.alternate; } | ||
} ], | ||
children: [ 'test', 'consequent', 'alternate' ] | ||
}, | ||
ConditionalExpression: { | ||
complexity: 1, | ||
operators: [ { identifier: ':?' } ], | ||
children: [ 'test', 'consequent', 'alternate' ] | ||
}, | ||
SwitchStatement: { | ||
lloc: 1, | ||
operators: [ { identifier: 'switch' } ], | ||
children: [ 'discriminant', 'cases' ] | ||
}, | ||
SwitchCase: { | ||
lloc: 1, | ||
complexity: function (node) { return settings.switchcase && node.test ? 1 : 0; }, | ||
operators: [ { | ||
identifier: function (node) { | ||
return node.test ? 'case' : 'default'; | ||
} | ||
} ], | ||
children: [ 'test', 'consequent' ] | ||
}, | ||
BreakStatement: getBreakContinueSyntaxDefinition('break'), | ||
ContinueStatement: getBreakContinueSyntaxDefinition('continue'), | ||
ForStatement: getLoopSyntaxDefinition('for', 1), | ||
ForInStatement: { | ||
lloc: 1, | ||
complexity: function (node) { return settings.forin ? 1 : 0; }, | ||
operators: [ { identifier: 'forin' } ], | ||
children: [ 'left', 'right', 'body' ] | ||
}, | ||
WhileStatement: getLoopSyntaxDefinition('while', 1), | ||
DoWhileStatement: getLoopSyntaxDefinition('dowhile', 2), | ||
FunctionDeclaration: getFunctionSyntaxDefinition(1), | ||
FunctionExpression: getFunctionSyntaxDefinition(0), | ||
ReturnStatement: { | ||
lloc: 1, | ||
operators: [ { identifier: 'return' } ], | ||
children: [ 'argument' ] | ||
}, | ||
TryStatement: { | ||
lloc: 1, | ||
children: [ 'block', 'handlers' ] | ||
}, | ||
CatchClause: { | ||
lloc: 1, | ||
complexity: function (node) { return settings.trycatch ? 1 : 0; }, | ||
operators: [ { identifier: 'catch' } ], | ||
children: [ 'param', 'body' ] | ||
}, | ||
ThrowStatement: { | ||
lloc: 1, | ||
operators: [ { identifier: 'throw' } ], | ||
children: [ 'argument' ] | ||
}, | ||
WithStatement: { | ||
lloc: 1, | ||
operators: [ { identifier: 'with' } ], | ||
children: [ 'object', 'body' ] | ||
} | ||
}; | ||
} | ||
function safeName (object, defaultName) { | ||
if (check.isObject(object) && check.isUnemptyString(object.name)) { | ||
return object.name; | ||
} | ||
if (check.isUnemptyString(defaultName)) { | ||
return defaultName; | ||
} | ||
return '<anonymous>'; | ||
} | ||
function getFunctionCallSyntaxDefinition (type) { | ||
return { | ||
lloc: function (node) { return node.callee.type === 'FunctionExpression' ? 1 : 0; }, | ||
operators: [ { identifier: type } ], | ||
children: [ 'arguments', 'callee' ] | ||
}; | ||
} | ||
function getBreakContinueSyntaxDefinition (type) { | ||
return { | ||
lloc: 1, | ||
operators: [ { identifier: true } ], | ||
children: [ 'label' ] | ||
}; | ||
} | ||
function getLoopSyntaxDefinition (type, lloc) { | ||
return { | ||
lloc: lloc, | ||
complexity: function (node) { return node.test ? 1 : 0; }, | ||
operators: [ { identifier: type } ], | ||
children: [ 'init', 'test', 'update', 'body' ] | ||
}; | ||
} | ||
function getFunctionSyntaxDefinition (lloc) { | ||
return { | ||
lloc: lloc, | ||
operators: [ { identifier: 'function' } ], | ||
operands: [ { identifier: function (node) { return safeName(node.id); } } ], | ||
children: [ 'params', 'body' ], | ||
isFunction: true | ||
}; | ||
} | ||
function createReport (lines) { | ||
@@ -345,8 +118,8 @@ return { | ||
function processNode (node, assignedName, currentReport) { | ||
var def; | ||
var syntax; | ||
if (check.isObject(node)) { | ||
def = syntaxDefinitions[node.type]; | ||
if (check.isObject(syntaxDefinitions[node.type])) { | ||
syntax = syntaxes[node.type]; | ||
if (check.isObject(syntaxes[node.type])) { | ||
processLloc(node, currentReport); | ||
@@ -356,4 +129,4 @@ processComplexity(node, currentReport); | ||
processOperands(node, currentReport); | ||
if (def.isFunction) { | ||
if (syntax.newScope) { | ||
processChildrenInNewScope(node, assignedName); | ||
@@ -372,3 +145,3 @@ } else { | ||
function incrementCounter (node, name, incrementFn, currentReport) { | ||
var amount = syntaxDefinitions[node.type][name]; | ||
var amount = syntaxes[node.type][name]; | ||
@@ -411,15 +184,15 @@ if (check.isNumber(amount)) { | ||
function processHalsteadMetric (node, metric, currentReport) { | ||
var def = syntaxDefinitions[node.type], i, identifier; | ||
var syntax = syntaxes[node.type], i, identifier; | ||
if (check.isArray(def[metric])) { | ||
for (i = 0; i < def[metric].length; i += 1) { | ||
if (check.isFunction(def[metric][i].identifier)) { | ||
identifier = def[metric][i].identifier(node); | ||
if (check.isArray(syntax[metric])) { | ||
for (i = 0; i < syntax[metric].length; i += 1) { | ||
if (check.isFunction(syntax[metric][i].identifier)) { | ||
identifier = syntax[metric][i].identifier(node); | ||
} else { | ||
identifier = def[metric][i].identifier; | ||
identifier = syntax[metric][i].identifier; | ||
} | ||
if ( | ||
check.isFunction(def[metric][i].optional) === false || | ||
def[metric][i].optional(node) === true | ||
check.isFunction(syntax[metric][i].filter) === false || | ||
syntax[metric][i].filter(node) === true | ||
) { | ||
@@ -482,9 +255,9 @@ halsteadItemEncountered(currentReport, metric, identifier); | ||
function processChildren (node, currentReport) { | ||
var def = syntaxDefinitions[node.type], i; | ||
var syntax = syntaxes[node.type], i; | ||
if (check.isArray(def.children)) { | ||
for (i = 0; i < def.children.length; i += 1) { | ||
if (check.isArray(syntax.children)) { | ||
for (i = 0; i < syntax.children.length; i += 1) { | ||
processChild( | ||
node[def.children[i]], | ||
check.isFunction(def.getAssignedName) ? def.getAssignedName(node) : '', | ||
node[syntax.children[i]], | ||
check.isFunction(syntax.assignableName) ? syntax.assignableName(node) : '', | ||
currentReport | ||
@@ -503,5 +276,5 @@ ); | ||
var i, data, averages, | ||
sums = [ 0, 0, 0 ], | ||
indices = { | ||
@@ -515,3 +288,3 @@ loc: 0, | ||
data = report.functions[i].complexity; | ||
calculateHalsteadMetrics(data.halstead); | ||
@@ -518,0 +291,0 @@ sumMaintainabilityMetrics(sums, indices, data); |
Sorry, the diff of this file is too big to display
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
138525
55
2937
191
7