lib/Nlint.js
var fs = require( 'fs' ),
async = require( 'async' ),
JSON5 = require( 'json5' ),
child_process = require( 'child_process' ),
rstartslash = /^\//,
rendslash = /\/$/,
rpathseparator = /\//g,
rjson = /\.json5?$/,
rfilename = /\/[^\/]+$/,
NlintFiles = [
'.nlint',
'.nlint.json',
'.nlint.json5',
'.nlint.js',
];
function Nlint( files, options, callback ) {
var self = this;
// Force Nlint instance
if ( ! ( self instanceof Nlint ) ) {
return new Nlint( files, options, callback );
}
// Force a file/dir to be defined
else if ( ! Nlint.isArray( files ) && ! Nlint.isString( files ) ) {
throw new Error( "No Files Argument" );
}
// Handle variable args
if ( callback === undefined && Nlint.isFunction( options ) ) {
callback = options || Nlint.noop;
options = null;
}
// For array of files
if ( ! Nlint.isArray( files ) ) {
files = [ files ];
}
// Generate events on this object
Nlint.Event( self );
// Setup
self.files = files;
self.options = Nlint.extend( true, {}, options || {} );
self.settings = new Nlint.Settings( Nlint.Defaults.Settings, self );
self.settings.update( self.options );
self.results = [];
self.ignored = [];
self.nodelints = [];
self._nodelints = {};
self._forks = [];
self._queue = [];
self._jobs = {};
self.fileCount = 0;
self.fileComplete = 0;
// Start processing
self.run( callback || Nlint.noop );
}
Nlint.prototype = {
// Starter for linting
run: function( callback ) {
var self = this;
// Start processing each file
async.each(
self.files,
function( path, callback ) {
path = Nlint.normalizePath( path );
fs.stat( path, function( e, stat ) {
if ( e ) {
callback( e );
}
else if ( stat.isDirectory() ) {
path = path[ path.length - 1 ] == '/' ? path : path + '/';
if ( self.files.length === 1 ) {
self.nodelint( path, function( e, settings ) {
if ( e ) {
return callback( e );
}
self.settings = settings;
self.forks();
self.dir( path, callback );
});
}
else {
self.forks();
self.dir( path, callback );
}
}
else {
self.forks();
self.single( path, callback );
}
});
},
function( e ) {
if ( e ) {
return callback( e, self );
}
self.close(function( e ) {
callback( e, self );
});
}
);
},
// Creates list of forks that will be used in the process
forks: function(){
var self = this;
if ( self._forks.length ) {
return;
}
Nlint.each( self.settings.fork, function(){
var child = child_process.fork( Nlint.FORK_FILE );
// Fail out when something goes wrong
child.on( 'error', function( e ) {
throw e;
});
// Lint job complete
child.on( 'message', function( message ) {
var job = self._jobs[ message.id ];
if ( job ) {
delete self._jobs[ job.id ];
self._forks.push( child );
job.callback.apply( self, message.args );
}
self._checkQueue();
});
// Closing of child
[ 'exit', 'close' ].forEach(function( name ) {
child.on( name, function(){
var index = self._forks.indexOf( child );
if ( index > -1 ) {
self._forks.splice( index, 1 );
}
if ( ! self._forks.length ) {
throw new Error( "All forks have failed" );
}
});
});
self._forks.push( child );
});
},
// Close off any any forks created for linters
close: function( callback ) {
var self = this;
async.each(
self._forks,
function( child, callback ) {
child.removeAllListeners();
[ 'exit', 'close' ].forEach(function( name ) {
child.on( name, function(){
if ( callback ) {
callback();
callback = null;
}
});
});
child.kill();
},
callback
);
},
// Renders a path based on options
render: function( path, settings, callback ) {
var self = this,
result = new Nlint.FileResult( path );
// Block paths that should be ignored
if ( settings.ignore( path ) ) {
self.ignored.push( path );
return callback();
}
// Add to results
self.results.push( result );
// Filter through list of linters to find one that will render the path
async.each(
Nlint.Linters,
function( linter, callback ) {
if ( linter.isMatch( path, settings, self ) && settings.use( linter.lname ) ) {
self._runLint( result, settings, linter, callback );
}
else {
callback();
}
},
callback
);
},
// Runs the actual linter on the path
_runLint: function( result, settings, linter, callback ){
var self = this,
// We have to create a copy of the options used for each linter in case
// the linter itself alters the options
// (First culprit being JSHint 2.1.4)
options = Nlint.extend( true, {}, settings.linters( linter.lname ) ),
// Callback for when rendering is finished
complete = function( e, errors, warnings, times ) {
if ( e ) {
return callback( e );
}
// Progress
self.fileComplete++;
self.emit( 'progress', result.path );
// Add results to the file object
try {
result.addResults( errors, warnings, times, linter.name );
}
catch ( formatError ) {
e = formatError;
}
callback( e );
};
// progress
self.fileCount++;
self.emit( 'progress' );
// Use a forked process to run the lint job
if ( self.settings.fork > 0 ) {
self._queue.push({
id: self.fileCount,
callback: complete,
send: {
id: self.fileCount,
path: result.path,
linter: linter.path,
settings: options
}
});
self._checkQueue();
}
// Run linter in the main process (potentially blocks)
else {
linter.render( result.path, options, complete );
}
},
// Checks fork queues to see if another path can be processed
_checkQueue: function(){
var self = this, job;
if ( self._queue.length && self._forks.length ) {
job = self._queue.shift();
self._jobs[ job.id ] = job;
self._forks.shift().send( job.send );
}
},
// Handles single file linting
single: function( path, callback ) {
var self = this;
self.nodelint( path.replace( rfilename, '/' ), function( e, settings ) {
if ( e ) {
return callback( e );
}
self.render( path, settings, callback );
});
},
// Handles directory of linting
dir: function( path, callback ) {
var self = this;
async.parallel([
function( callback ) {
fs.readdir( path, callback );
},
function( callback ) {
self.nodelint( path, callback );
},
],
function( e, results ) {
if ( e ) {
callback( e );
}
else {
self._dir( path, results[ 0 ], results[ 1 ], callback );
}
});
},
// Renders files or traverses subdirectories
_dir: function( path, files, settings, callback ) {
var self = this;
async.each(
files,
function( file, callback ) {
var filepath = path + file, full;
fs.stat( filepath, function( e, stat ) {
// There shouldn't be any errors, unless the file isn't accessible
if ( e ) {
callback( e );
}
// Follow the rabbit hole
else if ( stat.isDirectory() ) {
full = filepath[ filepath.length - 1 ] == '/' ? filepath : filepath + '/';
if ( settings.ignore( full ) ) {
self.ignored.push( full );
return callback();
}
self.dir( full, callback );
}
// Render each file (will handle ignores internally)
else {
self.render( filepath, settings, callback );
}
});
},
callback
);
},
// Pulls nodelint files together, and generates options object based on the current directory
nodelint: function( path, callback ) {
var self = this,
parts = path.replace( rendslash, '' ).split( rpathseparator ),
prefix = '';
async.map(
parts,
function( dir, callback ) {
self._nodelint( prefix += dir + '/', callback );
},
function( e ) {
callback( e, e ? undefined : self._renderSettings( path ) );
}
);
},
// Renders all possible Nlint config files in a directory
_nodelint: function( prefix, callback ) {
var self = this;
async.each(
NlintFiles,
function( ignore, callback ) {
var path = prefix + ignore;
// Use pre-read options if possible
if ( self._nodelints[ path ] === undefined ) {
self._renderNodelint( path, callback );
}
else {
callback();
}
},
callback
);
},
// Parses nodelint files and stores them
_renderNodelint: function( path, callback ) {
var self = this;
// Read contents and store them
fs.stat( path, function( e, stat ) {
var result = null;
// File isn't accessible
if ( e ) {
self._nodelints[ path ] = null;
return callback();
}
// Logger
self.nodelints.push( path );
// JSON/JSON5 nodelint files
if ( rjson.exec( path ) ) {
fs.readFile( path, 'utf8', function( e, contents ) {
if ( e ) {
return callback( e );
}
try {
self._nodelints[ path ] = JSON5.parse( contents );
}
catch ( jsonError ) {
self._nodelints[ path ] = null;
e = jsonError;
}
callback( e );
});
}
// Nlint js files have options assigned to module.exports
else {
result = Nlint.require( path );
// Mark as read(null) if require didn't work
self._nodelints[ path ] = result.result || null;
// Pass along any possible error
callback( result.error );
}
});
},
// Generates a settings object for the directory
_renderSettings: function( path ) {
var self = this,
parts = path.split( rpathseparator ),
settings = new Nlint.Settings( Nlint.Defaults.Settings, self ),
prefix = '';
// Cycle through each parent directory for nodelint updates
parts.forEach(function( dir ) {
prefix += dir + '/';
NlintFiles.forEach(function( ignore ) {
var key = prefix + ignore;
if ( self._nodelints[ key ] ) {
settings.update( self._nodelints[ key ], prefix );
}
});
});
// Options passed into root instance override all others
settings.update( self.options );
return settings;
}
};
// Expose Nlint
module.exports = Nlint;