├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── MIT-License.txt ├── README.md ├── experiments ├── A │ └── foo.js └── multiple-options.js ├── index.js ├── js-complexity-viz.js ├── package.json ├── src ├── Complexity.js ├── arguments.js ├── collector.js ├── history.js ├── js-complexity.js ├── metrics.js ├── report.html ├── reportChange.js ├── reporter.js ├── sourceFiles.js └── utils.js └── test ├── amd ├── withAmd.js └── withoutAmd.js ├── complexityExample.json ├── complexityTest.js ├── e2e.js ├── example_data.json ├── example_report.html ├── external.js ├── filenames.js └── prefix.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /test/cover 3 | /report.html 4 | /report.json 5 | /cover 6 | /test/report.json 7 | /report.txt 8 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "maxerr" : 50, 3 | 4 | "bitwise" : true, 5 | "camelcase" : false, 6 | "curly" : true, 7 | "eqeqeq" : true, 8 | "forin" : true, 9 | "immed" : false, 10 | "indent" : 4, 11 | "latedef" : false, 12 | "newcap" : true, 13 | "noarg" : true, 14 | "noempty" : true, 15 | "nonew" : false, 16 | "plusplus" : false, 17 | "quotmark" : "single", 18 | "undef" : true, 19 | "unused" : true, 20 | "strict" : false, 21 | "trailing" : false, 22 | "maxparams" : 4, 23 | "maxdepth" : 4, 24 | "maxstatements" : 20, 25 | "maxcomplexity" : 10, 26 | "maxlen" : 120, 27 | 28 | "asi" : false, 29 | "boss" : false, 30 | "debug" : false, 31 | "eqnull" : false, 32 | "es5" : false, 33 | "esnext" : false, 34 | "moz" : false, 35 | "evil" : false, 36 | "expr" : false, 37 | "funcscope" : false, 38 | "globalstrict" : false, 39 | "iterator" : false, 40 | "lastsemic" : false, 41 | "laxbreak" : false, 42 | "laxcomma" : false, 43 | "loopfunc" : false, 44 | "multistr" : false, 45 | "proto" : false, 46 | "scripturl" : false, 47 | "smarttabs" : false, 48 | "shadow" : false, 49 | "sub" : false, 50 | "supernew" : false, 51 | "validthis" : false, 52 | 53 | "browser" : false, 54 | "couch" : false, 55 | "devel" : true, 56 | "dojo" : false, 57 | "jquery" : false, 58 | "mootools" : false, 59 | "node" : true, 60 | "nonstandard" : false, 61 | "prototypejs" : false, 62 | "rhino" : false, 63 | "worker" : false, 64 | "wsh" : false, 65 | "yui" : false, 66 | 67 | "nomen" : false, 68 | "onevar" : false, 69 | "passfail" : false, 70 | "white" : true, 71 | 72 | "predef" : ["log"] 73 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | branches: 5 | only: 6 | - master 7 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function (grunt) { 3 | 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | jshint: { 7 | options: { 8 | jshintrc: '.jshintrc', 9 | }, 10 | 'default': { 11 | src: ['*.js', 'src/*.js'] 12 | } 13 | } 14 | }); 15 | 16 | grunt.loadNpmTasks('grunt-contrib-jshint'); 17 | grunt.loadNpmTasks('grunt-nice-package'); 18 | grunt.loadNpmTasks('grunt-bump'); 19 | 20 | grunt.registerTask('default', ['nice-package', 'jshint']); 21 | }; 22 | -------------------------------------------------------------------------------- /MIT-License.txt: -------------------------------------------------------------------------------- 1 | Copyright 2012 Gleb Bahmutov 2 | https://github.com/bahmutov/js-complexity-viz 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSC (formerly js-complexity-viz) 2 | 3 | [![NPM info][nodei.co]](https://npmjs.org/package/jsc) 4 | 5 | [![Build status][ci-image]][ci-status] 6 | [![dependencies][dependencies-image]][dependencies-url] 7 | [![endorse][endorse-image]][endorse-url] 8 | 9 | JavaScript source code complexity tool 10 | 11 | This is a runner/wrapper around Phil Booth's tool [*complexity report*](https://github.com/philbooth/complexityReport.js "Complexity report at github") 12 | 13 | ## Install and run 14 | 15 | npm install -g jsc 16 | jsc foo.js tests/*.js 17 | 18 | It can also be installed locally and used from other modules. See *js-complexity-viz.js* source file. 19 | 20 | Use -h option to see all (few) options 21 | 22 | Outputs: 23 | 24 | 1. *report.html* (based on template, uses Google charting library) 25 | 2. *report.json* (collected js file complexity statistics) 26 | 27 | For example, the html can be ingested into Jenkins build system when using *HTML publisher plugin* 28 | 29 | ## License 30 | 31 | The MIT License, see [*MIT-License.txt*](js-complexity-viz/blob/master/MIT-License.txt "MIT-License.txt") 32 | 33 | ## Contact 34 | 35 | Gleb Bahmutov 36 | 37 | [ci-image]: https://secure.travis-ci.org/bahmutov/js-complexity-viz.png?branch=master 38 | [ci-status]: http://travis-ci.org/#!/bahmutov/js-complexity-viz 39 | [nodei.co]: https://nodei.co/npm/jsc.png?downloads=true 40 | [dependencies-image]: https://david-dm.org/bahmutov/js-complexity-viz.png 41 | [dependencies-url]: https://david-dm.org/bahmutov/js-complexity-viz 42 | [endorse-image]: https://api.coderwall.com/bahmutov/endorsecount.png 43 | [endorse-url]: https://coderwall.com/bahmutov 44 | -------------------------------------------------------------------------------- /experiments/A/foo.js: -------------------------------------------------------------------------------- 1 | function foo() {} -------------------------------------------------------------------------------- /experiments/multiple-options.js: -------------------------------------------------------------------------------- 1 | var optimist = require("optimist"); 2 | var args = optimist.string("skip").alias("skip", "s").argv; 3 | 4 | console.log("arguments", args); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var measure = require('./src/metrics'); 4 | var sourceFiles = require('./src/sourceFiles'); 5 | var complexityInfo = require('./src/reporter').getComplexityInfo; 6 | var program = require('commander'); 7 | var package = require('./package.json'); 8 | var check = require('check-types'); 9 | var path = require('path'); 10 | 11 | var repoRevision = require('ggit').repoRevision; 12 | var repoRoot = require('ggit').repoRoot; 13 | 14 | program.version(package.version); 15 | program._name = package.name; 16 | 17 | program.command('help') 18 | .description('show help and exit') 19 | .action(function () { 20 | console.log('Complexity metrics:\n\n' + complexityInfo()); 21 | program.help(); 22 | }); 23 | 24 | program.command('changes [filename]') 25 | .description('analyze file complexity against Git repo version') 26 | .option('-f, --filename [filename]', 'filename to analyze') 27 | .action(function (filename, options) { 28 | filename = filename || options.filename; 29 | check.verifyString(filename, 'missing filename'); 30 | var files = sourceFiles(filename); 31 | check.verifyArray(files, 'should get full filenames'); 32 | if (!files.length) { 33 | console.log('not files found matching', filename); 34 | process.exit(0); 35 | } 36 | 37 | repoRoot(function (rootFolder) { 38 | var relativePaths = files.map(path.relative.bind(this, rootFolder)); 39 | relativePaths.forEach(reportChange); 40 | }); 41 | }); 42 | 43 | if (process.argv.length === 2) { 44 | process.argv.push('help'); 45 | } 46 | program.parse(process.argv); 47 | 48 | function reportChange(filename) { 49 | check.verifyString(filename, 'missing filename'); 50 | repoRevision(filename, function (contents) { 51 | check.verifyString(contents, 'could not get repo for', filename); 52 | var repoComplexity = measure.getSourceComplexity(contents, filename); 53 | var diskComplexity = measure.getFileComplexity(filename); 54 | diskComplexity.compare(repoComplexity, filename); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /js-complexity-viz.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var complexity = require('./src/js-complexity'); 4 | /* 5 | var metrics = require('./src/metrics'); 6 | var history = require('./src/history').run; 7 | 8 | if (module.parent) { 9 | module.exports = { 10 | complexity: complexity, 11 | metrics: metrics 12 | }; 13 | } else { 14 | var args = require('./src/arguments').run(); 15 | console.assert(args, 'could not find args structure'); 16 | 17 | var logger = require('optional-color-logger'); 18 | logger.init(args); 19 | 20 | console.assert(args.path, 'empty path'); 21 | if (!Array.isArray(args.path)) { 22 | args.path = [args.path]; 23 | } 24 | if (!Array.isArray(args.skip)) { 25 | args.skip = [args.skip]; 26 | } 27 | 28 | if (args.history > 0) { 29 | history({ 30 | filename: args.path[0], 31 | report: args.report, 32 | commits: args.history 33 | }); 34 | } else { 35 | complexity.run({ 36 | report: args.report, 37 | path: args.path, 38 | colors: args.colors, 39 | limit: args.limit, 40 | sort: args.sort, 41 | minimal: args.minimal, 42 | skip: args.skip 43 | }); 44 | } 45 | */ 46 | if (module.parent) { 47 | module.exports = complexity; 48 | } else { 49 | var updateNotifier = require('update-notifier'); 50 | var notifier = updateNotifier(); 51 | if (notifier.update) { 52 | notifier.notify(); 53 | } 54 | 55 | var fs = require("fs"); 56 | var path = require("path"); 57 | var check = require('check-types'); 58 | 59 | var arguments = require('./src/arguments').run(); 60 | console.assert(arguments, 'could not find args structure'); 61 | 62 | var logger = require('optional-color-logger'); 63 | logger.init(arguments); 64 | 65 | console.assert(arguments.path, 'empty path'); 66 | if (!Array.isArray(arguments.path)) { 67 | arguments.path = [arguments.path]; 68 | } 69 | if (!Array.isArray(arguments.skip)) { 70 | arguments.skip = [arguments.skip]; 71 | } 72 | log.debug('looking for js files in folders', arguments.path); 73 | 74 | complexity.run({ 75 | report: arguments.report, 76 | path: arguments.path, 77 | colors: arguments.colors, 78 | limit: arguments.limit, 79 | sort: arguments.sort, 80 | minimal: arguments.minimal, 81 | skip: arguments.skip 82 | }); 83 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsc", 3 | "description": "Javascript complexity tool", 4 | "version": "0.3.11", 5 | "author": "Gleb Bahmutov ", 6 | "bin": "./js-complexity-viz.js", 7 | "bugs": { 8 | "url": "https://github.com/bahmutov/js-complexity-viz/issues" 9 | }, 10 | "contributors": [], 11 | "dependencies": { 12 | "allong.es": "0.14.0", 13 | "check-types": "0.8.1", 14 | "cli-color": "0.3.2", 15 | "cli-table": "0.3.1", 16 | "colors": "0.6.2", 17 | "commander": "2.6.0", 18 | "complexity-report": "0.10.5", 19 | "custom-logger": "0.3.0", 20 | "gauss": "0.2.12", 21 | "ggit": "0.6.1", 22 | "glob": "3.2.11", 23 | "lodash": "2.4.1", 24 | "moment": "2.8.4", 25 | "optimist": "0.6.1", 26 | "optional-color-logger": "0.0.6", 27 | "sprintf": "0.1.5", 28 | "update-notifier": "0.2.2" 29 | }, 30 | "devDependencies": { 31 | "grunt": "0.4.5", 32 | "grunt-bump": "0.0.16", 33 | "grunt-contrib-jshint": "0.10.0", 34 | "grunt-nice-package": "0.9.2", 35 | "gt": "0.8.47" 36 | }, 37 | "engines": { 38 | "node": ">= 0.8.0" 39 | }, 40 | "homepage": "https://github.com/bahmutov/js-complexity-viz", 41 | "keywords": [ 42 | "code", 43 | "complexity", 44 | "code complexity", 45 | "js", 46 | "javascript" 47 | ], 48 | "license": "MIT", 49 | "main": "./js-complexity-viz.js", 50 | "preferGlobal": true, 51 | "repository": { 52 | "type": "git", 53 | "url": "https://github.com/bahmutov/js-complexity-viz.git" 54 | }, 55 | "scripts": { 56 | "test": "node ./node_modules/gt ./test/filenames.js ./test/e2e.js", 57 | "complexity": "node js-complexity-viz *.js src/*.js" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Complexity.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var check = require('check-types'); 3 | var clc = require('cli-color'); 4 | 5 | module.exports = Complexity; 6 | 7 | function Complexity(metrics) { 8 | check.verifyObject(metrics, 'missing metrics'); 9 | _.extend(this, metrics); 10 | } 11 | 12 | Complexity.prototype.getMaintainability = function() { 13 | return this.maintainability; 14 | }; 15 | 16 | Complexity.prototype.basicInfo = function() { 17 | var c = this.aggregate.complexity; 18 | return 'loc: ' + c.sloc.physical + 19 | ' cyclomatic: ' + c.cyclomatic + 20 | ' halstead: ' + c.halstead.difficulty.toFixed(2) + 21 | ' maintainability: ' + this.maintainability.toFixed(2); 22 | }; 23 | 24 | Complexity.prototype.info = function() { 25 | var c = this.aggregate.complexity; 26 | var info = { 27 | loc: { 28 | value: c.sloc.logical 29 | }, 30 | cyclomatic: { 31 | value: c.cyclomatic, 32 | }, 33 | halstead: { 34 | value: c.halstead.difficulty, 35 | }, 36 | maintainability: { 37 | value: this.maintainability, 38 | higherIsBetter: true 39 | } 40 | }; 41 | return info; 42 | }; 43 | 44 | var good = clc.greenBright; 45 | var bad = clc.redBright; 46 | 47 | function changeMessage(label, newValue, oldValue, precision, higherIsBetter) { 48 | check.verifyString(label, 'missing label'); 49 | check.verifyNumber(newValue, 'new value should be a number'); 50 | 51 | var msg = null; 52 | var diff = newValue - oldValue; 53 | var goodColor = (higherIsBetter ? bad : good); 54 | var badColor = (higherIsBetter ? good : bad); 55 | if (diff > 0) { 56 | msg = badColor(label + ': ' + newValue.toFixed(precision) + '(+' + diff.toFixed(precision) + ')'); 57 | } else { 58 | msg = goodColor(label + ': ' + newValue.toFixed(precision) + '(' + diff.toFixed(precision) + ')'); 59 | } 60 | return msg; 61 | } 62 | 63 | Complexity.prototype.compare = function(o, filename) { 64 | console.assert(o instanceof Complexity, 'invalid object to compare to', o); 65 | if (filename) { 66 | check.verifyString(filename, 'filename should be a string'); 67 | } 68 | var info = this.info(); 69 | check.verifyObject(info, 'missing info'); 70 | var oi = o.info(); 71 | check.verifyObject(oi, 'missing other info'); 72 | 73 | var isBetter = info.maintainability.value >= oi.maintainability.value; 74 | 75 | var msg = ''; 76 | if (filename) { 77 | msg = (isBetter ? good : bad)(filename + ' '); 78 | } 79 | var diff; 80 | 81 | msg += changeMessage('loc', info.loc.value, oi.loc.value); 82 | msg += ' ' + changeMessage('halstead', 83 | info.halstead.value, 84 | oi.halstead.value, 1); 85 | msg += ' ' + changeMessage('maintainability', 86 | info.maintainability.value, 87 | oi.maintainability.value, 1, true); 88 | if (info.maintainability.value !== oi.maintainability.value) { 89 | // do not print anything if things have not changed. 90 | console.log(msg); 91 | } 92 | return isBetter; 93 | }; -------------------------------------------------------------------------------- /src/arguments.js: -------------------------------------------------------------------------------- 1 | // process input command line arguments 2 | module.exports.run = function () { 3 | var optimist = require('optimist'); 4 | args = optimist.usage('Visualize JS files complexity.\nUsage: $0') 5 | .default({ 6 | help: false, 7 | path: [], 8 | skip: [], 9 | sort: 2, 10 | colors: true, 11 | recursive: true, 12 | limit: 15, 13 | log: 1, 14 | report: "report.json", 15 | minimal: false 16 | }).alias('h', 'help').alias('p', 'path').alias('r', 'report').alias('s', 'skip') 17 | .boolean("colors") 18 | .alias('colors', 'color') 19 | .string("path").string("report").string("skip") 20 | .boolean('recursive').boolean('h') 21 | .describe('help', 'show usage help and exit') 22 | .describe("path", "input filename|folder with JS files, use multiple if necessary") 23 | .describe("log", "logging level: 0 - debug, 1 - info") 24 | .describe("report", "name of the output report file") 25 | .describe("skip", "filename or folder to skip, use multiple time if necessary") 26 | .describe('sort', 'table column to sort on for command window output, reverse sorting using --sort !column') 27 | .describe('colors', 'use terminal colors for output, might not work with continuous build servers') 28 | .describe('recursive', 'recurse into subfolders when looking for js files') 29 | .describe('limit', 'highlight files with cyclomatic complexity above limit') 30 | .alias('m', 'minimal').boolean('minimal').describe('minimal', 'minimal output') 31 | .argv; 32 | 33 | if (typeof args.path === 'string') { 34 | args.path = [args.path]; 35 | } 36 | if (args.path.length === 0) { 37 | args.path = args._; 38 | } 39 | if (args.minimal) { 40 | args.log = 3; 41 | } 42 | 43 | if (args.path.length === 0 || args.h || args.help || args["?"]) { 44 | optimist.showHelp(); 45 | console.log(args); 46 | process.exit(0); 47 | } 48 | return args; 49 | }; -------------------------------------------------------------------------------- /src/collector.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var check = require('check-types'); 4 | var discoverSourceFiles = require('./sourceFiles'); 5 | 6 | function prepareExcludedFilenames(config) { 7 | config = config || {}; 8 | if (!Array.isArray(config.skip)) { 9 | config.skip = [config.skip]; 10 | } 11 | console.assert(Array.isArray(config.skip), 'expected skip to be an array'); 12 | 13 | log.debug("preparing excluded paths", config.skip); 14 | config.skip.forEach(function(name, index) { 15 | var fullname = path.resolve(config.path, name); 16 | fullname = fullname.toLowerCase(); 17 | config.skip[index] = fullname; 18 | }); 19 | 20 | log.debug("excluded files", config.skip); 21 | } 22 | 23 | function isJsExcluded(filename, config) { 24 | check.verifyString(filename, 'filename should be a string, not ' + filename); 25 | console.assert(config, 'missing config'); 26 | 27 | check.verifyArray(config.skip, "config.skip should be an array"); 28 | if (!config.skip.length) { 29 | return false; 30 | } 31 | 32 | // assuming both files are lowercase 33 | var found = config.skip.some(function (name) { 34 | return filename === name; 35 | }); 36 | return found; 37 | } 38 | 39 | var js = /\.js$/i; 40 | var allJsFiles = {}; 41 | 42 | function checkFile(filename, config) { 43 | filename = filename.toLowerCase(); 44 | 45 | if (filename.match(js)) { 46 | if (!isJsExcluded(filename, config)) { 47 | var fullPath = path.resolve(filename); 48 | allJsFiles[fullPath] = fullPath; 49 | // console.log('full path', fullPath); 50 | } else { 51 | log.debug('skipping file', filename); 52 | } 53 | } else { 54 | try { 55 | var stats = fs.lstatSync(filename); 56 | if (stats.isDirectory()) { 57 | if (!isJsExcluded(filename) && config.recursive) { 58 | collectJsFiles([filename], config); 59 | } else { 60 | log.debug('skipping folder', filename); 61 | } 62 | } 63 | } 64 | catch (e) {} 65 | } 66 | } 67 | 68 | function checkFolder(folder, config) { 69 | check.verifyString(folder, folder + ' should be a string folder name'); 70 | console.assert(config, 'missing config'); 71 | 72 | var stats = fs.lstatSync(folder); 73 | if (!stats.isDirectory()) { 74 | // console.log(folder, 'is a filename'); 75 | checkFile(folder, config); 76 | return; 77 | } 78 | 79 | var files = fs.readdirSync(folder); 80 | files.forEach(function (filename) { 81 | filename = path.resolve(folder, filename); 82 | checkFile(filename, config); 83 | }); 84 | } 85 | 86 | function collectJsFiles(config) { 87 | console.assert(config, 'missing config'); 88 | check.verifyArray(config.path, 'expected list of files'); 89 | log.debug('checking folders', config.path); 90 | 91 | config.path.forEach(function (folder) { 92 | var fullFolder = path.resolve(folder); 93 | checkFolder(fullFolder, config); 94 | }); 95 | } 96 | 97 | function discoverSourceFiles(files) { 98 | console.assert(Array.isArray(files), 'expect list of filenames, not', JSON.stringify(files)); 99 | var glob = require("glob"); 100 | 101 | var filenames = []; 102 | files.forEach(function (shortName) { 103 | var files = glob.sync(shortName); 104 | filenames = filenames.concat(files); 105 | }); 106 | 107 | filenames = filenames.map(function (shortName) { 108 | return path.resolve(shortName); 109 | }); 110 | return filenames; 111 | } 112 | 113 | module.exports = { 114 | collect: function(config) { 115 | console.assert(config, 'missing config'); 116 | config.skip = config.skip || []; 117 | 118 | var files = discoverSourceFiles(config.path); 119 | console.assert(Array.isArray(files), 'could not discover input files from path', config.path); 120 | config.path = files; 121 | 122 | var skipFiles = discoverSourceFiles(config.skip); 123 | console.assert(Array.isArray(skipFiles), 'could not find skip files'); 124 | config.skip = skipFiles; 125 | 126 | prepareExcludedFilenames(config); 127 | 128 | allJsFiles = {}; 129 | collectJsFiles(config); 130 | return allJsFiles; 131 | } 132 | }; -------------------------------------------------------------------------------- /src/history.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var moment = require('moment'); 4 | var metrics = require('./metrics'); 5 | var optimist = require('optimist'); 6 | var Table = require('cli-table'); 7 | var check = require('check-types'); 8 | 9 | var getGitLog = require('ggit').log; 10 | var getFileRevision = require('ggit').fileRevision; 11 | 12 | var config = { 13 | filename: '', 14 | report: '', 15 | commits: 30 16 | }; 17 | 18 | function run(options) { 19 | check.verifyObject(options, 'missing options'); 20 | check.verifyString(options.filename, 'missing input filename'); 21 | config.filename = options.filename; 22 | config.report = options.report || path.basename(options.filename) + '.complexityHistory.json'; 23 | config.commits = options.commits || config.commits; 24 | getGitLog(options.filename, config.commits, 25 | writeComplexityHistory.bind(null, options.filename)); 26 | } 27 | 28 | function writeComplexityHistory(filename, commits) { 29 | check.verifyString(filename, 'missing filename'); 30 | check.verifyArray(commits, 'expected commits'); 31 | 32 | filename = filename.replace(/\\/g, '/'); 33 | console.log('fetching revisions for', filename, 'for', commits.length, 'revisions'); 34 | var titles = ['Date', 'LOC', 'Cyclomatic', 'Halstead', 'Author']; 35 | var rows = []; 36 | commits.forEach(function (revision) { 37 | getFileRevision(revision.commit, filename, function (contents) { 38 | var report = metrics.getSourceComplexity(contents); 39 | check.verifyObject(report, 'missing report for', filename, 'commit', revision.commit); 40 | 41 | rows.push([revision.date, 42 | report.aggregate.complexity.sloc.logical, 43 | report.aggregate.complexity.cyclomatic, 44 | +report.aggregate.complexity.halstead.difficulty.toFixed(0), 45 | revision.author, 46 | revision.description]); 47 | if (rows.length === commits.length) { 48 | writeComplexityReport(filename, titles, rows); 49 | } 50 | }); 51 | }); 52 | } 53 | 54 | function writeComplexityReport(filename, titles, rows) { 55 | check.verifyString(filename, 'expected filename'); 56 | check.verifyArray(titles, 'expected titles array'); 57 | check.verifyArray(rows, 'expected rows array'); 58 | var comparison = function(a, b) { 59 | var first = a[0]; 60 | var second = b[0]; 61 | if (first < second) { 62 | return -1; 63 | } else if (first > second) { 64 | return 1; 65 | } else { 66 | return 0; 67 | } 68 | }; 69 | rows.sort(comparison); 70 | rows = rows.map(function (row) { 71 | row[0] = row[0].format("YYYY/MM/DD HH:mm:ss"); 72 | return row; 73 | }); 74 | 75 | var table = new Table({ 76 | head: titles 77 | }); 78 | rows.forEach(function(row) { 79 | table.push(row.slice(0, 5)); 80 | }); 81 | console.log(table.toString()); 82 | 83 | var report = rows.map(function (row) { 84 | return { 85 | date: row[0], 86 | loc: row[1], 87 | cyclomatic: row[2], 88 | halstead: row[3], 89 | author: row[4], 90 | description: row[5] 91 | }; 92 | }); 93 | var fileReport = { 94 | filename: filename, 95 | complexityHistory: report 96 | }; 97 | fs.writeFileSync(config.report, JSON.stringify(fileReport, null, 2), "utf-8"); 98 | console.log("Saved report text", config.report); 99 | } 100 | 101 | module.exports.run = run; -------------------------------------------------------------------------------- /src/js-complexity.js: -------------------------------------------------------------------------------- 1 | var check = require('check-types'); 2 | var collector = require('./collector'); 3 | 4 | if (!global.log) { 5 | global.log = console; 6 | global.log.debug = console.log; 7 | } 8 | 9 | // returns results object 10 | function run(config) { 11 | config = config || {}; 12 | config.limit = config.limit || 10; 13 | 14 | var json = /\.json$/i; 15 | if (config.report && !config.report.match(json)) { 16 | log.error('output report filename', config.report, 'is not json'); 17 | process.exit(1); 18 | } 19 | 20 | if (check.isString(config.path)) { 21 | config.path = [config.path]; 22 | } 23 | console.assert(Array.isArray(config.path), 'path should be a list of paths'); 24 | // console.log('config.path', config.path); 25 | 26 | var allJsFiles = collector.collect(config); 27 | console.assert(typeof allJsFiles === 'object', 'collector has not returned an object'); 28 | 29 | log.debug('found', Object.keys(allJsFiles).length, 'js files'); 30 | if (log.level < 1) { 31 | Object.keys(allJsFiles).forEach(console.log); 32 | } 33 | 34 | var files = Object.keys(allJsFiles); 35 | var metrics = require('./metrics').computeMetrics(files, config); 36 | check.verifyArray(metrics, 'complexity metrics not an array'); 37 | var results = arrayToMetrics(metrics); 38 | 39 | // first object - titles 40 | var filesN = files.length; 41 | console.assert(metrics.length === filesN + 1, 'output array size', metrics.length, '!== number of files', filesN); 42 | 43 | var reporter = require('./reporter'); 44 | if (config.report) { 45 | reporter.writeComplexityChart(metrics, config.report); 46 | } 47 | reporter.writeReportTables({ 48 | metrics: metrics, 49 | filename: config.report, 50 | colors: config.colors, 51 | limit: config.limit, 52 | minimal: config.minimal 53 | }); 54 | 55 | return results; 56 | } 57 | 58 | function arrayToMetrics(metrics) { 59 | check.verifyArray(metrics, 'expected metrics array'); 60 | var results = []; 61 | metrics.forEach(function (line, index) { 62 | if (!index) { 63 | // skip first line 64 | return; 65 | } 66 | results.push({ 67 | filename: line[0], 68 | loc: +line[1], 69 | cyclomatic: +line[2], 70 | difficulty: +line[3] 71 | }); 72 | }); 73 | return results; 74 | } 75 | 76 | module.exports = { 77 | run: run 78 | }; -------------------------------------------------------------------------------- /src/metrics.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | // var path = require('path'); 3 | var cr = require('complexity-report'); 4 | var check = require('check-types'); 5 | var Complexity = require('./Complexity'); 6 | 7 | module.exports = { 8 | computeMetrics: computeMetrics, 9 | getSourceComplexity: getSourceComplexity, 10 | getFileComplexity: getFileComplexity 11 | }; 12 | 13 | function getSourceComplexity(source) { 14 | check.verifyString(source, 'missing source'); 15 | 16 | var report = { 17 | aggregate: { 18 | complexity: { 19 | sloc: { 20 | logical: 0 21 | }, 22 | cyclomatic: 0, 23 | halstead: { 24 | difficulty: 0 25 | } 26 | } 27 | } 28 | }; 29 | 30 | try { 31 | report = cr.run(source); 32 | } catch (error) { 33 | log.debug('problem computing complexity', error); 34 | if (/Line 1:/.test(error)) { 35 | var lines = source.split('\n'); 36 | lines = lines.filter(function (line) { 37 | return (line[0] !== '#'); 38 | }); 39 | source = lines.join('\n'); 40 | report = cr.run(source); 41 | } 42 | } 43 | 44 | var c = new Complexity(report); 45 | return c; 46 | } 47 | 48 | function getFileComplexity(filename) { 49 | check.verifyString(filename, 'missing filename'); 50 | var source = fs.readFileSync(filename, 'utf-8'); 51 | check.verifyString(source, 'could not get source from file', filename); 52 | var report = getSourceComplexity(source, filename); 53 | check.verifyObject(report, 'could not get complexity for', filename); 54 | return report; 55 | } 56 | 57 | // returns an array of metrics by file 58 | function computeMetrics(filenames, options) { 59 | 60 | options = options || {}; 61 | options.sort = options.sort || 1; 62 | 63 | var complexityMetrics = []; 64 | filenames.forEach(function(filename) { 65 | log.debug('computing complexity for', filename); 66 | var report = getFileComplexity(filename); 67 | 68 | complexityMetrics.push({ 69 | name: filename, 70 | complexity: report 71 | }); 72 | }); 73 | 74 | var header = [['File', 'LOC', 'Cyclomatic', 'Halstead difficulty']]; 75 | 76 | var metrics = []; 77 | complexityMetrics.forEach(function(metric) { 78 | try { 79 | metrics.push([ 80 | metric.name, 81 | metric.complexity.aggregate.complexity.sloc.logical, 82 | metric.complexity.aggregate.complexity.cyclomatic, 83 | metric.complexity.aggregate.complexity.halstead.difficulty.toFixed(0) 84 | ]); 85 | } catch (error) { 86 | console.error(error); 87 | } 88 | }); 89 | 90 | if (metrics.length > 0) { 91 | var sortingColumn = options.sort; 92 | var reverseSort = false; 93 | var comparison = function(a, b) { 94 | var first = a[sortingColumn]; 95 | var second = b[sortingColumn]; 96 | if (first < second) { 97 | return -1; 98 | } else if (first > second) { 99 | return 1; 100 | } else { 101 | return 0; 102 | } 103 | }; 104 | if (/^!/.test(sortingColumn)) { 105 | sortingColumn = Number(sortingColumn.substr(1)); 106 | reverseSort = true; 107 | } 108 | check.verifyNumber(sortingColumn, 'invalid sorting column ' + sortingColumn); 109 | var maxColumn = header[0].length; 110 | console.assert(sortingColumn >= 0 && sortingColumn < maxColumn, 'invalid sorting column', sortingColumn); 111 | log.debug('sorting metrics by column', sortingColumn); 112 | metrics.sort(comparison); 113 | if (reverseSort) { 114 | metrics.reverse(); 115 | } 116 | } 117 | 118 | return header.concat(metrics); 119 | } 120 | -------------------------------------------------------------------------------- /src/report.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JS Complexity 6 | 7 | 8 | 9 | 12 | 53 | 54 | 55 |
56 | Usage: open in the browser this document, pass report.json using ?report=path.json 57 | 58 | -------------------------------------------------------------------------------- /src/reportChange.js: -------------------------------------------------------------------------------- 1 | var check = require('check-types'); 2 | var measure = require('./metrics'); 3 | var repoRevision = require('ggit').repoRevision; 4 | var path = require('path'); 5 | 6 | function showChange(filename) { 7 | check.verifyString(filename, 'missing filename'); 8 | repoRevision(filename, function(contents) { 9 | check.verifyString(contents, 'could not get repo for', filename); 10 | var repoComplexity = measure.getSourceComplexity(contents, filename); 11 | var diskComplexity = measure.getFileComplexity(filename); 12 | diskComplexity.compare(repoComplexity, filename); 13 | }); 14 | } 15 | 16 | function showChanges(rootFolder, files) { 17 | check.verifyArray(files, 'should get full filenames'); 18 | if (!files.length) { 19 | console.log('no files to report.'); 20 | process.exit(0); 21 | } 22 | 23 | var relativePaths = files.map(path.relative.bind(null, rootFolder)); 24 | relativePaths.forEach(showChange); 25 | } 26 | 27 | module.exports = { 28 | showChange: showChange, 29 | showChanges: showChanges 30 | }; -------------------------------------------------------------------------------- /src/reporter.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var check = require('check-types'); 3 | var _ = require('lodash'); 4 | var Vector = require('gauss').Vector; 5 | var removeMatchingPrefixes = require('./utils').removeMatchingPrefixes; 6 | 7 | var json = /\.json$/i; 8 | 9 | function getComplexityInfo() { 10 | var info = 'LOC - lines of code (logical, lower is better)\n' + 11 | 'Cyclomatic - McCabe complexity (lower is better)\n\thttp://en.wikipedia.org/wiki/Cyclomatic_complexity\n' + 12 | 'Halstead difficulty (lower is better)\n\thttp://en.wikipedia.org/wiki/Halstead_complexity_measures\n' + 13 | 'maintainability (higher is better)\n\thttp://jscomplexity.org/complexity\n'; 14 | return info; 15 | } 16 | 17 | function toNumber(value) { 18 | return +value; 19 | } 20 | 21 | // output complexity report chart to file 22 | function writeComplexityChart(metrics, filename) { 23 | check.verifyArray(metrics, 'expected array of metrics'); 24 | 25 | // transform array of arrays to object 26 | var o = {}; 27 | metrics.slice(1).forEach(function (measured) { 28 | var filename = measured[0]; 29 | var loc = measured[1]; 30 | var cyclomatic = measured[2]; 31 | var halstead = measured[3]; 32 | o[filename] = { 33 | loc: toNumber(loc), 34 | cyclomatic: toNumber(cyclomatic), 35 | halstead: toNumber(halstead) 36 | }; 37 | }); 38 | 39 | check.verifyString(filename, 'output filename ' + filename + ' should be a string'); 40 | log.debug('output report filename', filename); 41 | var data = JSON.stringify(o, null, 2); 42 | fs.writeFileSync(filename, data + '\n', 'utf-8'); 43 | log.info('Saved metrics to', filename); 44 | } 45 | 46 | var Table = require('cli-table'); 47 | function makeTable(titles, rows, colorful, complexityLimit) { 48 | console.assert(Array.isArray(titles), 'column titles should be an array, not', titles); 49 | console.assert(Array.isArray(rows), 'rows should be an array, not', rows); 50 | 51 | complexityLimit = complexityLimit || 1000; 52 | console.assert(complexityLimit > 0, 'invalid complexity limit', complexityLimit); 53 | 54 | var table; 55 | if (colorful) { 56 | table = new Table({ 57 | head: titles 58 | }); 59 | } else { 60 | table = new Table({ 61 | head: titles, 62 | style: { 63 | compact: true, 64 | 'padding-left': 1, 65 | 'padding-right': 1 66 | }, 67 | chars: { 68 | 'top': '-', 69 | 'top-mid': '+', 70 | 'top-left': '+', 71 | 'top-right': '+', 72 | 'bottom': '-', 73 | 'bottom-mid': '+', 74 | 'bottom-left': '+', 75 | 'bottom-right': '+', 76 | 'left': '|', 77 | 'left-mid': '+', 78 | 'mid': '-', 79 | 'mid-mid': '+', 80 | 'right': '|', 81 | 'right-mid': '+' 82 | } 83 | }); 84 | } 85 | 86 | var complexityColumn = 2; 87 | if (colorful) { 88 | rows.forEach(function (row, index) { 89 | var complexity = row[complexityColumn]; 90 | if (complexity > complexityLimit) { 91 | var redRow = row.map(function(cell) { 92 | return String(cell).bold.red; 93 | }); 94 | rows[index] = redRow; 95 | } 96 | }); 97 | } 98 | 99 | rows.forEach(function(row) { 100 | table.push(row); 101 | }); 102 | 103 | return table; 104 | } 105 | 106 | function writeReportTables(options) { 107 | options = options || {}; 108 | console.assert(Array.isArray(options.metrics), 'metrics should be an array, not', options.metrics); 109 | console.assert(options.metrics.length >= 1, 'invalid complexity length', options.metrics.length); 110 | 111 | if (options.metrics.length === 1) { 112 | log.log('nothing to report, empty complexity array'); 113 | return; 114 | } 115 | 116 | var info = getComplexityInfo(); 117 | check.verifyString(info, 'complexity info should be a string, not ' + info); 118 | if (options.minimal) { 119 | info = ''; 120 | } 121 | 122 | var titles = options.metrics[0]; 123 | var rows = options.metrics.slice(1); 124 | 125 | if (options.filename) { 126 | (function () { 127 | var table = makeTable(titles, rows, false); 128 | console.assert(table, 'could not make plain table'); 129 | var reportFilename = options.filename.replace(json, '.txt'); 130 | var text = table.toString() + '\n' + info; 131 | fs.writeFileSync(reportFilename, text, 'utf-8'); 132 | log.info('Saved report text', reportFilename); 133 | }()); 134 | } 135 | 136 | (function () { 137 | log.debug('making table, colors?', options.colors, 'complexity limit', options.limit); 138 | 139 | // grab values BEFORE they are obscured by terminal colors 140 | // make sure values are numbers 141 | var complexities = _.pluck(rows, 2).map(toNumber); 142 | var halsteadComplexities = _.pluck(rows, 3).map(toNumber); 143 | 144 | var rowsNoPrefix = removeMatchingPrefixes(rows); 145 | var table = makeTable(titles, rowsNoPrefix, options.colors, options.limit); 146 | console.assert(table, 'could not make table, colors?', options.colors); 147 | var text = table.toString(); 148 | if (info) { 149 | text += '\n' + info; 150 | } 151 | console.log(text); 152 | 153 | var set = new Vector(complexities); 154 | console.log('Cyclomatic: min', set.min(), 'mean', set.mean().toFixed(1), 'max', set.max()); 155 | set = new Vector(halsteadComplexities); 156 | console.log('Halstead: min', set.min(), 'mean', set.mean().toFixed(1), 'max', set.max()); 157 | }()); 158 | } 159 | 160 | module.exports = { 161 | writeComplexityChart: writeComplexityChart, 162 | writeReportTables: writeReportTables, 163 | getComplexityInfo: getComplexityInfo 164 | }; -------------------------------------------------------------------------------- /src/sourceFiles.js: -------------------------------------------------------------------------------- 1 | var glob = require('glob'); 2 | var check = require('check-types'); 3 | var path = require('path'); 4 | var unary = require('allong.es').es.unary; 5 | 6 | module.exports = sourceFiles; 7 | 8 | function sourceFiles(files) { 9 | if (check.isString(files)) { 10 | files = [files]; 11 | } 12 | check.verifyArray(files, 'expect list of filenames, not ' + JSON.stringify(files)); 13 | 14 | var filenames = files.reduce(function (filenames, shortName) { 15 | var files = glob.sync(shortName); 16 | return filenames.concat(files); 17 | }, []); 18 | 19 | filenames = filenames.map(unary(path.resolve)); 20 | return filenames; 21 | } -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | var check = require('check-types'); 2 | var path = require('path'); 3 | 4 | // assuming correct path separators, removes only folders 5 | module.exports.removeMatchingPrefixes = function (rows) { 6 | check.verifyArray(rows, 'expected array, not', JSON.stringify(rows)); 7 | if (rows.length < 2) { 8 | return rows; 9 | } 10 | var column = 0; 11 | rows.forEach(function (row) { 12 | check.verifyArray(row, 'expected row to be an array', row); 13 | check.verifyString(row[column], 'data in column cell', 14 | row[column], 'should be a string'); 15 | }); 16 | var parts = rows[0][column].split(path.sep); 17 | // console.log('sep', path.sep, 'parts', parts); 18 | if (parts.length < 2) { 19 | return rows; 20 | } 21 | 22 | var commonPrefix = ''; 23 | parts.every(function (part) { 24 | var prefix = commonPrefix + part + path.sep; 25 | var partIsCommon = rows.every(function (row) { 26 | return !row[column].indexOf(prefix); 27 | }); 28 | 29 | if (partIsCommon) { 30 | commonPrefix = prefix; 31 | // console.log('all values start with', commonPrefix); 32 | return true; 33 | } else { 34 | return false; 35 | } 36 | }); 37 | 38 | // console.log('found common prefix', commonPrefix); 39 | if (commonPrefix.length) { 40 | console.log(commonPrefix); 41 | rows = rows.map(function (row) { 42 | row[column] = row[column].substr(commonPrefix.length); 43 | return row; 44 | }); 45 | } 46 | return rows; 47 | }; 48 | 49 | // removes letter by letter without looking at path parts 50 | module.exports.removePrefixLetters = function (rows) { 51 | check.verifyArray(rows, 'expected array, not', JSON.stringify(rows)); 52 | if (rows.length < 2) { 53 | return rows; 54 | } 55 | rows.forEach(function (row) { 56 | check.verifyArray(row, 'expected row to be an array', row); 57 | }); 58 | 59 | var column = 0; 60 | var commonPrefix = ''; 61 | 62 | do { 63 | check.verifyString(rows[0][column], 'data in column cell', 64 | rows[0][column], 'should be a string'); 65 | if (!rows[0][column].length) { 66 | break; 67 | } 68 | var firstLetter = rows[0][column][0]; 69 | 70 | var firstLetterSame = rows.every(function (row) { 71 | return row[column][0] === firstLetter; 72 | }); 73 | if (!firstLetterSame) { 74 | break; 75 | } 76 | commonPrefix += firstLetter; 77 | 78 | rows = rows.map(function (row) { 79 | row[column] = row[column].substr(1); 80 | return row; 81 | }); 82 | } while (true); 83 | if (commonPrefix.length) { 84 | console.log(commonPrefix); 85 | } 86 | return rows; 87 | }; -------------------------------------------------------------------------------- /test/amd/withAmd.js: -------------------------------------------------------------------------------- 1 | define([], function () { 2 | var foo = { 3 | init: function () { 4 | return true; 5 | }, 6 | name: 'something' 7 | }; 8 | 9 | function bar() { 10 | return 'something else'; 11 | } 12 | }); -------------------------------------------------------------------------------- /test/amd/withoutAmd.js: -------------------------------------------------------------------------------- 1 | var foo = { 2 | init: function () { 3 | return true; 4 | }, 5 | name: 'something' 6 | }; 7 | 8 | function bar() { 9 | return 'something else'; 10 | } 11 | -------------------------------------------------------------------------------- /test/complexityExample.json: -------------------------------------------------------------------------------- 1 | { 2 | "aggregate": { 3 | "line": 1, 4 | "complexity": { 5 | "sloc": { 6 | "physical": 107, 7 | "logical": 72 8 | }, 9 | "cyclomatic": 9, 10 | "halstead": { 11 | "operators": { 12 | "distinct": 20, 13 | "total": 178, 14 | "identifiers": [ 15 | "var", 16 | "=", 17 | "()", 18 | "function", 19 | ".", 20 | "{}", 21 | ":", 22 | "catch", 23 | "if", 24 | "return", 25 | "!==", 26 | "||", 27 | "[]", 28 | ">", 29 | "else", 30 | "<", 31 | "- (postfix)", 32 | "+", 33 | "&&", 34 | ">=" 35 | ] 36 | }, 37 | "operands": { 38 | "distinct": 83, 39 | "total": 222, 40 | "identifiers": [ 41 | "fs", 42 | "\"fs\"", 43 | "require", 44 | "path", 45 | "\"path\"", 46 | "cr", 47 | "\"complexity-report\"", 48 | "check", 49 | "\"check-types\"", 50 | "getSourceComplexity", 51 | "source", 52 | "filename", 53 | "\"missing source\"", 54 | "verifyString", 55 | "report", 56 | "", 57 | "aggregate", 58 | "complexity", 59 | "sloc", 60 | "logical", 61 | 0, 62 | "cyclomatic", 63 | "halstead", 64 | "difficulty", 65 | "run", 66 | "error", 67 | {}, 68 | "test", 69 | "lines", 70 | "\"\n\"", 71 | "split", 72 | "line", 73 | "\"#\"", 74 | "filter", 75 | "join", 76 | "computeMetrics", 77 | "filenames", 78 | "options", 79 | "sort", 80 | 1, 81 | "complexityMetrics", 82 | "\"utf-8\"", 83 | "readFileSync", 84 | "\"could not get source from file\"", 85 | "\"could not get complexity for\"", 86 | "verifyObject", 87 | "name", 88 | "push", 89 | "forEach", 90 | "header", 91 | "\"File\"", 92 | "\"LOC\"", 93 | "\"Cyclomatic\"", 94 | "\"Halstead difficulty\"", 95 | "metrics", 96 | "metric", 97 | "toFixed", 98 | "console", 99 | "length", 100 | "sortingColumn", 101 | "reverseSort", 102 | false, 103 | "comparison", 104 | "a", 105 | "b", 106 | "first", 107 | "second", 108 | {}, 109 | "substr", 110 | "Number", 111 | true, 112 | "\"invalid sorting column \"", 113 | "verifyNumber", 114 | "maxColumn", 115 | "\"invalid sorting column\"", 116 | "assert", 117 | "\"sorting metrics by column\"", 118 | "log", 119 | "debug", 120 | "reverse", 121 | "concat", 122 | "module", 123 | "exports" 124 | ] 125 | }, 126 | "length": 400, 127 | "vocabulary": 103, 128 | "difficulty": 26.746987951807228, 129 | "volume": 2674.6002108732873, 130 | "effort": 71537.49961612889, 131 | "bugs": 0.8915334036244291, 132 | "time": 3974.305534229383 133 | } 134 | } 135 | }, 136 | "functions": [ 137 | { 138 | "name": "getSourceComplexity", 139 | "line": 6, 140 | "complexity": { 141 | "sloc": { 142 | "physical": 31, 143 | "logical": 18 144 | }, 145 | "cyclomatic": 2, 146 | "halstead": { 147 | "operators": { 148 | "distinct": 10, 149 | "total": 38, 150 | "identifiers": [ 151 | "()", 152 | ".", 153 | "var", 154 | "=", 155 | "{}", 156 | ":", 157 | "catch", 158 | "if", 159 | "function", 160 | "return" 161 | ] 162 | }, 163 | "operands": { 164 | "distinct": 25, 165 | "total": 47, 166 | "identifiers": [ 167 | "source", 168 | "filename", 169 | "\"missing source\"", 170 | "check", 171 | "verifyString", 172 | "report", 173 | "", 174 | "aggregate", 175 | "complexity", 176 | "sloc", 177 | "logical", 178 | 0, 179 | "cyclomatic", 180 | "halstead", 181 | "difficulty", 182 | "cr", 183 | "run", 184 | "error", 185 | {}, 186 | "test", 187 | "lines", 188 | "\"\n\"", 189 | "split", 190 | "filter", 191 | "join" 192 | ] 193 | }, 194 | "length": 85, 195 | "vocabulary": 35, 196 | "difficulty": 9.399999999999999, 197 | "volume": 435.98905644032214, 198 | "effort": 4098.297130539027, 199 | "bugs": 0.14532968548010738, 200 | "time": 227.68317391883485 201 | } 202 | } 203 | }, 204 | { 205 | "name": "", 206 | "line": 28, 207 | "complexity": { 208 | "sloc": { 209 | "physical": 3, 210 | "logical": 1 211 | }, 212 | "cyclomatic": 1, 213 | "halstead": { 214 | "operators": { 215 | "distinct": 3, 216 | "total": 3, 217 | "identifiers": [ 218 | "return", 219 | "!==", 220 | "." 221 | ] 222 | }, 223 | "operands": { 224 | "distinct": 3, 225 | "total": 4, 226 | "identifiers": [ 227 | "line", 228 | 0, 229 | "\"#\"" 230 | ] 231 | }, 232 | "length": 7, 233 | "vocabulary": 6, 234 | "difficulty": 2, 235 | "volume": 18.094737505048094, 236 | "effort": 36.18947501009619, 237 | "bugs": 0.006031579168349364, 238 | "time": 2.0105263894497885 239 | } 240 | } 241 | }, 242 | { 243 | "name": "computeMetrics", 244 | "line": 39, 245 | "complexity": { 246 | "sloc": { 247 | "physical": 64, 248 | "logical": 24 249 | }, 250 | "cyclomatic": 6, 251 | "halstead": { 252 | "operators": { 253 | "distinct": 15, 254 | "total": 65, 255 | "identifiers": [ 256 | "=", 257 | "||", 258 | "{}", 259 | ".", 260 | "var", 261 | "[]", 262 | "()", 263 | "function", 264 | "if", 265 | ">", 266 | "+", 267 | "&&", 268 | ">=", 269 | "<", 270 | "return" 271 | ] 272 | }, 273 | "operands": { 274 | "distinct": 38, 275 | "total": 79, 276 | "identifiers": [ 277 | "filenames", 278 | "options", 279 | "", 280 | "sort", 281 | 1, 282 | "source", 283 | "report", 284 | "complexityMetrics", 285 | "forEach", 286 | "header", 287 | "\"File\"", 288 | "\"LOC\"", 289 | "\"Cyclomatic\"", 290 | "\"Halstead difficulty\"", 291 | "metrics", 292 | "length", 293 | 0, 294 | "sortingColumn", 295 | "reverseSort", 296 | false, 297 | "comparison", 298 | {}, 299 | "test", 300 | "substr", 301 | "Number", 302 | true, 303 | "\"invalid sorting column \"", 304 | "check", 305 | "verifyNumber", 306 | "maxColumn", 307 | "\"invalid sorting column\"", 308 | "console", 309 | "assert", 310 | "\"sorting metrics by column\"", 311 | "log", 312 | "debug", 313 | "reverse", 314 | "concat" 315 | ] 316 | }, 317 | "length": 144, 318 | "vocabulary": 53, 319 | "difficulty": 15.592105263157896, 320 | "volume": 824.8205454571007, 321 | "effort": 12860.688767982427, 322 | "bugs": 0.27494018181903357, 323 | "time": 714.482709332357 324 | } 325 | } 326 | }, 327 | { 328 | "name": "", 329 | "line": 45, 330 | "complexity": { 331 | "sloc": { 332 | "physical": 10, 333 | "logical": 7 334 | }, 335 | "cyclomatic": 1, 336 | "halstead": { 337 | "operators": { 338 | "distinct": 5, 339 | "total": 14, 340 | "identifiers": [ 341 | "=", 342 | "()", 343 | ".", 344 | "{}", 345 | ":" 346 | ] 347 | }, 348 | "operands": { 349 | "distinct": 17, 350 | "total": 27, 351 | "identifiers": [ 352 | "filename", 353 | "source", 354 | "\"utf-8\"", 355 | "fs", 356 | "readFileSync", 357 | "\"could not get source from file\"", 358 | "check", 359 | "verifyString", 360 | "report", 361 | "getSourceComplexity", 362 | "\"could not get complexity for\"", 363 | "verifyObject", 364 | "", 365 | "name", 366 | "complexity", 367 | "complexityMetrics", 368 | "push" 369 | ] 370 | }, 371 | "length": 41, 372 | "vocabulary": 22, 373 | "difficulty": 3.9705882352941173, 374 | "volume": 182.83669636412918, 375 | "effort": 725.969235563454, 376 | "bugs": 0.06094556545470973, 377 | "time": 40.33162419796967 378 | } 379 | } 380 | }, 381 | { 382 | "name": "", 383 | "line": 59, 384 | "complexity": { 385 | "sloc": { 386 | "physical": 13, 387 | "logical": 4 388 | }, 389 | "cyclomatic": 1, 390 | "halstead": { 391 | "operators": { 392 | "distinct": 4, 393 | "total": 23, 394 | "identifiers": [ 395 | "()", 396 | "[]", 397 | ".", 398 | "catch" 399 | ] 400 | }, 401 | "operands": { 402 | "distinct": 16, 403 | "total": 29, 404 | "identifiers": [ 405 | "metric", 406 | "", 407 | "name", 408 | "complexity", 409 | "aggregate", 410 | "sloc", 411 | "logical", 412 | "cyclomatic", 413 | 0, 414 | "halstead", 415 | "difficulty", 416 | "toFixed", 417 | "metrics", 418 | "push", 419 | "error", 420 | "console" 421 | ] 422 | }, 423 | "length": 52, 424 | "vocabulary": 20, 425 | "difficulty": 3.625, 426 | "volume": 224.74026093414287, 427 | "effort": 814.6834458862679, 428 | "bugs": 0.07491342031138096, 429 | "time": 45.26019143812599 430 | } 431 | } 432 | }, 433 | { 434 | "name": "comparison", 435 | "line": 76, 436 | "complexity": { 437 | "sloc": { 438 | "physical": 11, 439 | "logical": 9 440 | }, 441 | "cyclomatic": 3, 442 | "halstead": { 443 | "operators": { 444 | "distinct": 9, 445 | "total": 16, 446 | "identifiers": [ 447 | "var", 448 | "=", 449 | ".", 450 | "if", 451 | "else", 452 | "<", 453 | "return", 454 | "- (postfix)", 455 | ">" 456 | ] 457 | }, 458 | "operands": { 459 | "distinct": 7, 460 | "total": 15, 461 | "identifiers": [ 462 | "a", 463 | "b", 464 | "first", 465 | "sortingColumn", 466 | "second", 467 | 1, 468 | 0 469 | ] 470 | }, 471 | "length": 31, 472 | "vocabulary": 16, 473 | "difficulty": 9.642857142857142, 474 | "volume": 124, 475 | "effort": 1195.7142857142856, 476 | "bugs": 0.04133333333333333, 477 | "time": 66.42857142857142 478 | } 479 | } 480 | } 481 | ], 482 | "maintainability": 105.01694942315783 483 | } -------------------------------------------------------------------------------- /test/complexityTest.js: -------------------------------------------------------------------------------- 1 | var complexity = require('./complexityExample'); 2 | var Complexity = require('../src/Complexity'); 3 | 4 | gt.module('complexity as object'); 5 | 6 | gt.test('loaded json', function() { 7 | gt.defined(complexity, 'loaded complexity'); 8 | gt.defined(complexity.aggregate, 'aggregate property'); 9 | gt.number(complexity.maintainability, 'maintainability'); 10 | gt.ok(complexity.maintainability > 0, 'maintainability is positive'); 11 | }); 12 | 13 | gt.test('make object', function() { 14 | gt.func(Complexity, 'Complexity is a function'); 15 | var c = new Complexity(complexity); 16 | gt.object(c, 'created Complexity object'); 17 | // console.log(c); 18 | gt.ok(c instanceof Complexity, 'correct constructor'); 19 | gt.ok(c.maintainability > 0, 'correct property'); 20 | gt.func(c.getMaintainability, 'has function'); 21 | gt.equal(c.getMaintainability(), c.maintainability, 'returns correct value'); 22 | }); 23 | 24 | gt.test(function basicInfo() { 25 | var c = new Complexity(complexity); 26 | gt.func(c.basicInfo, 'basicInfo is a function'); 27 | var str = c.basicInfo(); 28 | console.log(str); 29 | gt.string(str, 'basic info returns string'); 30 | }); -------------------------------------------------------------------------------- /test/e2e.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | gt.module('end 2 end tests'); 4 | 5 | gt.test('external use', function () { 6 | var complexity = require('../js-complexity-viz'); 7 | gt.func(complexity.run, 'has run function'); 8 | 9 | var results = complexity.run({ 10 | path: [path.join(__dirname, 'e2e.js')] 11 | }); 12 | gt.array(results, 'returned results'); 13 | gt.equal(results.length, 1, 'there should be info about 1 file'); 14 | }); 15 | 16 | gt.test('external use, single file', function () { 17 | var complexity = require('../js-complexity-viz'); 18 | gt.func(complexity.run, 'has run function'); 19 | 20 | var results = complexity.run({ 21 | path: path.join(__dirname, 'e2e.js') 22 | }); 23 | gt.array(results, 'returned results'); 24 | gt.equal(results.length, 1, 'there should be info about 1 file'); 25 | }); -------------------------------------------------------------------------------- /test/example_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | "File", 4 | "LOC", 5 | "Maintainability" 6 | ], 7 | [ 8 | "js-complexity-viz.js", 9 | 78, 10 | 122 11 | ], 12 | [ 13 | "test\\filenames.js", 14 | 19, 15 | 135 16 | ] 17 | ] -------------------------------------------------------------------------------- /test/example_report.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JS Complexity 6 | 7 | 8 | 9 | 12 | 53 | 54 | 55 |
56 | Usage: open in the browser this document, pass report.json using ?report=path.json 57 | 58 | -------------------------------------------------------------------------------- /test/external.js: -------------------------------------------------------------------------------- 1 | var jsc = require('..'); 2 | 3 | var results = jsc.run({ 4 | path: './external.js' 5 | }); 6 | 7 | console.log('results', results); -------------------------------------------------------------------------------- /test/filenames.js: -------------------------------------------------------------------------------- 1 | gt.module('filename matching'); 2 | 3 | var js = /\.js$/i; 4 | 5 | gt.test("detect js file", function () { 6 | gt.ok("foo.js".match(js), "foo.js is js file"); 7 | gt.ok("boo.js".match(js), "boo.js is js file"); 8 | gt.ok("js.js".match(js), "js.js is js file"); 9 | }); 10 | 11 | gt.test("detect JS file", function () { 12 | gt.ok("foo.JS".match(js), "foo.JS is js file"); 13 | gt.ok("boo.JS".match(js), "boo.JS is js file"); 14 | gt.ok("js.JS".match(js), "js.JS is js file"); 15 | }); 16 | 17 | gt.test("not JS file", function () { 18 | gt.ok(!"JS".match(js), "JS is NOT js file"); 19 | gt.ok(!"js".match(js), "js is NOT js file"); 20 | gt.ok(!"js.foo".match(js), "js.foo is NOT js file"); 21 | }); 22 | 23 | gt.test("string comparison", function () { 24 | var a = 'c:\git\js-complexity-viz\test\filenames.js'; 25 | var b = 'c:\git\js-complexity-viz\test\filenames.js'; 26 | gt.equal(a, b, "same names"); 27 | gt.ok(a === b, "same names using ==="); 28 | gt.ok(a == b, "same names using =="); 29 | }); -------------------------------------------------------------------------------- /test/prefix.js: -------------------------------------------------------------------------------- 1 | var remove = require('../src/utils').removeMatchingPrefixes; 2 | gt.module('filename paths prefix'); 3 | 4 | gt.test('empty and short list', function (){ 5 | gt.func(remove, 'remove is a function'); 6 | gt.aequal(remove([]), [], 'returns empty list'); 7 | gt.aequal(remove(['foo']), ['foo'], 'returns same list'); 8 | }); 9 | 10 | gt.test('remove nothing', function () { 11 | var list = [['foo'], ['bar'], ['zoo']]; 12 | gt.aequal(remove(list), list, 'returns original list'); 13 | }); 14 | 15 | gt.test('removes nothing despite common', function () { 16 | var list = [['foo\\foo'], ['foobar'], ['foozoo']]; 17 | var correct = [['foo\\foo'], ['foobar'], ['foozoo']]; 18 | gt.aequal(remove(list), correct, 'returns same list'); 19 | }); 20 | 21 | gt.test('removes first folder', function () { 22 | var list = [['foo\\foo'], ['foo\\bar']]; 23 | var correct = [['foo'], ['bar']]; 24 | gt.aequal(remove(list), correct, 'returns list'); 25 | }); 26 | 27 | gt.test('removes two folders', function () { 28 | var list = [['foo\\bar\\foo'], ['foo\\bar\\zoo']]; 29 | var correct = [['foo'], ['zoo']]; 30 | gt.aequal(remove(list), correct, 'returns list'); 31 | }); 32 | 33 | gt.test('folder and file are different', function () { 34 | var list = [['foo\\'], ['foo']]; 35 | var correct = [['foo\\'], ['foo']]; 36 | gt.aequal(remove(list), correct, 'returns list'); 37 | }); 38 | 39 | gt.test('cannot remove because not a folder', function () { 40 | var list = [['foo\\bar'], ['foo']]; 41 | var correct = [['foo\\bar'], ['foo']]; 42 | gt.aequal(remove(list), correct, 'returns list'); 43 | }); --------------------------------------------------------------------------------