Skip to content

Commit

Permalink
Use JSHint src/cli/cli.js directly. Fixes GH-35, fixes GH-34, fixes G…
Browse files Browse the repository at this point in the history
…H-31, fixes GH-13, fixes GH-8.
  • Loading branch information
shama committed Apr 22, 2013
1 parent d8d127d commit 9285d9f
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 75 deletions.
4 changes: 2 additions & 2 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ module.exports = function(grunt) {
jshint: {
all_files: [
'Gruntfile.js',
'tasks/*.js',
'tasks/**/*.js',
'<%= nodeunit.tests %>'
],
individual_files: {
files: [
{src: 'Gruntfile.js'},
{src: 'tasks/*.js'},
{src: 'tasks/**/*.js'},
{src: '<%= nodeunit.tests %>'},
]
},
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ Default value: `false`

Set `force` to `true` to report JSHint errors but not fail the task.

#### reporter
Type: `String`
Default value: `null`

Allows you to modify this plugins output. By default it will use a built-in Grunt reporter. Set the path to your own custom reporter or to one of the built-in JSHint reporters: `jslint` or `checkstyle`.

See also: [Writing your own JSHint reporter.](http://jshint.com/docs/reporter/)

### Usage examples

#### Wildcards
Expand Down Expand Up @@ -149,4 +157,4 @@ grunt.initConfig({

Task submitted by ["Cowboy" Ben Alman](http://benalman.com/)

*This file was generated on Mon Apr 08 2013 21:43:19.*
*This file was generated on Mon Apr 22 2013 13:09:34.*
8 changes: 8 additions & 0 deletions docs/jshint-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,11 @@ Type: `Boolean`
Default value: `false`

Set `force` to `true` to report JSHint errors but not fail the task.

## reporter
Type: `String`
Default value: `null`

Allows you to modify this plugins output. By default it will use a built-in Grunt reporter. Set the path to your own custom reporter or to one of the built-in JSHint reporters: `jslint` or `checkstyle`.

See also: [Writing your own JSHint reporter.](http://jshint.com/docs/reporter/)
47 changes: 12 additions & 35 deletions tasks/jshint.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@

module.exports = function(grunt) {

// Internal lib.
var jshint = require('./lib/jshint').init(grunt);

grunt.registerMultiTask('jshint', 'Validate files with JSHint.', function() {
var done = this.async();

// Merge task-specific and/or target-specific options with these defaults.
var options = this.options({
force: false
Expand All @@ -23,42 +24,18 @@ module.exports = function(grunt) {
var force = options.force;
delete options.force;

// Read JSHint options from a specified jshintrc file.
if (options.jshintrc) {
options = grunt.file.readJSON(options.jshintrc);
}
// If globals weren't specified, initialize them as an empty object.
if (!options.globals) {
options.globals = {};
}
// Convert deprecated "predef" array|object into globals.
if (options.predef) {
if (!Array.isArray(options.predef) && typeof options.predef === "object") {
options.predef = Object.keys(options.predef);
jshint.lint(this.filesSrc, options, function(results, data) {
var failed = 0;
if (grunt.fail.errorcount > 0) {
// Fail task if errors were logged except if force was set.
failed = force;
} else {
if (jshint.usingGruntReporter === true) {
grunt.log.ok(data.length + ' file' + (data.length === 1 ? '' : 's') + ' lint free.');
}
}
options.predef.forEach(function(key) {
options.globals[key] = true;
});
delete options.predef;
}
// Extract globals from options.
var globals = options.globals;
delete options.globals;

grunt.verbose.writeflags(options, 'JSHint options');
grunt.verbose.writeflags(globals, 'JSHint globals');

// Lint specified files.
var files = this.filesSrc;
files.forEach(function(filepath) {
jshint.lint(grunt.file.read(filepath), options, globals, filepath);
done(failed);
});

// Fail task if errors were logged except if force was set.
if (this.errorCount) { return force; }

// Otherwise, print a success message.
grunt.log.ok(files.length + ' file' + (files.length === 1 ? '' : 's') + ' lint free.');
});

};
139 changes: 109 additions & 30 deletions tasks/lib/jshint.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@

'use strict';

// External libs.
var path = require('path');
var jshint = require('jshint').JSHINT;
var jshintcli = require('jshint/src/cli/cli');

exports.init = function(grunt) {
var exports = {};
var exports = {
usingGruntReporter: false
};

// No idea why JSHint treats tabs as options.indent # characters wide, but it
// does. See issue: https://github.com/jshint/jshint/issues/430
Expand All @@ -31,45 +34,75 @@ exports.init = function(grunt) {

var tabregex = /\t/g;

// Lint source code with JSHint.
exports.lint = function(src, options, globals, extraMsg) {
// JSHint sometimes modifies objects you pass in, so clone them.
options = options ? grunt.util._.clone(options) : {};
globals = globals ? grunt.util._.clone(globals) : {};
// Enable/disable debugging if option explicitly set.
if (grunt.option('debug') !== undefined) {
options.devel = options.debug = grunt.option('debug');
// Tweak a few things.
if (grunt.option('debug')) {
options.maxerr = Infinity;
// Select a reporter (if not using the default Grunt reporter)
// Copied from jshint/src/cli/cli.js until that part is exposed
exports.selectReporter = function(options) {
switch (true) {
// JSLint reporter
case options.reporter === 'jslint':
case options['jslint-reporter']:
options.reporter = 'jshint/src/reporters/jslint_xml.js';
break;

// CheckStyle (XML) reporter
case options.reporter === 'checkstyle':
case options['checkstyle-reporter']:
options.reporter = 'jshint/src/reporters/checkstyle.js';
break;

// Reporter that displays additional JSHint data
case options['show-non-errors']:
options.reporter = 'jshint/src/reporters/non_error.js';
break;

// Custom reporter
case options.reporter !== undefined:
options.reporter = path.resolve(process.cwd(), options.reporter);
}

var reporter;
if (options.reporter) {
try {
reporter = require(options.reporter).reporter;
exports.usingGruntReporter = false;
} catch (err) {
grunt.fatal(err);
}
}
var msg = 'Linting' + (extraMsg ? ' ' + extraMsg : '') + '...';

// Use the default Grunt reporter if none are found
if (!reporter) {
reporter = exports.reporter;
exports.usingGruntReporter = true;
}

return reporter;
};

// Default Grunt JSHint reporter
exports.reporter = function(results, data) {
var msg = 'Linting' + (data[0].file ? ' ' + data[0].file : '') + '...';
grunt.verbose.write(msg);
// Tab size as reported by JSHint.
var tabstr = getTabStr(options);
var placeholderregex = new RegExp(tabstr, 'g');
// Lint.
var result = jshint(src, options || {}, globals || {});
// Attempt to work around JSHint erroneously reporting bugs.
// if (!result) {
// // Filter out errors that shouldn't be reported.
// jshint.errors = jshint.errors.filter(function(o) {
// return o && o.something === 'something';
// });
// // If no errors are left, JSHint actually succeeded.
// result = jshint.errors.length === 0;
// }
if (result) {

if (results.length === 0) {
// Success!
grunt.verbose.ok();
return;
}

var options = data[0].options;

// Tab size as reported by JSHint.
var tabstr = getTabStr(options);
var placeholderregex = new RegExp(tabstr, 'g');

// Something went wrong.
grunt.verbose.or.write(msg);
grunt.log.error();

// Iterate over all errors.
jshint.errors.forEach(function(e) {
results.forEach(function(result) {
var e = result.error;
// Sometimes there's no error object.
if (!e) { return; }
var pos;
Expand Down Expand Up @@ -112,5 +145,51 @@ exports.init = function(grunt) {
grunt.log.writeln();
};

// Run JSHint on the given files with the given options
exports.lint = function(files, options, done) {
// A list of non-dot-js extensions to check
var extraExt = options['extra-ext'] || 'js';
delete options['extra-ext'];

// Select a reporter to use
var reporter = exports.selectReporter(options);

// Read JSHint options from a specified jshintrc file.
if (options.jshintrc) {
options = grunt.file.readJSON(options.jshintrc);
}

grunt.verbose.writeflags(options, 'JSHint options');

// Enable/disable debugging if option explicitly set.
if (grunt.option('debug') !== undefined) {
options.devel = options.debug = grunt.option('debug');
// Tweak a few things.
if (grunt.option('debug')) {
options.maxerr = Infinity;
}
}

// Run JSHint on each file and collect results/data
var allResults = [];
var allData = [];
grunt.util.async.forEach(files, function(filepath, next) {
jshintcli.run({
args: [filepath],
extensions: extraExt,
config: options,
reporter: function(results, data) {
reporter(results, data);
allResults = allResults.concat(results);
allData = allData.concat(data);
next();
},
verbose: grunt.option('verbose')
});
}, function() {
done(allResults, allData);
});
};

return exports;
};
1 change: 1 addition & 0 deletions test/fixtures/missingsemicolon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
var missingsemicolon = true
4 changes: 4 additions & 0 deletions test/fixtures/nodemodule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'use strict';

module.exports = function() {
};
86 changes: 79 additions & 7 deletions test/jshint_test.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,87 @@
'use strict';

var path = require('path');
var grunt = require('grunt');
var jshint = require('../tasks/lib/jshint').init(grunt);

exports['jshint'] = function(test) {
test.expect(1);
grunt.log.muted = true;
var fixtures = path.join(__dirname, 'fixtures');

test.doesNotThrow(function() {
jshint.lint(grunt.file.read('test/fixtures/lint.txt'));
}, 'It should not blow up if an error occurs on character 0.');
// Helper for testing stdout
var hooker = grunt.util.hooker;
var stdoutEqual = function(callback, done) {
var actual = '';
// Hook process.stdout.write
hooker.hook(process.stdout, 'write', {
// This gets executed before the original process.stdout.write.
pre: function(result) {
// Concatenate uncolored result onto actual.
actual += grunt.log.uncolor(result);
// Prevent the original process.stdout.write from executing.
return hooker.preempt();
}
});
// Execute the logging code to be tested.
callback();
// Restore process.stdout.write to its original value.
hooker.unhook(process.stdout, 'write');
// Actually test the actually-logged stdout string to the expected value.
done(actual);
};

test.done();
exports.jshint = {
basic: function(test) {
test.expect(1);
var files = [path.join(fixtures, 'missingsemicolon.js')];
var options = {};
jshint.lint(files, options, function(results, data) {
test.equal(results[0].error.reason, 'Missing semicolon.', 'Should reporter a missing semicolon.');
test.done();
});
},
jshintrc: function(test) {
test.expect(1);
var files = [path.join(fixtures, 'nodemodule.js')];
var options = {
jshintrc: path.join(__dirname, '..', '.jshintrc')
};
jshint.lint(files, options, function(results, data) {
test.ok(results.length === 0, 'Should not have reported any errors with supplied .jshintrc');
test.done();
});
},
defaultReporter: function(test) {
test.expect(2);
grunt.log.muted = false;
var files = [path.join(fixtures, 'nodemodule.js')];
var options = {};
stdoutEqual(function() {
jshint.lint(files, options, function(results, data) {});
}, function(result) {
test.ok(jshint.usingGruntReporter, 'Should be using the default grunt reporter.');
test.ok(result.indexOf('[L3:C1] W117: \'module\' is not defined.') !== -1, 'Should have reported errors with the default grunt reporter.');
test.done();
});
},
alternateReporter: function(test) {
test.expect(2);
var files = [path.join(fixtures, 'nodemodule.js')];
var options = {
reporter: 'jslint'
};
stdoutEqual(function() {
jshint.lint(files, options, function(results, data) {});
}, function(result) {
test.ok((jshint.usingGruntReporter === false), 'Should NOT be using the default grunt reporter.');
test.ok(result.indexOf('<jslint>') !== -1, 'Should have reported errors with the jslint reporter.');
test.done();
});
},
dontBlowUp: function(test) {
test.expect(1);
var files = [path.join(fixtures, 'lint.txt')];
jshint.lint(files, {}, function(results, data) {
test.equal(results[0].error.code, 'W100', 'It should not blow up if an error occurs on character 0.');
test.done();
});
},
};

0 comments on commit 9285d9f

Please sign in to comment.