diff --git a/bin/fixme b/bin/fixme index f60c1c9..306f2f4 100755 --- a/bin/fixme +++ b/bin/fixme @@ -1,7 +1,7 @@ #!/usr/bin/env node var fs = require('fs'); -var FixMe = require('../lib/fix-me'); +var FixMe = require('../lib/fixme'); var config; fs.readFile('/config.json', function(err, data) { @@ -9,5 +9,6 @@ fs.readFile('/config.json', function(err, data) { config = JSON.parse(data); } - new FixMe().run(config) + var fixer = new FixMe(); + fixer.run(config); }); diff --git a/lib/fix-me.js b/lib/fix-me.js deleted file mode 100644 index 2a06012..0000000 --- a/lib/fix-me.js +++ /dev/null @@ -1,88 +0,0 @@ -var readline = require('readline'); -var spawn = require('child_process').spawn; -var fs = require('fs'); - -var DEFAULT_PATHS = ['./']; -var DEFAULT_STRINGS = ['BUG', 'FIXME', 'HACK', 'TODO', 'XXX']; -var GREP_OPTIONS = [ - '--binary-files=without-match', - '--extended-regexp', - '--line-number', - '--only-matching', - '--recursive', - '--with-filename', - '--word-regexp', -]; - -function FixMe(writable) { - this.output = writable || process.stdout; -} - -FixMe.prototype.run = function(engineConfig) { - var paths, strings; - - if (engineConfig) { - paths = engineConfig.include_paths; - } else { - paths = DEFAULT_PATHS; - } - - if (engineConfig && engineConfig.config && engineConfig.config.strings) { - strings = engineConfig.config.strings; - } else { - strings = DEFAULT_STRINGS; - } - - this.find(paths, strings); -}; - -var isItsOwnConfigFile = function(path) { - return path.indexOf(".codeclimate.yml") !== -1; -}; - -var isAYamlComment = function(path, lineNumber) { - var lines = fs.readFileSync(path, "utf8").split("\n"); - var line = lines[lineNumber - 1] || ""; - return line.match(/^\s*#/); -}; - -FixMe.prototype.find = function(paths, strings, callback) { - var pattern = `(${strings.join('|')})`; - var grep = spawn('grep', [...GREP_OPTIONS, pattern, ...paths]); - - readline.createInterface({ input: grep.stdout }).on('line', (line) => { - var parts = line.split(':'); - var path = parts[0].replace(/^\/code\//, ''); - var lineNumber = parseInt(parts[1], 10); - var matchedString = parts[2]; - - if (!path || !lineNumber || !matchedString) { - process.stderr.write("Ignoring malformed output: " + line + "\n"); - return; - } - - if(isItsOwnConfigFile(path) && !isAYamlComment(path, lineNumber)) { return; } - - var issue = { - 'categories': ['Bug Risk'], - 'check_name': matchedString, - 'description': `${matchedString} found`, - 'location': { - 'lines': { - 'begin': lineNumber, - 'end': lineNumber, - }, - 'path': path, - }, - 'type': 'issue', - }; - - this.output.write(JSON.stringify(issue) + '\0'); - }); - - if (callback) { - grep.stdout.on('close', _ => callback()); - } -}; - -module.exports = FixMe; diff --git a/lib/fixme.js b/lib/fixme.js new file mode 100644 index 0000000..4d82287 --- /dev/null +++ b/lib/fixme.js @@ -0,0 +1,103 @@ +const readline = require('readline'); +const { spawn } = require('child_process'); +const fs = require('fs'); + +const DEFAULT_PATHS = ['./']; +const DEFAULT_STRINGS = ['BUG', 'FIXME', 'HACK', 'TODO', 'XXX']; +const GREP_OPTIONS = [ + '--binary-files=without-match', + '--extended-regexp', + '--line-number', + '--only-matching', + '--recursive', + '--with-filename', + '--word-regexp', +]; + +class FixMe { + constructor(writable) { + this.output = writable || process.stdout; + this.maxPathLength = 4; // initial length of "Path" + this.maxLineLength = 4; // initial length of "Line" + this.maxTypeLength = 4; // initial length of "Type" + this.issues = []; + } + + run(engineConfig) { + const outputPathType = process.argv.includes('--json') + ? 'json' + : process.argv.includes('--table') + ? 'table' + : 'default'; + + if (outputPathType === 'default' || process.argv.includes('--help')) { + console.log('Usage: fixme [OPTIONS] [PATH]\n\nOptions:\n --json\tOutput results in JSON format.\n --table\tOutput results in table format.\n --help\tShow help.'); + return; + } + + let paths = DEFAULT_PATHS; + if (engineConfig && engineConfig.include_paths) { + paths = engineConfig.include_paths; + } else if (process.argv.length > 3) { + paths = process.argv.slice(3); + } + + const strings = (engineConfig && engineConfig.config && engineConfig.config.strings) || DEFAULT_STRINGS; + + this.find(paths, strings, outputPathType); + } + + find(paths, strings, outputPathType, callback) { + const pattern = `(${strings.join('|')})`; + const grep = spawn('grep', [...GREP_OPTIONS, pattern, ...paths]); + + readline.createInterface({ input: grep.stdout }).on('line', (line) => { + const [fullPath, lineStr, matchedString] = line.split(':'); + const path = fullPath.replace(/^\/code\//, ''); + const lineNumber = parseInt(lineStr, 10); + + if (!path || !lineNumber || !matchedString) { + process.stderr.write(`Ignoring malformed output: ${line}\n`); + return; + } + + // Update the maximum widths for each column for better formatting + this.maxPathLength = Math.max(this.maxPathLength, path.length); + this.maxLineLength = Math.max(this.maxLineLength, `${lineNumber}`.length); + this.maxTypeLength = Math.max(this.maxTypeLength, matchedString.length); + + const issue = { + 'categories': ['Bug Risk'], + 'check_name': matchedString, + 'description': `${matchedString} found`, + 'location': { + 'lines': { + 'begin': lineNumber, + 'end': lineNumber, + }, + 'path': path, + }, + 'type': 'issue', + }; + + this.issues.push(issue); + }); + + grep.stdout.on('close', () => { + if (outputPathType === 'json') { + this.output.write(JSON.stringify(this.issues)); + } else if (outputPathType === 'table') { + // Now that we've gathered all issues, print headers with appropriate padding + console.log(`| ${'Path'.padEnd(this.maxPathLength, ' ')} | ${'Line'.padEnd(this.maxLineLength, ' ')} | ${'Type'.padEnd(this.maxTypeLength, ' ')} |`); + console.log(`| ${'-'.repeat(this.maxPathLength)} | ${'-'.repeat(this.maxLineLength)} | ${'-'.repeat(this.maxTypeLength)} |`); + + for (const issue of this.issues) { + console.log(`| ${issue.location.path.padEnd(this.maxPathLength, ' ')} | ${issue.location.lines.begin.toString().padEnd(this.maxLineLength, ' ')} | ${issue.check_name.padEnd(this.maxTypeLength, ' ')} |`); + } + } + if (callback) callback(); + }); + } +} + +module.exports = FixMe; diff --git a/package.json b/package.json index 62dbccd..90f4b95 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,10 @@ "name": "codeclimate-fixme", "description": "Static analysis tool that finds FIXME, TODO, BUG, etc. comments in your code.", "version": "0.0.1", - "main": "./lib/fix-me.js", + "main": "./lib/fixme.js", + "bin": { + "fixme": "./bin/fixme" + }, "devDependencies": { "chai": "3.4.1", "mocha": "2.3.3" diff --git a/test/fix-me.js b/test/fixme.js similarity index 98% rename from test/fix-me.js rename to test/fixme.js index 3340c85..f348c58 100644 --- a/test/fix-me.js +++ b/test/fixme.js @@ -1,8 +1,8 @@ /* global define, it, describe, context */ var expect = require('chai').expect; -var FixMe = require('../lib/fix-me.js'); -var IssueBuffer = require('./support/issue_buffer'); +var FixMe = require('../lib/fixme.js'); +var IssueBuffer = require('./support/issue_buffer.js'); describe("fixMe", function(){ describe("#run(engineConfig)", function() {