├── .gitignore ├── test ├── cases │ ├── parse_error.js │ ├── unused_ignored.js │ ├── try.js │ ├── null_byte.js │ ├── search_scope_chain.js │ ├── unused_var.js │ ├── arrow_add.js │ ├── uninitialized_var.js │ ├── dead_zone.js │ ├── unused_function.js │ ├── default_arg_expression.js │ ├── trailing_comma.js │ ├── unused_arg.js │ ├── semicolon.js │ ├── declare_pattern.js │ ├── reused_index.js │ ├── debug.js │ ├── globals.js │ ├── let_double.js │ ├── pattern_params.js │ ├── class.js │ ├── infinite_loop.js │ └── nosemi.js └── runcases.js ├── .tern-project ├── options.js ├── package.json ├── LICENSE ├── README.md ├── bin └── blint ├── loop.js ├── globals.js ├── nosemicolons.js ├── scope.js └── blint.js /.gitignore: -------------------------------------------------------------------------------- 1 | .tern-port 2 | /node_modules 3 | -------------------------------------------------------------------------------- /test/cases/parse_error.js: -------------------------------------------------------------------------------- 1 | foo bar 2 | 3 | // --- 4 | // Unexpected token (1:4) 5 | -------------------------------------------------------------------------------- /test/cases/unused_ignored.js: -------------------------------------------------------------------------------- 1 | function _foo(_a, _b) { 2 | var _x = 10; 3 | } 4 | -------------------------------------------------------------------------------- /test/cases/try.js: -------------------------------------------------------------------------------- 1 | try {} 2 | catch(e) { throw e; } 3 | 4 | try {} 5 | catch(e) {} 6 | -------------------------------------------------------------------------------- /.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "node": {}, 4 | "complete_strings": {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/cases/null_byte.js: -------------------------------------------------------------------------------- 1 | var _x = "foo"; 2 | 3 | // --- 4 | // Undesirable character 0x0 (1:13) 5 | -------------------------------------------------------------------------------- /test/cases/search_scope_chain.js: -------------------------------------------------------------------------------- 1 | let x; 2 | if (true) { 3 | let y = 10; 4 | x = y; 5 | } 6 | x; 7 | -------------------------------------------------------------------------------- /test/cases/unused_var.js: -------------------------------------------------------------------------------- 1 | var x = 20, y = x + 1; 2 | 3 | // --- 4 | // Unused variable y (1:12) 5 | -------------------------------------------------------------------------------- /test/cases/arrow_add.js: -------------------------------------------------------------------------------- 1 | let data = "" 2 | let y = chunk => data += chunk 3 | y("hi") 4 | let _ = data 5 | -------------------------------------------------------------------------------- /test/cases/uninitialized_var.js: -------------------------------------------------------------------------------- 1 | var x, _y = x; 2 | 3 | // --- 4 | // Variable x is never written to (1:4) 5 | -------------------------------------------------------------------------------- /test/cases/dead_zone.js: -------------------------------------------------------------------------------- 1 | x + 1; 2 | 3 | let x = 4; 4 | 5 | // --- 6 | // Variable used before its declaration (1:0) 7 | -------------------------------------------------------------------------------- /test/cases/unused_function.js: -------------------------------------------------------------------------------- 1 | function foo() { 2 | return 100; 3 | } 4 | 5 | // --- 6 | // Unused function foo (1:9) 7 | -------------------------------------------------------------------------------- /test/cases/default_arg_expression.js: -------------------------------------------------------------------------------- 1 | var x = 10; 2 | 3 | function _foo(a, b = x, c = a) { 4 | return a + b + c; 5 | } 6 | -------------------------------------------------------------------------------- /test/cases/trailing_comma.js: -------------------------------------------------------------------------------- 1 | // options={"trailingCommas": false} 2 | 3 | [1, 2, 3,]; 4 | 5 | // --- 6 | // Trailing comma (3:8) 7 | -------------------------------------------------------------------------------- /test/cases/unused_arg.js: -------------------------------------------------------------------------------- 1 | function foo(a, b) { 2 | return b; 3 | } 4 | foo(1, 2); 5 | 6 | // --- 7 | // Unused argument a (1:13) 8 | -------------------------------------------------------------------------------- /test/cases/semicolon.js: -------------------------------------------------------------------------------- 1 | // options={"semicolons": true} 2 | 3 | var x = 10 4 | x = 2 + x; 5 | 6 | // --- 7 | // Missing semicolon (3:10) 8 | -------------------------------------------------------------------------------- /test/cases/declare_pattern.js: -------------------------------------------------------------------------------- 1 | var [a] = [10]; 2 | var {b} = {b: 20}; 3 | 4 | // --- 5 | // Unused variable a (1:5) 6 | // Unused variable b (2:5) 7 | -------------------------------------------------------------------------------- /test/cases/reused_index.js: -------------------------------------------------------------------------------- 1 | for (var i = 0; i < 100; i++) 2 | for (var i = 0; i < 20; i++) {} 3 | 4 | // --- 5 | // Redefined loop variable (2:11) 6 | -------------------------------------------------------------------------------- /test/cases/debug.js: -------------------------------------------------------------------------------- 1 | function _foo(x) { 2 | console.log(x); 3 | debugger; 4 | } 5 | 6 | // --- 7 | // Found console.log (2:2) 8 | // Found debugger statement (3:2) 9 | -------------------------------------------------------------------------------- /test/cases/globals.js: -------------------------------------------------------------------------------- 1 | x = 10; 2 | 3 | x + y; 4 | 5 | // --- 6 | // Assignment to global variable x (1:0) 7 | // Access to global variable x. (3:0) 8 | // Access to global variable y. (3:4) 9 | -------------------------------------------------------------------------------- /test/cases/let_double.js: -------------------------------------------------------------------------------- 1 | let _x = 10; 2 | { let _x = 20; } 3 | { let _y = 30; } 4 | { let _y = 40; } 5 | { let _z = 50; 6 | let _z = 60; } 7 | 8 | // --- 9 | // Identifier '_z' has already been declared (6:6) 10 | -------------------------------------------------------------------------------- /test/cases/pattern_params.js: -------------------------------------------------------------------------------- 1 | function _foo([x, y], {z: z = 10}, ...args) {} 2 | 3 | // --- 4 | // Unused argument x (1:15) 5 | // Unused argument y (1:18) 6 | // Unused argument z (1:26) 7 | // Unused argument args (1:38) 8 | -------------------------------------------------------------------------------- /test/cases/class.js: -------------------------------------------------------------------------------- 1 | new Foo; 2 | 3 | class Foo { 4 | constructor(a) {} 5 | 6 | method(b) { return b; } 7 | } 8 | 9 | // --- 10 | // Unused argument a (4:14) 11 | // Class name used before its declaration (1:4) 12 | -------------------------------------------------------------------------------- /test/cases/infinite_loop.js: -------------------------------------------------------------------------------- 1 | for (let i = 0; i < 100; i--) {} 2 | 3 | for (let i = 0, j = 100; i < 100 && j >= 0; i++, j++) {} 4 | 5 | // --- 6 | // Suspiciously infinite-looking loop (1:25) 7 | // Suspiciously infinite-looking loop (3:49) 8 | -------------------------------------------------------------------------------- /test/cases/nosemi.js: -------------------------------------------------------------------------------- 1 | // options={"semicolons": false} 2 | 3 | for (var x = 1; x < 10; x++) { 4 | ["foo", x] 5 | } 6 | let y = 10 7 | (y += 1) 8 | 9 | y += 1 10 | ;(y += 1) 11 | 12 | if (y) { 13 | ;["bar", y] 14 | } 15 | 16 | 100; 17 | 18 | for (;;) 19 | [1, 2] 20 | 21 | if (x) 22 | y(); 23 | 24 | // --- 25 | // Missing leading semicolon (4:2) 26 | // Possibly accidentally continued statement (6:0) 27 | // Semicolon found (16:4) 28 | // Semicolon found (22:6) 29 | -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | var defaultOptions = { 2 | ecmaVersion: 10, 3 | browser: false, 4 | tabs: false, 5 | trailingSpace: false, 6 | semicolons: null, 7 | trailingCommas: true, 8 | reservedProps: true, 9 | namedFunctions: true, 10 | console: false, 11 | declareGlobals: true, 12 | allowedGlobals: null, 13 | blob: false, 14 | message: null 15 | }; 16 | 17 | exports.getOptions = function(value) { 18 | var opts = {}; 19 | if (value.autoSemicolons === false) value.semicolons = true; 20 | for (var prop in defaultOptions) { 21 | if (value && Object.prototype.hasOwnProperty.call(value, prop)) 22 | opts[prop] = value[prop]; 23 | else 24 | opts[prop] = defaultOptions[prop]; 25 | } 26 | return opts; 27 | }; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blint", 3 | "description": "No-ceremony linter", 4 | "homepage": "http://github.com/marijnh/blint", 5 | "main": "blint.js", 6 | "version": "1.1.2", 7 | "engines": { 8 | "node": ">=0.4.0" 9 | }, 10 | "maintainers": [ 11 | { 12 | "name": "Marijn Haverbeke", 13 | "email": "marijnh@gmail.com", 14 | "web": "http://marijnhaverbeke.nl" 15 | } 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/marijnh/blint.git" 20 | }, 21 | "licenses": [ 22 | { 23 | "type": "MIT", 24 | "url": "https://github.com/marijnh/blint/blob/master/LICENSE" 25 | } 26 | ], 27 | "bin": { 28 | "blint": "./bin/blint" 29 | }, 30 | "dependencies": { 31 | "acorn": "^7.0.0", 32 | "acorn-walk": "^7.0.0", 33 | "commander": "^8.3.0" 34 | }, 35 | "scripts": { 36 | "test": "node test/runcases.js" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 by Marijn Haverbeke 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blint 2 | 3 | Simple JavaScript linter. 4 | 5 | ```javascript 6 | var blint = require("blint"); 7 | blint.checkFile("foo.js"); 8 | blint.checkDir("src"); 9 | ``` 10 | 11 | When the linter encounters problems, it will write something to 12 | stdout, and set a flag, which you can retrieve with `blint.success()`. 13 | 14 | ```javascript 15 | process.exit(blint.success() ? 0 : 1); 16 | ``` 17 | 18 | Both `checkFile` and `checkDir` take a second optional options 19 | argument. These are the defaults: 20 | 21 | ```javascript 22 | var defaultOptions = { 23 | // Version of the language to parse 24 | ecmaVersion: 6, 25 | // Whitelist globals exported by the browser 26 | browser: false, 27 | // Allow tabs 28 | tabs: false, 29 | // Allow trailing whitespace 30 | trailingSpace: false, 31 | // True to require semicolons, false to disallow them 32 | semicolons: null, 33 | // Allow trailing commas 34 | trailingCommas: true, 35 | // Allow unquoted properties that are reserved words 36 | reservedProps: true, 37 | // Whether to allow console.* expressions 38 | console: false, 39 | // An array of global variables to allow 40 | allowedGlobals: [], 41 | // Allow the code to declare top-level variables 42 | declareGlobals: true 43 | }; 44 | ``` 45 | 46 | Released under an MIT license. 47 | -------------------------------------------------------------------------------- /test/runcases.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"), path = require("path"); 2 | 3 | var blint = require("../blint"); 4 | 5 | var filter = process.argv[2]; 6 | 7 | var failed = 0; 8 | 9 | var casePath = path.resolve(__dirname, "cases"); 10 | fs.readdirSync(casePath).forEach(function(file) { 11 | if (!file.match(/\.js$/) || filter && file.indexOf(filter) != 0) return; 12 | 13 | var text = fs.readFileSync(path.resolve(casePath, file), "utf8"); 14 | var declaredOptions = /\/\/ options=(.*)/.exec(text); 15 | var options = declaredOptions ? JSON.parse(declaredOptions[1]) : {}; 16 | if (!options.ecmaVersion) options.ecmaVersion = 6; 17 | var declaredOutput = /\/\/ ---\n([^]+)/.exec(text); 18 | var expected = declaredOutput ? declaredOutput[1].trim().split("\n").map(function(line) { return /^\s*\/\/\s*(.*)/.exec(line)[1]; }) : []; 19 | 20 | var result = []; 21 | options.message = function(_file, msg) { result.push(msg); }; 22 | blint.checkFile(file, options, text); 23 | 24 | for (var i = 0; i < result.length; i++) { 25 | var found = expected.indexOf(result[i]); 26 | if (found > -1) { 27 | result.splice(i--, 1); 28 | expected.splice(found, 1); 29 | } 30 | } 31 | 32 | if (expected.length || expected.length) { 33 | if (failed) console.log(""); 34 | failed++; 35 | } 36 | if (expected.length) 37 | console.log(file + ": Missing messages\n " + expected.join("\n ")); 38 | if (result.length) 39 | console.log(file + ": Unexpected messages\n " + result.join("\n ")); 40 | }); 41 | 42 | process.exit(failed); 43 | -------------------------------------------------------------------------------- /bin/blint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var blint = require("../blint"), fs = require("fs"), path = require("path"); 4 | 5 | var program = require("commander").program 6 | 7 | program 8 | .option("--ecmaVersion ", "ECMAScript version to parse") 9 | .option("--browser", "Allow browser globals") 10 | .option("--tabs", "Allow tabs") 11 | .option("--trailing", "Allow trailing whitespace") 12 | .option("--requireSemicolons", "Disallow semicolon insertion") 13 | .option("--forbidSemicolons", "Disallow unneccesary semicolons") 14 | .option("--trailingCommas", "Allow trailing commas") 15 | .option("--noReservedProps", "Disallow reserved words as property names") 16 | .option("--namedFunctions", "Allow named function expressions") 17 | .option("--noDeclareGlobals", "Disallow code to declare globals") 18 | .option("--package", "Get options from package.json") 19 | 20 | program.parse(process.argv) 21 | 22 | var args = program.opts() 23 | 24 | function findPackage(dir) { 25 | for (;;) { 26 | try { 27 | var package = path.resolve(dir, "package.json"); 28 | if (fs.statSync(package).isFile()) 29 | return JSON.parse(fs.readFileSync(package, "utf8")); 30 | } catch(e) {} 31 | var shorter = path.dirname(dir); 32 | if (shorter == dir) return null; 33 | dir = shorter; 34 | } 35 | } 36 | 37 | var options; 38 | if (args.package && program.args[0]) { 39 | var conf = findPackage(program.args[0]); 40 | options = conf && conf.blint; 41 | } else { 42 | options = {ecmaVersion: args.ecmaVersion || 5, 43 | browser: args.browser, 44 | tabs: args.tabs, 45 | trailing: args.trailing, 46 | semicolons: args.requireSemicolons ? true : args.forbidSemicolons ? false : null, 47 | trailingCommas: args.trailingCommas, 48 | reservedProps: !args.noReservedProps, 49 | namedFunctions: args.namedFunctions, 50 | declareGlobals: !args.noDeclareGlobals}; 51 | } 52 | 53 | for (var i = 0; i < program.args.length; i++) { 54 | var stat = fs.statSync(program.args[i]); 55 | if (stat.isDirectory()) blint.checkDir(program.args[i], options); 56 | else blint.checkFile(program.args[i], options); 57 | } 58 | 59 | process.exit(blint.success() ? 0 : 1); 60 | -------------------------------------------------------------------------------- /loop.js: -------------------------------------------------------------------------------- 1 | var walk = require("acorn-walk"); 2 | 3 | exports.checkReusedIndex = function(node, fail) { 4 | if (!node.init || node.init.type != "VariableDeclaration") return; 5 | var name = node.init.declarations[0].id.name; 6 | walk.recursive(node.body, null, { 7 | Function: function() {}, 8 | VariableDeclaration: function(node, st, c) { 9 | for (var i = 0; i < node.declarations.length; i++) 10 | if (node.declarations[i].id.name == name) 11 | fail("Redefined loop variable", node.declarations[i].id.loc); 12 | walk.base.VariableDeclaration(node, st, c); 13 | } 14 | }); 15 | }; 16 | 17 | exports.checkObviousInfiniteLoop = function(test, update, fail) { 18 | var vars = Object.create(null); 19 | function compDir(op) { 20 | if (op == "<" || op == "<=") return 1; 21 | if (op == ">" || op == ">=") return -1; 22 | return 0; 23 | } 24 | function opDir(op) { 25 | if (/\+/.test(op)) return 1; 26 | if (/-/.test(op)) return -1; 27 | return 0; 28 | } 29 | function store(name, dir) { 30 | if (!(name in vars)) vars[name] = {below: false, above: false}; 31 | if (dir > 0) vars[name].up = true; 32 | if (dir < 0) vars[name].down = true; 33 | } 34 | function check(node, dir) { 35 | var known = vars[node.name]; 36 | if (!known) return; 37 | if (dir > 0 && known.down && !known.up || 38 | dir < 0 && known.up && !known.down) 39 | fail("Suspiciously infinite-looking loop", node.loc); 40 | } 41 | walk.simple(test, { 42 | BinaryExpression: function(node) { 43 | if (node.left.type == "Identifier") 44 | store(node.left.name, compDir(node.operator)); 45 | if (node.right.type == "Identifier") 46 | store(node.right.name, -compDir(node.operator)); 47 | } 48 | }); 49 | walk.simple(update, { 50 | UpdateExpression: function(node) { 51 | if (node.argument.type == "Identifier") 52 | check(node.argument, opDir(node.operator)); 53 | }, 54 | AssignmentExpression: function(node) { 55 | if (node.left.type == "Identifier") { 56 | if (node.operator == "=" && node.right.type == "BinaryExpression" && node.right.left.name == node.left.name) 57 | check(node.left, opDir(node.right.operator)); 58 | else 59 | check(node.left, opDir(node.operator)); 60 | } 61 | } 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /globals.js: -------------------------------------------------------------------------------- 1 | exports.jsGlobals = "Infinity undefined NaN Object Function Array String Number Boolean RegExp Date Error SyntaxError ReferenceError URIError EvalError RangeError TypeError parseInt parseFloat isNaN isFinite eval encodeURI encodeURIComponent decodeURI decodeURIComponent Math JSON require console exports module Symbol Promise Map WeakMap".split(" "); 2 | 3 | exports.browserGlobals = "location Node Element Text Document document XMLDocument HTMLElement HTMLAnchorElement HTMLAreaElement HTMLAudioElement HTMLBaseElement HTMLBodyElement HTMLBRElement HTMLButtonElement HTMLCanvasElement HTMLDataElement HTMLDataListElement HTMLDivElement HTMLDListElement HTMLDocument HTMLEmbedElement HTMLFieldSetElement HTMLFormControlsCollection HTMLFormElement HTMLHeadElement HTMLHeadingElement HTMLHRElement HTMLHtmlElement HTMLIFrameElement HTMLImageElement HTMLInputElement HTMLKeygenElement HTMLLabelElement HTMLLegendElement HTMLLIElement HTMLLinkElement HTMLMapElement HTMLMediaElement HTMLMetaElement HTMLMeterElement HTMLModElement HTMLObjectElement HTMLOListElement HTMLOptGroupElement HTMLOptionElement HTMLOptionsCollection HTMLOutputElement HTMLParagraphElement HTMLParamElement HTMLPreElement HTMLProgressElement HTMLQuoteElement HTMLScriptElement HTMLSelectElement HTMLSourceElement HTMLSpanElement HTMLStyleElement HTMLTableCaptionElement HTMLTableCellElement HTMLTableColElement HTMLTableDataCellElement HTMLTableElement HTMLTableHeaderCellElement HTMLTableRowElement HTMLTableSectionElement HTMLTextAreaElement HTMLTimeElement HTMLTitleElement HTMLTrackElement HTMLUListElement HTMLUnknownElement HTMLVideoElement Attr NodeList HTMLCollection NamedNodeMap DocumentFragment DOMException DOMTokenList XPathResult ClientRect Event TouchEvent WheelEvent MouseEvent KeyboardEvent HashChangeEvent ErrorEvent CustomEvent BeforeLoadEvent WebSocket Worker localStorage sessionStorage FileList File Blob FileReader URL Range XMLHttpRequest DOMParser Selection console top parent window opener self devicePixelRatio name closed pageYOffset pageXOffset scrollY scrollX screenTop screenLeft screenY screenX innerWidth innerHeight outerWidth outerHeight frameElement crypto navigator history screen postMessage close blur focus onload onunload onscroll onresize ononline onoffline onmousewheel onmouseup onmouseover onmouseout onmousemove onmousedown onclick ondblclick onmessage onkeyup onkeypress onkeydown oninput onpopstate onhashchange onfocus onblur onerror ondrop ondragstart ondragover ondragleave ondragenter ondragend ondrag oncontextmenu onchange onbeforeunload onabort getSelection alert confirm prompt scrollBy scrollTo scroll setTimeout clearTimeout setInterval clearInterval atob btoa addEventListener removeEventListener dispatchEvent getComputedStyle CanvasRenderingContext2D importScripts MutationObserver indexedDB open requestAnimationFrame cancelAnimationFrame requestIdleCallback cancelIdleCallback customElements performance applicationCache".split(" "); 4 | 5 | -------------------------------------------------------------------------------- /nosemicolons.js: -------------------------------------------------------------------------------- 1 | var walk = require("acorn-walk"); 2 | 3 | // Enforce (safe) semicolon-free style 4 | // 5 | // Complains about semicolons at end of statements, about statements 6 | // that are dangerous in semicolon-free style not being prefixed with 7 | // a semicolon, and tries to spot accidentally continued statements. 8 | 9 | function needsLeadingSemicolon(text, pos) { 10 | var ch = text.charCodeAt(pos); 11 | if (ch == 40 || ch == 91) return true; 12 | if ((ch == 43 || ch == 45) && text.charCodeAt(pos + 1) != ch) return true; 13 | if (ch == 47) { 14 | var next = text.charCodeAt(pos + 1); 15 | if (next != 47 && next != 42) return true; 16 | } 17 | return false; 18 | } 19 | 20 | function col(text, pos) { 21 | var col = 0; 22 | while (pos && text.charCodeAt(pos - 1) != 10) { --pos; ++col; } 23 | return col; 24 | } 25 | 26 | function textAfter(node, text) { 27 | for (var pos = node.end; pos < text.length; pos++) { 28 | var ch = text.charCodeAt(pos); 29 | if (ch == 10) return false; 30 | if (ch == 47) { 31 | var after = text.charCodeAt(pos + 1); 32 | if (after == 47) return false; 33 | if (after == 42) { 34 | var end = text.indexOf("*/", pos + 2) + 2; 35 | var comment = text.slice(pos, end); 36 | if (comment.indexOf("\n") > -1) return false; 37 | pos = end - 1; 38 | } 39 | } 40 | if (ch != 32) return true; 41 | } 42 | return false; 43 | } 44 | 45 | module.exports = function(text, ast, fail) { 46 | walk.ancestor(ast, { 47 | Statement: function(node, parents) { 48 | var parent = parents[parents.length - 2]; 49 | 50 | if (parent.init == this || parent.left == this || 51 | /^(?:(?:For|ForIn|ForOf|While|Block|Empty|If|Labeled|With|Switch|Try)Statement|Program)$/.test(node.type)) 52 | return; // A for or for/in init clause or a block 53 | 54 | if (text.charAt(node.end - 1) == ";" && !textAfter(node, text)) 55 | fail("Semicolon found", node.loc, true); 56 | if (needsLeadingSemicolon(text, node.start) && 57 | (parent.type == "BlockStatement" || parent.type == "Program") && 58 | text.charAt(node.start - 1) != ";") 59 | fail("Missing leading semicolon", node.loc); 60 | 61 | var nextNewline = text.indexOf("\n", node.start); 62 | if (nextNewline < node.end) { 63 | var statementCol = col(text, node.start); 64 | while (nextNewline < node.end) { 65 | var firstNonWs = nextNewline + 1, startCol = 0; 66 | while (text.charCodeAt(firstNonWs + 1) == 32) { ++firstNonWs; ++startCol; } 67 | if (statementCol >= startCol && needsLeadingSemicolon(text, firstNonWs)) { 68 | fail("Possibly accidentally continued statement", node.loc); 69 | break; 70 | } 71 | nextNewline = text.indexOf("\n", nextNewline + 1); 72 | if (nextNewline < 0) break 73 | } 74 | } 75 | } 76 | }); 77 | }; 78 | -------------------------------------------------------------------------------- /scope.js: -------------------------------------------------------------------------------- 1 | var walk = require("acorn-walk"); 2 | 3 | exports.buildScopes = function(ast, fail) { 4 | var scopes = []; 5 | 6 | function makeScope(prev, type) { 7 | var scope = {vars: Object.create(null), prev: prev, type: type}; 8 | scopes.push(scope); 9 | return scope; 10 | } 11 | function fnScope(scope) { 12 | while (scope.type != "fn") scope = scope.prev; 13 | return scope; 14 | } 15 | function addVar(scope, name, type, node, deadZone, written) { 16 | if (deadZone && (name in scope.vars)) 17 | fail("Duplicate definition of " + name, node.loc); 18 | scope.vars[name] = {type: type, node: node, deadZone: deadZone && scope, 19 | written: written, read: false}; 20 | } 21 | 22 | function makeCx(scope, binding) { 23 | return {scope: scope, binding: binding}; 24 | } 25 | 26 | function isBlockScopedDecl(node) { 27 | return node.type == "VariableDeclaration" && node.kind != "var"; 28 | } 29 | 30 | var topScope = makeScope(null, "fn"); 31 | 32 | walk.recursive(ast, makeCx(topScope), { 33 | Function: function(node, cx, c) { 34 | var inner = node.scope = node.body.scope = makeScope(cx.scope, "fn"); 35 | var innerCx = makeCx(inner, {scope: inner, type: "argument", deadZone: true, written: true}); 36 | for (var i = 0; i < node.params.length; ++i) 37 | c(node.params[i], innerCx, "Pattern"); 38 | 39 | if (node.id) { 40 | var decl = node.type == "FunctionDeclaration"; 41 | addVar(decl ? cx.scope : inner, node.id.name, 42 | decl ? "function" : "function name", node.id, false, true); 43 | } 44 | c(node.body, innerCx, node.expression ? "Expression" : "Statement") 45 | }, 46 | TryStatement: function(node, cx, c) { 47 | c(node.block, cx, "Statement"); 48 | if (node.handler) { 49 | var inner = node.handler.body.scope = makeScope(cx.scope, "block"); 50 | if (node.handler.param) addVar(inner, node.handler.param.name, "catch clause", node.handler.param, false, true); 51 | c(node.handler.body, makeCx(inner), "Statement"); 52 | } 53 | if (node.finalizer) c(node.finalizer, cx, "Statement"); 54 | }, 55 | Class: function(node, cx, c) { 56 | if (node.id && node.type == "ClassDeclaration") 57 | addVar(cx.scope, node.id.name, "class name", node, true, true); 58 | if (node.superClass) c(node.superClass, cx, "Expression"); 59 | for (var i = 0; i < node.body.body.length; i++) 60 | c(node.body.body[i], cx); 61 | }, 62 | ImportDeclaration: function(node, cx) { 63 | for (var i = 0; i < node.specifiers.length; i++) { 64 | var spec = node.specifiers[i].local 65 | addVar(cx.scope, spec.name, "import", spec, false, true) 66 | } 67 | }, 68 | Expression: function(node, cx, c) { 69 | if (cx.binding) cx = makeCx(cx.scope) 70 | c(node, cx); 71 | }, 72 | VariableDeclaration: function(node, cx, c) { 73 | for (var i = 0; i < node.declarations.length; ++i) { 74 | var decl = node.declarations[i]; 75 | c(decl.id, makeCx(cx.scope, { 76 | scope: node.kind == "var" ? fnScope(cx.scope) : cx.scope, 77 | type: node.kind == "const" ? "constant" : "variable", 78 | deadZone: node.kind != "var", 79 | written: !!decl.init 80 | }), "Pattern"); 81 | if (decl.init) c(decl.init, cx, "Expression"); 82 | } 83 | }, 84 | VariablePattern: function(node, cx) { 85 | var b = cx.binding; 86 | if (b) addVar(b.scope, node.name, b.type, node, b.deadZone, b.written); 87 | }, 88 | BlockStatement: function(node, cx, c) { 89 | if (!node.scope && node.body.some(isBlockScopedDecl)) { 90 | node.scope = makeScope(cx.scope, "block"); 91 | cx = makeCx(node.scope) 92 | } 93 | walk.base.BlockStatement(node, cx, c); 94 | }, 95 | ForInStatement: function(node, cx, c) { 96 | if (!node.scope && isBlockScopedDecl(node.left)) { 97 | node.scope = node.body.scope = makeScope(cx.scope, "block"); 98 | cx = makeCx(node.scope); 99 | } 100 | walk.base.ForInStatement(node, cx, c); 101 | }, 102 | ForStatement: function(node, cx, c) { 103 | if (!node.scope && node.init && isBlockScopedDecl(node.init)) { 104 | node.scope = node.body.scope = makeScope(cx.scope, "block"); 105 | cx = makeCx(node.scope); 106 | } 107 | walk.base.ForStatement(node, cx, c); 108 | } 109 | }, null); 110 | 111 | return {all: scopes, top: topScope}; 112 | }; 113 | -------------------------------------------------------------------------------- /blint.js: -------------------------------------------------------------------------------- 1 | /* 2 | Simple linter, based on the Acorn [1] parser module 3 | 4 | All of the existing linters either cramp my style or have huge 5 | dependencies (Closure). So here's a very simple, non-invasive one 6 | that only spots 7 | 8 | - missing semicolons and trailing commas 9 | - variables or properties that are reserved words 10 | - assigning to a variable you didn't declare 11 | - access to non-whitelisted globals 12 | (use a '// declare global: foo, bar' comment to declare extra 13 | globals in a file) 14 | 15 | [1]: https://github.com/marijnh/acorn/ 16 | */ 17 | 18 | var fs = require("fs"), acorn = require("acorn"), walk = require("acorn-walk"); 19 | 20 | var getOptions = require("./options").getOptions; 21 | var buildScopes = require("./scope").buildScopes; 22 | var globals = require("./globals"); 23 | var loop = require("./loop"); 24 | 25 | var scopePasser = walk.make({ 26 | Statement: function(node, prev, c) { c(node, node.scope || prev); }, 27 | Function: function(node, _prev, c) { walk.base.Function(node, node.scope, c) } 28 | }); 29 | 30 | function checkFile(fileName, options, text) { 31 | options = getOptions(options); 32 | if (text == null) text = fs.readFileSync(fileName, "utf8"); 33 | 34 | var bad, msg; 35 | if (!options.trailingSpace) 36 | bad = text.match(/[\t ]\n/); 37 | if (!bad && !options.tabs) 38 | bad = text.match(/\t/); 39 | if (!bad) 40 | bad = text.match(/[\x00-\x08\x0b\x0c\x0e-\x19\uFEFF]/); 41 | if (bad) { 42 | if (bad[0].indexOf("\n") > -1) msg = "Trailing whitespace"; 43 | else if (bad[0] == "\t") msg = "Found tab character"; 44 | else msg = "Undesirable character 0x" + bad[0].charCodeAt(0).toString(16); 45 | var info = acorn.getLineInfo(text, bad.index); 46 | fail(msg, {start: info, source: fileName}); 47 | } 48 | 49 | if (options.blob && text.slice(0, options.blob.length) != options.blob) 50 | fail("Missing license blob", {source: fileName}); 51 | 52 | try { 53 | var ast = acorn.parse(text, { 54 | locations: true, 55 | ecmaVersion: options.ecmaVersion, 56 | onInsertedSemicolon: options.semicolons !== true ? null : function(_, loc) { 57 | fail("Missing semicolon", {source: fileName, start: loc}); 58 | }, 59 | onTrailingComma: options.trailingCommas ? null : function(_, loc) { 60 | fail("Trailing comma", {source: fileName, start: loc}); 61 | }, 62 | forbidReserved: options.reservedProps ? false : "everywhere", 63 | sourceFile: fileName, 64 | sourceType: "module" 65 | }); 66 | } catch (e) { 67 | fail(e.message, {source: fileName}); 68 | return; 69 | } 70 | 71 | if (options.semicolons === false) 72 | require("./nosemicolons")(text, ast, fail) 73 | 74 | var scopes = buildScopes(ast, fail); 75 | 76 | var globalsSeen = Object.create(null); 77 | var ignoredGlobals = Object.create(null); 78 | 79 | var checkWalker = { 80 | UpdateExpression: function(node, scope) {assignToPattern(node.argument, scope);}, 81 | AssignmentExpression: function(node, scope) {assignToPattern(node.left, scope);}, 82 | Identifier: function(node, scope) { 83 | if (node.name == "arguments") return; 84 | readVariable(node, scope); 85 | }, 86 | ExportNamedDeclaration: function(node, scope) { 87 | if (!node.source) for (var i = 0; i < node.specifiers.length; i++) 88 | readVariable(node.specifiers[i].local, scope); 89 | exportDecl(node.declaration, scope); 90 | }, 91 | ExportDefaultDeclaration: function(node, scope) { 92 | exportDecl(node.declaration, scope); 93 | }, 94 | FunctionExpression: function(node) { 95 | if (node.id && !options.namedFunctions) fail("Named function expression", node.loc); 96 | }, 97 | ForStatement: function(node) { 98 | loop.checkReusedIndex(node, fail); 99 | if (node.test && node.update) 100 | loop.checkObviousInfiniteLoop(node.test, node.update, fail); 101 | }, 102 | ForInStatement: function(node, scope) { 103 | assignToPattern(node.left.type == "VariableDeclaration" ? node.left.declarations[0].id : node.left, scope); 104 | }, 105 | ForOfStatement: function(node, scope) { 106 | assignToPattern(node.left.type == "VariableDeclaration" ? node.left.declarations[0].id : node.left, scope); 107 | }, 108 | MemberExpression: function(node) { 109 | if (!options.console && node.object.type == "Identifier" && node.object.name == "console" && !node.computed) 110 | fail("Found console." + node.property.name, node.loc); 111 | }, 112 | DebuggerStatement: function(node) { 113 | fail("Found debugger statement", node.loc); 114 | } 115 | }; 116 | 117 | function check(node, scope) { 118 | walk.simple(node, checkWalker, scopePasser, scope); 119 | } 120 | check(ast, scopes.top); 121 | 122 | function assignToPattern(node, scope) { 123 | walk.recursive(node, null, { 124 | Expression: function(node) { 125 | check(node, scope); 126 | }, 127 | VariablePattern: function(node) { 128 | var found = searchScope(node.name, scope); 129 | if (found) { 130 | found.written = true; 131 | } else if (!(node.name in ignoredGlobals)) { 132 | ignoredGlobals[node.name] = true; 133 | fail("Assignment to global variable " + node.name, node.loc); 134 | } 135 | } 136 | }, null, "Pattern"); 137 | } 138 | 139 | function readFromPattern(node, scope) { 140 | walk.recursive(node, null, { 141 | Expression: function() {}, 142 | VariablePattern: function(node) { readVariable(node, scope); } 143 | }, null, "Pattern"); 144 | } 145 | 146 | function readVariable(node, scope) { 147 | var found = searchScope(node.name, scope); 148 | if (found) { 149 | found.read = true; 150 | if (found.deadZone && node.start < found.node.start && sameFunction(scope, found.deadZone)) 151 | fail(found.type.charAt(0).toUpperCase() + found.type.slice(1) + " used before its declaration", node.loc); 152 | } else { 153 | globalsSeen[node.name] = node.loc; 154 | } 155 | } 156 | 157 | function exportDecl(decl, scope) { 158 | if (!decl) return; 159 | if (decl.id) { 160 | readVariable(decl.id, scope); 161 | } else if (decl.declarations) { 162 | for (var i = 0; i < decl.declarations.length; i++) 163 | readFromPattern(decl.declarations[i].id, scope); 164 | } 165 | } 166 | 167 | function sameFunction(inner, outer) { 168 | for (;;) { 169 | if (inner == outer) return true; 170 | if (inner.type == "fn") return false; 171 | inner = inner.prev; 172 | } 173 | } 174 | 175 | function searchScope(name, scope) { 176 | for (var cur = scope; cur; cur = cur.prev) 177 | if (name in cur.vars) return cur.vars[name]; 178 | } 179 | 180 | var allowedGlobals = Object.create(options.declareGlobals ? scopes.top.vars : null), m; 181 | if (options.allowedGlobals) options.allowedGlobals.forEach(function(v) { allowedGlobals[v] = true; }); 182 | for (var i = 0; i < globals.jsGlobals.length; i++) 183 | allowedGlobals[globals.jsGlobals[i]] = true; 184 | if (options.browser) 185 | for (var i = 0; i < globals.browserGlobals.length; i++) 186 | allowedGlobals[globals.browserGlobals[i]] = true; 187 | 188 | if (m = text.match(/\/\/ declare global:\s+(.*)/)) 189 | m[1].split(/,\s*/g).forEach(function(n) { allowedGlobals[n] = true; }); 190 | for (var glob in globalsSeen) 191 | if (!(glob in allowedGlobals)) 192 | fail("Access to global variable " + glob + ".", globalsSeen[glob]); 193 | 194 | for (var i = 0; i < scopes.all.length; ++i) { 195 | var scope = scopes.all[i]; 196 | for (var name in scope.vars) { 197 | var info = scope.vars[name]; 198 | if (!info.read) { 199 | if (info.type != "catch clause" && info.type != "function name" && name.charAt(0) != "_") 200 | fail("Unused " + info.type + " " + name, info.node.loc); 201 | } else if (!info.written) { 202 | fail(info.type.charAt(0).toUpperCase() + info.type.slice(1) + " " + name + " is never written to", 203 | info.node.loc); 204 | } 205 | } 206 | } 207 | 208 | function fail(msg, pos, end) { 209 | var loc = end ? pos.end : pos.start 210 | if (loc) msg += " (" + loc.line + ":" + loc.column + ")"; 211 | if (options.message) 212 | options.message(pos.source, msg) 213 | else 214 | console["log"](pos.source + ": " + msg); 215 | failed = true; 216 | } 217 | } 218 | 219 | var failed = false; 220 | 221 | function checkDir(dir, options) { 222 | fs.readdirSync(dir).forEach(function(file) { 223 | var fname = dir + "/" + file; 224 | if (/\.js$/.test(file)) checkFile(fname, options); 225 | else if (fs.lstatSync(fname).isDirectory()) checkDir(fname, options); 226 | }); 227 | } 228 | 229 | exports.checkDir = checkDir; 230 | exports.checkFile = checkFile; 231 | exports.success = function() { return !failed; }; 232 | --------------------------------------------------------------------------------