├── .gitignore ├── .rvmrc ├── icons ├── more.png ├── clean.png └── error.png ├── lib ├── autolint.js ├── linter.js ├── pluralize.js ├── jshint-linter.js ├── blank-config-file.js ├── watch-for-changes.js ├── on-interrupt.js ├── cli-confirm.js ├── print.js ├── lint-once.js ├── new-file-reporter.js ├── lint-reporter.js ├── checked-file.js ├── ansi.js ├── jslint-linter.js ├── start-autolint.js ├── configuration.js ├── clean-reporter.js ├── summary-reporter.js ├── growl-reporter.js ├── lint-scanner.js ├── repository.js └── default-configuration.js ├── buster.js ├── test ├── pluralize-test.js ├── watch-for-changes-test.js ├── lint-reporter-test.js ├── print-test.js ├── new-file-reporter-test.js ├── configuration-test.js ├── clean-reporter-test.js ├── checked-file-test.js ├── growl-reporter-test.js ├── summary-reporter-test.js ├── jslint-linter-test.js ├── lint-scanner-test.js └── repository-test.js ├── autolint-config.js ├── watch-tests.watchr ├── todo.org ├── changelog.md ├── bin └── autolint ├── package.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm use 1.9.2@autotest --create -------------------------------------------------------------------------------- /icons/more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/autolint/master/icons/more.png -------------------------------------------------------------------------------- /icons/clean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/autolint/master/icons/clean.png -------------------------------------------------------------------------------- /icons/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/autolint/master/icons/error.png -------------------------------------------------------------------------------- /lib/autolint.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | configuration: require("./configuration"), 3 | linter: require("./linter") 4 | }; 5 | -------------------------------------------------------------------------------- /lib/linter.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | create: function (conf) { 3 | return require('./' + conf.linter + '-linter').create(conf.linterOptions); 4 | } 5 | }; -------------------------------------------------------------------------------- /lib/pluralize.js: -------------------------------------------------------------------------------- 1 | function pluralize(num, name) { 2 | if (num !== 1) { 3 | name = name + 's'; 4 | } 5 | return num + " " + name; 6 | } 7 | 8 | module.exports = pluralize; -------------------------------------------------------------------------------- /buster.js: -------------------------------------------------------------------------------- 1 | var config = module.exports; 2 | 3 | config["Node tests"] = { 4 | environment: "node", 5 | tests: ["test/*.js"], 6 | extensions: [require("buster-lint")], 7 | "buster-lint": require("./autolint-config") 8 | }; -------------------------------------------------------------------------------- /lib/jshint-linter.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./jslint-linter'); 2 | module.exports.create = function (options) { 3 | if (!options) { throw new TypeError('options is required (at least an empty object)'); } 4 | return Object.create(this, { 5 | options: { value: options }, 6 | linter: { value: { check: require('../vendor/jshint').JSHINT } } 7 | }); 8 | }; -------------------------------------------------------------------------------- /test/pluralize-test.js: -------------------------------------------------------------------------------- 1 | var buster = require('buster'); 2 | var pluralize = require('../lib/pluralize'); 3 | 4 | buster.testCase("pluralize", { 5 | "should not pluralize one item": function () { 6 | assert.equals(pluralize(1, "item"), "1 item"); 7 | }, 8 | 9 | "should pluralize several items": function () { 10 | assert.equals(pluralize(2, "item"), "2 items"); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /autolint-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "paths": [ 3 | "test/*.js", 4 | "lib/*.js" 5 | ], 6 | "linterOptions": { 7 | "node": true, 8 | "indent": 2, 9 | "maxlen": 120, 10 | "vars": true, 11 | "nomen": true, 12 | "sloppy": true, 13 | "predef": [ 14 | "assert", 15 | "refute" 16 | ] 17 | }, 18 | "excludes": [ 19 | "ansi.js" 20 | ] 21 | }; 22 | -------------------------------------------------------------------------------- /lib/blank-config-file.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | paths: [ "./**/*.js" ], // a list of paths to the files you want linted 3 | linter: "jslint", // optionally: jshint 4 | linterOptions: { // see default-configuration.js for a list of all options 5 | predef: [] // a list of known global variables 6 | }, 7 | excludes: [] // a list of strings/regexes matching filenames that should not be linted 8 | }; 9 | -------------------------------------------------------------------------------- /lib/watch-for-changes.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | 3 | function lintOnChange(linter, file) { 4 | var watcher = fs.watch(file, function (event) { 5 | linter.checkFile(file); 6 | if (event === "rename") { 7 | watcher.close(); 8 | lintOnChange(linter, file); 9 | } 10 | }); 11 | } 12 | 13 | module.exports = function (repository, linter) { 14 | repository.on('newFile', function (file) { 15 | lintOnChange(linter, file.name); 16 | }); 17 | }; -------------------------------------------------------------------------------- /lib/on-interrupt.js: -------------------------------------------------------------------------------- 1 | module.exports = function (message, callback) { 2 | var interrupted = false; 3 | var signal = process.platform === "win32" ? "exit" : "SIGINT"; 4 | process.on(signal, function () { 5 | if (interrupted) { 6 | process.exit(); 7 | } 8 | console.log(message + " Interrupt a second time to quit"); 9 | interrupted = true; 10 | setTimeout(function () { 11 | interrupted = false; 12 | callback(); 13 | }, 1500); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /lib/cli-confirm.js: -------------------------------------------------------------------------------- 1 | function prompt(q, callback) { 2 | var rl = require("readline"); 3 | var i = rl.createInterface(process.stdin, process.stdout, null); 4 | i.question(q, function (a) { 5 | i.close(); 6 | process.stdin.destroy(); 7 | callback(a); 8 | }); 9 | } 10 | 11 | function confirmation(answer) { 12 | return answer.trim().substring(0, 1).toLowerCase() === "y"; 13 | } 14 | 15 | module.exports = function (q, callback) { 16 | prompt(q + " [y/N]: ", function (a) { 17 | if (confirmation(a)) { 18 | callback(); 19 | } 20 | }); 21 | }; -------------------------------------------------------------------------------- /lib/print.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var _ = require('underscore'); 3 | var ansi = require('./ansi'); 4 | 5 | function putStringWithUtil(text) { 6 | util.puts(text); 7 | } 8 | 9 | function print(args, colorFunc) { 10 | _.flatten(args).map(colorFunc.bind(ansi)).forEach(putStringWithUtil); 11 | } 12 | 13 | function nop(str) { 14 | return str; 15 | } 16 | 17 | function black() { 18 | print(arguments, nop); 19 | } 20 | 21 | function red() { 22 | print(arguments, ansi.RED); 23 | } 24 | 25 | function green() { 26 | print(arguments, ansi.GREEN); 27 | } 28 | 29 | exports.black = black; 30 | exports.red = red; 31 | exports.green = green; 32 | -------------------------------------------------------------------------------- /lib/lint-once.js: -------------------------------------------------------------------------------- 1 | var glob = require('glob'); 2 | 3 | module.exports = function (conf) { 4 | var linter = require('./linter').create(conf); 5 | 6 | var lintScanner = require('./lint-scanner').create(linter, glob, conf.excludes); 7 | var repository = require('./repository').create(linter); 8 | 9 | var lintReporter = require('./lint-reporter').create(repository); 10 | var summaryReporter = require('./summary-reporter').create(repository); 11 | 12 | repository.listen(); 13 | lintReporter.listen(); 14 | 15 | lintScanner.scan(conf.paths).then(function () { 16 | summaryReporter.print(); 17 | if (summaryReporter.numDirtyFiles() > 0) { 18 | process.exit(-1); 19 | } 20 | }); 21 | 22 | }; -------------------------------------------------------------------------------- /lib/new-file-reporter.js: -------------------------------------------------------------------------------- 1 | var print = require('./print'); 2 | 3 | function create(repository) { 4 | return Object.create(this, { 5 | repository: { value: repository } 6 | }); 7 | } 8 | 9 | function listen() { 10 | this.repository.on('newFile', this.handleNewFile.bind(this)); 11 | } 12 | 13 | function handleNewFile(file) { 14 | if (file.errors.length > 0) { 15 | print.red('Found ' + file.name + ' - ' + file.errorDescription()); 16 | } else if (this.verbose) { 17 | print.black('Found ' + file.name + ' - clean'); 18 | } 19 | } 20 | 21 | function beVerbose() { 22 | this.verbose = true; 23 | } 24 | 25 | module.exports = { 26 | create: create, 27 | listen: listen, 28 | handleNewFile: handleNewFile, 29 | beVerbose: beVerbose 30 | }; -------------------------------------------------------------------------------- /lib/lint-reporter.js: -------------------------------------------------------------------------------- 1 | var print = require('./print'); 2 | 3 | function create(repository) { 4 | return Object.create(this, { 5 | repository: { value: repository } 6 | }); 7 | } 8 | 9 | function printHeader(file) { 10 | print.red('', 'Lint in ' + file.name + ', ' + file.errorDescription() + ':'); 11 | } 12 | 13 | function printError(error) { 14 | if (error) { 15 | print.black(' line ' + error.line + ' char ' + error.character + ': ' + error.reason); 16 | } else { 17 | print.black(' and more ...'); 18 | } 19 | } 20 | 21 | function handleLint(file) { 22 | printHeader(file); 23 | file.errors.forEach(printError); 24 | } 25 | 26 | function listen() { 27 | this.repository.on('dirty', handleLint.bind(this)); 28 | } 29 | 30 | exports.create = create; 31 | exports.listen = listen; 32 | -------------------------------------------------------------------------------- /watch-tests.watchr: -------------------------------------------------------------------------------- 1 | ENV["WATCHR"] = "1" 2 | system 'clear' 3 | 4 | def run(cmd) 5 | `#{cmd}` 6 | end 7 | 8 | def run_all_tests 9 | system('clear') 10 | result = run "buster test -F warning" 11 | puts result 12 | end 13 | 14 | run_all_tests 15 | watch('.*.js') { run_all_tests } 16 | 17 | # Ctrl-\ 18 | Signal.trap 'QUIT' do 19 | puts " --- Running all tests ---\n\n" 20 | run_all_tests 21 | end 22 | 23 | @interrupted = false 24 | 25 | # Ctrl-C 26 | Signal.trap 'INT' do 27 | if @interrupted then 28 | @wants_to_quit = true 29 | abort("\n") 30 | else 31 | puts "Interrupt a second time to quit" 32 | @interrupted = true 33 | Kernel.sleep 1.5 34 | # raise Interrupt, nil # let the run loop catch it 35 | run_all_tests 36 | @interrupted = false 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /todo.org: -------------------------------------------------------------------------------- 1 | * Todo [4/10] 2 | ** DONE Avoid errors because of too many open files 3 | ** TODO Dont bundle jslint or jshint, but fetch them with npm 4 | - requires some loading magic for jslint, since Crockford refuses 5 | to make jslint node-friendly. 6 | ** TODO Look for changes in directories using some watch lib 7 | ** TODO Create plugin for js2-mode.el that sets global-externs 8 | ** DONE Suggest random new files to clean 9 | ** DONE Use when.js instead of buster-promise 10 | ** TODO Being blamed for introducing an error, AND praised for fixing it, 11 | when changing around whitespace (changes line numbers) 12 | ** TODO Keep getting "nice cleanup!" in a file with too many errors 13 | ** DONE Files with more than 50 errors are reported as having 52 errors. 14 | ** TODO Switching git branches can be spammy, any way around that? 15 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | Changes from 0.1.5 to 1.0.0 2 | --------------------------- 3 | 4 | Autolint now uses [semantic versioning](http://semver.org). 5 | 6 | * The configuration file is no longer a `json`-file, but a proper node 7 | module. Add `module.exports =` to the start of the file and rename to 8 | `autolint.js` to upgrade. 9 | * Autolint no longer runs without a config file. Running it without one will 10 | prompt you for a default config file to be created. 11 | * Passing `--once` to autolint makes it not-so-auto. Instead it is run once, 12 | exiting with a `-1` error code if any lint is found. This makes it well 13 | suited for pre-commit-hooks and the like. 14 | * Updated bundled versions of jslint and jshint - these have been significantly 15 | changed since last, so your configuration file will certainly need an upgrade 16 | too. 17 | -------------------------------------------------------------------------------- /bin/autolint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var lintOnce = process.argv[2] === "--once"; 4 | var lint = lintOnce ? require('../lib/lint-once') : require('../lib/start-autolint'); 5 | 6 | var configuration = require('../lib/configuration'); 7 | var cliConfirm = require("../lib/cli-confirm"); 8 | var util = require('util'); 9 | 10 | if (configuration.exists()) { 11 | lint(configuration.load()); 12 | } else { 13 | util.puts("No configuration file (autolint.js) found in this directory."); 14 | util.puts("Autolint can create a default config file for you."); 15 | cliConfirm("Create default autolint.js in current directory?", function () { 16 | configuration.createDefaultConfigFile(); 17 | util.puts(""); 18 | util.puts("File autolint.js created."); 19 | util.puts("Now open it up in your favorite editor and fix it up a little."); 20 | util.puts("Run autolint again to start linting."); 21 | }); 22 | } -------------------------------------------------------------------------------- /lib/checked-file.js: -------------------------------------------------------------------------------- 1 | var pluralize = require('./pluralize'); 2 | 3 | function validateParams(name, errors) { 4 | if (typeof name !== 'string') { 5 | throw new Error('fileName is required'); 6 | } 7 | if (!Array.isArray(errors)) { 8 | throw new Error('errors array is required (can be empty though)'); 9 | } 10 | } 11 | 12 | function create(name, errors) { 13 | validateParams(name, errors); 14 | return Object.create(this, { 15 | name: { value: name }, 16 | errors: { value: errors } 17 | }); 18 | } 19 | 20 | function tooManyErrors() { 21 | return this.errors[this.errors.length - 1] === null; 22 | } 23 | 24 | function errorDescription() { 25 | var more = this.tooManyErrors() ? 'more than ' : ''; 26 | return more + pluralize(this.errors.length, 'error'); 27 | } 28 | 29 | module.exports = { 30 | create: create, 31 | tooManyErrors: tooManyErrors, 32 | errorDescription: errorDescription 33 | }; 34 | -------------------------------------------------------------------------------- /test/watch-for-changes-test.js: -------------------------------------------------------------------------------- 1 | var buster = require('buster'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var fs = require("fs"); 4 | 5 | var watchForChanges = require('../lib/watch-for-changes'); 6 | 7 | buster.testCase("watchForChanges", { 8 | setUp: function () { 9 | this.linter = { checkFile: this.stub() }; 10 | this.stub(fs, 'watch'); 11 | this.repository = new EventEmitter(); 12 | }, 13 | 14 | "should register new files to be watched": function () { 15 | watchForChanges(this.repository, this.linter); 16 | this.repository.emit('newFile', {name: 'file1.js'}); 17 | assert.calledOnce(fs.watch); 18 | assert.calledWith(fs.watch, 'file1.js'); 19 | }, 20 | 21 | "should check changed files for lint": function () { 22 | watchForChanges(this.repository, this.linter); 23 | fs.watch.yields('change'); 24 | this.repository.emit('newFile', {name: 'file1.js'}); 25 | assert.calledOnce(this.linter.checkFile); 26 | assert.calledWith(this.linter.checkFile, 'file1.js'); 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /lib/ansi.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | red: function (str) { 3 | return this.colorize(str, 31); 4 | }, 5 | 6 | RED: function (str) { 7 | return this.colorize(str, 31, true); 8 | }, 9 | 10 | yellow: function (str) { 11 | return this.colorize(str, 33); 12 | }, 13 | 14 | YELLOW: function (str) { 15 | return this.colorize(str, 33, true); 16 | }, 17 | 18 | green: function (str) { 19 | return this.colorize(str, 32); 20 | }, 21 | 22 | GREEN: function (str) { 23 | return this.colorize(str, 32, true); 24 | }, 25 | 26 | purple: function (str) { 27 | return this.colorize(str, 35); 28 | }, 29 | 30 | PURPLE: function (str) { 31 | return this.colorize(str, 35, true); 32 | }, 33 | 34 | cyan: function (str) { 35 | return this.colorize(str, 36); 36 | }, 37 | 38 | CYAN: function (str) { 39 | return this.colorize(str, 36, true); 40 | }, 41 | 42 | colorize: function (str, color, bright) { 43 | return (bright ? "\033[1m" : "") + 44 | "\033[" + color + "m" + str + "\033[0m"; 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "autolint", 3 | "version": "1.1.4", 4 | "main": "./lib/autolint", 5 | "description": "Autolint watches your files for jslint-errors.", 6 | "author": "Magnar Sveen ", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/magnars/autolint.git" 10 | }, 11 | "homepage": "https://github.com/magnars/autolint", 12 | "scripts": { 13 | "test": "buster test" 14 | }, 15 | "engines": { 16 | "node": ">=0.8" 17 | }, 18 | "bin": { 19 | "autolint": "./bin/autolint" 20 | }, 21 | "dependencies": { 22 | "glob": "~3.0", 23 | "growl": "~1.7", 24 | "underscore": "~1.1", 25 | "when": "https://github.com/cujojs/when/tarball/1.0.2" 26 | }, 27 | "devDependencies": { 28 | "buster": "~0.6", 29 | "buster-lint": "~0", 30 | "require-subvert": "0.0.1" 31 | }, 32 | "keywords": [ 33 | "JavaScript", 34 | "lint", 35 | "jslint", 36 | "jshint" 37 | ], 38 | "licenses": [ 39 | { 40 | "type": "Modified MIT / BSD", 41 | "url": "https://github.com/magnars/autolint/blob/master/LICENSE" 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /lib/jslint-linter.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var when = require("when"); 3 | var EventEmitter = require('events').EventEmitter; 4 | var checkedFile = require('./checked-file'); 5 | 6 | function check(file, fileName) { 7 | var errors = this.linter.check(file, this.options) ? [] : this.linter.check.errors; 8 | return checkedFile.create(fileName, errors); 9 | } 10 | 11 | function checkFile(promise, fileName, err, file) { 12 | if (err) { return require("./print").red(err); } 13 | var checked = check.call(this, file, fileName); 14 | this.emit('fileChecked', checked); 15 | promise.resolve(checked.errors); 16 | } 17 | 18 | function loadAndCheckFile(fileName) { 19 | var d = when.defer(); 20 | fs.readFile(fileName, 'utf-8', checkFile.bind(this, d, fileName)); 21 | return d.promise; 22 | } 23 | 24 | function create(options) { 25 | if (!options) { throw new TypeError('options is required (at least an empty object)'); } 26 | return Object.create(this, { 27 | options: { value: options }, 28 | linter: { value: { check: require('../vendor/jslint').JSLINT } } 29 | }); 30 | } 31 | 32 | module.exports = new EventEmitter(); 33 | module.exports.create = create; 34 | module.exports.checkFile = loadAndCheckFile; 35 | module.exports.check = check; 36 | -------------------------------------------------------------------------------- /test/lint-reporter-test.js: -------------------------------------------------------------------------------- 1 | var buster = require('buster'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var print = require('../lib/print'); 4 | var checkedFile = require('../lib/checked-file'); 5 | 6 | var lintReporter = require('../lib/lint-reporter'); 7 | 8 | buster.testCase("lintReporter", { 9 | setUp: function () { 10 | this.repository = new EventEmitter(); 11 | this.reporter = lintReporter.create(this.repository); 12 | this.reporter.listen(); 13 | this.stub(print, 'red'); 14 | this.stub(print, 'black'); 15 | }, 16 | 17 | "should be an object": function () { 18 | assert.isObject(lintReporter); 19 | }, 20 | 21 | "should print filename with number of errors": function () { 22 | var file = checkedFile.create('file1.js', [{}, {}]); 23 | this.repository.emit('dirty', file); 24 | assert.called(print.red); 25 | assert.calledWith(print.red, '', 'Lint in file1.js, 2 errors:'); 26 | }, 27 | 28 | "should print error": function () { 29 | var file = checkedFile.create('file1.js', [{ 30 | line: 17, 31 | character: 9, 32 | reason: 'Bazinga!' 33 | }]); 34 | this.repository.emit('dirty', file); 35 | assert.calledWith(print.black, ' line 17 char 9: Bazinga!'); 36 | } 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /lib/start-autolint.js: -------------------------------------------------------------------------------- 1 | var watchForChanges = require('./watch-for-changes'); 2 | var onInterrupt = require('./on-interrupt'); 3 | var glob = require('glob'); 4 | 5 | module.exports = function (conf) { 6 | var linter = require('./linter').create(conf); 7 | 8 | var lintScanner = require('./lint-scanner').create(linter, glob, conf.excludes); 9 | var repository = require('./repository').create(linter); 10 | 11 | var newFileReporter = require('./new-file-reporter').create(repository); 12 | var lintReporter = require('./lint-reporter').create(repository); 13 | var cleanReporter = require('./clean-reporter').create(repository); 14 | var summaryReporter = require('./summary-reporter').create(repository); 15 | var growlReporter = require('./growl-reporter').create(repository); 16 | 17 | repository.listen(); 18 | newFileReporter.listen(); 19 | watchForChanges(repository, linter); 20 | 21 | lintScanner.scan(conf.paths).then(function () { 22 | newFileReporter.beVerbose(); 23 | cleanReporter.listen(); 24 | lintReporter.listen(); 25 | growlReporter.listen(); 26 | summaryReporter.listen(); 27 | summaryReporter.print(); 28 | }); 29 | 30 | onInterrupt("Checking all files.", function () { 31 | lintScanner.scan(conf.paths).then(function () { 32 | summaryReporter.print(); 33 | }); 34 | }); 35 | }; -------------------------------------------------------------------------------- /lib/configuration.js: -------------------------------------------------------------------------------- 1 | var _ = require("underscore"); 2 | var fs = require("fs"); 3 | var util = require("util"); 4 | var defaultConfig = require("./default-configuration"); 5 | 6 | // for node 0.6 backwards compatibility 7 | if (!fs.existsSync) { 8 | fs.existsSync = require("path").existsSync; 9 | } 10 | 11 | module.exports = { 12 | exists: function () { 13 | return fs.existsSync("./autolint.js") || fs.existsSync("./autolint-config.js"); 14 | }, 15 | 16 | defaultsPlus: function (config) { 17 | var conf = _.extend({}, defaultConfig, config); 18 | conf.linterOptions = _.extend({}, 19 | defaultConfig.linterOptions, 20 | defaultConfig[conf.linter + "Options"], 21 | config.linterOptions, 22 | config[conf.linter + "Options"]); 23 | return conf; 24 | }, 25 | 26 | load: function () { 27 | var fileConfig = fs.existsSync("./autolint.js") ? 28 | require(process.cwd() + "/autolint") : 29 | require(process.cwd() + "/autolint-config"); 30 | return this.defaultsPlus(fileConfig); 31 | }, 32 | 33 | createDefaultConfigFile: function () { 34 | if (this.exists()) { 35 | throw new Error("autolint.js (or autolint-config.js) already exists in " + process.cwd()); 36 | } 37 | 38 | var newFile = fs.createWriteStream('./autolint.js'); 39 | var oldFile = fs.createReadStream(__dirname + '/blank-config-file.js'); 40 | 41 | newFile.once('open', function () { 42 | util.pump(oldFile, newFile); 43 | }); 44 | } 45 | }; -------------------------------------------------------------------------------- /test/print-test.js: -------------------------------------------------------------------------------- 1 | var buster = require('buster'); 2 | var util = require('util'); 3 | var ansi = require('../lib/ansi'); 4 | 5 | var print = require('../lib/print'); 6 | 7 | buster.testCase("print", { 8 | setUp: function () { 9 | this.stub(util, 'puts'); 10 | }, 11 | 12 | "should be a object": function () { 13 | assert.isObject(print); 14 | }, 15 | 16 | "should print using util.puts": function () { 17 | print.black('hello'); 18 | assert.called(util.puts); 19 | assert.calledWith(util.puts, 'hello'); 20 | }, 21 | 22 | "should print multiple lines": function () { 23 | print.black('hello', 'world'); 24 | assert.calledTwice(util.puts); 25 | assert.calledWith(util.puts, 'hello'); 26 | assert.calledWith(util.puts, 'world'); 27 | }, 28 | 29 | "should print array of lines": function () { 30 | print.black(["hey", "ho", "let's go"]); 31 | assert.calledThrice(util.puts); 32 | assert.calledWith(util.puts, "hey"); 33 | assert.calledWith(util.puts, "ho"); 34 | assert.calledWith(util.puts, "let's go"); 35 | }, 36 | 37 | "should print in red": function () { 38 | print.red('hello'); 39 | assert.called(util.puts); 40 | assert.calledWith(util.puts, 'RED: hello'); 41 | }, 42 | 43 | "should print in green": function () { 44 | print.green('hey', 'dude'); 45 | assert.calledTwice(util.puts); 46 | assert.calledWith(util.puts, 'GREEN: hey'); 47 | assert.calledWith(util.puts, 'GREEN: dude'); 48 | } 49 | }); 50 | 51 | ansi.RED = function (text) { 52 | return "RED: " + text; 53 | }; 54 | 55 | ansi.GREEN = function (text) { 56 | return "GREEN: " + text; 57 | }; 58 | -------------------------------------------------------------------------------- /test/new-file-reporter-test.js: -------------------------------------------------------------------------------- 1 | var buster = require('buster'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var checkedFile = require('../lib/checked-file'); 4 | var print = require('../lib/print'); 5 | 6 | var newFileReporter = require('../lib/new-file-reporter'); 7 | 8 | buster.testCase("newFileReporter", { 9 | setUp: function () { 10 | this.repository = new EventEmitter(); 11 | this.reporter = newFileReporter.create(this.repository); 12 | this.stub(print, 'red'); 13 | this.stub(print, 'black'); 14 | }, 15 | 16 | "should have listen method": function () { 17 | assert.isFunction(newFileReporter.listen); 18 | }, 19 | 20 | "should handle newFile event": function () { 21 | this.stub(this.reporter, 'handleNewFile'); 22 | this.reporter.listen(); 23 | this.repository.emit('newFile'); 24 | 25 | assert.called(this.reporter.handleNewFile); 26 | }, 27 | 28 | "should not print when found a clean file": function () { 29 | this.reporter.handleNewFile(checkedFile.create('file1.js', [])); 30 | refute.called(print.red); 31 | refute.called(print.black); 32 | }, 33 | 34 | "should print when found a clean file in verbose mode": function () { 35 | this.reporter.beVerbose(); 36 | this.reporter.handleNewFile(checkedFile.create('file1.js', [])); 37 | assert.called(print.black); 38 | assert.calledWith(print.black, 'Found file1.js - clean'); 39 | }, 40 | 41 | "should print when found a dirty file": function () { 42 | this.reporter.handleNewFile(checkedFile.create('file1.js', [{}, {}])); 43 | assert.calledWith(print.red, 'Found file1.js - 2 errors'); 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /lib/clean-reporter.js: -------------------------------------------------------------------------------- 1 | var print = require('./print'); 2 | var pluralize = require('./pluralize'); 3 | 4 | function create(repository) { 5 | return Object.create(this, { 6 | repository: { value: repository } 7 | }); 8 | } 9 | 10 | function shuffle(list) { 11 | var i, j, tempi, tempj; 12 | for (i = list.length - 1; i > 0; i -= 1) { 13 | j = Math.floor(Math.random() * (i + 1)); 14 | tempi = list[i]; 15 | tempj = list[j]; 16 | list[i] = tempj; 17 | list[j] = tempi; 18 | } 19 | return list; 20 | } 21 | 22 | function fileWithErrorCount(file) { 23 | return " " + file.name + " (" + pluralize(file.errors.length, "error") + ")"; 24 | } 25 | 26 | function differentName(file1) { 27 | return function (file2) { 28 | return file1.name !== file2.name; 29 | }; 30 | } 31 | 32 | function congratulateButMoreErrors(file, dirty) { 33 | var twoOthers = shuffle(dirty).splice(0, 2).map(fileWithErrorCount); 34 | print.green('', 'Nice! ' + file.name + ' is clean. Want to clean more?'); 35 | print.black(twoOthers); 36 | } 37 | 38 | function congratulateWithNoMoreErrors(file) { 39 | print.green('', 'Excellent! ' + file.name + ' (and everything else) is clean.'); 40 | } 41 | 42 | function handleErrorsFixed(file, errorsFixed) { 43 | if (file.errors.length === 0) { 44 | var dirty = this.repository.getDirtyFiles().filter(differentName(file)); 45 | if (dirty.length === 0) { 46 | congratulateWithNoMoreErrors(file); 47 | } else { 48 | congratulateButMoreErrors(file, dirty); 49 | } 50 | } 51 | } 52 | 53 | function listen() { 54 | this.repository.on('errorsFixed', handleErrorsFixed.bind(this)); 55 | } 56 | 57 | exports.create = create; 58 | exports.listen = listen; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2011 Magnar Sveen 2 | All rights reserved. 3 | 4 | Redistribution and use of this software in source and binary forms, 5 | with or without modification, are permitted provided that the following 6 | conditions are met: 7 | 8 | * Redistributions of source code must retain the above 9 | copyright notice, this list of conditions and the 10 | following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the 14 | following disclaimer in the documentation and/or other 15 | materials provided with the distribution. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 18 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 19 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 20 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | === 30 | 31 | This license applies to everything except JSLint and JSHint. 32 | JSLint, located at vendor/jslint.js, is copyright Douglas Crockford. 33 | JSLint is provided under a customized MIT license, which is 34 | included in the header of vendor/jslint.js. 35 | 36 | JSHint is provided under the same license as JSLint, which is 37 | included in the header of vendor/jshint.js. -------------------------------------------------------------------------------- /test/configuration-test.js: -------------------------------------------------------------------------------- 1 | var buster = require("buster"); 2 | 3 | var fs = require("fs"); 4 | var util = require("util"); 5 | 6 | var configuration = require("../lib/configuration"); 7 | 8 | buster.testCase('Configuration', { 9 | "should check if autolint.js exists": function () { 10 | this.stub(fs, "existsSync").returns(true); 11 | 12 | assert(configuration.exists()); 13 | assert.calledOnceWith(fs.existsSync, "./autolint.js"); 14 | }, 15 | 16 | "should load config from autolint.js": function () { 17 | assert.match(configuration.load(), { linterOptions: { node: true } }); 18 | }, 19 | 20 | "should load config from defaults": function () { 21 | assert.match(configuration.load(), { linter: "jslint" }); 22 | }, 23 | 24 | "should use jslintOptions when linter is jslint": function () { 25 | assert.match(configuration.load(), { linterOptions: { sloppy: true } }); 26 | }, 27 | 28 | "should overwrite defaults from autolint.js": function () { 29 | assert.match(configuration.load(), { linterOptions: { indent: 2 } }); 30 | }, 31 | 32 | "should not overwrite existing config file": function () { 33 | assert.exception(function () { 34 | configuration.createDefaultConfigFile(); 35 | }); 36 | }, 37 | 38 | "should do lots of mumbo jumbo to copy the blank config file": function () { 39 | var oldFile = {}; 40 | var newFile = { once: this.stub().yields() }; 41 | 42 | this.stub(configuration, "exists").returns(false); 43 | this.stub(fs, "createReadStream").returns(oldFile); 44 | this.stub(fs, "createWriteStream").returns(newFile); 45 | this.stub(util, "pump"); 46 | 47 | configuration.createDefaultConfigFile(); 48 | 49 | assert.calledOnceWith(fs.createWriteStream, "./autolint.js"); 50 | assert.calledOnce(fs.createReadStream); // jeez, __dirname for the lose 51 | assert.calledOnceWith(util.pump, oldFile, newFile); 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /lib/summary-reporter.js: -------------------------------------------------------------------------------- 1 | var _print = require('./print'); 2 | var pluralize = require('./pluralize'); 3 | 4 | function create(repository) { 5 | return Object.create(this, { 6 | repository: { value: repository } 7 | }); 8 | } 9 | 10 | function listen() { 11 | var print = this.print.bind(this); 12 | this.repository.on('change', function () { 13 | process.nextTick(print); 14 | }); 15 | } 16 | 17 | function numCleanFiles() { 18 | return this.repository.getCleanFiles().length; 19 | } 20 | 21 | function numDirtyFiles() { 22 | return this.repository.getDirtyFiles().length; 23 | } 24 | 25 | function hasUnknownNumberOfErrors() { 26 | return this.repository.getDirtyFiles().some(function (file) { 27 | return file.tooManyErrors(); 28 | }); 29 | } 30 | 31 | function numErrors() { 32 | var files = this.repository.getDirtyFiles(); 33 | return files.reduce(function (total, file) { return total + file.errors.length; }, 0); 34 | } 35 | 36 | function cleanFiles() { 37 | return pluralize(this.numCleanFiles(), "clean file"); 38 | } 39 | 40 | function dirtyFiles() { 41 | return pluralize(this.numDirtyFiles(), "dirty file"); 42 | } 43 | 44 | function errors() { 45 | var more = this.hasUnknownNumberOfErrors() ? 'more than ' : ''; 46 | return more + pluralize(this.numErrors(), "error"); 47 | } 48 | 49 | function getSummary() { 50 | return this.cleanFiles() + ", " + this.errors() + " in " + this.dirtyFiles(); 51 | } 52 | 53 | function print() { 54 | var text = this.getSummary(); 55 | 56 | if (this.numDirtyFiles() > 0) { 57 | _print.red('', text); 58 | } else { 59 | _print.green('', text); 60 | } 61 | } 62 | 63 | module.exports = { 64 | create: create, 65 | listen: listen, 66 | numCleanFiles: numCleanFiles, 67 | numDirtyFiles: numDirtyFiles, 68 | numErrors: numErrors, 69 | cleanFiles: cleanFiles, 70 | dirtyFiles: dirtyFiles, 71 | errors: errors, 72 | hasUnknownNumberOfErrors: hasUnknownNumberOfErrors, 73 | getSummary: getSummary, 74 | print: print 75 | }; 76 | -------------------------------------------------------------------------------- /test/clean-reporter-test.js: -------------------------------------------------------------------------------- 1 | var buster = require('buster'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var print = require('../lib/print'); 4 | var checkedFile = require('../lib/checked-file'); 5 | 6 | var cleanReporter = require('../lib/clean-reporter'); 7 | 8 | buster.testCase("cleanReporter", { 9 | setUp: function () { 10 | this.stub(print, 'black'); 11 | this.stub(print, 'green'); 12 | this.repository = new EventEmitter(); 13 | this.repository.getDirtyFiles = this.stub().returns([]); 14 | this.reporter = cleanReporter.create(this.repository); 15 | }, 16 | 17 | "should be an object": function () { 18 | assert.isObject(cleanReporter); 19 | }, 20 | 21 | "should have listen method": function () { 22 | assert.isFunction(cleanReporter.listen); 23 | }, 24 | 25 | "when cleaning up a file": { 26 | setUp: function () { 27 | this.file = checkedFile.create('file1.js', []); 28 | this.reporter.listen(); 29 | }, 30 | 31 | "should congratulate": function () { 32 | this.repository.emit('errorsFixed', this.file, [{}]); 33 | assert.called(print.green); 34 | assert.calledWith(print.green, '', 'Excellent! file1.js (and everything else) is clean.'); 35 | }, 36 | 37 | "should list other files with errors": function () { 38 | this.repository.getDirtyFiles = this.stub().returns([ 39 | checkedFile.create('file1.js', [{}]), 40 | checkedFile.create('file2.js', [{}]) 41 | ]); 42 | this.repository.emit('errorsFixed', this.file, [{}]); 43 | assert.called(print.green); 44 | assert.called(print.black); 45 | assert.calledWith(print.green, '', 'Nice! file1.js is clean. Want to clean more?'); 46 | assert.calledWith(print.black, [' file2.js (1 error)']); 47 | } 48 | }, 49 | 50 | "should not congratulate when errors remain": function () { 51 | var file = checkedFile.create('file1.js', [{}]); 52 | this.reporter.listen(); 53 | this.repository.emit('errorsFixed', file, [{}]); 54 | refute.called(print.green); 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /test/checked-file-test.js: -------------------------------------------------------------------------------- 1 | var buster = require('buster'); 2 | var file = require('../lib/checked-file'); 3 | 4 | buster.testCase("checkedFile", { 5 | "should be an object": function () { 6 | assert.isObject(file); 7 | }, 8 | 9 | "should have create method": function () { 10 | assert.isFunction(file.create); 11 | }, 12 | 13 | "when creating should complain about missing file name": function () { 14 | assert.exception(function () { 15 | file.create(); 16 | }); 17 | }, 18 | 19 | "when creating should complain about missing errors array": function () { 20 | assert.exception(function () { 21 | file.create("name"); 22 | }); 23 | }, 24 | 25 | "should expose file name": function () { 26 | var f = file.create('file1.js', []); 27 | assert.equals(f.name, 'file1.js'); 28 | }, 29 | 30 | "should expose errors": function () { 31 | var errors = [{}, {}]; 32 | var f = file.create('', errors); 33 | assert.equals(f.errors, errors); 34 | }, 35 | 36 | "too many errors": { 37 | "should look for trailing null": function () { 38 | var f = file.create('', [{}, {}, null]); 39 | assert(f.tooManyErrors()); 40 | }, 41 | 42 | "should be false without trailing null": function () { 43 | var f = file.create('', [{}, {}]); 44 | refute(f.tooManyErrors()); 45 | } 46 | }, 47 | 48 | "errorDescription": { 49 | setUp: function () { 50 | this.assertErrorDescription = function (errors, desc) { 51 | assert.equals(file.create('', errors).errorDescription(), desc); 52 | }; 53 | }, 54 | 55 | "should be function": function () { 56 | assert.isFunction(file.errorDescription); 57 | }, 58 | 59 | "should return string describing number of errors": function () { 60 | this.assertErrorDescription([], '0 errors'); 61 | this.assertErrorDescription([{}, {}], '2 errors'); 62 | }, 63 | 64 | "should pluralize properly": function () { 65 | this.assertErrorDescription([{}], '1 error'); 66 | }, 67 | 68 | "should state if too many errors": function () { 69 | this.assertErrorDescription([{}, null], 'more than 2 errors'); 70 | } 71 | } 72 | 73 | }); 74 | -------------------------------------------------------------------------------- /lib/growl-reporter.js: -------------------------------------------------------------------------------- 1 | var growl = require('growl'); 2 | var pluralize = require('./pluralize'); 3 | 4 | function create(repository) { 5 | if (!repository) { throw new Error('repository is required'); } 6 | return Object.create(this, { 7 | repository: { value: repository } 8 | }); 9 | } 10 | 11 | function listen() { 12 | this.repository.on('errorsIntroduced', this.handleErrorsIntroduced.bind(this)); 13 | this.repository.on('errorsFixed', this.handleErrorsFixed.bind(this)); 14 | } 15 | 16 | function escape(text) { 17 | return text.replace('$', '\\$'); 18 | } 19 | 20 | function notify(title, text, icon) { 21 | growl(escape(text), { 22 | title: escape(title), 23 | image: __dirname + '/../icons/' + icon + '.png' 24 | }); 25 | } 26 | 27 | function handleErrorsIntroduced(file, errors) { 28 | var error = 'First error at line ' + errors[0].line + 29 | ' char ' + errors[0].character + 30 | ':\n' + errors[0].reason; 31 | var title = 'You introduced ' + pluralize(errors.length, 'lint error') + ' in ' + file.name + ':'; 32 | notify(title, error, 'error'); 33 | } 34 | 35 | var cheers = [ 36 | "You're the new Cleanup Gold Champion!", 37 | 'Awesome!', 38 | 'Rock on!', 39 | 'Brilliant!', 40 | 'Most excellent!', 41 | 'Wonderful!', 42 | 'Kick ass!', 43 | 'Sparkling clean!', 44 | 'Pro linting!', 45 | 'Smashing work!', 46 | 'You rock!', 47 | 'Lint ... terminated.', 48 | 'Sweet!', 49 | 'Good job!' 50 | ]; 51 | 52 | var lastCheerIndex; 53 | 54 | function cheer() { 55 | if (!lastCheerIndex || lastCheerIndex === 0) { 56 | lastCheerIndex = cheers.length; 57 | } 58 | lastCheerIndex -= 1; 59 | return cheers[lastCheerIndex]; 60 | } 61 | 62 | function congratulate(file) { 63 | notify(cheer(), file.name + ' is clean.', 'clean'); 64 | } 65 | 66 | function nextError(file) { 67 | var error = file.errors[0]; 68 | var text = 'Next error at line ' + error.line + 69 | ' char ' + error.character + 70 | ':\n' + error.reason; 71 | notify('Nice cleanup!', text, 'more'); 72 | } 73 | 74 | function handleErrorsFixed(file, fixedError) { 75 | if (file.errors.length === 0) { 76 | congratulate(file); 77 | } else { 78 | nextError(file); 79 | } 80 | } 81 | 82 | exports.create = create; 83 | exports.listen = listen; 84 | exports.handleErrorsIntroduced = handleErrorsIntroduced; 85 | exports.handleErrorsFixed = handleErrorsFixed; -------------------------------------------------------------------------------- /test/growl-reporter-test.js: -------------------------------------------------------------------------------- 1 | var buster = require('buster'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var requireSubvert = require('require-subvert')(__dirname); 4 | 5 | var growl, growlReporter; 6 | 7 | buster.testCase("growlReporter", { 8 | setUp: function () { 9 | growl = this.stub(); 10 | requireSubvert.subvert('growl', growl); 11 | growlReporter = requireSubvert.require('../lib/growl-reporter'); 12 | this.repository = new EventEmitter(); 13 | this.reporter = growlReporter.create(this.repository); 14 | }, 15 | 16 | "should complain about missing repository": function () { 17 | assert.exception(function () { 18 | growlReporter.create(); 19 | }); 20 | }, 21 | 22 | "should handle errorsIntroduced event": function () { 23 | this.stub(this.reporter, 'handleErrorsIntroduced'); 24 | this.reporter.listen(); 25 | 26 | this.repository.emit('errorsIntroduced'); 27 | assert.called(this.reporter.handleErrorsIntroduced); 28 | }, 29 | 30 | "should growl about introduced errors": function () { 31 | this.reporter.handleErrorsIntroduced({name: 'file.js'}, [{}]); 32 | 33 | assert.calledOnce(growl); 34 | var options = growl.getCall(0).args[1]; 35 | assert.match(options.title, 'You introduced 1 lint error in file.js:'); 36 | }, 37 | 38 | "should growl with errors": function () { 39 | this.reporter.handleErrorsIntroduced({name: 'file.js'}, [{ 40 | line: 17, 41 | character: 9, 42 | reason: 'Booyah!' 43 | }, {}]); 44 | 45 | assert.calledWith(growl, 'First error at line 17 char 9:\nBooyah!'); 46 | }, 47 | 48 | "should handle errorsFixed event": function () { 49 | this.stub(this.reporter, 'handleErrorsFixed'); 50 | this.reporter.listen(); 51 | 52 | this.repository.emit('errorsFixed'); 53 | assert.called(this.reporter.handleErrorsFixed); 54 | }, 55 | 56 | "should congratulate on clean file": function () { 57 | this.reporter.handleErrorsFixed({name: 'file.js', errors: []}, [{}]); 58 | assert.calledWith(growl, 'file.js is clean.'); 59 | }, 60 | 61 | "should list next error": function () { 62 | this.reporter.handleErrorsFixed({name: 'file.js', errors: [{ 63 | line: 19, 64 | character: 0, 65 | reason: 'Bah, humbug!' 66 | }]}, [{}]); 67 | assert.calledWith(growl, 'Next error at line 19 char 0:\nBah, humbug!'); 68 | }, 69 | 70 | "should escape $": function () { 71 | this.reporter.handleErrorsFixed({name: 'file$.js', errors: []}, [{}]); 72 | assert.calledWith(growl, 'file\\$.js is clean.'); 73 | } 74 | 75 | }); 76 | -------------------------------------------------------------------------------- /lib/lint-scanner.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var when = require("when"); 3 | var print = require('./print'); 4 | 5 | function fileStartsWith(start) { 6 | return new RegExp("(/|^)" + start + "[^/]+$"); 7 | } 8 | 9 | var defaultExcludes = [ 10 | fileStartsWith('#'), 11 | fileStartsWith('\\.'), 12 | fileStartsWith('~') 13 | ]; 14 | 15 | function exists(o) { 16 | return !!o; 17 | } 18 | 19 | function create(linter, glob, excludes) { 20 | if (!linter) { throw new Error('linter is required'); } 21 | if (!glob) { throw new Error('glob is required'); } 22 | return Object.create(this, { 23 | linter: { value: linter }, 24 | glob: { value: glob }, 25 | excludes: { value: defaultExcludes.concat(excludes).filter(exists) } 26 | }); 27 | } 28 | 29 | function handleGlobError(path) { 30 | print.red("\nWarning: No files in path " + path); 31 | if (path.match(/\*\*/)) { 32 | print.black(" There's a problem with ** on some systems.", 33 | " Try using multiple paths with single stars instead."); 34 | } 35 | } 36 | 37 | function globPath(path) { 38 | var d = when.defer(); 39 | this.glob(path, function (err, files) { 40 | if (err || files.length === 0) { 41 | handleGlobError(path); 42 | d.reject(); 43 | } else { 44 | d.resolve(files); 45 | } 46 | }); 47 | return d.promise; 48 | } 49 | 50 | function findAllFiles(paths) { 51 | var d = when.defer(); 52 | when.all(paths.map(globPath.bind(this))).then(function () { 53 | d.resolve(_.flatten(arguments)); 54 | }); 55 | return d.promise; 56 | } 57 | 58 | function filterExcludedFiles(files) { 59 | var excludes = this.excludes; 60 | return _(files).reject(function (file) { 61 | return excludes.some(function (exclude) { 62 | return file.match(exclude); 63 | }); 64 | }); 65 | } 66 | 67 | function checkLint(file) { 68 | return this.linter.checkFile(file); 69 | } 70 | 71 | function checkNextFile(files, callback) { 72 | if (files.length === 0) { 73 | callback(); 74 | } 75 | checkLint.call(this, files.pop()).then(function () { 76 | checkNextFile.call(this, files, callback); 77 | }.bind(this)); 78 | } 79 | 80 | function checkFiles(files) { 81 | var d = when.defer(); 82 | checkNextFile.call(this, files, function () { 83 | d.resolve(); 84 | }); 85 | return d.promise; 86 | } 87 | 88 | function scan(paths) { 89 | var d = when.defer(); 90 | var self = this; 91 | self.findAllFiles(paths).then(function (files) { 92 | files = self.filterExcludedFiles(files); 93 | self.checkFiles(files).then(function () { 94 | d.resolve(); 95 | }); 96 | }); 97 | return d.promise; 98 | } 99 | 100 | module.exports = { 101 | create: create, 102 | findAllFiles: findAllFiles, 103 | filterExcludedFiles: filterExcludedFiles, 104 | checkFiles: checkFiles, 105 | scan: scan 106 | }; -------------------------------------------------------------------------------- /test/summary-reporter-test.js: -------------------------------------------------------------------------------- 1 | var buster = require('buster'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var print = require('../lib/print'); 4 | var checkedFile = require('../lib/checked-file'); 5 | 6 | var summaryReporter = require('../lib/summary-reporter'); 7 | 8 | buster.testCase("summaryReporter", { 9 | setUp: function () { 10 | this.stub(print, 'red'); 11 | this.stub(print, 'green'); 12 | this.repository = new EventEmitter(); 13 | this.repository.getCleanFiles = this.stub().returns([]); 14 | this.repository.getDirtyFiles = this.stub().returns([]); 15 | this.repository.files = {}; 16 | this.reporter = summaryReporter.create(this.repository); 17 | this.reporter.listen(); 18 | }, 19 | 20 | "should be an object": function () { 21 | assert.isObject(summaryReporter); 22 | }, 23 | 24 | "should count number of clean files": function () { 25 | this.repository.getCleanFiles = this.stub().returns([ 26 | checkedFile.create('file1.js', []), 27 | checkedFile.create('file2.js', []) 28 | ]); 29 | assert.equals(this.reporter.numCleanFiles(), 2); 30 | }, 31 | 32 | "should count number of dirty files": function () { 33 | this.repository.getDirtyFiles = this.stub().returns([ 34 | checkedFile.create('file2.js', [{}]) 35 | ]); 36 | assert.equals(this.reporter.numDirtyFiles(), 1); 37 | }, 38 | 39 | "should count number of errors": function () { 40 | this.repository.getDirtyFiles = this.stub().returns([ 41 | checkedFile.create('file1.js', [{}]), 42 | checkedFile.create('file2.js', [{}, {}]) 43 | ]); 44 | assert.equals(this.reporter.numErrors(), 3); 45 | }, 46 | 47 | "should create summary": function () { 48 | assert.equals(this.reporter.getSummary(), '0 clean files, 0 errors in 0 dirty files'); 49 | }, 50 | 51 | "should say 'more than' when unknown number of errors": function () { 52 | this.repository.getDirtyFiles = this.stub().returns([ 53 | checkedFile.create('file1.js', [{}]), 54 | checkedFile.create('file2.js', [{}, {}, {}, null]) 55 | ]); 56 | assert.equals(this.reporter.getSummary(), '0 clean files, more than 5 errors in 2 dirty files'); 57 | }, 58 | 59 | "should print summary without errors in green": function () { 60 | this.reporter.print(); 61 | assert.calledOnce(print.green); 62 | assert.calledWith(print.green, '', '0 clean files, 0 errors in 0 dirty files'); 63 | }, 64 | 65 | "should print summary with errors in red": function () { 66 | this.repository.getDirtyFiles = this.stub().returns([ 67 | checkedFile.create('file1.js', [{}]) 68 | ]); 69 | this.reporter.print(); 70 | assert.calledOnce(print.red); 71 | assert.calledWith(print.red, '', '0 clean files, 1 error in 1 dirty file'); 72 | }, 73 | 74 | "should print summary on change event": function (done) { 75 | this.repository.emit('change'); 76 | process.nextTick(function () { 77 | assert.calledOnce(print.green); 78 | done(); 79 | }); 80 | } 81 | }); 82 | -------------------------------------------------------------------------------- /lib/repository.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var EventEmitter = require('events').EventEmitter; 3 | 4 | function checkForNewFile(file) { 5 | if (!this.files[file.name]) { 6 | this.emit('newFile', file); 7 | } 8 | } 9 | 10 | function checkForDirtyFile(file) { 11 | if (file.errors.length > 0) { 12 | this.emit('dirty', file); 13 | } 14 | } 15 | 16 | function equals(error1, error2) { 17 | return error1.line === error2.line && 18 | error1.character === error2.character && 19 | error1.reason === error2.reason; 20 | } 21 | 22 | function subtract(allErrors, unwantedErrors) { 23 | allErrors = _.compact(allErrors); 24 | unwantedErrors = _.compact(unwantedErrors); 25 | return _(allErrors).reject(function (error) { 26 | return _(unwantedErrors).any(function (unwanted) { 27 | return equals(error, unwanted); 28 | }); 29 | }); 30 | } 31 | 32 | function checkForIntroducedErrors(file) { 33 | if (!this.files[file.name]) { return; } 34 | if (this.files[file.name].tooManyErrors()) { return; } 35 | var oldErrors = this.files[file.name].errors; 36 | var newErrors = subtract(file.errors, oldErrors); 37 | if (newErrors.length > 0) { 38 | this.emit('errorsIntroduced', file, newErrors); 39 | } 40 | } 41 | 42 | function introducedTooManyErrors(file) { 43 | return file.tooManyErrors() && !this.files[file.name].tooManyErrors(); 44 | } 45 | 46 | function checkForFixedErrors(file) { 47 | if (!this.files[file.name]) { return; } 48 | if (introducedTooManyErrors.call(this, file)) { return; } 49 | var oldErrors = this.files[file.name].errors; 50 | var fixedErrors = subtract(oldErrors, file.errors); 51 | if (fixedErrors.length > 0) { 52 | this.emit('errorsFixed', file, fixedErrors); 53 | } 54 | } 55 | 56 | function checkForChanges(file) { 57 | if (!this.files[file.name]) { return; } 58 | var oldErrors = this.files[file.name].errors; 59 | var differentNumberOfErrors = oldErrors && oldErrors.length !== file.errors.length; 60 | if (differentNumberOfErrors) { 61 | this.emit('change', file); 62 | } 63 | } 64 | 65 | function storeFile(file) { 66 | this.files[file.name] = file; 67 | } 68 | 69 | function getCleanFiles() { 70 | return _(this.files).values().filter(function (file) { 71 | return file.errors.length === 0; 72 | }); 73 | } 74 | 75 | function getDirtyFiles() { 76 | return _(this.files).values().filter(function (file) { 77 | return file.errors.length > 0; 78 | }); 79 | } 80 | 81 | function listen() { 82 | this.linter.on('fileChecked', checkForNewFile.bind(this)); 83 | this.linter.on('fileChecked', checkForDirtyFile.bind(this)); 84 | this.linter.on('fileChecked', checkForIntroducedErrors.bind(this)); 85 | this.linter.on('fileChecked', checkForFixedErrors.bind(this)); 86 | this.linter.on('fileChecked', checkForChanges.bind(this)); 87 | this.linter.on('fileChecked', storeFile.bind(this)); 88 | } 89 | 90 | function create(linter) { 91 | return Object.create(this, { 92 | linter: { value: linter }, 93 | files: { value: {} } 94 | }); 95 | } 96 | 97 | module.exports = new EventEmitter(); 98 | module.exports.create = create; 99 | module.exports.listen = listen; 100 | module.exports.getCleanFiles = getCleanFiles; 101 | module.exports.getDirtyFiles = getDirtyFiles; 102 | -------------------------------------------------------------------------------- /test/jslint-linter-test.js: -------------------------------------------------------------------------------- 1 | var buster = require('buster'); 2 | var fs = require('fs'); 3 | var jslint = require('../vendor/jslint'); 4 | 5 | var linter = require('../lib/jslint-linter'); 6 | 7 | buster.testCase("jslint-linter", { 8 | setUp: function () { 9 | this.stub(fs, 'readFile'); 10 | fs.readFile.yields(null, 'file contents'); 11 | this.stub(jslint, 'JSLINT').returns(true); 12 | this.callback = this.stub(); 13 | this.linter = linter.create({}); 14 | }, 15 | 16 | "should be object": function () { 17 | assert.isObject(linter); 18 | }, 19 | 20 | "should have checkFile function": function () { 21 | assert.isFunction(linter.checkFile); 22 | }, 23 | 24 | "test should complain about missing options": function () { 25 | assert.exception(function () { 26 | linter.create(); 27 | }); 28 | }, 29 | 30 | "should read file": function () { 31 | this.linter.checkFile('file.js'); 32 | assert.calledOnce(fs.readFile); 33 | assert.calledWith(fs.readFile, 'file.js'); 34 | }, 35 | 36 | "should check file with jslint and options": function () { 37 | var options = {}; 38 | this.linter = linter.create(options); 39 | this.linter.checkFile('file.js'); 40 | assert.calledOnce(jslint.JSLINT); 41 | assert.calledWith(jslint.JSLINT, 'file contents', options); 42 | }, 43 | 44 | "should return promise": function () { 45 | assert.isFunction(this.linter.checkFile('file.js').then); 46 | }, 47 | 48 | "if check fails": { 49 | setUp: function () { 50 | jslint.JSLINT.returns(false); 51 | jslint.JSLINT.errors = [{}]; 52 | }, 53 | 54 | "should emit fileChecked event": function () { 55 | this.linter.on('fileChecked', this.callback); 56 | this.linter.checkFile('file.js'); 57 | assert.calledOnce(this.callback); 58 | }, 59 | 60 | "should pass file with event": function () { 61 | this.linter.on('fileChecked', this.callback); 62 | this.linter.checkFile('file.js'); 63 | var args = this.callback.getCall(0).args; 64 | assert.match(args[0], { 65 | name: 'file.js', 66 | errors: [{}] 67 | }); 68 | }, 69 | 70 | "should resolve promise": function () { 71 | var callback = this.stub(); 72 | this.linter.checkFile('file.js').then(callback); 73 | assert.called(callback); 74 | }, 75 | 76 | "should resolve promise with errors": function () { 77 | this.linter.checkFile('file.js').then(function (errors) { 78 | assert.equals(errors, [{}]); 79 | }); 80 | } 81 | }, 82 | 83 | "if check succeeds": { 84 | setUp: function () { 85 | jslint.JSLINT.returns(true); 86 | }, 87 | 88 | "should emit fileChecked event": function () { 89 | this.linter.on('fileChecked', this.callback); 90 | 91 | this.linter.checkFile('file.js'); 92 | assert.calledOnce(this.callback); 93 | }, 94 | 95 | "should resolve promise": function () { 96 | var callback = this.stub(); 97 | this.linter.checkFile('file.js').then(callback); 98 | assert.called(callback); 99 | }, 100 | 101 | "should resolve promise with empty array": function () { 102 | this.linter.checkFile('file.js').then(function (errors) { 103 | assert.equals(errors, []); 104 | }); 105 | } 106 | } 107 | 108 | }); 109 | -------------------------------------------------------------------------------- /test/lint-scanner-test.js: -------------------------------------------------------------------------------- 1 | var buster = require('buster'); 2 | var when = require("when"); 3 | var EventEmitter = require('events').EventEmitter; 4 | var print = require('../lib/print'); 5 | 6 | var lintScanner = require('../lib/lint-scanner'); 7 | 8 | buster.testCase("lintScanner", { 9 | setUp: function () { 10 | this.glob = this.stub(); 11 | this.linter = new EventEmitter(); 12 | this.scanner = lintScanner.create(this.linter, this.glob); 13 | }, 14 | 15 | "should be an object": function () { 16 | assert.isObject(lintScanner); 17 | }, 18 | 19 | "should have create method": function () { 20 | assert.isFunction(lintScanner.create); 21 | }, 22 | 23 | "should complain about missing linter": function () { 24 | assert.exception(function () { 25 | lintScanner.create(); 26 | }); 27 | }, 28 | 29 | "findAllFiles": { 30 | "should use glob to find files": function () { 31 | this.scanner.findAllFiles(['*.js']); 32 | assert.calledOnce(this.glob); 33 | assert.calledWith(this.glob, '*.js'); 34 | }, 35 | 36 | "should find all files": function () { 37 | this.glob.withArgs('lib/*.js').yields(null, ['file1.js']); 38 | this.glob.withArgs('test/*.js').yields(null, ['file2.js', 'file3.js']); 39 | this.scanner.findAllFiles(['lib/*.js', 'test/*.js']).then(function (files) { 40 | assert.equals(files, ['file1.js', 'file2.js', 'file3.js']); 41 | }); 42 | }, 43 | 44 | "with unknown path": { 45 | setUp: function () { 46 | this.stub(print, 'red'); 47 | this.stub(print, 'black'); 48 | this.glob.yields({}); 49 | }, 50 | 51 | "should print warning": function () { 52 | this.scanner.findAllFiles(['lib/*.js']); 53 | assert.called(print.red); 54 | assert.calledWith(print.red, "\nWarning: No files in path lib/*.js"); 55 | }, 56 | 57 | "should print explanation about ** bug": function () { 58 | this.scanner.findAllFiles(['lib/**/*.js']); 59 | assert.called(print.black); 60 | assert.calledWith(print.black, 61 | " There's a problem with ** on some systems.", 62 | " Try using multiple paths with single stars instead."); 63 | } 64 | } 65 | 66 | }, 67 | 68 | "filterExcludedFiles": { 69 | "should keep normal files": function () { 70 | var files = ['file1.js', 'lib/file2.js']; 71 | assert.equals(this.scanner.filterExcludedFiles(files), ['file1.js', 'lib/file2.js']); 72 | }, 73 | 74 | "should exclude emacs autosave files (starts with hash)": function () { 75 | var files = ['file1.js', '#file1.js']; 76 | assert.equals(this.scanner.filterExcludedFiles(files), ['file1.js']); 77 | }, 78 | 79 | "shouldn't be baffled by preceding directories": function () { 80 | var files = ['lib/file1.js', 'lib/#file1.js']; 81 | assert.equals(this.scanner.filterExcludedFiles(files), ['lib/file1.js']); 82 | }, 83 | 84 | "should exclude emacs backup files (starts with tilde)": function () { 85 | var files = ['test/file1.js', 'test/~file1.js']; 86 | assert.equals(this.scanner.filterExcludedFiles(files), ['test/file1.js']); 87 | }, 88 | 89 | "should exclude system files (starts with .)": function () { 90 | var files = ['test/file1.js', 'test/.git']; 91 | assert.equals(this.scanner.filterExcludedFiles(files), ['test/file1.js']); 92 | }, 93 | 94 | "should not exclude files in parent directories (starts with ..)": function () { 95 | var files = ['../test/file1.js']; 96 | assert.equals(this.scanner.filterExcludedFiles(files), ['../test/file1.js']); 97 | }, 98 | 99 | "should exclude files from configuration": function () { 100 | var scanner = lintScanner.create(this.linter, this.glob, [/jquery/]); 101 | var files = ['file.js', 'jquery.js', 'color.jquery.js']; 102 | assert.equals(scanner.filterExcludedFiles(files), ['file.js']); 103 | } 104 | }, 105 | 106 | "checkFiles": { 107 | setUp: function () { 108 | this.promise = when.defer(); 109 | this.linter.checkFile = this.stub().returns(this.promise); 110 | }, 111 | 112 | "should check files with linter": function () { 113 | this.scanner.checkFiles(['file.js']); 114 | assert.calledOnce(this.linter.checkFile); 115 | }, 116 | 117 | "should return promise": function () { 118 | var promise = this.scanner.checkFiles(['file.js']); 119 | assert.isFunction(promise.then); 120 | }, 121 | 122 | "should resolve promise when all files are checked": function () { 123 | var callback = this.stub(); 124 | 125 | this.scanner.checkFiles(['file.js']).then(callback); 126 | refute.called(callback); 127 | this.promise.resolve(); 128 | assert.called(callback); 129 | } 130 | }, 131 | 132 | "scan": { 133 | setUp: function () { 134 | this.findPromise = when.defer(); 135 | this.checkPromise = when.defer(); 136 | this.stub(this.scanner, 'findAllFiles').returns(this.findPromise); 137 | this.stub(this.scanner, 'checkFiles').returns(this.checkPromise); 138 | }, 139 | 140 | "should find all files": function () { 141 | this.scanner.scan(['*.js']); 142 | assert.called(this.scanner.findAllFiles); 143 | assert.calledWith(this.scanner.findAllFiles, ['*.js']); 144 | }, 145 | 146 | "should check all files": function () { 147 | this.scanner.scan(); 148 | this.findPromise.resolve(['file1.js', '#file1.js']); 149 | assert.called(this.scanner.checkFiles); 150 | assert.calledWith(this.scanner.checkFiles, ['file1.js']); 151 | }, 152 | 153 | "should return promise": function () { 154 | assert.isFunction(this.scanner.scan().then); 155 | }, 156 | 157 | "should resolve promise when all files checked": function () { 158 | var callback = this.stub(); 159 | this.scanner.scan().then(callback); 160 | 161 | this.findPromise.resolve(); 162 | this.checkPromise.resolve(); 163 | 164 | assert.called(callback); 165 | } 166 | } 167 | }); 168 | -------------------------------------------------------------------------------- /test/repository-test.js: -------------------------------------------------------------------------------- 1 | var buster = require('buster'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var checkedFile = require('../lib/checked-file'); 4 | 5 | var repository = require('../lib/repository'); 6 | 7 | function file(name, errors) { 8 | return checkedFile.create(name, errors); 9 | } 10 | 11 | buster.testCase("repository", { 12 | setUp: function () { 13 | this.linter = new EventEmitter(); 14 | this.repo = repository.create(this.linter); 15 | this.repo.listen(); 16 | }, 17 | 18 | "should keep track of files": function () { 19 | var f = file('file1.js', [{}, {}]); 20 | this.linter.emit('fileChecked', f); 21 | assert.equals(this.repo.files['file1.js'], f); 22 | }, 23 | 24 | "when getting files": { 25 | setUp: function () { 26 | this.clean = file('clean.js', []); 27 | this.dirty = file('dirty.js', [{}]); 28 | this.repo.files['clean.js'] = this.clean; 29 | this.repo.files['dirty.js'] = this.dirty; 30 | }, 31 | 32 | "should get just clean files": function () { 33 | assert.equals(this.repo.getCleanFiles(), [this.clean]); 34 | }, 35 | 36 | "should get just dirty files": function () { 37 | assert.equals(this.repo.getDirtyFiles(), [this.dirty]); 38 | } 39 | }, 40 | 41 | "dirty event": { 42 | setUp: function () { 43 | this.callback = this.stub(); 44 | this.repo.on('dirty', this.callback); 45 | }, 46 | 47 | "should emit when we have errors": function () { 48 | var f = file('file3.js', [{}, {}, {}]); 49 | this.linter.emit('fileChecked', f); 50 | assert.called(this.callback); 51 | assert.calledWith(this.callback, f); 52 | }, 53 | 54 | "should not emit without errors": function () { 55 | this.linter.emit('fileChecked', file('file4.js', [])); 56 | refute.called(this.callback); 57 | } 58 | }, 59 | 60 | "newFile event": { 61 | setUp: function () { 62 | this.repo.files['old-file.js'] = file('old-file.js', []); 63 | this.callback = this.stub(); 64 | this.repo.on('newFile', this.callback); 65 | }, 66 | 67 | "should not emit event for old files": function () { 68 | this.linter.emit('fileChecked', file('old-file.js', [])); 69 | refute.called(this.callback); 70 | }, 71 | 72 | "should emit event for new files": function () { 73 | var f = file('new-file.js', [{}]); 74 | this.linter.emit('fileChecked', f); 75 | assert.called(this.callback); 76 | assert.calledWith(this.callback, f); 77 | } 78 | }, 79 | 80 | "errorsIntroduced event": { 81 | setUp: function () { 82 | this.oldError = { 83 | line: 9, 84 | character: 1, 85 | reason: 'Whatever' 86 | }; 87 | this.newError = { 88 | line: 11, 89 | character: 9, 90 | reason: 'You screwed up, dude' 91 | }; 92 | this.repo.files['file.js'] = file('file.js', [this.oldError]); 93 | this.callback = this.stub(); 94 | this.repo.on('errorsIntroduced', this.callback); 95 | }, 96 | 97 | "should emit event with new error": function () { 98 | var f = file('file.js', [Object.create(this.oldError), this.newError]); 99 | this.linter.emit('fileChecked', f); 100 | assert.called(this.callback); 101 | assert.calledWith(this.callback, f, [this.newError]); 102 | }, 103 | 104 | "should not emit event without new errors": function () { 105 | this.linter.emit('fileChecked', file('file.js', [this.oldError])); 106 | refute.called(this.callback); 107 | }, 108 | 109 | "should ignore null errors": function () { 110 | var f = file('file.js', [Object.create(this.oldError), this.newError, null]); 111 | this.linter.emit('fileChecked', f); 112 | assert.calledWith(this.callback, f, [this.newError]); 113 | }, 114 | 115 | "should not blame for introducing errors when unknown errors exist": function () { 116 | this.repo.files['file.js'] = file('file.js', [this.oldError, null]); 117 | var f = file('file.js', [Object.create(this.oldError), this.newError]); 118 | this.linter.emit('fileChecked', f); 119 | refute.called(this.callback); 120 | } 121 | }, 122 | 123 | "errorsFixed event": { 124 | setUp: function () { 125 | this.error1 = { 126 | line: 7, 127 | character: 3, 128 | reason: 'Bah' 129 | }; 130 | this.error2 = { 131 | line: 3, 132 | character: 19, 133 | reason: 'Humbug' 134 | }; 135 | this.repo.files['file.js'] = file('file.js', [this.error1, this.error2]); 136 | this.callback = this.stub(); 137 | this.repo.on('errorsFixed', this.callback); 138 | }, 139 | 140 | "should emit when error fixed": function () { 141 | var f = file('file.js', [Object.create(this.error2)]); 142 | this.linter.emit('fileChecked', f); 143 | assert.calledOnce(this.callback); 144 | assert.calledWith(this.callback, f, [this.error1]); 145 | }, 146 | 147 | "should not emit when nothing fixed": function () { 148 | this.linter.emit('fileChecked', file('file.js', [ 149 | Object.create(this.error1), 150 | Object.create(this.error2) 151 | ])); 152 | refute.called(this.callback); 153 | }, 154 | 155 | "should emit also when we have an unknown number of errors": function () { 156 | this.repo.files['file.js'] = file('file.js', [this.error1, this.error2, null]); 157 | var f = file('file.js', [Object.create(this.error2), null]); 158 | this.linter.emit('fileChecked', f); 159 | assert.calledOnce(this.callback); 160 | }, 161 | 162 | "should not emit when we introduce an unknown number of errors": function () { 163 | var f = file('file.js', [Object.create(this.error2), null]); 164 | this.linter.emit('fileChecked', f); 165 | refute.called(this.callback); 166 | } 167 | }, 168 | 169 | "change event": { 170 | setUp: function () { 171 | this.repo.files['file.js'] = file('file.js', [{}, {}]); 172 | this.callback = this.stub(); 173 | this.repo.on('change', this.callback); 174 | }, 175 | 176 | "should emit when number of errors have changed": function () { 177 | var f = file('file.js', [{}]); 178 | this.linter.emit('fileChecked', f); 179 | assert.called(this.callback); 180 | assert.calledWith(this.callback, f); 181 | }, 182 | 183 | "should not emit when numbers are the same": function () { 184 | this.linter.emit('fileChecked', file('file.js', [{}, {}])); 185 | refute.called(this.callback); 186 | } 187 | } 188 | }); 189 | -------------------------------------------------------------------------------- /lib/default-configuration.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | paths: ["**/*.js"], 3 | linter: "jslint", 4 | jslintOptions: { 5 | indent : 4, // the indentation factor 6 | maxlen : 80, // the maximum length of a source line 7 | maxerr : 50, // the maximum number of errors to report per file 8 | anon : false, // true, if the space may be omitted in anonymous function declarations 9 | bitwise : false, // true, if bitwise operators should be allowed 10 | browser : false, // true, if the standard browser globals should be predefined 11 | cap : false, // true, if upper case HTML should be allowed 12 | "continue" : false, // true, if the continuation statement should be tolerated 13 | css : false, // true, if CSS workarounds should be tolerated 14 | debug : false, // true, if debugger statements should be allowed 15 | devel : false, // true, if logging should be allowed (console, alert, etc.) 16 | eqeq : false, // true, if == should be allowed 17 | es5 : false, // true, if ES5 syntax should be allowed 18 | evil : false, // true, if eval should be allowed 19 | forin : false, // true, if for in statements need not filter 20 | fragment : false, // true, if HTML fragments should be allowed 21 | newcap : false, // true, if constructor names capitalization is ignored 22 | node : false, // true, if Node.js globals should be predefined 23 | nomen : false, // true, if names may have dangling _ 24 | on : false, // true, if HTML event handlers should be allowed 25 | passfail : false, // true, if the scan should stop on first error 26 | plusplus : false, // true, if increment/decrement should be allowed 27 | properties : false, // true, if all property names must be declared with /*properties*/ 28 | regexp : false, // true, if the . should be allowed in regexp literals 29 | rhino : false, // true, if the Rhino environment globals should be predefined 30 | undef : false, // true, if variables can be declared out of order 31 | unparam : false, // true, if unused parameters should be tolerated 32 | sloppy : false, // true, if the 'use strict'; pragma is optional 33 | sub : false, // true, if all forms of subscript notation are tolerated 34 | vars : false, // true, if multiple var statements per function should be allowed 35 | white : false, // true, if sloppy whitespace is tolerated 36 | widget : false, // true if the Yahoo Widgets globals should be predefined 37 | windows : false // true, if MS Windows-specific globals should be predefined 38 | }, 39 | jshintOptions: { 40 | asi : true, // true, if automatic semicolon insertion should be tolerated 41 | bitwise : true, // true, if bitwise operators should not be allowed 42 | boss : true, // true, if advanced usage of assignments should be allowed 43 | browser : true, // true, if the standard browser globals should be predefined 44 | couch : true, // true, if CouchDB globals should be predefined 45 | curly : true, // true, if curly braces around all blocks should be required 46 | debug : true, // true, if debugger statements should be allowed 47 | devel : true, // true, if logging globals should be predefined (console, alert, etc.) 48 | dojo : true, // true, if Dojo Toolkit globals should be predefined 49 | eqeqeq : true, // true, if === should be required 50 | eqnull : true, // true, if == null comparisons should be tolerated 51 | es5 : true, // true, if ES5 syntax should be allowed 52 | esnext : true, // true, if es.next specific syntax should be allowed 53 | evil : true, // true, if eval should be allowed 54 | expr : true, // true, if ExpressionStatement should be allowed as Programs 55 | forin : true, // true, if for in statements must filter 56 | funcscope : true, // true, if only function scope should be used for scope tests 57 | globalstrict: true, // true, if global "use strict"; should be allowed (also enables 'strict') 58 | immed : true, // true, if immediate invocations must be wrapped in parens 59 | iterator : true, // true, if the `__iterator__` property should be allowed 60 | jquery : true, // true, if jQuery globals should be predefined 61 | lastsemic : true, // true, if semicolons may be ommitted for the trailing statements inside of a one-line blocks. 62 | latedef : true, // true, if the use before definition should not be tolerated 63 | laxbreak : true, // true, if line breaks should not be checked 64 | laxcomma : true, // true, if line breaks should not be checked around commas 65 | loopfunc : true, // true, if functions should be allowed to be defined within loops 66 | mootools : true, // true, if MooTools globals should be predefined 67 | multistr : true, // true, allow multiline strings 68 | newcap : true, // true, if constructor names must be capitalized 69 | noarg : true, // true, if arguments.caller and arguments.callee should be disallowed 70 | node : true, // true, if the Node.js environment globals should be predefined 71 | noempty : true, // true, if empty blocks should be disallowed 72 | nonew : true, // true, if using `new` for side-effects should be disallowed 73 | nonstandard : true, // true, if non-standard (but widely adopted) globals should be predefined 74 | nomen : true, // true, if names should be checked 75 | onevar : true, // true, if only one var statement per function should be allowed 76 | onecase : true, // true, if one case switch statements should be allowed 77 | passfail : true, // true, if the scan should stop on first error 78 | plusplus : true, // true, if increment/decrement should not be allowed 79 | proto : true, // true, if the `__proto__` property should be allowed 80 | prototypejs : true, // true, if Prototype and Scriptaculous globals should be predefined 81 | regexdash : true, // true, if unescaped first/last dash (-) inside brackets should be tolerated 82 | regexp : true, // true, if the . should not be allowed in regexp literals 83 | rhino : true, // true, if the Rhino environment globals should be predefined 84 | undef : true, // true, if variables should be declared before used 85 | scripturl : true, // true, if script-targeted URLs should be tolerated 86 | shadow : true, // true, if variable shadowing should be tolerated 87 | smarttabs : true, // true, if smarttabs should be tolerated (http://www.emacswiki.org/emacs/SmartTabs) 88 | strict : true, // true, if the "use strict"; pragma is required 89 | sub : true, // true, if all forms of subscript notation are tolerated 90 | supernew : true, // true, if `new function () { ... };` and `new Object;` should be tolerated 91 | trailing : true, // true, if trailing whitespace rules apply 92 | validthis : true, // true, if 'this' inside a non-constructor function is valid. 93 | white : true, // true, if strict whitespace rules apply 94 | wsh : true // true, if the Windows Scripting Host environment globals should be predefined 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Autolint 2 | 3 | Autolint watches your files for jslint-errors. DRY up your js-files, freeing 4 | them of all those linting config comments. Gather all your linting preferences 5 | in one place per project. 6 | 7 | ## Installation 8 | 9 | Make sure you've got [node.js](http://nodejs.org/) and [npm](http://npmjs.org/), then: 10 | 11 | npm install autolint -g 12 | 13 | ## Basic usage 14 | 15 | Create a default configuration file by running: 16 | 17 | autolint 18 | 19 | Tweak the config to your liking, then start linting with: 20 | 21 | autolint 22 | 23 | Once running, you can see all errors in all files by pressing ctrl-c in 24 | the terminal window. To see errors in a single file, update its mtime by 25 | saving or touching it. 26 | 27 | You can also skip the watching-part, and just lint the entire project once: 28 | 29 | autolint --once 30 | 31 | It terminates with a non-zero exit code if any lint is found, making it 32 | well suited for pre-commit hooks if you are so inclined. 33 | 34 | If you're confused by the linting error messages, check out [jslinterrors.com](http://jslinterrors.com/). 35 | 36 | There's also an 37 | [autolint maven plugin](https://github.com/magnars/autolint-maven-plugin) 38 | that's handy for continuous integration. 39 | 40 | ## Configuration 41 | 42 | Look at the default configuration 43 | [`lib/default-configuration.js`](autolint/blob/master/lib/default-configuration.js) 44 | then override specific items in `autolint.js`. `autolint` always looks for this 45 | file in the current directory. 46 | 47 | Example: 48 | 49 | module.exports = { 50 | paths: [ 51 | "lib/**/*.js", 52 | "test/**/*.js" 53 | ], 54 | linterOptions: { 55 | node: true 56 | } 57 | }; 58 | 59 | ### Excluding files 60 | 61 | You can also tell autolint to skip linting some files, like so: 62 | 63 | module.exports = { 64 | excludes: [ 65 | "jquery", 66 | "underscore", 67 | "sinon", 68 | "raphael" 69 | ] 70 | }; 71 | 72 | Any files (or paths) containing those words will not be linted, or counted 73 | towards your error total. 74 | 75 | ## Growl 76 | 77 | If you want autolint to notify you when new lint errors are introduced, 78 | you can [download Growl here](http://growl.info/). 79 | 80 | ## Using JSHint 81 | 82 | If JSLint is hurting your feelings, you can easily switch to 83 | [JSHint](http://jshint.com) by adding this to your configuration: 84 | 85 | module.exports = { 86 | "linter": "jshint" 87 | }; 88 | 89 | ## Linter options 90 | 91 | The defaults are very strict, so tweak them to your liking. 92 | 93 | For jslint: 94 | 95 | module.exports = { 96 | linterOptions: { 97 | indent : 4, // the indentation factor 98 | maxlen : 80, // the maximum length of a source line 99 | maxerr : 50, // the maximum number of errors to report per file 100 | anon : false, // true, if the space may be omitted in anonymous function declarations 101 | bitwise : false, // true, if bitwise operators should be allowed 102 | browser : false, // true, if the standard browser globals should be predefined 103 | cap : false, // true, if upper case HTML should be allowed 104 | "continue" : false, // true, if the continuation statement should be tolerated 105 | css : false, // true, if CSS workarounds should be tolerated 106 | debug : false, // true, if debugger statements should be allowed 107 | devel : false, // true, if logging should be allowed (console, alert, etc.) 108 | eqeq : false, // true, if == should be allowed 109 | es5 : false, // true, if ES5 syntax should be allowed 110 | evil : false, // true, if eval should be allowed 111 | forin : false, // true, if for in statements need not filter 112 | fragment : false, // true, if HTML fragments should be allowed 113 | newcap : false, // true, if constructor names capitalization is ignored 114 | node : false, // true, if Node.js globals should be predefined 115 | nomen : false, // true, if names may have dangling _ 116 | on : false, // true, if HTML event handlers should be allowed 117 | passfail : false, // true, if the scan should stop on first error 118 | plusplus : false, // true, if increment/decrement should be allowed 119 | properties : false, // true, if all property names must be declared with /*properties*/ 120 | regexp : false, // true, if the . should be allowed in regexp literals 121 | rhino : false, // true, if the Rhino environment globals should be predefined 122 | undef : false, // true, if variables can be declared out of order 123 | unparam : false, // true, if unused parameters should be tolerated 124 | sloppy : false, // true, if the 'use strict'; pragma is optional 125 | sub : false, // true, if all forms of subscript notation are tolerated 126 | vars : false, // true, if multiple var statements per function should be allowed 127 | white : false, // true, if sloppy whitespace is tolerated 128 | widget : false, // true if the Yahoo Widgets globals should be predefined 129 | windows : false // true, if MS Windows-specific globals should be predefined 130 | } 131 | }; 132 | 133 | For jshint: 134 | 135 | module.exports = { 136 | linterOptions: { 137 | asi : true, // true, if automatic semicolon insertion should be tolerated 138 | bitwise : true, // true, if bitwise operators should not be allowed 139 | boss : true, // true, if advanced usage of assignments should be allowed 140 | browser : true, // true, if the standard browser globals should be predefined 141 | couch : true, // true, if CouchDB globals should be predefined 142 | curly : true, // true, if curly braces around all blocks should be required 143 | debug : true, // true, if debugger statements should be allowed 144 | devel : true, // true, if logging globals should be predefined (console, alert, etc.) 145 | dojo : true, // true, if Dojo Toolkit globals should be predefined 146 | eqeqeq : true, // true, if === should be required 147 | eqnull : true, // true, if == null comparisons should be tolerated 148 | es5 : true, // true, if ES5 syntax should be allowed 149 | esnext : true, // true, if es.next specific syntax should be allowed 150 | evil : true, // true, if eval should be allowed 151 | expr : true, // true, if ExpressionStatement should be allowed as Programs 152 | forin : true, // true, if for in statements must filter 153 | funcscope : true, // true, if only function scope should be used for scope tests 154 | globalstrict: true, // true, if global "use strict"; should be allowed (also enables 'strict') 155 | immed : true, // true, if immediate invocations must be wrapped in parens 156 | iterator : true, // true, if the `__iterator__` property should be allowed 157 | jquery : true, // true, if jQuery globals should be predefined 158 | lastsemic : true, // true, if semicolons may be ommitted for the trailing statements inside of a one-line blocks. 159 | latedef : true, // true, if the use before definition should not be tolerated 160 | laxbreak : true, // true, if line breaks should not be checked 161 | laxcomma : true, // true, if line breaks should not be checked around commas 162 | loopfunc : true, // true, if functions should be allowed to be defined within loops 163 | mootools : true, // true, if MooTools globals should be predefined 164 | multistr : true, // true, allow multiline strings 165 | newcap : true, // true, if constructor names must be capitalized 166 | noarg : true, // true, if arguments.caller and arguments.callee should be disallowed 167 | node : true, // true, if the Node.js environment globals should be predefined 168 | noempty : true, // true, if empty blocks should be disallowed 169 | nonew : true, // true, if using `new` for side-effects should be disallowed 170 | nonstandard : true, // true, if non-standard (but widely adopted) globals should be predefined 171 | nomen : true, // true, if names should be checked 172 | onevar : true, // true, if only one var statement per function should be allowed 173 | onecase : true, // true, if one case switch statements should be allowed 174 | passfail : true, // true, if the scan should stop on first error 175 | plusplus : true, // true, if increment/decrement should not be allowed 176 | proto : true, // true, if the `__proto__` property should be allowed 177 | prototypejs : true, // true, if Prototype and Scriptaculous globals should be predefined 178 | regexdash : true, // true, if unescaped first/last dash (-) inside brackets should be tolerated 179 | regexp : true, // true, if the . should not be allowed in regexp literals 180 | rhino : true, // true, if the Rhino environment globals should be predefined 181 | undef : true, // true, if variables should be declared before used 182 | scripturl : true, // true, if script-targeted URLs should be tolerated 183 | shadow : true, // true, if variable shadowing should be tolerated 184 | smarttabs : true, // true, if smarttabs should be tolerated (http://www.emacswiki.org/emacs/SmartTabs) 185 | strict : true, // true, if the "use strict"; pragma is required 186 | sub : true, // true, if all forms of subscript notation are tolerated 187 | supernew : true, // true, if `new function () { ... };` and `new Object;` should be tolerated 188 | trailing : true, // true, if trailing whitespace rules apply 189 | validthis : true, // true, if 'this' inside a non-constructor function is valid. 190 | white : true, // true, if strict whitespace rules apply 191 | wsh : true // true, if the Windows Scripting Host environment globals should be predefined 192 | } 193 | }; 194 | 195 | The rules in autolint.js are project-wide, but you can still have file and function specific rules, like this: 196 | 197 | /*jslint bitwise:true*/ 198 | 199 | Adding it to the top of the file will allow bitwise operators in the entire file, or you can add it to a single function: 200 | 201 | function justHere() { 202 | /*jslint bitwise:true*/ 203 | return 1 << 1; 204 | } 205 | 206 | ## Changes 207 | 208 | ### 1.1.4 209 | 210 | * Upgrade to a windows compatible version of node-growl. ([Stian Veum Møllersen](https://github.com/mollerse)) 211 | 212 | ### 1.1.3 213 | 214 | * Better stability of file-watching. 215 | 216 | ### 1.1.0 217 | 218 | * The configuration file can now also be called `autolint-config.js` 219 | to avoid issues where Windows will try to execute the `autolint.js` 220 | config file when running `autolint`. 221 | 222 | * Now supports the exit signal on Windows to check all files. 223 | 224 | ### 1.0.0 225 | 226 | Autolint now uses [semantic versioning](http://semver.org). 227 | 228 | * The configuration file is no longer a `json`-file, but a proper node 229 | module. Add `module.exports =` to the start of the file and rename to 230 | `autolint.js` to upgrade. 231 | * Autolint no longer runs without a config file. Running it without one will 232 | prompt you for a default config file to be created. 233 | * Passing `--once` to autolint makes it not-so-auto. Instead it is run once, 234 | exiting with a `-1` error code if any lint is found. This makes it well 235 | suited for pre-commit-hooks and the like. 236 | * Updated bundled versions of jslint and jshint - these have been significantly 237 | changed since last, so your configuration file will certainly need an upgrade 238 | too. 239 | 240 | ## Contributors 241 | 242 | * [Stian Veum Møllersen](https://github.com/mollerse) upgraded to a windows compatible version of node-growl. 243 | 244 | Thanks! 245 | 246 | ## Contribute 247 | 248 | If you want to help out with features or bug fixes, that's awesome. 249 | Check out [`todo.org`](autolint/blob/master/todo.org) for inspiration. 250 | 251 | * Fork the project. 252 | * Make your feature addition or bug fix. 253 | * Don't forget tests. This is important so I don't break it in a 254 | future version unintentionally. 255 | * Commit and send me a pull request. 256 | 257 | ### Setting up development environment 258 | 259 | Check out the source code from your fork: 260 | 261 | git clone 262 | cd autolint 263 | 264 | Install [buster.js](http://busterjs.org) if you haven't already: 265 | 266 | npm install buster -g 267 | 268 | Then link buster in: 269 | 270 | npm link buster 271 | 272 | Fetch the dependencies with npm: 273 | 274 | npm install 275 | 276 | Run the tests to make sure everything works: 277 | 278 | buster test 279 | 280 | Install [watchr](https://github.com/mynyml/watchr) to run the tests automatically: 281 | 282 | gem install watchr 283 | 284 | Then start the autotest with: 285 | 286 | watchr watch-tests.watchr 287 | 288 | If watchr can't be interrupted with 2x ctrl-c, switch to ruby ~1.9 289 | 290 | Also make sure you follow the linting rules with: 291 | 292 | autolint 293 | 294 | of course. ^^ 295 | 296 | ## License 297 | 298 | See LICENSE file. 299 | --------------------------------------------------------------------------------