blob: d56a0c5a5905edf752f7c8ebf6013bf8dd10c804 [file] [log] [blame]
"use strict";
var _ = require("underscore");
var parser = require("esprima");
var events = require("events");
var utils = require("./utils.js");
var reason = require("./reason.js");
var regexp = require("./regexp.js");
var vars = require("../shared/vars.js");
var MAXERR = 50;
// Converts errors spitted out by Esprima into JSHint errors.
function esprima(linter) {
linter.on("lint:end", function () {
var mapping = {
"Illegal return statement": "E003",
"Strict mode code may not include a with statement": "E002"
};
_.each(linter.tree.errors, function (err) {
var msg = err.message.split(": ")[1];
linter.report.addError(mapping[msg], err.lineNumber);
});
});
}
function Linter(code) {
this.code = code;
this.config = {};
this.tree = {};
this.scopes = new utils.ScopeStack();
this.report = new utils.Report(code);
this.tokens = null;
this.modules = [];
this.emitter = new events.EventEmitter();
this.addModule(esprima);
this.addModule(reason.register);
this.addModule(regexp.register);
// Pre-populate globals array with reserved variables,
// standard ECMAScript globals and user-supplied globals.
this.setGlobals(vars.reservedVars);
this.setGlobals(vars.ecmaIdentifiers);
this.setGlobals({ "undefined": false });
}
Linter.prototype = {
on: function (names, listener) {
var self = this;
names.split(" ").forEach(function (name) {
self.emitter.on(name, listener);
});
},
trigger: function () {
this.emitter.emit.apply(this.emitter, Array.prototype.slice.call(arguments));
},
addModule: function (func) {
this.modules.push(func);
},
setGlobals: function (globals) {
var scopes = this.scopes;
_.each(globals, function (writeable, name) {
scopes.addGlobalVariable({ name: name, writeable: writeable });
});
},
parse: function () {
var self = this;
self.tree = parser.parse(self.code, {
range: true, // Include range-based location data.
loc: true, // Include column-based location data.
comment: true, // Include a list of all found code comments.
tokens: true, // Include a list of all found tokens.
tolerant: true // Don't break on non-fatal errors.
});
self.tokens = new utils.Tokens(self.tree.tokens);
_.each(self.modules, function (func) {
func(self);
});
function _parseComments(from, to) {
var slice = self.tree.comments.filter(function (comment) {
return comment.range[0] >= from && comment.range[1] <= to;
});
slice.forEach(function (comment) {
comment = utils.parseComment(comment.value);
switch (comment.type) {
case "set":
comment.value.forEach(function (name) {
self.scopes.addSwitch(name);
});
break;
case "ignore":
comment.value.forEach(function (code) {
self.scopes.addIgnore(code);
});
break;
}
});
}
// Walk the tree using recursive* depth-first search and trigger
// appropriate events when needed.
//
// * - and probably horribly inefficient.
function _parse(tree) {
if (tree.type)
self.trigger(tree.type, tree);
if (self.report.length > MAXERR)
return;
_.each(tree, function (val) {
if (val === null)
return;
if (!_.isObject(val) && !_.isArray(val))
return;
switch (val.type) {
case "ExpressionStatement":
if (val.expression.type === "Literal" && val.expression.value === "use strict")
self.scopes.current.strict = true;
_parse(val);
break;
case "FunctionDeclaration":
self.scopes.addVariable({ name: val.id.name });
self.scopes.push(val.id.name);
// If this function is not empty, parse its leading comments (if any).
if (val.body.type === "BlockStatement" && val.body.body.length > 0)
_parseComments(val.range[0], val.body.body[0].range[0]);
_parse(val);
self.scopes.pop();
break;
case "FunctionExpression":
if (val.id && val.id.type === "Identifier")
self.scopes.addVariable({ name: val.id.name });
self.scopes.push("(anon)");
// If this function is not empty, parse its leading comments (if any).
if (val.body.type === "BlockStatement" && val.body.body.length > 0)
_parseComments(val.range[0], val.body.body[0].range[0]);
_parse(val);
self.scopes.pop();
break;
case "WithStatement":
self.scopes.runtimeOnly = true;
_parse(val);
self.scopes.runtimeOnly = false;
break;
default:
_parse(val);
}
});
}
self.trigger("lint:start");
_parseComments(0, self.tree.range[0]);
_parse(self.tree.body);
self.trigger("lint:end");
}
};
function JSHINT(args) {
var linter = new Linter(args.code);
linter.setGlobals(args.predefined || {});
linter.parse();
return {
tree: linter.tree,
report: linter.report
};
}
exports.Linter = Linter;
exports.lint = JSHINT;