Permalink
Browse files

Parse JSHint options from comments.

This patch makes JSHint to parse comments and store all the options
defined within them. Options now have new syntax and new placement
rules:

	* Options can be defined as block comments as well as one-line
		comments.
	* Options comments must come before any other code in the scope.
	* New syntax: 'jshint:[set|ignore] [comma separated values].

Example:

	// jshint:set var

	function main(cb) {
		// jshint:ignore W001
		"use strict";

		cb(function () {
			var a = 1;
			// jshint:set strict
			return a;
		});
	}

The code above sets one global option, var, and ignores all W001
warnings within the function main. Comment on line 9 is ignored
because it comes after the start of the function body.

Relevant:

	GH-16: Ability to set options
	(https://github.com/jshint/jshint-next/issues/16)
  • Loading branch information...
1 parent 98f1727 commit 9cfb2fe6543b5058e2b554a7f0e7419b774332fa @valueof valueof committed Aug 13, 2012
Showing with 174 additions and 19 deletions.
  1. +34 −2 src/jshint.js
  2. +57 −17 src/utils.js
  3. +13 −0 test/fixtures/parser/comments.js
  4. +39 −0 test/unit/parser.js
  5. +31 −0 test/unit/utils.js
View
@@ -85,12 +85,34 @@ Linter.prototype = {
});
self.tokens = new utils.Tokens(self.tree.tokens);
- self.comments = new peakle.Peakle(self.tree.comments);
_.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.
//
@@ -119,14 +141,23 @@ Linter.prototype = {
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;
@@ -142,6 +173,7 @@ Linter.prototype = {
}
self.trigger("lint:start");
+ _parseComments(0, self.tree.range[0]);
_parse(self.tree.body);
self.trigger("lint:end");
}
View
@@ -77,11 +77,13 @@ ScopeStack.prototype = {
this.curid = this.stack.length;
this.stack.push({
- parid: curid,
- name: name,
- strict: false,
- vars: {},
- uses: {}
+ parid: curid,
+ name: name,
+ strict: false,
+ switches: {},
+ ignores: {},
+ vars: {},
+ uses: {}
});
},
@@ -94,11 +96,11 @@ ScopeStack.prototype = {
this.curid = this.current.parid;
},
- isDefined: function (name, env) {
+ any: function (cond, env) {
env = env || this.current;
while (env) {
- if (_.has(env.vars, safe(name)))
+ if (cond.call(env))
return true;
env = this.stack[env.parid];
@@ -107,17 +109,20 @@ ScopeStack.prototype = {
return false;
},
- isStrictMode: function (env) {
- env = env || this.current;
+ isDefined: function (name, env) {
+ return this.any(function () { return _.has(this.vars, safe(name)); }, env);
+ },
- while (env) {
- if (env.strict)
- return true;
+ isStrictMode: function (env) {
+ return this.any(function () { return this.strict; }, env);
+ },
- env = this.stack[env.parid];
- }
+ isSwitchEnabled: function (name, env) {
+ return this.any(function () { return this.switches[name]; }, env);
+ },
- return false;
+ isMessageIgnored: function (code, env) {
+ return this.any(function () { return this.ignores[code]; }, env);
},
addUse: function (name, range) {
@@ -142,6 +147,14 @@ ScopeStack.prototype = {
this.stack[0].vars[safe(opts.name)] = {
writeable: opts.writeable || false
};
+ },
+
+ addSwitch: function (name) {
+ this.current.switches[name] = true;
+ },
+
+ addIgnore: function (name) {
+ this.current.ignores[name] = true;
}
};
@@ -237,9 +250,9 @@ _.each(["Punctuator", "Keyword", "Identifier"], function (name) {
if (!Array.isArray(values))
values = [ values ];
- return _.any(values, _.bind(function (value) {
+ return values.some(function (value) {
return this.type === name && this.value === value;
- }, this));
+ }, this);
};
});
@@ -315,7 +328,34 @@ Tokens.prototype.getRange = function (range) {
return new Tokens(slice);
};
+var commentsCache = {};
+var commentsTypes = { "set": true, "ignore": true };
+
+function parseComment(text) {
+ var parts = text.trim().split(" ");
+ var defval = { type: "text", value: text };
+ var values = [];
+ var head, body;
+
+ if (parts.length === 0)
+ return defval;
+
+ head = parts[0].split(":");
+ if (head[0] !== "jshint" || commentsTypes[head[1]] !== true)
+ return defval;
+
+ body = parts.slice(1).join(" ").split(",").map(function (s) {
+ return s.trim();
+ });
+
+ return {
+ type: head[1],
+ value: body
+ };
+}
+
exports.Report = Report;
exports.Token = Token;
exports.Tokens = Tokens;
exports.ScopeStack = ScopeStack;
+exports.parseComment = parseComment;
@@ -0,0 +1,13 @@
+// jshint:set var
+
+function main(cb) {
+ // jshint:set strict
+ // jshint:ignore W001
+ "use strict";
+
+ cb(function () {
+ // jshint:ignore E001
+ return function () {}; // jshint:ignore E002
+ // jshint:set hula
+ });
+}
View
@@ -41,3 +41,42 @@ exports.testTokens = function (test) {
test.deepEqual(linter.lint({ code: code }).tree.tokens, tokens);
test.done();
};
+
+exports.testComments = function (test) {
+ test.expect(8);
+
+ var linterObj = new linter.Linter(fixtures.get("comments.js"));
+ var addIgnore = linterObj.scopes.addIgnore;
+ var addSwitch = linterObj.scopes.addSwitch;
+
+ var ignores = [
+ [ "main", "W001" ],
+ [ "(anon)", "E001" ]
+ ];
+
+ var switches = [
+ [ "(global)", "var" ],
+ [ "main", "strict" ]
+ ];
+
+ linterObj.scopes.addIgnore = function (code) {
+ var exp = ignores.shift();
+ test.equal(linterObj.scopes.current.name, exp[0]);
+ test.equal(code, exp[1]);
+ addIgnore.call(linterObj.scopes, code);
+ };
+
+ linterObj.scopes.addSwitch = function (name) {
+ var exp = switches.shift();
+ test.equal(linterObj.scopes.current.name, exp[0]);
+ test.equal(name, exp[1]);
+ addSwitch.call(linterObj.scopes, name);
+ };
+
+ linterObj.parse();
+
+ linterObj.scopes.addIgnore = addIgnore;
+ linterObj.scopes.addSwitch = addSwitch;
+
+ test.done();
+};
View
@@ -57,16 +57,25 @@ exports.testScopeStack = function (test) {
test.equal(scope.current.name, "(global)");
scope.addVariable({ name: "weebly" });
+ scope.addSwitch("strict");
+ scope.addIgnore("W002");
scope.current.strict = true;
test.ok(scope.isDefined("weebly"));
+ test.ok(scope.isSwitchEnabled("strict"));
+ test.ok(scope.isMessageIgnored("W002"));
+ test.ok(!scope.isSwitchEnabled("var"));
+ test.ok(!scope.isMessageIgnored("E001"));
scope.push("(anon)");
test.ok(scope.isStrictMode());
test.equal(scope.current.name, "(anon)");
scope.addVariable({ name: "wobly" });
+ scope.addSwitch("var");
test.ok(scope.isDefined("wobly"));
test.ok(scope.isDefined("weebly"));
+ test.ok(scope.isSwitchEnabled("var"));
+ test.ok(scope.isSwitchEnabled("strict"));
scope.addGlobalVariable({ name: "stuff" });
test.ok(scope.isDefined("stuff"));
@@ -76,6 +85,8 @@ exports.testScopeStack = function (test) {
test.ok(scope.isDefined("weebly"));
test.ok(scope.isDefined("stuff"));
test.ok(!scope.isDefined("wobly"));
+ test.ok(!scope.isSwitchEnabled("var"));
+ test.ok(scope.isSwitchEnabled("strict"));
scope.addGlobalVariable({ name: "__proto__" });
test.ok(scope.isDefined("__proto__"));
@@ -93,3 +104,23 @@ exports.testSpecialVariables = function (test) {
test.equal(_.size(scope.current.uses), 4);
test.done();
};
+
+exports.testParseComment = function (test) {
+ var com = utils.parseComment("TODO: Write more programs.");
+ test.equal(com.type, "text");
+ test.equal(com.value, "TODO: Write more programs.");
+
+ com = utils.parseComment("jshint:sup unused");
+ test.equal(com.type, "text");
+ test.equal(com.value, "jshint:sup unused");
+
+ com = utils.parseComment("jshint:set var, strict");
+ test.equal(com.type, "set");
+ test.deepEqual(com.value, ["var", "strict"]);
+
+ com = utils.parseComment(" jshint:ignore W001, E002 ");
+ test.equal(com.type, "ignore");
+ test.deepEqual(com.value, ["W001", "E002"]);
+
+ test.done();
+};

0 comments on commit 9cfb2fe

Please sign in to comment.