├── .gitignore ├── .travis.yml ├── README.md ├── grunt.js ├── package.json ├── src ├── constants.js ├── jshint.js ├── reason.js ├── regexp.js └── utils.js └── test ├── fixtures ├── parser │ ├── comments.js │ ├── simple_file.js │ └── tokens.json ├── reason │ ├── arguments.js │ ├── asi.js │ ├── bitwise.js │ ├── comparison.js │ ├── debugger.js │ ├── esprima.js │ ├── expr_in_test.js │ ├── fifty.js │ ├── iterator.js │ ├── native.js │ ├── proto.js │ ├── shadow.js │ ├── trailing.js │ └── undef.js ├── regexp │ └── dashes.js └── utils │ └── simple_file.js ├── lib └── helpers.js └── unit ├── parser.js ├── reason.js ├── regexp.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | TODO 3 | dist/* 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ATTENTION: This project is obsolete. 2 | =================================== 3 | 4 | This project is obsolete. It was merged into [the main repository](https://github.com/jshint/jshint/). -------------------------------------------------------------------------------- /grunt.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.initConfig({ 3 | lint: { 4 | all: [ "src/**/*.js" ] 5 | }, 6 | 7 | test: { 8 | all: [ "test/unit/**/*.js" ] 9 | }, 10 | 11 | jshint: { 12 | options: { 13 | "es5": true, 14 | "node": true, 15 | "globalstrict": true, 16 | "strict": true, 17 | "white": true, 18 | "smarttabs": true 19 | } 20 | } 21 | }); 22 | 23 | grunt.registerTask("default", "lint test"); 24 | 25 | grunt.registerTask("browserify", "Builds a browserified copy of JSHint", function () { 26 | var browserify = require("browserify"); 27 | var bundle = browserify({ debug: true }); 28 | 29 | bundle.addEntry("./src/jshint.js"); 30 | grunt.file.mkdir("./dist"); 31 | grunt.file.write("./dist/jshint.js", bundle.bundle()); 32 | }); 33 | 34 | grunt.registerTask("cover", "Shows the test coverage report", function () { 35 | var coveraje, done, runHelper, countdown; 36 | var fileCount = 0; 37 | var tests = {}; 38 | var useServer = !!grunt.option("server"); 39 | 40 | try { 41 | coveraje = require("coveraje"); 42 | } catch (ex) { 43 | grunt.log.error('coveraje not installed. '.red + 44 | 'Use "' + 'npm install coveraje'.bold + '"'); 45 | return false; 46 | } 47 | 48 | done = this.async(); 49 | runHelper = coveraje.runHelper; 50 | 51 | grunt.file.expandFiles(grunt.config("test.all")).forEach(function (f) { 52 | fileCount++; 53 | tests[f] = function (context, instance) { 54 | return runHelper("nodeunit", { reporterName: "minimal" }) 55 | .run(f) 56 | .onComplete(function () { 57 | if (!useServer) 58 | countdown.one(); 59 | }) 60 | ; 61 | }; 62 | }); 63 | 64 | if (!useServer) { 65 | countdown = runHelper.createCountdown( 66 | runHelper 67 | .createEmitter(function () {}) 68 | .onComplete(function () { 69 | setTimeout(done, 200); 70 | }) 71 | .onError(function () { 72 | done(false, "timeout"); 73 | }), 74 | fileCount, 75 | 5000 76 | ); 77 | } 78 | 79 | var c = coveraje.cover( 80 | "var jshint = require(require('path').join('" + __dirname.replace(/\\/g, "\\\\") + "', 'src', 'jshint.js'));", 81 | tests, 82 | { 83 | useServer: useServer, 84 | globals: "node", 85 | resolveRequires: ["*"] 86 | } 87 | ); 88 | }); 89 | }; 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jshint-next", 3 | "version": "0.0.1", 4 | "main": "./src/jshint.js", 5 | 6 | "scripts": { 7 | "test": "./node_modules/grunt/bin/grunt test", 8 | "lint": "./node_modules/grunt/bin/grunt lint" 9 | }, 10 | 11 | "dependencies": { 12 | "esprima": "https://github.com/ariya/esprima/tarball/master", 13 | "underscore": "*", 14 | "peakle": "*" 15 | }, 16 | 17 | "devDependencies": { 18 | "browserify": "*", 19 | "coveraje": "*", 20 | "grunt": "*", 21 | "nodeunit": "*", 22 | "jshint": "*" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("underscore"); 4 | 5 | // Identifiers provided by the ECMAScript standard 6 | 7 | exports.reservedVars = { 8 | undefined : false, 9 | arguments : false, 10 | NaN : false 11 | }; 12 | 13 | exports.ecmaIdentifiers = { 14 | Array : false, 15 | Boolean : false, 16 | Date : false, 17 | decodeURI : false, 18 | decodeURIComponent : false, 19 | encodeURI : false, 20 | encodeURIComponent : false, 21 | Error : false, 22 | "eval" : false, 23 | EvalError : false, 24 | Function : false, 25 | hasOwnProperty : false, 26 | isFinite : false, 27 | isNaN : false, 28 | JSON : false, 29 | Math : false, 30 | Number : false, 31 | Object : false, 32 | parseInt : false, 33 | parseFloat : false, 34 | RangeError : false, 35 | ReferenceError : false, 36 | RegExp : false, 37 | String : false, 38 | SyntaxError : false, 39 | TypeError : false, 40 | URIError : false 41 | }; 42 | 43 | 44 | // Errors and warnings 45 | 46 | var errors = { 47 | E001: "Trailing comma causes errors in some versions of IE.", 48 | E002: "'with' statement is prohibited in strict mode.", 49 | E003: "'return' can be used only within functions.", 50 | E004: "'__iterator__' property is only available in JavaScript 1.7.", 51 | E005: "'__proto___' property is deprecated.", 52 | E006: "Missing semicolon.", 53 | E007: "Unexpected debugger statement.", 54 | E008: "'arguments.callee' is prohibited in strict mode.", 55 | E009: "Undefined variable in strict mode." 56 | }; 57 | 58 | var warnings = { 59 | W001: "Bitwise operator. (mistyped logical operator?)", 60 | W002: "Unsafe comparison.", 61 | W003: "Redefined variable.", 62 | W004: "Undefined variable.", 63 | W005: "Avoid arguments.caller.", 64 | W006: "Avoid arguments.callee.", 65 | W007: "Object arguments outside of a function body.", 66 | W008: "Assignment instead of a conditionial expression. (typo?)", 67 | W009: "Insecure use of {sym} in a regular expression.", 68 | W010: "Empty regular expression class.", 69 | W011: "Unescaped {sym} in a regular expression.", 70 | W012: "Don't extend native objects." 71 | }; 72 | 73 | exports.errors = {}; 74 | exports.warnings = {}; 75 | 76 | _.each(errors, function (desc, code) { 77 | exports.errors[code] = { code: code, desc: desc }; 78 | }); 79 | 80 | _.each(warnings, function (desc, code) { 81 | exports.warnings[code] = { code: code, desc: desc }; 82 | }); 83 | -------------------------------------------------------------------------------- /src/jshint.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("underscore"); 4 | var parser = require("esprima"); 5 | var events = require("events"); 6 | var peakle = require("peakle"); 7 | var utils = require("./utils.js"); 8 | var reason = require("./reason.js"); 9 | var regexp = require("./regexp.js"); 10 | var constants = require("./constants.js"); 11 | 12 | var MAXERR = 50; 13 | 14 | // Converts errors spitted out by Esprima into JSHint errors. 15 | 16 | function esprima(linter) { 17 | linter.on("lint:end", function () { 18 | var mapping = { 19 | "Illegal return statement": "E003", 20 | "Strict mode code may not include a with statement": "E002" 21 | }; 22 | 23 | _.each(linter.tree.errors, function (err) { 24 | var msg = err.message.split(": ")[1]; 25 | linter.report.addError(mapping[msg], err.lineNumber); 26 | }); 27 | }); 28 | } 29 | 30 | function Linter(code) { 31 | this.code = code; 32 | this.config = {}; 33 | this.tree = {}; 34 | this.scopes = new utils.ScopeStack(); 35 | this.report = new utils.Report(code); 36 | this.tokens = null; 37 | this.modules = []; 38 | this.emitter = new events.EventEmitter(); 39 | 40 | this.addModule(esprima); 41 | this.addModule(reason.register); 42 | this.addModule(regexp.register); 43 | 44 | // Pre-populate globals array with reserved variables, 45 | // standard ECMAScript globals and user-supplied globals. 46 | 47 | this.setGlobals(constants.reservedVars); 48 | this.setGlobals(constants.ecmaIdentifiers); 49 | } 50 | 51 | Linter.prototype = { 52 | on: function (names, listener) { 53 | var self = this; 54 | 55 | names.split(" ").forEach(function (name) { 56 | self.emitter.on(name, listener); 57 | }); 58 | }, 59 | 60 | trigger: function () { 61 | this.emitter.emit.apply(this.emitter, Array.prototype.slice.call(arguments)); 62 | }, 63 | 64 | addModule: function (func) { 65 | this.modules.push(func); 66 | }, 67 | 68 | setGlobals: function (globals) { 69 | var scopes = this.scopes; 70 | 71 | _.each(globals, function (writeable, name) { 72 | scopes.addGlobalVariable({ name: name, writeable: writeable }); 73 | }); 74 | }, 75 | 76 | parse: function () { 77 | var self = this; 78 | 79 | self.tree = parser.parse(self.code, { 80 | range: true, // Include range-based location data. 81 | loc: true, // Include column-based location data. 82 | comment: true, // Include a list of all found code comments. 83 | tokens: true, // Include a list of all found tokens. 84 | tolerant: true // Don't break on non-fatal errors. 85 | }); 86 | 87 | self.tokens = new utils.Tokens(self.tree.tokens); 88 | 89 | _.each(self.modules, function (func) { 90 | func(self); 91 | }); 92 | 93 | function _parseComments(from, to) { 94 | var slice = self.tree.comments.filter(function (comment) { 95 | return comment.range[0] >= from && comment.range[1] <= to; 96 | }); 97 | 98 | slice.forEach(function (comment) { 99 | comment = utils.parseComment(comment.value); 100 | 101 | switch (comment.type) { 102 | case "set": 103 | comment.value.forEach(function (name) { 104 | self.scopes.addSwitch(name); 105 | }); 106 | break; 107 | case "ignore": 108 | comment.value.forEach(function (code) { 109 | self.scopes.addIgnore(code); 110 | }); 111 | break; 112 | } 113 | }); 114 | } 115 | 116 | // Walk the tree using recursive* depth-first search and trigger 117 | // appropriate events when needed. 118 | // 119 | // * - and probably horribly inefficient. 120 | 121 | function _parse(tree) { 122 | if (tree.type) 123 | self.trigger(tree.type, tree); 124 | 125 | if (self.report.length > MAXERR) 126 | return; 127 | 128 | _.each(tree, function (val, key) { 129 | if (val === null) 130 | return; 131 | 132 | if (!_.isObject(val) && !_.isArray(val)) 133 | return; 134 | 135 | switch (val.type) { 136 | case "ExpressionStatement": 137 | if (val.expression.type === "Literal" && val.expression.value === "use strict") 138 | self.scopes.current.strict = true; 139 | _parse(val); 140 | break; 141 | case "FunctionDeclaration": 142 | self.scopes.addVariable({ name: val.id.name }); 143 | self.scopes.push(val.id.name); 144 | 145 | // If this function is not empty, parse its leading comments (if any). 146 | if (val.body.type === "BlockStatement" && val.body.body.length > 0) 147 | _parseComments(val.range[0], val.body.body[0].range[0]); 148 | 149 | _parse(val); 150 | self.scopes.pop(); 151 | break; 152 | case "FunctionExpression": 153 | if (val.id && val.id.type === "Identifier") 154 | self.scopes.addVariable({ name: val.id.name }); 155 | self.scopes.push("(anon)"); 156 | 157 | // If this function is not empty, parse its leading comments (if any). 158 | if (val.body.type === "BlockStatement" && val.body.body.length > 0) 159 | _parseComments(val.range[0], val.body.body[0].range[0]); 160 | 161 | _parse(val); 162 | self.scopes.pop(); 163 | break; 164 | case "WithStatement": 165 | self.scopes.runtimeOnly = true; 166 | _parse(val); 167 | self.scopes.runtimeOnly = false; 168 | break; 169 | default: 170 | _parse(val); 171 | } 172 | }); 173 | } 174 | 175 | self.trigger("lint:start"); 176 | _parseComments(0, self.tree.range[0]); 177 | _parse(self.tree.body); 178 | self.trigger("lint:end"); 179 | } 180 | }; 181 | 182 | function JSHINT(args) { 183 | var linter = new Linter(args.code); 184 | linter.setGlobals(args.predefined || {}); 185 | linter.parse(); 186 | 187 | return { 188 | tree: linter.tree, 189 | report: linter.report 190 | }; 191 | } 192 | 193 | exports.Linter = Linter; 194 | exports.lint = JSHINT; 195 | -------------------------------------------------------------------------------- /src/reason.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("underscore"); 4 | var utils = require("./utils.js"); 5 | var constants = require('./constants'); 6 | 7 | exports.register = function (linter) { 8 | var report = linter.report; 9 | var tokens = linter.tokens; 10 | var scopes = linter.scopes; 11 | 12 | // Check for trailing commas in arrays and objects. 13 | 14 | linter.on("ArrayExpression ObjectExpression", function (expr) { 15 | var token = tokens.move(tokens.find(expr.range[1] - 3)); 16 | 17 | if (token.isPunctuator(",")) 18 | report.addError("E001", token.range); 19 | }); 20 | 21 | // Check for properties named __iterator__. This is a special property 22 | // available only in browsers with JavaScript 1.7 implementation. 23 | 24 | linter.on("MemberExpression", function (expr) { 25 | var prop = expr.property; 26 | 27 | if (prop.type === "Identifier" && prop.name === "__iterator__") 28 | report.addError("E004", prop.range); 29 | }); 30 | 31 | // Check for properties named __proto__. This special property was 32 | // deprecated long time ago. 33 | 34 | linter.on("MemberExpression", function (expr) { 35 | var prop = expr.property; 36 | 37 | if (prop.type === "Identifier" && prop.name === "__proto__") 38 | report.addError("E005", prop.range); 39 | }); 40 | 41 | // Check for missing semicolons but only when they have a potential 42 | // of breaking things due to automatic semicolon insertion. 43 | 44 | linter.on("ExpressionStatement", function (expr) { 45 | var type = expr.expression.type; 46 | 47 | if (type !== "CallExpression" && type !== "MemberExpression") 48 | return; 49 | 50 | var slice = tokens.getRange(expr.range); 51 | var token = slice.move(1); 52 | var prev, curLine, prevLine; 53 | 54 | while (token !== null) { 55 | if (token.isPunctuator(["(", "["])) { 56 | prev = slice.peak(-1); 57 | curLine = report.lineFromRange(token.range); 58 | prevLine = report.lineFromRange(prev.range); 59 | 60 | if (curLine !== prevLine && !prev.isPunctuator(";")) { 61 | report.addError("E006", prev.range); 62 | } 63 | } 64 | 65 | token = slice.next(); 66 | } 67 | }); 68 | 69 | linter.on("BinaryExpression", function (expr) { 70 | var op = expr.operator; 71 | 72 | if (op !== "+" && op !== "*" && op !== "/") 73 | return; 74 | 75 | if (expr.left.loc.end.line < expr.right.loc.start.line) 76 | report.addError("E006", expr.range); 77 | }); 78 | 79 | // Catch cases where you put a new line after a `return` statement 80 | // by mistake. 81 | 82 | linter.on("ReturnStatement", function (expr) { 83 | var cur = tokens.move(tokens.find(expr.range[0])); 84 | var next = tokens.peak(); 85 | 86 | if (report.lineFromRange(next.range) === report.lineFromRange(cur.range)) 87 | return; 88 | 89 | if (next && next.isPunctuator(";")) 90 | return; 91 | 92 | if (next && next.isKeyword("var")) 93 | return; 94 | 95 | if (next && next.isKeyword("case")) 96 | return; 97 | 98 | report.addError("E006", cur.range); 99 | }); 100 | 101 | // Check for debugger statements. You really don't want them in your 102 | // production code. 103 | 104 | linter.on("DebuggerStatement", function (expr) { 105 | report.addError("E007", expr.range); 106 | }); 107 | 108 | // Disallow bitwise operators: they are slow in JavaScript and 109 | // more often than not are simply typoed logical operators. 110 | 111 | linter.on("BinaryExpression UnaryExpression", function (expr) { 112 | var ops = { 113 | "|" : true, 114 | "&" : true, 115 | "^" : true, 116 | "~" : true, 117 | "<<" : true, 118 | ">>" : true, 119 | ">>>": true 120 | }; 121 | 122 | if (expr.operator && ops[expr.operator] === true) 123 | report.addWarning("W001", expr.range); 124 | }); 125 | 126 | // Complain about comparisons that can blow up because of type 127 | // coercion. 128 | 129 | linter.on("BinaryExpression", function (expr) { 130 | function isUnsafe(el) { 131 | if (el.type === "Identifier" && el.name === "undefined") 132 | return true; 133 | 134 | if (el.type !== "Literal") 135 | return false; 136 | 137 | return _.any([ 138 | el.value === 0, 139 | el.value === null, 140 | el.value === "", 141 | el.value === false, 142 | el.value === true 143 | ]); 144 | } 145 | 146 | if (expr.operator !== "==" && expr.operator !== "!=") 147 | return; 148 | 149 | if (isUnsafe(expr.left)) 150 | report.addWarning("W002", expr.left.range); 151 | 152 | if (isUnsafe(expr.right)) 153 | report.addWarning("W002", expr.right.range); 154 | }); 155 | 156 | // Complain about variables defined twice. 157 | 158 | function isRedefined(name, range) { 159 | if (scopes.isDefined(name)) 160 | report.addWarning("W003", range); 161 | } 162 | 163 | linter.on("VariableDeclarator", function (expr) { 164 | isRedefined(expr.id.name, expr.id.range); 165 | scopes.addVariable({ name: expr.id.name }); 166 | }); 167 | 168 | linter.on("FunctionExpression FunctionDeclaration", function (expr) { 169 | _.each(expr.params, function (param, key) { 170 | isRedefined(param.name, param.range); 171 | scopes.addVariable({ name: param.name }); 172 | }); 173 | }); 174 | 175 | // Check if identifier is a free variable and record its 176 | // use. Later in the code we'll use that to spot undefined 177 | // variables. 178 | 179 | linter.on("Identifier", function (ident) { 180 | var index = tokens.find(ident.range[0]); 181 | var token, prev, next; 182 | 183 | if (index > 0) { 184 | token = tokens.move(index); 185 | prev = tokens.peak(-1); 186 | next = tokens.peak(1) || { isPunctuator: function () { return false; } }; 187 | 188 | // This identifier is a property key, not a free variable. 189 | 190 | if (next.isPunctuator(":") && !prev.isPunctuator("?")) 191 | return; 192 | 193 | // This identifier is a property itself, not a free variable. 194 | 195 | if (prev.isPunctuator(".")) 196 | return; 197 | 198 | // Operators typeof and delete do not raise runtime errors 199 | // even if the base object of a reference is null, so we don't 200 | // need to display warnings in these cases. 201 | 202 | if (prev.isKeyword("typeof") || prev.isKeyword("delete")) { 203 | 204 | // Unless you're trying to subscript a null references. That 205 | // will throw a runtime error. 206 | 207 | if (!next.isPunctuator(".") && !next.isPunctuator("[")) 208 | return; 209 | } 210 | } 211 | 212 | scopes.addUse(ident.name, ident.range); 213 | }); 214 | 215 | // Look for arguments.callee and arguments.caller usage and warn about 216 | // them. In strict mode, instead of warning about arguments.callee, return 217 | // an error. This also supports [] notation. 218 | 219 | linter.on("Identifier Literal", function (expr) { 220 | if (scopes.current.name === "(global)") { 221 | if (expr.type === "Identifier" && expr.name === "arguments") 222 | report.addWarning("W007", expr.range); 223 | 224 | return; 225 | } 226 | 227 | var name = expr.type === "Identifier" ? expr.name : expr.value; 228 | var punc = expr.type === "Identifier" ? "." : "["; 229 | var range = expr.range; 230 | 231 | if (name !== "callee" && name !== "caller") 232 | return; 233 | 234 | var index = tokens.find(range[0]); 235 | 236 | if (index < 1) 237 | return; 238 | 239 | tokens.move(index); 240 | 241 | if (tokens.peak(-1).isPunctuator(punc) && tokens.peak(-2).isIdentifier("arguments")) { 242 | switch (name) { 243 | case "caller": 244 | report.addWarning("W005", range); 245 | break; 246 | case "callee": 247 | if (scopes.isStrictMode()) 248 | report.addError("E008", range); 249 | else 250 | report.addWarning("W006", range); 251 | } 252 | } 253 | }); 254 | 255 | // Warn when assignments are used instead of conditionals. 256 | linter.on("ForStatement IfStatement WhileStatement DoWhileStatement", function (expr) { 257 | if (expr.test && expr.test.type === "AssignmentExpression") 258 | report.addWarning("W008", expr.range); 259 | }); 260 | 261 | // Warn when extending prototypes of built-in objects 262 | linter.on("AssignmentExpression", function (expr) { 263 | var left = expr.left; 264 | var obj = left.object; 265 | 266 | function isNativeProto(expr) { 267 | if (!expr.property || !expr.object) 268 | return false; 269 | 270 | return expr.object.name in constants.ecmaIdentifiers && expr.property.name === "prototype"; 271 | } 272 | 273 | if (left.type !== "MemberExpression") 274 | return; 275 | 276 | // Check for Object.prototype.prop = "" 277 | // Check for Native.prototype = {} 278 | if (isNativeProto(left) || isNativeProto(obj)) 279 | report.addWarning("W012", expr.range); 280 | }); 281 | 282 | // Go over all stacks and find all variables that were used but 283 | // never defined. 284 | // 285 | // This is not very efficient--for starters we can mark visited 286 | // scopes and not visit them again. 287 | 288 | linter.on("lint:end", function () { 289 | _.each(scopes.stack, function (env) { 290 | _.each(env.uses, function (ranges, name) { 291 | if (scopes.isDefined(name, env)) 292 | return; 293 | 294 | _.each(ranges, function (range) { 295 | if (scopes.isStrictMode(env)) 296 | return void linter.report.addError("E009", range); 297 | linter.report.addWarning("W004", range); 298 | }); 299 | }); 300 | }); 301 | }); 302 | }; 303 | -------------------------------------------------------------------------------- /src/regexp.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("underscore"); 4 | var events = require("events"); 5 | 6 | function Tokens(exp) { 7 | this.exp = exp; 8 | this.pos = 0; 9 | this.emitter = new events.EventEmitter(); 10 | } 11 | 12 | Tokens.prototype = { 13 | get current() { 14 | return this.peak(0); 15 | }, 16 | 17 | at: function (index) { 18 | var chr = this.exp.charAt(this.pos); 19 | return chr === "" ? null : chr; 20 | }, 21 | 22 | peak: function (offset) { 23 | var pos = this.pos + (offset === undefined ? 1 : offset); 24 | var chr = this.exp.charAt(pos); 25 | 26 | return chr === "" ? null : chr; 27 | }, 28 | 29 | next: function () { 30 | if (this.current !== null) 31 | this.pos += 1; 32 | 33 | return this.current; 34 | } 35 | }; 36 | 37 | exports.register = function (linter) { 38 | var report = linter.report; 39 | 40 | linter.on("Literal", function (literal) { 41 | var value = (literal.value || "").toString(); 42 | var range = literal.range; 43 | 44 | value = value.match(/^\/(.+)\/[igm]?$/); 45 | if (value === null) 46 | return; 47 | 48 | var tokens = new Tokens(value[1]); 49 | var isLiteral = false; 50 | var inRange = false; 51 | 52 | tokens.emitter.on("[", function () { 53 | tokens.next(); 54 | 55 | if (tokens.current === "^") { 56 | report.addWarning("W009", literal.range, { sym: tokens.current }); 57 | tokens.next(); 58 | } 59 | 60 | if (tokens.current === "]") { 61 | report.addWarning("W010", literal.range); 62 | } 63 | 64 | do { 65 | switch (tokens.current) { 66 | case "[": 67 | case "^": 68 | report.addWarning("W011", literal.range, { sym: tokens.current }); 69 | if (inRange) inRange = false; 70 | else isLiteral = true; 71 | break; 72 | case "-": 73 | if (isLiteral && !inRange) { 74 | isLiteral = false; 75 | inRange = true; 76 | } else if (inRange) { 77 | inRange = false; 78 | } else if (tokens.peak() === "]") { 79 | inRange = true; 80 | } else { 81 | report.addWarning("W011", literal.range, { sym: "-" }); 82 | isLiteral = true; 83 | } 84 | break; 85 | case "]": 86 | if (inRange) 87 | report.addWarning("W011", literal.range, { sym: "-" }); 88 | return; 89 | case "\\": 90 | tokens.next(); 91 | 92 | // \w, \s and \d are never part of a character range. 93 | if (/[wsd]/i.test(tokens.current)) { 94 | if (inRange) { 95 | report.addWarning("W011", literal.range, { sym: "-" }); 96 | inRange = false; 97 | } 98 | isLiteral = false; 99 | } else if (inRange) { 100 | inRange = false; 101 | } else { 102 | isLiteral = true; 103 | } 104 | break; 105 | case "/": 106 | report.addWarning("W011", literal.range, { sym: tokens.current }); 107 | /* falls through */ 108 | default: 109 | if (inRange) inRange = false; 110 | else isLiteral = true; 111 | } 112 | 113 | } while (tokens.next()); 114 | }); 115 | 116 | tokens.emitter.on(".", function () { 117 | if (tokens.peak(-1) !== "\\") { 118 | report.addWarning("W009", literal.range, { sym: tokens.current }); 119 | } 120 | }); 121 | 122 | while (tokens.current !== null) { 123 | tokens.emitter.emit(tokens.current); 124 | tokens.next(); 125 | } 126 | }); 127 | }; 128 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("underscore"); 4 | var util = require("util"); 5 | var Peakle = require("peakle").Peakle; 6 | var warnings = require("./constants.js").warnings; 7 | var errors = require("./constants.js").errors; 8 | 9 | function safe(name) { 10 | if (name === "__proto__") 11 | return "(__proto__)"; 12 | 13 | var special = Object.getOwnPropertyNames(Object.prototype); 14 | for (var i = 0; i < special.length; i++) { 15 | if (name === special[i]) 16 | return "(" + name + ")"; 17 | } 18 | 19 | return name; 20 | } 21 | 22 | function interpolate(string, data) { 23 | return string.replace(/\{([^{}]*)\}/g, function (match, key) { 24 | var repl = data[key]; 25 | 26 | if (typeof repl === 'string' || typeof repl === 'number') 27 | return repl; 28 | 29 | return match; 30 | }); 31 | } 32 | 33 | // ScopeStack stores all the environments we encounter while 34 | // traversing syntax trees. It also keeps track of all 35 | // variables defined and/or used in these environments. 36 | // 37 | // We use linked-list implementation of a stack. The first 38 | // element, representing global environment, doesn't have 39 | // a reference to its parent. 40 | // 41 | // runtimeOnly means that we can't tell if identifier 42 | // is a variable or a property by analysing the source. It 43 | // is true only within the `with` statement. 44 | 45 | function ScopeStack() { 46 | this.stack = []; 47 | this.curid = null; 48 | 49 | this.runtimeOnly = false; 50 | this.push("(global)"); 51 | } 52 | 53 | ScopeStack.prototype = { 54 | get current() { 55 | if (this.curid === null) 56 | return null; 57 | 58 | return this.stack[this.curid]; 59 | }, 60 | 61 | get parent() { 62 | if (this.curid === null) 63 | return null; 64 | 65 | var parid = this.current.parid; 66 | 67 | if (parid === null) 68 | return null; 69 | 70 | return this.stack[parid]; 71 | }, 72 | 73 | // Push a new environment into the stack. 74 | 75 | push: function (name) { 76 | var curid = this.curid; 77 | this.curid = this.stack.length; 78 | 79 | this.stack.push({ 80 | parid: curid, 81 | name: name, 82 | strict: false, 83 | switches: {}, 84 | ignores: {}, 85 | vars: {}, 86 | uses: {} 87 | }); 88 | }, 89 | 90 | // Exit from the current environment. Even though 91 | // this method is called `pop` it doesn't actually 92 | // delete the environment--it simply jumps into the 93 | // parent one. 94 | 95 | pop: function () { 96 | this.curid = this.current.parid; 97 | }, 98 | 99 | any: function (cond, env) { 100 | env = env || this.current; 101 | 102 | while (env) { 103 | if (cond.call(env)) 104 | return true; 105 | 106 | env = this.stack[env.parid]; 107 | } 108 | 109 | return false; 110 | }, 111 | 112 | isDefined: function (name, env) { 113 | return this.any(function () { return _.has(this.vars, safe(name)); }, env); 114 | }, 115 | 116 | isStrictMode: function (env) { 117 | return this.any(function () { return this.strict; }, env); 118 | }, 119 | 120 | isSwitchEnabled: function (name, env) { 121 | return this.any(function () { return this.switches[name]; }, env); 122 | }, 123 | 124 | isMessageIgnored: function (code, env) { 125 | return this.any(function () { return this.ignores[code]; }, env); 126 | }, 127 | 128 | addUse: function (name, range) { 129 | name = safe(name); 130 | 131 | if (this.runtimeOnly) 132 | return; 133 | 134 | if (this.current.uses[name] === undefined) 135 | this.current.uses[name] = [range]; 136 | else 137 | this.current.uses[name].push(range); 138 | }, 139 | 140 | addVariable: function (opts) { 141 | this.current.vars[safe(opts.name)] = { 142 | writeable: opts.writeable || false 143 | }; 144 | }, 145 | 146 | addGlobalVariable: function (opts) { 147 | this.stack[0].vars[safe(opts.name)] = { 148 | writeable: opts.writeable || false 149 | }; 150 | }, 151 | 152 | addSwitch: function (name) { 153 | this.current.switches[name] = true; 154 | }, 155 | 156 | addIgnore: function (name) { 157 | this.current.ignores[name] = true; 158 | } 159 | }; 160 | 161 | function Report(source) { 162 | this.ERROR = 1; 163 | this.WARNING = 2; 164 | 165 | this.length = 0; 166 | this.messages = {}; 167 | this.ranges = []; 168 | this.source = source; 169 | } 170 | 171 | Report.prototype = { 172 | lineFromRange: function (range) { 173 | var lines = this.source.slice(0, range[0]).split("\n"); 174 | return lines.length || -1; 175 | }, 176 | 177 | getMessages: function (cond) { 178 | var ret = []; 179 | cond = cond || function () { return true; }; 180 | 181 | _.each(this.messages, function (pool, line) { 182 | _.each(pool, function (msg) { 183 | if (cond(msg)) 184 | ret.push(msg); 185 | }); 186 | }); 187 | 188 | return ret; 189 | }, 190 | 191 | get errors() { 192 | var type = this.ERROR; 193 | 194 | return this.getMessages(function (msg) { 195 | return msg.type === type; 196 | }); 197 | }, 198 | 199 | get warnings() { 200 | var type = this.WARNING; 201 | 202 | return this.getMessages(function (msg) { 203 | return msg.type === type; 204 | }); 205 | }, 206 | 207 | addMessage: function (obj) { 208 | var line = obj.line; 209 | this.messages[line] = _.union(this.messages[line] || [], [obj]); 210 | this.length += 1; 211 | }, 212 | 213 | addWarning: function (label, loc, data) { 214 | var line = _.isArray(loc) ? this.lineFromRange(loc) : loc; 215 | var warn = warnings[label]; 216 | 217 | if (!warn) 218 | throw new Error("Warning " + label + "is not defined."); 219 | 220 | warn.desc = interpolate(warn.desc, data); 221 | this.addMessage({ 222 | type: this.WARNING, 223 | line: line, 224 | data: warn 225 | }); 226 | }, 227 | 228 | addError: function (label, loc, data) { 229 | var line = _.isArray(loc) ? this.lineFromRange(loc) : loc; 230 | var err = errors[label]; 231 | 232 | if (!err) 233 | throw new Error("Error " + label + " is not defined."); 234 | 235 | err.desc = interpolate(err.desc, data); 236 | this.addMessage({ 237 | type: this.ERROR, 238 | line: line, 239 | data: err 240 | }); 241 | } 242 | }; 243 | 244 | function Token(obj) { 245 | _.extend(this, obj); 246 | } 247 | 248 | _.each(["Punctuator", "Keyword", "Identifier"], function (name) { 249 | Token.prototype["is" + name] = function (values) { 250 | if (!Array.isArray(values)) 251 | values = [ values ]; 252 | 253 | return values.some(function (value) { 254 | return this.type === name && this.value === value; 255 | }, this); 256 | }; 257 | }); 258 | 259 | function Tokens(list) { 260 | var self = this; 261 | Peakle.call(self, list); 262 | 263 | // A hash-table to make tokens lookup by their starting 264 | // position cheaper (see Tokens.find for more info). 265 | self.byStart = {}; 266 | 267 | self.list = _.map(list || [], function (obj, i) { 268 | var token = new Token(obj); 269 | self.byStart[token.range[0]] = i; 270 | return token; 271 | }); 272 | } 273 | 274 | util.inherits(Tokens, Peakle); 275 | 276 | Tokens.prototype.find = function (rangeIndex) { 277 | // First try to lookup our token in byStart in 278 | // case this index is the starting point for the token. 279 | 280 | var index = this.byStart[rangeIndex]; 281 | 282 | if (index) 283 | return index; 284 | 285 | // If we could find it, step back, token by token 286 | // until we find one that starts before the one we're 287 | // looking for, 288 | 289 | var cur = rangeIndex - 1; 290 | 291 | do { 292 | index = this.byStart[cur]; 293 | cur = cur - 1; 294 | } while (index === undefined && cur > 0); 295 | 296 | // If we're in the beginning and still nothing--return. 297 | 298 | if (index === undefined) 299 | return -1; 300 | 301 | // Otherwise go in a slow O(N) loop looking for our token. 302 | 303 | for (var i = index; i < this.length; i++) { 304 | if (this.list[i].range[0] >= rangeIndex) 305 | return i; 306 | } 307 | 308 | return -1; 309 | }; 310 | 311 | Tokens.prototype.getRange = function (range) { 312 | var slice = []; 313 | var length = this.list.length; 314 | var token; 315 | 316 | for (var i = this.byStart[range[0]] || 0; i < length; i++) { 317 | token = this.list[i]; 318 | 319 | if (token.range[0] < range[0]) 320 | continue; 321 | 322 | if (token.range[1] <= range[1]) 323 | slice.push(token); 324 | else 325 | break; 326 | } 327 | 328 | return new Tokens(slice); 329 | }; 330 | 331 | var commentsCache = {}; 332 | var commentsTypes = { "set": true, "ignore": true }; 333 | 334 | function parseComment(text) { 335 | var parts = text.trim().split(" "); 336 | var defval = { type: "text", value: text }; 337 | var values = []; 338 | var head, body; 339 | 340 | if (parts.length === 0) 341 | return defval; 342 | 343 | head = parts[0].split(":"); 344 | if (head[0] !== "jshint" || commentsTypes[head[1]] !== true) 345 | return defval; 346 | 347 | body = parts.slice(1).join(" ").split(",").map(function (s) { 348 | return s.trim(); 349 | }); 350 | 351 | return { 352 | type: head[1], 353 | value: body 354 | }; 355 | } 356 | 357 | exports.Report = Report; 358 | exports.Token = Token; 359 | exports.Tokens = Tokens; 360 | exports.ScopeStack = ScopeStack; 361 | exports.parseComment = parseComment; 362 | -------------------------------------------------------------------------------- /test/fixtures/parser/comments.js: -------------------------------------------------------------------------------- 1 | // jshint:set var 2 | 3 | function main(cb) { 4 | // jshint:set strict 5 | // jshint:ignore W001 6 | "use strict"; 7 | 8 | cb(function () { 9 | // jshint:ignore E001 10 | return function () {}; // jshint:ignore E002 11 | // jshint:set hula 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/parser/simple_file.js: -------------------------------------------------------------------------------- 1 | /* [linter] */ 2 | 3 | var number = 1; 4 | 5 | function add(num) { 6 | return number + num; 7 | } 8 | 9 | add(1); 10 | -------------------------------------------------------------------------------- /test/fixtures/parser/tokens.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "Keyword", 4 | "value": "var", 5 | "range": [ 6 | 16, 7 | 19 8 | ], 9 | "loc": { 10 | "start": { 11 | "line": 3, 12 | "column": 0 13 | }, 14 | "end": { 15 | "line": 3, 16 | "column": 3 17 | } 18 | } 19 | }, 20 | { 21 | "type": "Identifier", 22 | "value": "number", 23 | "range": [ 24 | 20, 25 | 26 26 | ], 27 | "loc": { 28 | "start": { 29 | "line": 3, 30 | "column": 4 31 | }, 32 | "end": { 33 | "line": 3, 34 | "column": 10 35 | } 36 | } 37 | }, 38 | { 39 | "type": "Punctuator", 40 | "value": "=", 41 | "range": [ 42 | 27, 43 | 28 44 | ], 45 | "loc": { 46 | "start": { 47 | "line": 3, 48 | "column": 11 49 | }, 50 | "end": { 51 | "line": 3, 52 | "column": 12 53 | } 54 | } 55 | }, 56 | { 57 | "type": "Numeric", 58 | "value": "1", 59 | "range": [ 60 | 29, 61 | 30 62 | ], 63 | "loc": { 64 | "start": { 65 | "line": 3, 66 | "column": 13 67 | }, 68 | "end": { 69 | "line": 3, 70 | "column": 14 71 | } 72 | } 73 | }, 74 | { 75 | "type": "Punctuator", 76 | "value": ";", 77 | "range": [ 78 | 30, 79 | 31 80 | ], 81 | "loc": { 82 | "start": { 83 | "line": 3, 84 | "column": 14 85 | }, 86 | "end": { 87 | "line": 3, 88 | "column": 15 89 | } 90 | } 91 | }, 92 | { 93 | "type": "Keyword", 94 | "value": "function", 95 | "range": [ 96 | 33, 97 | 41 98 | ], 99 | "loc": { 100 | "start": { 101 | "line": 5, 102 | "column": 0 103 | }, 104 | "end": { 105 | "line": 5, 106 | "column": 8 107 | } 108 | } 109 | }, 110 | { 111 | "type": "Identifier", 112 | "value": "add", 113 | "range": [ 114 | 42, 115 | 45 116 | ], 117 | "loc": { 118 | "start": { 119 | "line": 5, 120 | "column": 9 121 | }, 122 | "end": { 123 | "line": 5, 124 | "column": 12 125 | } 126 | } 127 | }, 128 | { 129 | "type": "Punctuator", 130 | "value": "(", 131 | "range": [ 132 | 45, 133 | 46 134 | ], 135 | "loc": { 136 | "start": { 137 | "line": 5, 138 | "column": 12 139 | }, 140 | "end": { 141 | "line": 5, 142 | "column": 13 143 | } 144 | } 145 | }, 146 | { 147 | "type": "Identifier", 148 | "value": "num", 149 | "range": [ 150 | 46, 151 | 49 152 | ], 153 | "loc": { 154 | "start": { 155 | "line": 5, 156 | "column": 13 157 | }, 158 | "end": { 159 | "line": 5, 160 | "column": 16 161 | } 162 | } 163 | }, 164 | { 165 | "type": "Punctuator", 166 | "value": ")", 167 | "range": [ 168 | 49, 169 | 50 170 | ], 171 | "loc": { 172 | "start": { 173 | "line": 5, 174 | "column": 16 175 | }, 176 | "end": { 177 | "line": 5, 178 | "column": 17 179 | } 180 | } 181 | }, 182 | { 183 | "type": "Punctuator", 184 | "value": "{", 185 | "range": [ 186 | 51, 187 | 52 188 | ], 189 | "loc": { 190 | "start": { 191 | "line": 5, 192 | "column": 18 193 | }, 194 | "end": { 195 | "line": 5, 196 | "column": 19 197 | } 198 | } 199 | }, 200 | { 201 | "type": "Keyword", 202 | "value": "return", 203 | "range": [ 204 | 55, 205 | 61 206 | ], 207 | "loc": { 208 | "start": { 209 | "line": 6, 210 | "column": 2 211 | }, 212 | "end": { 213 | "line": 6, 214 | "column": 8 215 | } 216 | } 217 | }, 218 | { 219 | "type": "Identifier", 220 | "value": "number", 221 | "range": [ 222 | 62, 223 | 68 224 | ], 225 | "loc": { 226 | "start": { 227 | "line": 6, 228 | "column": 9 229 | }, 230 | "end": { 231 | "line": 6, 232 | "column": 15 233 | } 234 | } 235 | }, 236 | { 237 | "type": "Punctuator", 238 | "value": "+", 239 | "range": [ 240 | 69, 241 | 70 242 | ], 243 | "loc": { 244 | "start": { 245 | "line": 6, 246 | "column": 16 247 | }, 248 | "end": { 249 | "line": 6, 250 | "column": 17 251 | } 252 | } 253 | }, 254 | { 255 | "type": "Identifier", 256 | "value": "num", 257 | "range": [ 258 | 71, 259 | 74 260 | ], 261 | "loc": { 262 | "start": { 263 | "line": 6, 264 | "column": 18 265 | }, 266 | "end": { 267 | "line": 6, 268 | "column": 21 269 | } 270 | } 271 | }, 272 | { 273 | "type": "Punctuator", 274 | "value": ";", 275 | "range": [ 276 | 74, 277 | 75 278 | ], 279 | "loc": { 280 | "start": { 281 | "line": 6, 282 | "column": 21 283 | }, 284 | "end": { 285 | "line": 6, 286 | "column": 22 287 | } 288 | } 289 | }, 290 | { 291 | "type": "Punctuator", 292 | "value": "}", 293 | "range": [ 294 | 76, 295 | 77 296 | ], 297 | "loc": { 298 | "start": { 299 | "line": 7, 300 | "column": 0 301 | }, 302 | "end": { 303 | "line": 7, 304 | "column": 1 305 | } 306 | } 307 | }, 308 | { 309 | "type": "Identifier", 310 | "value": "add", 311 | "range": [ 312 | 79, 313 | 82 314 | ], 315 | "loc": { 316 | "start": { 317 | "line": 9, 318 | "column": 0 319 | }, 320 | "end": { 321 | "line": 9, 322 | "column": 3 323 | } 324 | } 325 | }, 326 | { 327 | "type": "Punctuator", 328 | "value": "(", 329 | "range": [ 330 | 82, 331 | 83 332 | ], 333 | "loc": { 334 | "start": { 335 | "line": 9, 336 | "column": 3 337 | }, 338 | "end": { 339 | "line": 9, 340 | "column": 4 341 | } 342 | } 343 | }, 344 | { 345 | "type": "Numeric", 346 | "value": "1", 347 | "range": [ 348 | 83, 349 | 84 350 | ], 351 | "loc": { 352 | "start": { 353 | "line": 9, 354 | "column": 4 355 | }, 356 | "end": { 357 | "line": 9, 358 | "column": 5 359 | } 360 | } 361 | }, 362 | { 363 | "type": "Punctuator", 364 | "value": ")", 365 | "range": [ 366 | 84, 367 | 85 368 | ], 369 | "loc": { 370 | "start": { 371 | "line": 9, 372 | "column": 5 373 | }, 374 | "end": { 375 | "line": 9, 376 | "column": 6 377 | } 378 | } 379 | }, 380 | { 381 | "type": "Punctuator", 382 | "value": ";", 383 | "range": [ 384 | 85, 385 | 86 386 | ], 387 | "loc": { 388 | "start": { 389 | "line": 9, 390 | "column": 6 391 | }, 392 | "end": { 393 | "line": 9, 394 | "column": 7 395 | } 396 | } 397 | } 398 | ] -------------------------------------------------------------------------------- /test/fixtures/reason/arguments.js: -------------------------------------------------------------------------------- 1 | function print() { 2 | } 3 | 4 | function doarg() { 5 | print(arguments.callee); 6 | print(arguments.caller); 7 | print(arguments.length); 8 | print(arguments); 9 | } 10 | 11 | print(arguments.callee); 12 | print(arguments.caller); 13 | print(arguments.length); 14 | print(arguments); 15 | 16 | function doarg2() { 17 | print(arguments["callee"]); 18 | print(arguments["caller"]); 19 | print(arguments["length"]); 20 | print(arguments); 21 | } 22 | 23 | function strictarg() { 24 | "use strict"; 25 | 26 | print(arguments.callee); 27 | print(arguments["callee"]); 28 | } 29 | -------------------------------------------------------------------------------- /test/fixtures/reason/asi.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | function foo() { 4 | if (true) return 5 | var x = 1 6 | return 7 | "foo" 8 | } 9 | 10 | function bar() { 11 | for (var i = 0; i < 10; i++) { 12 | if (i === 0) continue 13 | var y = 2 14 | if (i === 1) break 15 | var z = 3 16 | 17 | switch (z) { 18 | case 3: 19 | var m = "" 20 | return 21 | case 2: 22 | break 23 | default: 24 | break 25 | } 26 | } 27 | } 28 | 29 | foo() 30 | 31 | (function () {})() 32 | 33 | [ "one", "two" ].forEach(function (i) { 34 | console.log(i) 35 | }) 36 | 37 | var a = 1 38 | var b = '1' 39 | var c = "1" 40 | 41 | function foobar() {} 42 | (function () {})() 43 | 44 | a = 2 45 | +b 46 | *c 47 | -------------------------------------------------------------------------------- /test/fixtures/reason/bitwise.js: -------------------------------------------------------------------------------- 1 | var a = 1; 2 | var b = 2; 3 | var c; 4 | 5 | c = a | b; 6 | c = a & b; 7 | c = a ^ b; 8 | c = ~a; 9 | c = a << b; 10 | c = a >> b; 11 | c = a >>> b; 12 | 13 | // Shouldn't warn (safe operators) 14 | c = c + b; 15 | c = c - b; 16 | c = c / b; 17 | c = c * b; 18 | 19 | // Shouldn't warn (logical operators) 20 | c = a || b; 21 | c = a && b; 22 | -------------------------------------------------------------------------------- /test/fixtures/reason/comparison.js: -------------------------------------------------------------------------------- 1 | function compare(a) { 2 | if (a == null) 3 | return; 4 | 5 | if (a == undefined) 6 | return; 7 | 8 | if (a == 0) 9 | return; 10 | 11 | if (a == "") 12 | return; 13 | 14 | if (a == false) 15 | return; 16 | 17 | if (a == true) 18 | return; 19 | 20 | if (a === false) 21 | return; 22 | 23 | if (a == "Hello, World") 24 | return; 25 | } 26 | 27 | function compare2(a) { 28 | if (a != null) 29 | return; 30 | 31 | if (a != undefined) 32 | return; 33 | 34 | if (a != 0) 35 | return; 36 | 37 | if (a != "") 38 | return; 39 | 40 | if (a != false) 41 | return; 42 | 43 | if (a != true) 44 | return; 45 | 46 | if (a !== false) 47 | return; 48 | 49 | if (a != "Hello, World") 50 | return; 51 | } 52 | -------------------------------------------------------------------------------- /test/fixtures/reason/debugger.js: -------------------------------------------------------------------------------- 1 | var a = 0; 2 | var b = 1; 3 | var c; 4 | 5 | debugger; 6 | 7 | c = a + b; 8 | -------------------------------------------------------------------------------- /test/fixtures/reason/esprima.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | with (greatPower) { 4 | comes(greatResponsibility); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/reason/expr_in_test.js: -------------------------------------------------------------------------------- 1 | var i = 0; 2 | 3 | if (i == 1) {} 4 | for (var j = 0; j < 1; j++) {} 5 | while (j < 1) {} 6 | do {} while (j < 1); 7 | 8 | if (i = 0) {} 9 | for (var x = 0; x = 1; x++) {} 10 | while (j = 1) {} 11 | do {} while (j = 1); 12 | -------------------------------------------------------------------------------- /test/fixtures/reason/fifty.js: -------------------------------------------------------------------------------- 1 | var a = 0; 2 | var b = 0; 3 | var c; 4 | 5 | c = a & b; 6 | c = a & b; 7 | c = a & b; 8 | c = a & b; 9 | c = a & b; 10 | c = a & b; 11 | c = a & b; 12 | c = a & b; 13 | c = a & b; 14 | c = a & b; 15 | c = a & b; 16 | c = a & b; 17 | c = a & b; 18 | c = a & b; 19 | c = a & b; 20 | c = a & b; 21 | c = a & b; 22 | c = a & b; 23 | c = a & b; 24 | c = a & b; 25 | c = a & b; 26 | c = a & b; 27 | c = a & b; 28 | c = a & b; 29 | c = a & b; 30 | c = a & b; 31 | c = a & b; 32 | c = a & b; 33 | c = a & b; 34 | c = a & b; 35 | c = a & b; 36 | c = a & b; 37 | c = a & b; 38 | c = a & b; 39 | c = a & b; 40 | c = a & b; 41 | c = a & b; 42 | c = a & b; 43 | c = a & b; 44 | c = a & b; 45 | c = a & b; 46 | c = a & b; 47 | c = a & b; 48 | c = a & b; 49 | c = a & b; 50 | c = a & b; 51 | c = a & b; 52 | c = a & b; 53 | c = a & b; 54 | c = a & b; 55 | c = a & b; 56 | 57 | c = a & b; 58 | c = a & b; 59 | -------------------------------------------------------------------------------- /test/fixtures/reason/iterator.js: -------------------------------------------------------------------------------- 1 | function RangeIterator(range) { 2 | this.range = range; 3 | this.current = this.range.low; 4 | } 5 | 6 | RangeIterator.prototype.next = function next() { 7 | if (this.current > this.range.high) { 8 | throw new Error(); 9 | } else { 10 | return this.current++; 11 | } 12 | }; 13 | 14 | function Range(low, high) { 15 | this.low = low; 16 | this.high = high; 17 | } 18 | 19 | Range.prototype.__iterator__ = function __iterator__() { 20 | return new RangeIterator(this); 21 | }; 22 | 23 | // Nothing wrong with a function named __iterator__ 24 | function __iterator__() { 25 | return "I have a stupid name!"; 26 | } 27 | -------------------------------------------------------------------------------- /test/fixtures/reason/native.js: -------------------------------------------------------------------------------- 1 | // should complain 2 | Object.prototype.forIn = function () {}; 3 | 4 | // should complain 5 | Array.prototype.myName = "Mr. Array"; 6 | 7 | // should complain 8 | Number.prototype = { 9 | toString: function() { 10 | return "42"; 11 | } 12 | }; 13 | 14 | // no worries 15 | function Awesome() {}; 16 | Awesome.prototype.forEvery = function (fn) {}; 17 | 18 | // no worries 19 | function SoCool() {}; 20 | SoCool.prototype = { 21 | isGreat: true 22 | }; 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/fixtures/reason/proto.js: -------------------------------------------------------------------------------- 1 | function RangeIterator(range) { 2 | this.range = range; 3 | this.current = this.range.low; 4 | } 5 | 6 | function hasKey(object, key) { 7 | var original = object.__proto__, result; 8 | object.__proto__ = null; 9 | result = key in object; 10 | object.__proto__ = original; 11 | return result; 12 | } 13 | 14 | RangeIterator.prototype.next = function next() { 15 | if (this.current > this.range.high) { 16 | throw new Error(); 17 | } else { 18 | return this.current++; 19 | } 20 | }; 21 | 22 | function Range(low, high) { 23 | this.low = low; 24 | this.high = high; 25 | } 26 | 27 | function SubArray(length) { 28 | var result = arguments.length === 1 ? Array(length) : Array.apply(this, arguments); 29 | result.__proto__ = SubArray.prototype; 30 | return result; 31 | } 32 | 33 | SubArray.prototype.__proto__ = Array.prototype; 34 | 35 | // Nothing wrong with a function named __proto__ 36 | function __proto__() { 37 | return "I have a stupid name!"; 38 | } 39 | -------------------------------------------------------------------------------- /test/fixtures/reason/shadow.js: -------------------------------------------------------------------------------- 1 | var one; 2 | 3 | function funcOne(four, one) { 4 | var one; 5 | var two; 6 | 7 | var funcTwo = function () { 8 | var two; 9 | }; 10 | 11 | three = 4; 12 | var four; 13 | } 14 | 15 | function two() { 16 | function three(two) { 17 | var three; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/reason/trailing.js: -------------------------------------------------------------------------------- 1 | var a1 = [ 1, 2, 3 ]; 2 | var a2 = [ 4, 5, 6, ]; 3 | 4 | [ 7, 8, 9, ].forEach(function (num) {}); 5 | 6 | var o1 = { "name": "Anton" }; 7 | var o2 = { 8 | "name": "Anton", 9 | "project": "JSHint", 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/reason/undef.js: -------------------------------------------------------------------------------- 1 | var one; 2 | 3 | function test(a, b) { 4 | two = 2; // W 5 | } 6 | 7 | three = 2; // W 8 | four.five = 4; // W 9 | one.two; 10 | one["two"]; 11 | test(); 12 | 13 | // This line shouldn't generate a warning because 14 | // typeof accepts a reference even when the base 15 | // object of that reference is null. 16 | if (typeof undef) {} 17 | 18 | if (typeof undef['attr' + 0]) { 19 | delete undef['attr' + 0]; 20 | } 21 | 22 | if (typeof undef.attr) { 23 | delete undef.attr; 24 | } 25 | 26 | var fn = function () { 27 | localUndef(); // W 28 | 29 | if (typeof localUndef) 30 | return; 31 | }; 32 | 33 | lateFn(); 34 | function lateFn() { 35 | fn(a); 36 | var a; 37 | } 38 | 39 | function strictundef() { 40 | "use strict"; 41 | zz(); 42 | } 43 | -------------------------------------------------------------------------------- /test/fixtures/regexp/dashes.js: -------------------------------------------------------------------------------- 1 | var a = /[-ab]/; 2 | var b = /[ab-]/; 3 | var c = /[a-c-e]/; 4 | var d = /[\s-]/; 5 | var e = /[-\d]/; 6 | var f = /[a-]/; 7 | var g = /[-z]/; 8 | var h = /[\d-z]/; 9 | var i = /[^-ab]/; 10 | var j = /[^ab-]/; 11 | -------------------------------------------------------------------------------- /test/fixtures/utils/simple_file.js: -------------------------------------------------------------------------------- 1 | /* [linter] */ 2 | 3 | var number = 1; 4 | 5 | function add(num) { 6 | return number + num; 7 | } 8 | 9 | add(1); 10 | -------------------------------------------------------------------------------- /test/lib/helpers.js: -------------------------------------------------------------------------------- 1 | var _ = require("underscore"); 2 | var assert = require("assert"); 3 | var fs = require("fs"); 4 | var path = require("path"); 5 | var linter = require("../../src/jshint.js"); 6 | 7 | // Returns contents of a fixture. 8 | // 9 | // Fixture's parent directory depends on the suite's file name. For example, 10 | // fixtures for test/unit/parser.js should be in test/fixtures/parser/.js 11 | 12 | function Fixtures(dirname, filename) { 13 | this.dirname = dirname; 14 | this.filename = filename; 15 | } 16 | 17 | Fixtures.prototype.get = function (name) { 18 | var dir, stream; 19 | 20 | dir = path.basename(this.filename).replace(".js", ""); 21 | stream = fs.readFileSync(path.resolve(this.dirname, "..", "fixtures", dir, name)); 22 | 23 | return stream.toString().replace(/\r\n/g, "\n"); 24 | }; 25 | 26 | // A test helper designed specifically for JSHint. It allows us to write 27 | // tests in a declarative manner thus reducing code duplication. 28 | 29 | function createRunner(dirname, filename) { 30 | var fixtures = new Fixtures(dirname, filename); 31 | 32 | function runner(test) { 33 | var expected = []; 34 | 35 | var helper = { 36 | addError: function (line, code) { 37 | expected.push({ 38 | line: line, 39 | code: code 40 | }); 41 | 42 | return helper; 43 | }, 44 | 45 | addErrors: function (lines, code) { 46 | _.each(lines, function (line) { 47 | helper.addError(line, code); 48 | }); 49 | 50 | return helper; 51 | }, 52 | 53 | test: function (source, options, globals) { 54 | var retval = linter.lint({ code: source, predefined: globals }); 55 | var errors = retval.report.getMessages(); 56 | 57 | // If the linter didn't produce any errors and we don't 58 | // expect any, quietly return. 59 | 60 | if (errors.length === 0 && expected.length === 0) 61 | return; 62 | 63 | // Otherwise get a list of unexpected errors. 64 | 65 | var unexpected = _.reject(errors, function (err, line) { 66 | return _.any(expected, function (exp) { 67 | return exp.line === err.line && exp.code === err.data.code; 68 | }); 69 | }); 70 | 71 | // And errors that were expected but not thrown by the linter. 72 | 73 | var unthrown = _.reject(expected, function (exp) { 74 | return _.any(errors, function (err) { 75 | return err.line === exp.line && err.data.code === exp.code; 76 | }); 77 | }); 78 | 79 | // If we expected all errors thrown by the linter, quietly return. 80 | 81 | if (unexpected.length === 0 && unthrown.length === 0) 82 | return void test.ok(true); 83 | 84 | // Otherwise format a message listing all unexpected and unthrown 85 | // errors and fail the test case by failing an assertion. 86 | 87 | var message = ""; 88 | 89 | if (unexpected.length > 0) { 90 | message += "\n\tUnexpected errors"; 91 | message += "\n" + _.map(unexpected, function (err) { 92 | return "\t L" + err.line + ": " + err.data.code; 93 | }).join("\n"); 94 | } 95 | 96 | if (unthrown.length > 0) { 97 | message += "\n\tErrors defined, but not thrown by JSHint"; 98 | message += "\n" + _.map(unthrown, function (err) { 99 | return "\t L" + err.line + ": " + err.code; 100 | }).join("\n"); 101 | } 102 | 103 | test.ok(false, message); 104 | }, 105 | 106 | // Shortcut to helper.test that allows us to provide a fixture file name 107 | // instead of a string. 108 | 109 | testFile: function (name, options, globals) { 110 | helper.test(fixtures.get(name), options, globals); 111 | } 112 | }; 113 | 114 | return helper; 115 | } 116 | 117 | return runner; 118 | } 119 | 120 | exports.Fixtures = Fixtures; 121 | exports.createRunner = createRunner; 122 | -------------------------------------------------------------------------------- /test/unit/parser.js: -------------------------------------------------------------------------------- 1 | // Esprima integration tests. 2 | // 3 | // These shouldn't test actual JSHint behaviour. Instead they should ensure 4 | // that underlying Esprima parser works as expected. 5 | 6 | var linter = require("../../src/jshint.js"); 7 | var helpers = require("../lib/helpers.js"); 8 | 9 | var fixtures = new helpers.Fixtures(__dirname, __filename); 10 | 11 | exports.testTree = function (test) { 12 | test.expect(11); 13 | 14 | var tree = linter.lint({ code: fixtures.get("simple_file.js") }).tree; 15 | 16 | test.equal(tree.type, "Program"); 17 | test.equal(tree.body[0].type, "VariableDeclaration"); 18 | test.equal(tree.body[0].declarations[0].type, "VariableDeclarator"); 19 | test.equal(tree.body[0].declarations[0].id.name, "number"); 20 | test.equal(tree.body[1].type, "FunctionDeclaration"); 21 | test.equal(tree.body[1].id.name, "add"); 22 | test.equal(tree.body[2].type, "ExpressionStatement"); 23 | test.equal(tree.body[2].expression.callee.name, "add"); 24 | test.equal(tree.comments[0].type, "Block"); 25 | test.equal(tree.comments[0].value, " [linter] "); 26 | test.deepEqual(tree.comments[0].range, [ 0, 14 ]); 27 | 28 | test.done(); 29 | }; 30 | 31 | exports.testTokens = function (test) { 32 | test.expect(1); 33 | 34 | // The tokens.json file is a tree snapshot I got by using Esprima's 35 | // online parser demo with the code from simple_file.js. 36 | // * http://esprima.org/demo/parse.html 37 | 38 | var code = fixtures.get("simple_file.js"); 39 | var tokens = JSON.parse(fixtures.get("tokens.json")); 40 | 41 | test.deepEqual(linter.lint({ code: code }).tree.tokens, tokens); 42 | test.done(); 43 | }; 44 | 45 | exports.testComments = function (test) { 46 | test.expect(8); 47 | 48 | var linterObj = new linter.Linter(fixtures.get("comments.js")); 49 | var addIgnore = linterObj.scopes.addIgnore; 50 | var addSwitch = linterObj.scopes.addSwitch; 51 | 52 | var ignores = [ 53 | [ "main", "W001" ], 54 | [ "(anon)", "E001" ] 55 | ]; 56 | 57 | var switches = [ 58 | [ "(global)", "var" ], 59 | [ "main", "strict" ] 60 | ]; 61 | 62 | linterObj.scopes.addIgnore = function (code) { 63 | var exp = ignores.shift(); 64 | test.equal(linterObj.scopes.current.name, exp[0]); 65 | test.equal(code, exp[1]); 66 | addIgnore.call(linterObj.scopes, code); 67 | }; 68 | 69 | linterObj.scopes.addSwitch = function (name) { 70 | var exp = switches.shift(); 71 | test.equal(linterObj.scopes.current.name, exp[0]); 72 | test.equal(name, exp[1]); 73 | addSwitch.call(linterObj.scopes, name); 74 | }; 75 | 76 | linterObj.parse(); 77 | 78 | linterObj.scopes.addIgnore = addIgnore; 79 | linterObj.scopes.addSwitch = addSwitch; 80 | 81 | test.done(); 82 | }; 83 | -------------------------------------------------------------------------------- /test/unit/reason.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("underscore"); 4 | var linter = require("../../src/jshint.js"); 5 | var helpers = require("../lib/helpers.js"); 6 | var runner = helpers.createRunner(__dirname, __filename); 7 | 8 | exports.testMaxErr = function (test) { 9 | var lines = []; 10 | for (var i = 5; i <= 55; i++) { 11 | lines.push(i); 12 | } 13 | 14 | runner(test) 15 | .addErrors(lines, "W001") 16 | .testFile("fifty.js"); 17 | 18 | test.done(); 19 | }; 20 | 21 | exports.testEsprimaErrors = function (test) { 22 | runner(test) 23 | .addError(3, "E002") 24 | .testFile("esprima.js", {}, { greatPower: false }); 25 | 26 | test.done(); 27 | }; 28 | 29 | exports.testTrailingComma = function (test) { 30 | runner(test) 31 | .addErrors([2, 4, 9], "E001") 32 | .testFile("trailing.js"); 33 | 34 | test.done(); 35 | }; 36 | 37 | exports.testDunderIterator = function (test) { 38 | runner(test) 39 | .addError(19, "E004") 40 | .testFile("iterator.js"); 41 | 42 | test.done(); 43 | }; 44 | 45 | exports.testDunderProto = function (test) { 46 | var globals = { 47 | Error: false 48 | }; 49 | 50 | runner(test) 51 | .addErrors([7, 8, 10, 29, 33], "E005") 52 | .testFile("proto.js", {}, globals); 53 | 54 | test.done(); 55 | }; 56 | 57 | exports.testMissingSemicolon = function (test) { 58 | runner(test) 59 | .addErrors([6, 29, 31, 44, 45], "E006") 60 | .testFile("asi.js", {}, { console: false }); 61 | 62 | test.done(); 63 | }; 64 | 65 | exports.testDebugger = function (test) { 66 | runner(test) 67 | .addError(5, "E007") 68 | .testFile("debugger.js"); 69 | 70 | test.done(); 71 | }; 72 | 73 | exports.testBitwiseOperators = function (test) { 74 | runner(test) 75 | .addErrors([5, 6, 7, 8, 9, 10, 11], "W001") 76 | .testFile("bitwise.js"); 77 | 78 | test.done(); 79 | }; 80 | 81 | exports.testUnsafeComparison = function (test) { 82 | var lines = [2, 5, 8, 11, 14, 17, 28, 31, 34, 37, 40, 43]; 83 | runner(test) 84 | .addErrors(lines, "W002") 85 | .testFile("comparison.js"); 86 | 87 | test.done(); 88 | }; 89 | 90 | exports.testShadow = function (test) { 91 | runner(test) 92 | .addErrors([3, 4, 8, 12, 16, 17], "W003") 93 | .testFile("shadow.js", {}, { three: false }); 94 | 95 | test.done(); 96 | }; 97 | 98 | exports.testUndef = function (test) { 99 | runner(test) 100 | .addErrors([4, 7, 8, 18, 19, 22, 23, 27], "W004") 101 | .addError(41, "E009") 102 | .testFile("undef.js"); 103 | 104 | test.done(); 105 | }; 106 | 107 | exports.testArguments = function (test) { 108 | runner(test) 109 | .addErrors([5, 17], "W006") 110 | .addErrors([6, 18], "W005") 111 | .addErrors([11, 12, 13, 14], "W007") 112 | .addErrors([26, 27], "E008") 113 | .testFile("arguments.js"); 114 | 115 | test.done(); 116 | }; 117 | 118 | exports.testExpressionsAsTests = function (test) { 119 | runner(test) 120 | .addErrors([8, 9, 10, 11], "W008") 121 | .testFile("expr_in_test.js"); 122 | 123 | test.done(); 124 | }; 125 | 126 | exports.testNative = function (test) { 127 | runner(test) 128 | .addErrors([2, 5, 8], "W012") 129 | .testFile("native.js"); 130 | 131 | test.done(); 132 | }; 133 | -------------------------------------------------------------------------------- /test/unit/regexp.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("underscore"); 4 | var linter = require("../../src/jshint.js"); 5 | var helpers = require("../lib/helpers.js"); 6 | var runner = helpers.createRunner(__dirname, __filename); 7 | 8 | exports.testSuccess = function (test) { 9 | runner(test).test("/hello/g"); 10 | runner(test).test("/hello\s(world)/g"); 11 | test.done(); 12 | }; 13 | 14 | exports.testUnsafeSymbols = function (test) { 15 | runner(test) 16 | .addError(1, "W009") 17 | .test("/h[^...]/"); 18 | 19 | runner(test) 20 | .addError(1, "W009") 21 | .test("/h.ey/"); 22 | 23 | test.done(); 24 | }; 25 | 26 | exports.testEmptyClass = function (test) { 27 | runner(test) 28 | .addError(1, "W010") 29 | .test("/h[]/"); 30 | 31 | test.done(); 32 | }; 33 | 34 | exports.testDashes = function (test) { 35 | runner(test) 36 | .addErrors([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], "W011") 37 | .addErrors([ 9, 10 ], "W009") 38 | .testFile("dashes.js"); 39 | 40 | test.done(); 41 | }; 42 | -------------------------------------------------------------------------------- /test/unit/utils.js: -------------------------------------------------------------------------------- 1 | var _ = require("underscore"); 2 | var linter = require("../../src/jshint.js"); 3 | var utils = require("../../src/utils.js"); 4 | var helpers = require("../lib/helpers.js"); 5 | 6 | var fixtures = new helpers.Fixtures(__dirname, __filename); 7 | 8 | exports.testReport = function (test) { 9 | var report = new utils.Report(); 10 | 11 | test.equal(_.size(report.messages), 0); 12 | test.equal(report.errors.length, 0); 13 | test.equal(report.warnings.length, 0); 14 | 15 | report.addError("E003", 1); 16 | report.addError("E002", 2); 17 | report.addWarning("W003", 3); 18 | 19 | test.equal(_.size(report.messages), 3); 20 | test.equal(report.errors.length, 2); 21 | test.equal(report.warnings.length, 1); 22 | 23 | test.deepEqual(report.errors[0], { 24 | type: report.ERROR, 25 | line: 1, 26 | data: { 27 | code: "E003", 28 | desc: "'return' can be used only within functions." 29 | } 30 | }); 31 | 32 | try { 33 | report.addError("RandomError", 1); 34 | test.ok(false); 35 | } catch (err) { 36 | test.ok(err !== undefined); 37 | } 38 | 39 | test.done(); 40 | }; 41 | 42 | exports.testTokens = function (test) { 43 | var code = fixtures.get("simple_file.js"); 44 | var tokens = new utils.Tokens(linter.lint({ code: code }).tree.tokens); 45 | var slice = tokens.getRange([ 0, 28 ]); 46 | 47 | test.equal(slice.length, 3); 48 | test.equal(slice.current.value, "var"); 49 | test.equal(slice.next().value, "number"); 50 | test.equal(slice.next().value, "="); 51 | 52 | test.done(); 53 | }; 54 | 55 | exports.testScopeStack = function (test) { 56 | var scope = new utils.ScopeStack(); 57 | test.equal(scope.current.name, "(global)"); 58 | 59 | scope.addVariable({ name: "weebly" }); 60 | scope.addSwitch("strict"); 61 | scope.addIgnore("W002"); 62 | scope.current.strict = true; 63 | test.ok(scope.isDefined("weebly")); 64 | test.ok(scope.isSwitchEnabled("strict")); 65 | test.ok(scope.isMessageIgnored("W002")); 66 | test.ok(!scope.isSwitchEnabled("var")); 67 | test.ok(!scope.isMessageIgnored("E001")); 68 | 69 | scope.push("(anon)"); 70 | test.ok(scope.isStrictMode()); 71 | test.equal(scope.current.name, "(anon)"); 72 | 73 | scope.addVariable({ name: "wobly" }); 74 | scope.addSwitch("var"); 75 | test.ok(scope.isDefined("wobly")); 76 | test.ok(scope.isDefined("weebly")); 77 | test.ok(scope.isSwitchEnabled("var")); 78 | test.ok(scope.isSwitchEnabled("strict")); 79 | 80 | scope.addGlobalVariable({ name: "stuff" }); 81 | test.ok(scope.isDefined("stuff")); 82 | 83 | scope.pop(); 84 | test.equal(scope.current.name, "(global)"); 85 | test.ok(scope.isDefined("weebly")); 86 | test.ok(scope.isDefined("stuff")); 87 | test.ok(!scope.isDefined("wobly")); 88 | test.ok(!scope.isSwitchEnabled("var")); 89 | test.ok(scope.isSwitchEnabled("strict")); 90 | 91 | scope.addGlobalVariable({ name: "__proto__" }); 92 | test.ok(scope.isDefined("__proto__")); 93 | 94 | test.done(); 95 | }; 96 | 97 | exports.testSpecialVariables = function (test) { 98 | var scope = new utils.ScopeStack(); 99 | scope.addUse("foo", [ 0, 1 ]); 100 | scope.addUse("__proto__", [ 1, 2 ]); 101 | scope.addUse("toString", [ 2, 3 ]); 102 | scope.addUse("constructor", [ 3, 4 ]); 103 | 104 | test.equal(_.size(scope.current.uses), 4); 105 | test.done(); 106 | }; 107 | 108 | exports.testParseComment = function (test) { 109 | var com = utils.parseComment("TODO: Write more programs."); 110 | test.equal(com.type, "text"); 111 | test.equal(com.value, "TODO: Write more programs."); 112 | 113 | com = utils.parseComment("jshint:sup unused"); 114 | test.equal(com.type, "text"); 115 | test.equal(com.value, "jshint:sup unused"); 116 | 117 | com = utils.parseComment("jshint:set var, strict"); 118 | test.equal(com.type, "set"); 119 | test.deepEqual(com.value, ["var", "strict"]); 120 | 121 | com = utils.parseComment(" jshint:ignore W001, E002 "); 122 | test.equal(com.type, "ignore"); 123 | test.deepEqual(com.value, ["W001", "E002"]); 124 | 125 | test.done(); 126 | }; 127 | --------------------------------------------------------------------------------