diff options
Diffstat (limited to 'tools/eslint/lib/rules')
157 files changed, 11868 insertions, 0 deletions
diff --git a/tools/eslint/lib/rules/block-scoped-var.js b/tools/eslint/lib/rules/block-scoped-var.js new file mode 100644 index 0000000000..6b23167fd1 --- /dev/null +++ b/tools/eslint/lib/rules/block-scoped-var.js @@ -0,0 +1,318 @@ +/** + * @fileoverview Rule to check for "block scoped" variables by binding context + * @author Matt DuVall <http://www.mattduvall.com> + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var scopeStack = []; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Determines whether an identifier is in declaration position or is a non-declaration reference. + * @param {ASTNode} id The identifier. + * @param {ASTNode} parent The identifier's parent AST node. + * @returns {Boolean} true when the identifier is in declaration position. + */ + function isDeclaration(id, parent) { + switch (parent.type) { + case "FunctionDeclaration": + case "FunctionExpression": + return parent.params.indexOf(id) > -1 || id === parent.id; + + case "VariableDeclarator": + return id === parent.id; + + case "CatchClause": + return id === parent.param; + + default: + return false; + } + } + + /** + * Determines whether an identifier is in property position. + * @param {ASTNode} id The identifier. + * @param {ASTNode} parent The identifier's parent AST node. + * @returns {Boolean} true when the identifier is in property position. + */ + function isProperty(id, parent) { + switch (parent.type) { + case "MemberExpression": + return id === parent.property && !parent.computed; + + case "Property": + return id === parent.key; + + default: + return false; + } + } + + /** + * Pushes a new scope object on the scope stack. + * @returns {void} + */ + function pushScope() { + scopeStack.push([]); + } + + /** + * Removes the topmost scope object from the scope stack. + * @returns {void} + */ + function popScope() { + scopeStack.pop(); + } + + /** + * Declares the given names in the topmost scope object. + * @param {[String]} names A list of names to declare. + * @returns {void} + */ + function declare(names) { + [].push.apply(scopeStack[scopeStack.length - 1], names); + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + /** + * Declares all relevant identifiers for module imports. + * @param {ASTNode} node The AST node representing an import. + * @returns {void} + * @private + */ + function declareImports(node) { + declare([node.local.name]); + + if (node.imported && node.imported.name !== node.local.name) { + declare([node.imported.name]); + } + } + + /** + * Declares all relevant identifiers for classes. + * @param {ASTNode} node The AST node representing a class. + * @returns {void} + * @private + */ + function declareClass(node) { + + if (node.id) { + declare([node.id.name]); + } + + pushScope(); + } + + /** + * Declares all relevant identifiers for classes. + * @param {ASTNode} node The AST node representing a class. + * @returns {void} + * @private + */ + function declareClassMethod(node) { + pushScope(); + + declare([node.key.name]); + } + + /** + * Add declarations based on the type of node being passed. + * @param {ASTNode} node The node containing declarations. + * @returns {void} + * @private + */ + function declareByNodeType(node) { + + var declarations = []; + + switch (node.type) { + case "Identifier": + declarations.push(node.name); + break; + + case "ObjectPattern": + node.properties.forEach(function(property) { + declarations.push(property.key.name); + if (property.value) { + declarations.push(property.value.name); + } + }); + break; + + case "ArrayPattern": + node.elements.forEach(function(element) { + if (element) { + declarations.push(element.name); + } + }); + break; + + case "AssignmentPattern": + declareByNodeType(node.left); + break; + + case "RestElement": + declareByNodeType(node.argument); + break; + + // no default + } + + declare(declarations); + + } + + /** + * Adds declarations of the function parameters and pushes the scope + * @param {ASTNode} node The node containing declarations. + * @returns {void} + * @private + */ + function functionHandler(node) { + pushScope(); + + node.params.forEach(function(param) { + declareByNodeType(param); + }); + + declare(node.rest ? [node.rest.name] : []); + declare(["arguments"]); + } + + /** + * Adds declaration of the function name in its parent scope then process the function + * @param {ASTNode} node The node containing declarations. + * @returns {void} + * @private + */ + function functionDeclarationHandler(node) { + declare(node.id ? [node.id.name] : []); + functionHandler(node); + } + + /** + * Process function declarations and declares its name in its own scope + * @param {ASTNode} node The node containing declarations. + * @returns {void} + * @private + */ + function functionExpressionHandler(node) { + functionHandler(node); + declare(node.id ? [node.id.name] : []); + } + + function variableDeclarationHandler(node) { + node.declarations.forEach(function(declaration) { + declareByNodeType(declaration.id); + }); + + } + + return { + "Program": function() { + var scope = context.getScope(); + scopeStack = [scope.variables.map(function(v) { + return v.name; + })]; + + // global return creates another scope + if (context.ecmaFeatures.globalReturn) { + scope = scope.childScopes[0]; + scopeStack.push(scope.variables.map(function(v) { + return v.name; + })); + } + }, + + "ImportSpecifier": declareImports, + "ImportDefaultSpecifier": declareImports, + "ImportNamespaceSpecifier": declareImports, + + "BlockStatement": function(node) { + var statements = node.body; + pushScope(); + statements.forEach(function(stmt) { + if (stmt.type === "VariableDeclaration") { + variableDeclarationHandler(stmt); + } else if (stmt.type === "FunctionDeclaration") { + declare([stmt.id.name]); + } + }); + }, + + "VariableDeclaration": function (node) { + variableDeclarationHandler(node); + }, + + "BlockStatement:exit": popScope, + + "CatchClause": function(node) { + pushScope(); + declare([node.param.name]); + }, + "CatchClause:exit": popScope, + + "FunctionDeclaration": functionDeclarationHandler, + "FunctionDeclaration:exit": popScope, + + "ClassDeclaration": declareClass, + "ClassDeclaration:exit": popScope, + + "ClassExpression": declareClass, + "ClassExpression:exit": popScope, + + "MethodDefinition": declareClassMethod, + "MethodDefinition:exit": popScope, + + "FunctionExpression": functionExpressionHandler, + "FunctionExpression:exit": popScope, + + // Arrow functions cannot have names + "ArrowFunctionExpression": functionHandler, + "ArrowFunctionExpression:exit": popScope, + + "ForStatement": function() { + pushScope(); + }, + "ForStatement:exit": popScope, + + "ForInStatement": function() { + pushScope(); + }, + "ForInStatement:exit": popScope, + + "ForOfStatement": function() { + pushScope(); + }, + "ForOfStatement:exit": popScope, + + "Identifier": function(node) { + var ancestor = context.getAncestors().pop(); + if (isDeclaration(node, ancestor) || isProperty(node, ancestor) || ancestor.type === "LabeledStatement") { + return; + } + + for (var i = 0, l = scopeStack.length; i < l; i++) { + if (scopeStack[i].indexOf(node.name) > -1) { + return; + } + } + + context.report(node, "\"" + node.name + "\" used outside of binding context."); + } + }; + +}; diff --git a/tools/eslint/lib/rules/brace-style.js b/tools/eslint/lib/rules/brace-style.js new file mode 100644 index 0000000000..599e86f68b --- /dev/null +++ b/tools/eslint/lib/rules/brace-style.js @@ -0,0 +1,204 @@ +/** + * @fileoverview Rule to flag block statements that do not use the one true brace style + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + var style = context.options[0] || "1tbs"; + var params = context.options[1] || {}; + + var OPEN_MESSAGE = "Opening curly brace does not appear on the same line as controlling statement.", + BODY_MESSAGE = "Statement inside of curly braces should be on next line.", + CLOSE_MESSAGE = "Closing curly brace does not appear on the same line as the subsequent block.", + CLOSE_MESSAGE_SINGLE = "Closing curly brace should be on the same line as opening curly brace or on the line after the previous block.", + CLOSE_MESSAGE_STROUSTRUP = "Closing curly brace appears on the same line as the subsequent block."; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Determines if a given node is a block statement. + * @param {ASTNode} node The node to check. + * @returns {boolean} True if the node is a block statement, false if not. + * @private + */ + function isBlock(node) { + return node && node.type === "BlockStatement"; + } + + + /** + * Binds a list of properties to a function that verifies that the opening + * curly brace is on the same line as its controlling statement of a given + * node. + * @param {...string} The properties to check on the node. + * @returns {Function} A function that will perform the check on a node + * @private + */ + function checkBlock() { + var blockProperties = arguments; + + return function(node) { + [].forEach.call(blockProperties, function(blockProp) { + var block = node[blockProp], previousToken, curlyToken, curlyTokenEnd, curlyTokensOnSameLine; + block = node[blockProp]; + + if (isBlock(block)) { + + previousToken = context.getTokenBefore(block); + curlyToken = context.getFirstToken(block); + curlyTokenEnd = context.getLastToken(block); + curlyTokensOnSameLine = curlyToken.loc.start.line === curlyTokenEnd.loc.start.line; + + if (previousToken.loc.start.line !== curlyToken.loc.start.line) { + context.report(node, OPEN_MESSAGE); + } else if (block.body.length && params.allowSingleLine) { + + if (curlyToken.loc.start.line === block.body[0].loc.start.line && !curlyTokensOnSameLine) { + context.report(block.body[0], BODY_MESSAGE); + } else if (curlyTokenEnd.loc.start.line === block.body[block.body.length - 1].loc.start.line && !curlyTokensOnSameLine) { + context.report(block.body[block.body.length - 1], CLOSE_MESSAGE_SINGLE); + } + + } else if (block.body.length && curlyToken.loc.start.line === block.body[0].loc.start.line) { + context.report(block.body[0], BODY_MESSAGE); + } + } + }); + }; + } + + /** + * Enforces the configured brace style on IfStatements + * @param {ASTNode} node An IfStatement node. + * @returns {void} + * @private + */ + function checkIfStatement(node) { + var tokens, + alternateIsBlock = false, + alternateIsIfBlock = false; + + checkBlock("consequent", "alternate")(node); + + if (node.alternate) { + + alternateIsBlock = isBlock(node.alternate); + alternateIsIfBlock = node.alternate.type === "IfStatement" && isBlock(node.alternate.consequent); + + if (alternateIsBlock || alternateIsIfBlock) { + tokens = context.getTokensBefore(node.alternate, 2); + + if (style === "1tbs") { + if (tokens[0].loc.start.line !== tokens[1].loc.start.line) { + context.report(node.alternate, CLOSE_MESSAGE); + } + } else if (style === "stroustrup") { + if (tokens[0].loc.start.line === tokens[1].loc.start.line) { + context.report(node.alternate, CLOSE_MESSAGE_STROUSTRUP); + } + } + } + + } + } + + /** + * Enforces the configured brace style on TryStatements + * @param {ASTNode} node A TryStatement node. + * @returns {void} + * @private + */ + function checkTryStatement(node) { + var tokens; + + checkBlock("block", "finalizer")(node); + + if (isBlock(node.finalizer)) { + tokens = context.getTokensBefore(node.finalizer, 2); + if (style === "1tbs") { + if (tokens[0].loc.start.line !== tokens[1].loc.start.line) { + context.report(node.finalizer, CLOSE_MESSAGE); + } + } else if (style === "stroustrup") { + if (tokens[0].loc.start.line === tokens[1].loc.start.line) { + context.report(node.finalizer, CLOSE_MESSAGE_STROUSTRUP); + } + } + } + } + + /** + * Enforces the configured brace style on CatchClauses + * @param {ASTNode} node A CatchClause node. + * @returns {void} + * @private + */ + function checkCatchClause(node) { + var previousToken = context.getTokenBefore(node), + firstToken = context.getFirstToken(node); + + checkBlock("body")(node); + + if (isBlock(node.body)) { + if (style === "1tbs") { + if (previousToken.loc.start.line !== firstToken.loc.start.line) { + context.report(node, CLOSE_MESSAGE); + } + } else if (style === "stroustrup") { + if (previousToken.loc.start.line === firstToken.loc.start.line) { + context.report(node, CLOSE_MESSAGE_STROUSTRUP); + } + } + } + } + + /** + * Enforces the configured brace style on SwitchStatements + * @param {ASTNode} node A SwitchStatement node. + * @returns {void} + * @private + */ + function checkSwitchStatement(node) { + var tokens; + if (node.cases && node.cases.length) { + tokens = context.getTokensBefore(node.cases[0], 2); + if (tokens[0].loc.start.line !== tokens[1].loc.start.line) { + context.report(node, OPEN_MESSAGE); + } + } else { + tokens = context.getLastTokens(node, 3); + if (tokens[0].loc.start.line !== tokens[1].loc.start.line) { + context.report(node, OPEN_MESSAGE); + } + } + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + "FunctionDeclaration": checkBlock("body"), + "FunctionExpression": checkBlock("body"), + "ArrowFunctionExpression": checkBlock("body"), + "IfStatement": checkIfStatement, + "TryStatement": checkTryStatement, + "CatchClause": checkCatchClause, + "DoWhileStatement": checkBlock("body"), + "WhileStatement": checkBlock("body"), + "WithStatement": checkBlock("body"), + "ForStatement": checkBlock("body"), + "ForInStatement": checkBlock("body"), + "ForOfStatement": checkBlock("body"), + "SwitchStatement": checkSwitchStatement + }; + +}; diff --git a/tools/eslint/lib/rules/camelcase.js b/tools/eslint/lib/rules/camelcase.js new file mode 100644 index 0000000000..5b1b8020da --- /dev/null +++ b/tools/eslint/lib/rules/camelcase.js @@ -0,0 +1,99 @@ +/** + * @fileoverview Rule to flag non-camelcased identifiers + * @author Nicholas C. Zakas + * @copyright 2015 Dieter Oberkofler. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Checks if a string contains an underscore and isn't all upper-case + * @param {String} name The string to check. + * @returns {boolean} if the string is underscored + * @private + */ + function isUnderscored(name) { + + // if there's an underscore, it might be A_CONSTANT, which is okay + return name.indexOf("_") > -1 && name !== name.toUpperCase(); + } + + /** + * Reports an AST node as a rule violation. + * @param {ASTNode} node The node to report. + * @returns {void} + * @private + */ + function report(node) { + context.report(node, "Identifier '{{name}}' is not in camel case.", { name: node.name }); + } + + var options = context.options[0] || {}, + properties = options.properties || ""; + + if (properties !== "always" && properties !== "never") { + properties = "always"; + } + + return { + + "Identifier": function(node) { + + // Leading and trailing underscores are commonly used to flag private/protected identifiers, strip them + var name = node.name.replace(/^_+|_+$/g, ""), + effectiveParent = (node.parent.type === "MemberExpression") ? node.parent.parent : node.parent; + + // MemberExpressions get special rules + if (node.parent.type === "MemberExpression") { + + // "never" check properties + if (properties === "never") { + return; + } + + // Always report underscored object names + if (node.parent.object.type === "Identifier" && + node.parent.object.name === node.name && + isUnderscored(name)) { + report(node); + + // Report AssignmentExpressions only if they are the left side of the assignment + } else if (effectiveParent.type === "AssignmentExpression" && + isUnderscored(name) && + (effectiveParent.right.type !== "MemberExpression" || + effectiveParent.left.type === "MemberExpression" && + effectiveParent.left.property.name === node.name)) { + report(node); + } + + // Properties have their own rules + } else if (node.parent.type === "Property") { + + // "never" check properties + if (properties === "never") { + return; + } + + if (isUnderscored(name) && effectiveParent.type !== "CallExpression") { + report(node); + } + + // Report anything that is underscored that isn't a CallExpression + } else if (isUnderscored(name) && effectiveParent.type !== "CallExpression") { + report(node); + } + } + + }; + +}; diff --git a/tools/eslint/lib/rules/comma-dangle.js b/tools/eslint/lib/rules/comma-dangle.js new file mode 100644 index 0000000000..da3776d424 --- /dev/null +++ b/tools/eslint/lib/rules/comma-dangle.js @@ -0,0 +1,58 @@ +/** + * @fileoverview Rule to forbid or enforce dangling commas. + * @author Ian Christian Myers + * @copyright 2015 Mathias Schreck + * @copyright 2013 Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function (context) { + var allowDangle = context.options[0]; + var forbidDangle = allowDangle !== "always-multiline" && allowDangle !== "always"; + var UNEXPECTED_MESSAGE = "Unexpected trailing comma."; + var MISSING_MESSAGE = "Missing trailing comma."; + + /** + * Checks the given node for trailing comma and reports violations. + * @param {ASTNode} node The node of an ObjectExpression or ArrayExpression + * @returns {void} + */ + function checkForTrailingComma(node) { + var items = node.properties || node.elements, + length = items.length, + nodeIsMultiLine = node.loc.start.line !== node.loc.end.line, + lastItem, + penultimateToken, + hasDanglingComma; + + if (length) { + lastItem = items[length - 1]; + if (lastItem) { + penultimateToken = context.getLastToken(node, 1); + hasDanglingComma = penultimateToken.value === ","; + + if (forbidDangle && hasDanglingComma) { + context.report(lastItem, penultimateToken.loc.start, UNEXPECTED_MESSAGE); + } else if (allowDangle === "always-multiline") { + if (hasDanglingComma && !nodeIsMultiLine) { + context.report(lastItem, penultimateToken.loc.start, UNEXPECTED_MESSAGE); + } else if (!hasDanglingComma && nodeIsMultiLine) { + context.report(lastItem, penultimateToken.loc.end, MISSING_MESSAGE); + } + } else if (allowDangle === "always" && !hasDanglingComma) { + context.report(lastItem, lastItem.loc.end, MISSING_MESSAGE); + } + } + } + } + + return { + "ObjectExpression": checkForTrailingComma, + "ArrayExpression": checkForTrailingComma + }; +}; diff --git a/tools/eslint/lib/rules/comma-spacing.js b/tools/eslint/lib/rules/comma-spacing.js new file mode 100644 index 0000000000..32136a3523 --- /dev/null +++ b/tools/eslint/lib/rules/comma-spacing.js @@ -0,0 +1,159 @@ +/** + * @fileoverview Comma spacing - validates spacing before and after comma + * @author Vignesh Anand aka vegetableman. + * @copyright 2014 Vignesh Anand. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var options = { + before: context.options[0] ? !!context.options[0].before : false, + after: context.options[0] ? !!context.options[0].after : true + }; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + // the index of the last comment that was checked + var lastCommentIndex = 0; + + /** + * Determines whether two adjacent tokens have whitespace between them. + * @param {Object} left - The left token object. + * @param {Object} right - The right token object. + * @returns {boolean} Whether or not there is space between the tokens. + */ + function isSpaced(left, right) { + var punctuationLength = context.getTokensBetween(left, right).length; // the length of any parenthesis + return (left.range[1] + punctuationLength) < right.range[0]; + } + + /** + * Checks whether two tokens are on the same line. + * @param {ASTNode} left The leftmost token. + * @param {ASTNode} right The rightmost token. + * @returns {boolean} True if the tokens are on the same line, false if not. + * @private + */ + function isSameLine(left, right) { + return left.loc.end.line === right.loc.start.line; + } + + /** + * Determines if a given token is a comma operator. + * @param {ASTNode} token The token to check. + * @returns {boolean} True if the token is a comma, false if not. + * @private + */ + function isComma(token) { + return !!token && (token.type === "Punctuator") && (token.value === ","); + } + + /** + * Reports a spacing error with an appropriate message. + * @param {ASTNode} node The binary expression node to report. + * @param {string} dir Is the error "before" or "after" the comma? + * @returns {void} + * @private + */ + function report(node, dir) { + context.report(node, options[dir] ? + "A space is required " + dir + " ','." : + "There should be no space " + dir + " ','."); + } + + /** + * Validates the spacing around a comma token. + * @param {Object} tokens - The tokens to be validated. + * @param {Token} tokens.comma The token representing the comma. + * @param {Token} [tokens.left] The last token before the comma. + * @param {Token} [tokens.right] The first token after the comma. + * @param {Token|ASTNode} reportItem The item to use when reporting an error. + * @returns {void} + * @private + */ + function validateCommaItemSpacing(tokens, reportItem) { + if (tokens.left && isSameLine(tokens.left, tokens.comma) && + (options.before !== isSpaced(tokens.left, tokens.comma)) + ) { + report(reportItem, "before"); + } + if (tokens.right && isSameLine(tokens.comma, tokens.right) && + (options.after !== isSpaced(tokens.comma, tokens.right)) + ) { + report(reportItem, "after"); + } + } + + /** + * Determines if a given source index is in a comment or not by checking + * the index against the comment range. Since the check goes straight + * through the file, once an index is passed a certain comment, we can + * go to the next comment to check that. + * @param {int} index The source index to check. + * @param {ASTNode[]} comments An array of comment nodes. + * @returns {boolean} True if the index is within a comment, false if not. + * @private + */ + function isIndexInComment(index, comments) { + + var comment; + + while (lastCommentIndex < comments.length) { + + comment = comments[lastCommentIndex]; + + if (comment.range[0] <= index && index < comment.range[1]) { + return true; + } else if (index > comment.range[1]) { + lastCommentIndex++; + } else { + break; + } + + } + + return false; + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + "Program": function() { + + var source = context.getSource(), + allComments = context.getAllComments(), + pattern = /,/g, + commaToken, + previousToken, + nextToken; + + while (pattern.test(source)) { + + // do not flag anything inside of comments + if (!isIndexInComment(pattern.lastIndex, allComments)) { + commaToken = context.getTokenByRangeStart(pattern.lastIndex - 1); + + if (commaToken && commaToken.type !== "JSXText") { + previousToken = context.getTokenBefore(commaToken); + nextToken = context.getTokenAfter(commaToken); + validateCommaItemSpacing({ + comma: commaToken, + left: isComma(previousToken) ? null : previousToken, + right: isComma(nextToken) ? null : nextToken + }, commaToken); + } + } + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/comma-style.js b/tools/eslint/lib/rules/comma-style.js new file mode 100644 index 0000000000..4b8d145775 --- /dev/null +++ b/tools/eslint/lib/rules/comma-style.js @@ -0,0 +1,177 @@ +/** + * @fileoverview Comma style - enforces comma styles of two types: last and first + * @author Vignesh Anand aka vegetableman + * @copyright 2014 Vignesh Anand. All rights reserved. + * @copyright 2015 Evan Simmons. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var style = context.options[0] || "last", + exceptions = {}; + + if (context.options.length === 2 && context.options[1].hasOwnProperty("exceptions")) { + exceptions = context.options[1].exceptions; + } + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Checks whether two tokens are on the same line. + * @param {ASTNode} left The leftmost token. + * @param {ASTNode} right The rightmost token. + * @returns {boolean} True if the tokens are on the same line, false if not. + * @private + */ + function isSameLine(left, right) { + return left.loc.end.line === right.loc.start.line; + } + + /** + * Determines if a given token is a comma operator. + * @param {ASTNode} token The token to check. + * @returns {boolean} True if the token is a comma, false if not. + * @private + */ + function isComma(token) { + return !!token && (token.type === "Punctuator") && (token.value === ","); + } + + /** + * Validates the spacing around single items in lists. + * @param {Token} previousItemToken The last token from the previous item. + * @param {Token} commaToken The token representing the comma. + * @param {Token} currentItemToken The first token of the current item. + * @param {Token} reportItem The item to use when reporting an error. + * @returns {void} + * @private + */ + function validateCommaItemSpacing(previousItemToken, commaToken, currentItemToken, reportItem) { + + // if single line + if (isSameLine(commaToken, currentItemToken) && + isSameLine(previousItemToken, commaToken)) { + + return; + + } else if (!isSameLine(commaToken, currentItemToken) && + !isSameLine(previousItemToken, commaToken)) { + + // lone comma + context.report(reportItem, { + line: commaToken.loc.end.line, + column: commaToken.loc.start.column + }, "Bad line breaking before and after ','."); + + } else if (style === "first" && !isSameLine(commaToken, currentItemToken)) { + + context.report(reportItem, "',' should be placed first."); + + } else if (style === "last" && isSameLine(commaToken, currentItemToken)) { + + context.report(reportItem, { + line: commaToken.loc.end.line, + column: commaToken.loc.end.column + }, "',' should be placed last."); + } + } + + /** + * Checks the comma placement with regards to a declaration/property/element + * @param {ASTNode} node The binary expression node to check + * @param {string} property The property of the node containing child nodes. + * @private + * @returns {void} + */ + function validateComma(node, property) { + var items = node[property], + arrayLiteral = (node.type === "ArrayExpression"), + previousItemToken; + + if (items.length > 1 || arrayLiteral) { + + // seed as opening [ + previousItemToken = context.getFirstToken(node); + + items.forEach(function(item) { + var commaToken = item ? context.getTokenBefore(item) : previousItemToken, + currentItemToken = item ? context.getFirstToken(item) : context.getTokenAfter(commaToken), + reportItem = item || currentItemToken; + + /* + * This works by comparing three token locations: + * - previousItemToken is the last token of the previous item + * - commaToken is the location of the comma before the current item + * - currentItemToken is the first token of the current item + * + * These values get switched around if item is undefined. + * previousItemToken will refer to the last token not belonging + * to the current item, which could be a comma or an opening + * square bracket. currentItemToken could be a comma. + * + * All comparisons are done based on these tokens directly, so + * they are always valid regardless of an undefined item. + */ + if (isComma(commaToken)) { + validateCommaItemSpacing(previousItemToken, commaToken, + currentItemToken, reportItem); + } + + previousItemToken = item ? context.getLastToken(item) : previousItemToken; + }); + + /* + * Special case for array literals that have empty last items, such + * as [ 1, 2, ]. These arrays only have two items show up in the + * AST, so we need to look at the token to verify that there's no + * dangling comma. + */ + if (arrayLiteral) { + + var lastToken = context.getLastToken(node), + nextToLastToken = context.getTokenBefore(lastToken); + + if (isComma(nextToLastToken)) { + validateCommaItemSpacing( + context.getTokenBefore(nextToLastToken), + nextToLastToken, + lastToken, + lastToken + ); + } + } + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + var nodes = {}; + + if (!exceptions.VariableDeclaration) { + nodes.VariableDeclaration = function(node) { + validateComma(node, "declarations"); + }; + } + if (!exceptions.ObjectExpression) { + nodes.ObjectExpression = function(node) { + validateComma(node, "properties"); + }; + } + if (!exceptions.ArrayExpression) { + nodes.ArrayExpression = function(node) { + validateComma(node, "elements"); + }; + } + + return nodes; +}; diff --git a/tools/eslint/lib/rules/complexity.js b/tools/eslint/lib/rules/complexity.js new file mode 100644 index 0000000000..36c9c1f467 --- /dev/null +++ b/tools/eslint/lib/rules/complexity.js @@ -0,0 +1,88 @@ +/** + * @fileoverview Counts the cyclomatic complexity of each function of the script. See http://en.wikipedia.org/wiki/Cyclomatic_complexity. + * Counts the number of if, conditional, for, whilte, try, switch/case, + * @author Patrick Brosset + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var THRESHOLD = context.options[0]; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + // Using a stack to store complexity (handling nested functions) + var fns = []; + + // When parsing a new function, store it in our function stack + function startFunction() { + fns.push(1); + } + + function endFunction(node) { + var complexity = fns.pop(), + name = "anonymous"; + + if (node.id) { + name = node.id.name; + } else if (node.parent.type === "MethodDefinition") { + name = node.parent.key.name; + } + + if (complexity > THRESHOLD) { + context.report(node, "Function '{{name}}' has a complexity of {{complexity}}.", { name: name, complexity: complexity }); + } + } + + function increaseComplexity() { + if (fns.length) { + fns[fns.length - 1] ++; + } + } + + function increaseSwitchComplexity(node) { + // Avoiding `default` + if (node.test) { + increaseComplexity(node); + } + } + + function increaseLogicalComplexity(node) { + // Avoiding && + if (node.operator === "||") { + increaseComplexity(node); + } + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + "FunctionDeclaration": startFunction, + "FunctionExpression": startFunction, + "ArrowFunctionExpression": startFunction, + "FunctionDeclaration:exit": endFunction, + "FunctionExpression:exit": endFunction, + "ArrowFunctionExpression:exit": endFunction, + + "CatchClause": increaseComplexity, + "ConditionalExpression": increaseComplexity, + "LogicalExpression": increaseLogicalComplexity, + "ForStatement": increaseComplexity, + "ForInStatement": increaseComplexity, + "ForOfStatement": increaseComplexity, + "IfStatement": increaseComplexity, + "SwitchCase": increaseSwitchComplexity, + "WhileStatement": increaseComplexity, + "DoWhileStatement": increaseComplexity + }; + +}; diff --git a/tools/eslint/lib/rules/consistent-return.js b/tools/eslint/lib/rules/consistent-return.js new file mode 100644 index 0000000000..38adba7f31 --- /dev/null +++ b/tools/eslint/lib/rules/consistent-return.js @@ -0,0 +1,73 @@ +/** + * @fileoverview Rule to flag consistent return values + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var functions = []; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Marks entrance into a function by pushing a new object onto the functions + * stack. + * @returns {void} + * @private + */ + function enterFunction() { + functions.push({}); + } + + /** + * Marks exit of a function by popping off the functions stack. + * @returns {void} + * @private + */ + function exitFunction() { + functions.pop(); + } + + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + "Program": enterFunction, + "FunctionDeclaration": enterFunction, + "FunctionExpression": enterFunction, + "ArrowFunctionExpression": enterFunction, + + "Program:exit": exitFunction, + "FunctionDeclaration:exit": exitFunction, + "FunctionExpression:exit": exitFunction, + "ArrowFunctionExpression:exit": exitFunction, + + "ReturnStatement": function(node) { + + var returnInfo = functions[functions.length - 1], + returnTypeDefined = "type" in returnInfo; + + if (returnTypeDefined) { + + if (returnInfo.type !== !!node.argument) { + context.report(node, "Expected " + (returnInfo.type ? "a" : "no") + " return value."); + } + + } else { + returnInfo.type = !!node.argument; + } + + } + }; + +}; diff --git a/tools/eslint/lib/rules/consistent-this.js b/tools/eslint/lib/rules/consistent-this.js new file mode 100644 index 0000000000..3efb2cd372 --- /dev/null +++ b/tools/eslint/lib/rules/consistent-this.js @@ -0,0 +1,108 @@ +/** + * @fileoverview Rule to enforce consistent naming of "this" context variables + * @author Raphael Pigulla + * @copyright 2015 Timothy Jones. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + var alias = context.options[0]; + + /** + * Reports that a variable declarator or assignment expression is assigning + * a non-'this' value to the specified alias. + * @param {ASTNode} node - The assigning node. + * @returns {void} + */ + function reportBadAssignment(node) { + context.report(node, + "Designated alias '{{alias}}' is not assigned to 'this'.", + { alias: alias }); + } + + /** + * Checks that an assignment to an identifier only assigns 'this' to the + * appropriate alias, and the alias is only assigned to 'this'. + * @param {ASTNode} node - The assigning node. + * @param {Identifier} name - The name of the variable assigned to. + * @param {Expression} value - The value of the assignment. + * @returns {void} + */ + function checkAssignment(node, name, value) { + var isThis = value.type === "ThisExpression"; + + if (name === alias) { + if (!isThis || node.operator && node.operator !== "=") { + reportBadAssignment(node); + } + } else if (isThis) { + context.report(node, + "Unexpected alias '{{name}}' for 'this'.", { name: name }); + } + } + + /** + * Ensures that a variable declaration of the alias in a program or function + * is assigned to the correct value. + * @returns {void} + */ + function ensureWasAssigned() { + var scope = context.getScope(); + + scope.variables.some(function (variable) { + var lookup; + + if (variable.name === alias) { + if (variable.defs.some(function (def) { + return def.node.type === "VariableDeclarator" && + def.node.init !== null; + })) { + return true; + } + + lookup = scope.type === "global" ? scope : variable; + + // The alias has been declared and not assigned: check it was + // assigned later in the same scope. + if (!lookup.references.some(function (reference) { + var write = reference.writeExpr; + + if (reference.from === scope && + write && write.type === "ThisExpression" && + write.parent.operator === "=") { + return true; + } + })) { + variable.defs.map(function (def) { + return def.node; + }).forEach(reportBadAssignment); + } + + return true; + } + }); + } + + return { + "Program:exit": ensureWasAssigned, + "FunctionExpression:exit": ensureWasAssigned, + "FunctionDeclaration:exit": ensureWasAssigned, + + "VariableDeclarator": function (node) { + if (node.init !== null) { + checkAssignment(node, node.id.name, node.init); + } + }, + + "AssignmentExpression": function (node) { + if (node.left.type === "Identifier") { + checkAssignment(node, node.left.name, node.right); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/curly.js b/tools/eslint/lib/rules/curly.js new file mode 100644 index 0000000000..2bfcfb2798 --- /dev/null +++ b/tools/eslint/lib/rules/curly.js @@ -0,0 +1,103 @@ +/** + * @fileoverview Rule to flag statements without curly braces + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var multiOnly = (context.options[0] === "multi"); + var multiLine = (context.options[0] === "multi-line"); + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Determines if a given node is a one-liner that's on the same line as it's preceding code. + * @param {ASTNode} node The node to check. + * @returns {boolean} True if the node is a one-liner that's on the same line as it's preceding code. + * @private + */ + function isCollapsedOneLiner(node) { + var before = context.getTokenBefore(node), + last = context.getLastToken(node); + return before.loc.start.line === last.loc.end.line; + } + + /** + * Checks the body of a node to see if it's a block statement. Depending on + * the rule options, reports the appropriate problems. + * @param {ASTNode} node The node to report if there's a problem. + * @param {ASTNode} body The body node to check for blocks. + * @param {string} name The name to report if there's a problem. + * @param {string} suffix Additional string to add to the end of a report. + * @returns {void} + */ + function checkBody(node, body, name, suffix) { + var hasBlock = (body.type === "BlockStatement"); + + if (multiOnly) { + if (hasBlock && body.body.length === 1) { + context.report(node, "Unnecessary { after '{{name}}'{{suffix}}.", + { + name: name, + suffix: (suffix ? " " + suffix : "") + } + ); + } + } else if (multiLine) { + if (!hasBlock && !isCollapsedOneLiner(body)) { + context.report(node, "Expected { after '{{name}}'{{suffix}}.", + { + name: name, + suffix: (suffix ? " " + suffix : "") + } + ); + } + } else { + if (!hasBlock) { + context.report(node, "Expected { after '{{name}}'{{suffix}}.", + { + name: name, + suffix: (suffix ? " " + suffix : "") + } + ); + } + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + "IfStatement": function(node) { + + checkBody(node, node.consequent, "if", "condition"); + + if (node.alternate && node.alternate.type !== "IfStatement") { + checkBody(node, node.alternate, "else"); + } + + }, + + "WhileStatement": function(node) { + checkBody(node, node.body, "while", "condition"); + }, + + "DoWhileStatement": function (node) { + checkBody(node, node.body, "do"); + }, + + "ForStatement": function(node) { + checkBody(node, node.body, "for", "condition"); + } + }; + +}; diff --git a/tools/eslint/lib/rules/default-case.js b/tools/eslint/lib/rules/default-case.js new file mode 100644 index 0000000000..da0da1ad7d --- /dev/null +++ b/tools/eslint/lib/rules/default-case.js @@ -0,0 +1,64 @@ +/** + * @fileoverview require default case in switch statements + * @author Aliaksei Shytkin + */ +"use strict"; + +var COMMENT_VALUE = "no default"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Shortcut to get last element of array + * @param {*[]} collection Array + * @returns {*} Last element + */ + function last(collection) { + return collection[collection.length - 1]; + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + "SwitchStatement": function(node) { + + if (!node.cases.length) { + // skip check of empty switch because there is no easy way + // to extract comments inside it now + return; + } + + var hasDefault = node.cases.some(function(v) { + return v.test === null; + }); + + if (!hasDefault) { + + var comment; + var comments; + + var lastCase = last(node.cases); + comments = context.getComments(lastCase).trailing; + + if (comments.length) { + comment = last(comments); + } + + if (!comment || comment.value.trim() !== COMMENT_VALUE) { + context.report(node, "Expected a default case."); + } + } + } + }; +}; diff --git a/tools/eslint/lib/rules/dot-notation.js b/tools/eslint/lib/rules/dot-notation.js new file mode 100644 index 0000000000..bf89cbfdf8 --- /dev/null +++ b/tools/eslint/lib/rules/dot-notation.js @@ -0,0 +1,104 @@ +/** + * @fileoverview Rule to warn about using dot notation instead of square bracket notation when possible. + * @author Josh Perez + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +var validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; +var keywords = [ + "this", + "function", + "if", + "return", + "var", + "else", + "for", + "new", + "in", + "typeof", + "while", + "case", + "break", + "try", + "catch", + "delete", + "throw", + "switch", + "continue", + "default", + "instanceof", + "do", + "void", + "finally", + "with", + "debugger", + "implements", + "interface", + "package", + "private", + "protected", + "public", + "static", + "class", + "enum", + "export", + "extends", + "import", + "super", + "true", + "false", + "null", + "abstract", + "boolean", + "byte", + "char", + "const", + "double", + "final", + "float", + "goto", + "int", + "long", + "native", + "short", + "synchronized", + "throws", + "transient", + "volatile" +]; + +module.exports = function(context) { + var options = context.options[0] || {}; + var allowKeywords = options.allowKeywords === void 0 || !!options.allowKeywords; + + var allowPattern; + if (options.allowPattern) { + allowPattern = new RegExp(options.allowPattern); + } + + return { + "MemberExpression": function(node) { + if ( + node.computed && + node.property.type === "Literal" && + validIdentifier.test(node.property.value) && + (allowKeywords || keywords.indexOf("" + node.property.value) === -1) + ) { + if (!(allowPattern && allowPattern.test(node.property.value))) { + context.report(node, "[" + JSON.stringify(node.property.value) + "] is better written in dot notation."); + } + } + if ( + !allowKeywords && + !node.computed && + keywords.indexOf("" + node.property.name) !== -1 + ) { + context.report(node, "." + node.property.name + " is a syntax error."); + } + } + }; +}; diff --git a/tools/eslint/lib/rules/eol-last.js b/tools/eslint/lib/rules/eol-last.js new file mode 100644 index 0000000000..96c78c18b4 --- /dev/null +++ b/tools/eslint/lib/rules/eol-last.js @@ -0,0 +1,36 @@ +/** + * @fileoverview Require file to end with single newline. + * @author Nodeca Team <https://github.com/nodeca> + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + "Program": function checkBadEOF(node) { + // Get the whole source code, not for node only. + var src = context.getSource(), location = {column: 1}; + + if (src.length === 0) { + return; + } + + if (src[src.length - 1] !== "\n") { + // file is not newline-terminated + location.line = src.split(/\n/g).length; + context.report(node, location, "Newline required at end of file but not found."); + } + } + + }; + +}; diff --git a/tools/eslint/lib/rules/eqeqeq.js b/tools/eslint/lib/rules/eqeqeq.js new file mode 100644 index 0000000000..e847a8327e --- /dev/null +++ b/tools/eslint/lib/rules/eqeqeq.js @@ -0,0 +1,90 @@ +/** + * @fileoverview Rule to flag statements that use != and == instead of !== and === + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + /** + * Checks if an expression is a typeof expression + * @param {ASTNode} node The node to check + * @returns {boolean} if the node is a typeof expression + */ + function isTypeOf(node) { + return node.type === "UnaryExpression" && node.operator === "typeof"; + } + + /** + * Checks if either operand of a binary expression is a typeof operation + * @param {ASTNode} node The node to check + * @returns {boolean} if one of the operands is typeof + * @private + */ + function isTypeOfBinary(node) { + return isTypeOf(node.left) || isTypeOf(node.right); + } + + /** + * Checks if operands are literals of the same type (via typeof) + * @param {ASTNode} node The node to check + * @returns {boolean} if operands are of same type + * @private + */ + function areLiteralsAndSameType(node) { + return node.left.type === "Literal" && node.right.type === "Literal" && + typeof node.left.value === typeof node.right.value; + } + + /** + * Checks if one of the operands is a literal null + * @param {ASTNode} node The node to check + * @returns {boolean} if operands are null + * @private + */ + function isNullCheck(node) { + return (node.right.type === "Literal" && node.right.value === null) || + (node.left.type === "Literal" && node.left.value === null); + } + + /** + * Gets the location (line and column) of the binary expression's operator + * @param {ASTNode} node The binary expression node to check + * @param {String} operator The operator to find + * @returns {Object} { line, column } location of operator + * @private + */ + function getOperatorLocation(node) { + var opToken = context.getTokenAfter(node.left); + return {line: opToken.loc.start.line, column: opToken.loc.start.column}; + } + + return { + "BinaryExpression": function(node) { + if (node.operator !== "==" && node.operator !== "!=") { + return; + } + + if (context.options[0] === "smart" && (isTypeOfBinary(node) || + areLiteralsAndSameType(node)) || isNullCheck(node)) { + return; + } + + if (context.options[0] === "allow-null" && isNullCheck(node)) { + return; + } + + context.report( + node, getOperatorLocation(node), + "Expected '{{op}}=' and instead saw '{{op}}'.", + {op: node.operator} + ); + } + }; + +}; diff --git a/tools/eslint/lib/rules/func-names.js b/tools/eslint/lib/rules/func-names.js new file mode 100644 index 0000000000..eb4d9be10a --- /dev/null +++ b/tools/eslint/lib/rules/func-names.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Rule to warn when a function expression does not have a name. + * @author Kyle T. Nunery + * @copyright 2015 Brandon Mills. All rights reserved. + * @copyright 2014 Kyle T. Nunery. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + /** + * Determines whether the current FunctionExpression node is a get, set, or + * shorthand method in an object literal or a class. + * @returns {boolean} True if the node is a get, set, or shorthand method. + */ + function isObjectOrClassMethod() { + var parent = context.getAncestors().pop(); + + return (parent.type === "MethodDefinition" || ( + parent.type === "Property" && ( + parent.method || + parent.kind === "get" || + parent.kind === "set" + ) + )); + } + + return { + "FunctionExpression": function(node) { + + var name = node.id && node.id.name; + + if (!name && !isObjectOrClassMethod()) { + context.report(node, "Missing function expression name."); + } + } + }; +}; diff --git a/tools/eslint/lib/rules/func-style.js b/tools/eslint/lib/rules/func-style.js new file mode 100644 index 0000000000..f54350e765 --- /dev/null +++ b/tools/eslint/lib/rules/func-style.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Rule to enforce a particular function style + * @author Nicholas C. Zakas + * @copyright 2013 Nicholas C. Zakas. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var style = context.options[0], + enforceDeclarations = (style === "declaration"); + + return { + + "FunctionDeclaration": function(node) { + if (!enforceDeclarations) { + context.report(node, "Expected a function expression."); + } + }, + + "FunctionExpression": function() { + var parent = context.getAncestors().pop(); + + if (enforceDeclarations && parent.type === "VariableDeclarator") { + context.report(parent, "Expected a function declaration."); + } + }, + + "ArrowFunctionExpression": function() { + var parent = context.getAncestors().pop(); + + if (enforceDeclarations && parent.type === "VariableDeclarator") { + context.report(parent, "Expected a function declaration."); + } + } + + }; + +}; diff --git a/tools/eslint/lib/rules/generator-star-spacing.js b/tools/eslint/lib/rules/generator-star-spacing.js new file mode 100644 index 0000000000..fe0b82bd19 --- /dev/null +++ b/tools/eslint/lib/rules/generator-star-spacing.js @@ -0,0 +1,83 @@ +/** + * @fileoverview Rule to check the spacing around the * in generator functions. + * @author Jamund Ferguson + * @copyright 2015 Brandon Mills. All rights reserved. + * @copyright 2014 Jamund Ferguson. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var mode = { + before: { before: true, after: false }, + after: { before: false, after: true }, + both: { before: true, after: true }, + neither: { before: false, after: false } + }[context.options[0] || "before"]; + + /** + * Checks the spacing between two tokens before or after the star token. + * @param {string} side Either "before" or "after". + * @param {Token} leftToken `function` keyword token if side is "before", or + * star token if side is "after". + * @param {Token} rightToken Star token if side is "before", or identifier + * token if side is "after". + * @returns {void} + */ + function checkSpacing(side, leftToken, rightToken) { + if (!!(rightToken.range[0] - leftToken.range[1]) !== mode[side]) { + context.report( + leftToken.value === "*" ? leftToken : rightToken, + "{{type}} space {{side}} *.", + { + type: mode[side] ? "Missing" : "Unexpected", + side: side + } + ); + } + } + + /** + * Enforces the spacing around the star if node is a generator function. + * @param {ASTNode} node A function expression or declaration node. + * @returns {void} + */ + function checkFunction(node) { + var isMethod, starToken, tokenBefore, tokenAfter; + + if (!node.generator) { + return; + } + + isMethod = !!context.getAncestors().pop().method; + + if (isMethod) { + starToken = context.getTokenBefore(node, 1); + } else { + starToken = context.getFirstToken(node, 1); + } + + // Only check before when preceded by `function` keyword + tokenBefore = context.getTokenBefore(starToken); + if (tokenBefore.value === "function") { + checkSpacing("before", tokenBefore, starToken); + } + + // Only check after when followed by an identifier + tokenAfter = context.getTokenAfter(starToken); + if (tokenAfter.type === "Identifier") { + checkSpacing("after", starToken, tokenAfter); + } + } + + return { + "FunctionDeclaration": checkFunction, + "FunctionExpression": checkFunction + }; + +}; diff --git a/tools/eslint/lib/rules/generator-star.js b/tools/eslint/lib/rules/generator-star.js new file mode 100644 index 0000000000..4541e6711c --- /dev/null +++ b/tools/eslint/lib/rules/generator-star.js @@ -0,0 +1,70 @@ +/** + * @fileoverview Rule to check for the position of the * in your generator functions + * @author Jamund Ferguson + * @copyright 2014 Jamund Ferguson. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var position = context.options[0] || "end"; + + /** + * Check the position of the star compared to the expected position. + * @param {ASTNode} node - the entire function node + * @returns {void} + */ + function checkStarPosition(node) { + var starToken; + + if (!node.generator) { + return; + } + + // Blocked, pending decision to fix or work around in eslint/espree#36 + if (context.getAncestors().pop().method) { + return; + } + + starToken = context.getFirstToken(node, 1); + + // check for function *name() {} + if (position === "end") { + + // * starts where the next identifier begins + if (starToken.range[1] !== context.getTokenAfter(starToken).range[0]) { + context.report(node, "Expected a space before *."); + } + } + + // check for function* name() {} + if (position === "start") { + + // * begins where the previous identifier ends + if (starToken.range[0] !== context.getTokenBefore(starToken).range[1]) { + context.report(node, "Expected no space before *."); + } + } + + // check for function * name() {} + if (position === "middle") { + + // must be a space before and afer the * + if (starToken.range[0] <= context.getTokenBefore(starToken).range[1] || + starToken.range[1] >= context.getTokenAfter(starToken).range[0]) { + context.report(node, "Expected spaces around *."); + } + } + } + + return { + "FunctionDeclaration": checkStarPosition, + "FunctionExpression": checkStarPosition + }; + +}; diff --git a/tools/eslint/lib/rules/global-strict.js b/tools/eslint/lib/rules/global-strict.js new file mode 100644 index 0000000000..c16c62db92 --- /dev/null +++ b/tools/eslint/lib/rules/global-strict.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Rule to flag or require global strict mode. + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var mode = context.options[0]; + + if (mode === "always") { + + return { + "Program": function(node) { + if (node.body.length > 0) { + var statement = node.body[0]; + + if (!(statement.type === "ExpressionStatement" && statement.expression.value === "use strict")) { + context.report(node, "Use the global form of \"use strict\"."); + } + } + } + }; + + } else { // mode = "never" + + return { + "ExpressionStatement": function(node) { + var parent = context.getAncestors().pop(); + + if (node.expression.value === "use strict" && parent.type === "Program") { + context.report(node, "Use the function form of \"use strict\"."); + } + } + }; + + } + +}; diff --git a/tools/eslint/lib/rules/guard-for-in.js b/tools/eslint/lib/rules/guard-for-in.js new file mode 100644 index 0000000000..d79651d38c --- /dev/null +++ b/tools/eslint/lib/rules/guard-for-in.js @@ -0,0 +1,30 @@ +/** + * @fileoverview Rule to flag for-in loops without if statements inside + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "ForInStatement": function(node) { + + /* + * If the for-in statement has {}, then the real body is the body + * of the BlockStatement. Otherwise, just use body as provided. + */ + var body = node.body.type === "BlockStatement" ? node.body.body[0] : node.body; + + if (body && body.type !== "IfStatement") { + context.report(node, "The body of a for-in should be wrapped in an if statement to filter unwanted properties from the prototype."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/handle-callback-err.js b/tools/eslint/lib/rules/handle-callback-err.js new file mode 100644 index 0000000000..f57e4d1752 --- /dev/null +++ b/tools/eslint/lib/rules/handle-callback-err.js @@ -0,0 +1,118 @@ +/** + * @fileoverview Ensure handling of errors when we know they exist. + * @author Jamund Ferguson + * @copyright 2014 Jamund Ferguson. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var errorArgument = context.options[0] || "err"; + var callbacks = []; + var scopes = 0; + + /** + * Checks if the given argument should be interpreted as a regexp pattern. + * @param {string} stringToCheck The string which should be checked. + * @returns {boolean} Whether or not the string should be interpreted as a pattern. + */ + function isPattern(stringToCheck) { + var firstChar = stringToCheck[0]; + return firstChar === "^"; + } + + /** + * Checks if the given name matches the configured error argument. + * @param {string} name The name which should be compared. + * @returns {boolean} Whether or not the given name matches the configured error variable name. + */ + function matchesConfiguredErrorName(name) { + if (isPattern(errorArgument)) { + var regexp = new RegExp(errorArgument); + return regexp.test(name); + } + return name === errorArgument; + } + + /** + * Check the arguments to see if we need to start tracking the error object. + * @param {ASTNode} node The AST node to check. + * @returns {void} + */ + function startFunction(node) { + + // keep track of nested scopes + scopes++; + + // check if the first argument matches our argument name + var firstArg = node.params && node.params[0]; + if (firstArg && matchesConfiguredErrorName(firstArg.name)) { + callbacks.push({handled: false, depth: scopes, errorVariableName: firstArg.name}); + } + } + + /** + * At the end of a function check to see if the error was handled. + * @param {ASTNode} node The AST node to check. + * @returns {void} + */ + function endFunction(node) { + + var callback = callbacks[callbacks.length - 1] || {}; + + // check if a callback is ending, if so pop it off the stack + if (callback.depth === scopes) { + callbacks.pop(); + + // check if there were no handled errors since the last callback + if (!callback.handled) { + context.report(node, "Expected error to be handled."); + } + } + + // less nested functions + scopes--; + + } + + /** + * Check to see if we're handling the error object properly. + * @param {ASTNode} node The AST node to check. + * @returns {void} + */ + function checkForError(node) { + if (callbacks.length > 0) { + var callback = callbacks[callbacks.length - 1] || {}; + + // make sure the node's name matches our error argument name + var isAboutError = node.name === callback.errorVariableName; + + // we don't consider these use cases as "handling" the error + var doNotCount = ["FunctionDeclaration", "ArrowFunctionExpression", "FunctionExpression", "CatchClause"]; + + // make sure this identifier isn't used as part of one of them + var isHandled = doNotCount.indexOf(node.parent.type) === -1; + + if (isAboutError && isHandled) { + // record that this callback handled its error + callback.handled = true; + } + } + } + + return { + "FunctionDeclaration": startFunction, + "FunctionExpression": startFunction, + "ArrowFunctionExpression": startFunction, + "Identifier": checkForError, + "FunctionDeclaration:exit": endFunction, + "FunctionExpression:exit": endFunction, + "ArrowFunctionExpression:exit": endFunction + }; + +}; diff --git a/tools/eslint/lib/rules/indent.js b/tools/eslint/lib/rules/indent.js new file mode 100644 index 0000000000..b0e838e74b --- /dev/null +++ b/tools/eslint/lib/rules/indent.js @@ -0,0 +1,464 @@ +/** + * @fileoverview This option sets a specific tab width for your code + * This rule has been ported and modified from JSCS. + * @author Dmitriy Shekhovtsov + * @copyright 2015 Dmitriy Shekhovtsov. All rights reserved. + * @copyright 2013 Dulin Marat and other contributors. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +/*eslint no-use-before-define:[2, "nofunc"]*/ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function (context) { + // indentation defaults: 4 spaces + var indentChar = " "; + var indentSize = 4; + var options = {indentSwitchCase: false}; + + var lines = null; + var indentStack = [0]; + var linesToCheck = null; + var breakIndents = null; + + if (context.options.length) { + if (context.options[0] === "tab") { + indentChar = "\t"; + indentSize = 1; + } else if (typeof context.options[0] === "number") { + indentSize = context.options[0]; + } + + if (context.options[1]) { + var opts = context.options[1]; + options.indentSwitchCase = opts.indentSwitchCase === true; + } + } + + var blockParents = [ + "IfStatement", + "WhileStatement", + "DoWhileStatement", + "ForStatement", + "ForInStatement", + "ForOfStatement", + "FunctionDeclaration", + "FunctionExpression", + "ArrowExpression", + "CatchClause", + "WithStatement" + ]; + + var indentableNodes = { + BlockStatement: "body", + Program: "body", + ObjectExpression: "properties", + ArrayExpression: "elements", + SwitchStatement: "cases" + }; + + if (options.indentSwitchCase) { + indentableNodes.SwitchCase = "consequent"; + } + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Mark line to be checked + * @param {Number} line - line number + * @returns {void} + */ + function markCheckLine(line) { + linesToCheck[line].check = true; + } + + /** + * Mark line with targeted node to be checked + * @param {ASTNode} checkNode - targeted node + * @returns {void} + */ + function markCheck(checkNode) { + markCheckLine(checkNode.loc.start.line - 1); + } + + /** + * Sets pushing indent of current node + * @param {ASTNode} node - targeted node + * @param {Number} indents - indents count to push + * @returns {void} + */ + function markPush(node, indents) { + linesToCheck[node.loc.start.line - 1].push.push(indents); + } + + /** + * Marks line as outdent, end of block statement for example + * @param {ASTNode} node - targeted node + * @param {Number} outdents - count of outedents in targeted line + * @returns {void} + */ + function markPop(node, outdents) { + linesToCheck[node.loc.end.line - 1].pop.push(outdents); + } + + /** + * Set alt push for current node + * @param {ASTNode} node - targeted node + * @returns {void} + */ + function markPushAlt(node) { + linesToCheck[node.loc.start.line - 1].pushAltLine.push(node.loc.end.line - 1); + } + + /** + * Marks end of node block to be checked + * and marks targeted node as indent pushing + * @param {ASTNode} pushNode - targeted node + * @param {Number} indents - indent count to push + * @returns {void} + */ + function markPushAndEndCheck(pushNode, indents) { + markPush(pushNode, indents); + markCheckLine(pushNode.loc.end.line - 1); + } + + /** + * Mark node as switch case statement + * and set push\pop indentation changes + * @param {ASTNode} caseNode - targeted node + * @param {ASTNode[]} children - consequent child nodes of case node + * @returns {void} + */ + function markCase(caseNode, children) { + var outdentNode = getCaseOutdent(children); + + if (outdentNode) { + // If a case statement has a `break` as a direct child and it is the + // first one encountered, use it as the example for all future case indentation + if (breakIndents === null) { + breakIndents = (caseNode.loc.start.column === outdentNode.loc.start.column) ? 1 : 0; + } + markPop(outdentNode, breakIndents); + } else { + markPop(caseNode, 0); + } + } + + /** + * Mark child nodes to be checked later of targeted node, + * only if child node not in same line as targeted one + * (if child and parent nodes wrote in single line) + * @param {ASTNode} node - targeted node + * @returns {void} + */ + function markChildren(node) { + getChildren(node).forEach(function(childNode) { + if (childNode.loc.start.line !== node.loc.start.line || node.type === "Program") { + markCheck(childNode); + } + }); + } + + /** + * Mark child block as scope pushing and mark to check + * @param {ASTNode} node - target node + * @param {String} property - target node property containing child + * @returns {void} + */ + function markAlternateBlockStatement(node, property) { + var child = node[property]; + if (child && child.type === "BlockStatement") { + markCheck(child); + } + } + + /** + * Checks whether node is multiline or single line + * @param {ASTNode} node - target node + * @returns {boolean} - is multiline node + */ + function isMultiline(node) { + return node.loc.start.line !== node.loc.end.line; + } + + /** + * Get switch case statement outdent node if any + * @param {ASTNode[]} caseChildren - case statement childs + * @returns {ASTNode} - outdent node + */ + function getCaseOutdent(caseChildren) { + var outdentNode; + caseChildren.some(function(node) { + if (node.type === "BreakStatement") { + outdentNode = node; + return true; + } + }); + + return outdentNode; + } + + /** + * Returns block containing node + * @param {ASTNode} node - targeted node + * @returns {ASTNode} - block node + */ + function getBlockNodeToMark(node) { + var parent = node.parent; + + // The parent of an else is the entire if/else block. To avoid over indenting + // in the case of a non-block if with a block else, mark push where the else starts, + // not where the if starts! + if (parent.type === "IfStatement" && parent.alternate === node) { + return node; + } + + // The end line to check of a do while statement needs to be the location of the + // closing curly brace, not the while statement, to avoid marking the last line of + // a multiline while as a line to check. + if (parent.type === "DoWhileStatement") { + return node; + } + + // Detect bare blocks: a block whose parent doesn"t expect blocks in its syntax specifically. + if (blockParents.indexOf(parent.type) === -1) { + return node; + } + + return parent; + } + + /** + * Get node's children + * @param {ASTNode} node - current node + * @returns {ASTNode[]} - children + */ + function getChildren(node) { + var childrenProperty = indentableNodes[node.type]; + return node[childrenProperty]; + } + + /** + * Gets indentation in line `i` + * @param {Number} i - number of line to get indentation + * @returns {Number} - count of indentation symbols + */ + function getIndentationFromLine(i) { + var rNotIndentChar = new RegExp("[^" + indentChar + "]"); + var firstContent = lines[i].search(rNotIndentChar); + if (firstContent === -1) { + firstContent = lines[i].length; + } + return firstContent; + } + + /** + * Compares expected and actual indentation + * and reports any violations + * @param {ASTNode} node - node used only for reporting + * @returns {void} + */ + function checkIndentations(node) { + linesToCheck.forEach(function(line, i) { + var actualIndentation = getIndentationFromLine(i); + var expectedIndentation = getExpectedIndentation(line, actualIndentation); + + if (line.check) { + + if (actualIndentation !== expectedIndentation) { + context.report(node, + {line: i + 1, column: expectedIndentation}, + "Expected indentation of " + expectedIndentation + " characters."); + // correct the indentation so that future lines + // can be validated appropriately + actualIndentation = expectedIndentation; + } + } + + if (line.push.length) { + pushExpectedIndentations(line, actualIndentation); + } + }); + } + + /** + * Counts expected indentation for given line number + * @param {Number} line - line number + * @param {Number} actual - actual indentation + * @returns {number} - expected indentation + */ + function getExpectedIndentation(line, actual) { + var outdent = indentSize * Math.max.apply(null, line.pop); + + var idx = indentStack.length - 1; + var expected = indentStack[idx]; + + if (!Array.isArray(expected)) { + expected = [expected]; + } + + expected = expected.map(function(value) { + if (line.pop.length) { + value -= outdent; + } + + return value; + }).reduce(function(previous, current) { + // when the expected is an array, resolve the value + // back into a Number by checking both values are the actual indentation + return actual === current ? current : previous; + }); + + indentStack[idx] = expected; + + line.pop.forEach(function() { + indentStack.pop(); + }); + + return expected; + } + + /** + * Store in stack expected indentations + * @param {Number} line - current line + * @param {Number} actualIndentation - actual indentation at current line + * @returns {void} + */ + function pushExpectedIndentations(line, actualIndentation) { + var indents = Math.max.apply(null, line.push); + var expected = actualIndentation + (indentSize * indents); + + // when a line has alternate indentations, push an array of possible values + // on the stack, to be resolved when checked against an actual indentation + if (line.pushAltLine.length) { + expected = [expected]; + line.pushAltLine.forEach(function(altLine) { + expected.push(getIndentationFromLine(altLine) + (indentSize * indents)); + }); + } + + line.push.forEach(function() { + indentStack.push(expected); + }); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + "Program": function (node) { + lines = context.getSourceLines(); + linesToCheck = lines.map(function () { + return { + push: [], + pushAltLine: [], + pop: [], + check: false + }; + }); + + if (!isMultiline(node)) { + return; + } + + markChildren(node); + }, + "Program:exit": function (node) { + checkIndentations(node); + }, + + "BlockStatement": function (node) { + if (!isMultiline(node)) { + return; + } + + markChildren(node); + markPop(node, 1); + + markPushAndEndCheck(getBlockNodeToMark(node), 1); + }, + + "IfStatement": function (node) { + markAlternateBlockStatement(node, "alternate"); + }, + + "TryStatement": function (node) { + markAlternateBlockStatement(node, "handler"); + markAlternateBlockStatement(node, "finalizer"); + }, + + "SwitchStatement": function (node) { + if (!isMultiline(node)) { + return; + } + + var indents = 1; + var children = getChildren(node); + + if (children.length && node.loc.start.column === children[0].loc.start.column) { + indents = 0; + } + + markChildren(node); + markPop(node, indents); + markPushAndEndCheck(node, indents); + }, + + "SwitchCase": function (node) { + if (!options.indentSwitchCase) { + return; + } + + if (!isMultiline(node)) { + return; + } + + var children = getChildren(node); + + if (children.length === 1 && children[0].type === "BlockStatement") { + return; + } + + markPush(node, 1); + markCheck(node); + markChildren(node); + + markCase(node, children); + }, + + // indentations inside of function expressions can be offset from + // either the start of the function or the end of the function, therefore + // mark all starting lines of functions as potential indentations + "FunctionDeclaration": function (node) { + markPushAlt(node); + }, + "FunctionExpression": function (node) { + markPushAlt(node); + } + }; + +}; diff --git a/tools/eslint/lib/rules/key-spacing.js b/tools/eslint/lib/rules/key-spacing.js new file mode 100644 index 0000000000..3ffaf9302f --- /dev/null +++ b/tools/eslint/lib/rules/key-spacing.js @@ -0,0 +1,307 @@ +/** + * @fileoverview Rule to specify spacing of object literal keys and values + * @author Brandon Mills + * @copyright 2014 Brandon Mills. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether a string contains a line terminator as defined in + * http://www.ecma-international.org/ecma-262/5.1/#sec-7.3 + * @param {string} str String to test. + * @returns {boolean} True if str contains a line terminator. + */ +function containsLineTerminator(str) { + return /[\n\r\u2028\u2029]/.test(str); +} + +/** + * Gets the last element of an array. + * @param {Array} arr An array. + * @returns {any} Last element of arr. + */ +function last(arr) { + return arr[arr.length - 1]; +} + +/** + * Checks whether a property is a member of the property group it follows. + * @param {ASTNode} lastMember The last Property known to be in the group. + * @param {ASTNode} candidate The next Property that might be in the group. + * @returns {boolean} True if the candidate property is part of the group. + */ +function continuesPropertyGroup(lastMember, candidate) { + var groupEndLine = lastMember.loc.end.line, + candidateStartLine = candidate.loc.start.line, + comments, i; + + if (candidateStartLine - groupEndLine <= 1) { + return true; + } + + // Check that the first comment is adjacent to the end of the group, the + // last comment is adjacent to the candidate property, and that successive + // comments are adjacent to each other. + comments = candidate.leadingComments; + if ( + comments && + comments[0].loc.start.line - groupEndLine <= 1 && + candidateStartLine - last(comments).loc.end.line <= 1 + ) { + for (i = 1; i < comments.length; i++) { + if (comments[i].loc.start.line - comments[i - 1].loc.end.line > 1) { + return false; + } + } + return true; + } + + return false; +} + +/** + * Checks whether a node is contained on a single line. + * @param {ASTNode} node AST Node being evaluated. + * @returns {boolean} True if the node is a single line. + */ +function isSingleLine(node) { + return (node.loc.end.line === node.loc.start.line); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +var messages = { + key: "{{error}} space after {{computed}}key \"{{key}}\".", + value: "{{error}} space before value for {{computed}}key \"{{key}}\"." +}; + +module.exports = function(context) { + + /** + * OPTIONS + * "key-spacing": [2, { + * beforeColon: false, + * afterColon: true, + * align: "colon" // Optional, or "value" + * } + */ + + var options = context.options[0] || {}, + align = options.align, + beforeColon = +!!options.beforeColon, // Defaults to false + afterColon = +!(options.afterColon === false); // Defaults to true + + /** + * Gets an object literal property's key as the identifier name or string value. + * @param {ASTNode} property Property node whose key to retrieve. + * @returns {string} The property's key. + */ + function getKey(property) { + var key = property.key; + + if (property.computed) { + return context.getSource().slice(key.range[0], key.range[1]); + } + + return property.key.name || property.key.value; + } + + /** + * Reports an appropriately-formatted error if spacing is incorrect on one + * side of the colon. + * @param {ASTNode} property Key-value pair in an object literal. + * @param {string} side Side being verified - either "key" or "value". + * @param {string} whitespace Actual whitespace string. + * @param {int} expected Expected whitespace length. + * @returns {void} + */ + function report(property, side, whitespace, expected) { + var diff = whitespace.length - expected, + key = property.key, + firstTokenAfterColon = context.getTokenAfter(key, 1), + location = side === "key" ? key.loc.start : firstTokenAfterColon.loc.start; + + if (diff && !(expected && containsLineTerminator(whitespace))) { + context.report(property[side], location, messages[side], { + error: diff > 0 ? "Extra" : "Missing", + computed: property.computed ? "computed " : "", + key: getKey(property) + }); + } + } + + /** + * Gets the number of characters in a key, including quotes around string + * keys and braces around computed property keys. + * @param {ASTNode} property Property of on object literal. + * @returns {int} Width of the key. + */ + function getKeyWidth(property) { + var key = property.key, + startToken, endToken; + + // [computed]: value + if (property.computed) { + startToken = context.getTokenBefore(key); + endToken = context.getTokenAfter(key); + return endToken.range[1] - startToken.range[0]; + } + + // name: value + if (key.type === "Identifier") { + return key.name.length; + } + + // "literal": value + // 42: value + if (key.type === "Literal") { + return key.raw.length; + } + } + + /** + * Gets the whitespace around the colon in an object literal property. + * @param {ASTNode} property Property node from an object literal. + * @returns {Object} Whitespace before and after the property's colon. + */ + function getPropertyWhitespace(property) { + var whitespace = /(\s*):(\s*)/.exec(context.getSource().slice( + property.key.range[1], property.value.range[0] + )); + + if (whitespace) { + return { + beforeColon: whitespace[1], + afterColon: whitespace[2] + }; + } + } + + /** + * Creates groups of properties. + * @param {ASTNode} node ObjectExpression node being evaluated. + * @returns {Array.<ASTNode[]>} Groups of property AST node lists. + */ + function createGroups(node) { + if (node.properties.length === 1) { + return [node.properties]; + } + + return node.properties.reduce(function(groups, property) { + var currentGroup = last(groups), + prev = last(currentGroup); + + if (!prev || continuesPropertyGroup(prev, property)) { + currentGroup.push(property); + } else { + groups.push([property]); + } + + return groups; + }, [[]]); + } + + /** + * Verifies correct vertical alignment of a group of properties. + * @param {ASTNode[]} properties List of Property AST nodes. + * @returns {void} + */ + function verifyGroupAlignment(properties) { + var length = properties.length, + widths = properties.map(getKeyWidth), // Width of keys, including quotes + targetWidth = Math.max.apply(null, widths), + i, property, whitespace, width; + + // Conditionally include one space before or after colon + targetWidth += (align === "colon" ? beforeColon : afterColon); + + for (i = 0; i < length; i++) { + property = properties[i]; + whitespace = getPropertyWhitespace(property); + + if (!whitespace) { + continue; // Object literal getters/setters lack a colon + } + + width = widths[i]; + + if (align === "value") { + report(property, "key", whitespace.beforeColon, beforeColon); + report(property, "value", whitespace.afterColon, targetWidth - width); + } else { // align = "colon" + report(property, "key", whitespace.beforeColon, targetWidth - width); + report(property, "value", whitespace.afterColon, afterColon); + } + } + } + + /** + * Verifies vertical alignment, taking into account groups of properties. + * @param {ASTNode} node ObjectExpression node being evaluated. + * @returns {void} + */ + function verifyAlignment(node) { + createGroups(node).forEach(function(group) { + verifyGroupAlignment(group); + }); + } + + /** + * Verifies spacing of property conforms to specified options. + * @param {ASTNode} node Property node being evaluated. + * @returns {void} + */ + function verifySpacing(node) { + var whitespace = getPropertyWhitespace(node); + if (whitespace) { // Object literal getters/setters lack colons + report(node, "key", whitespace.beforeColon, beforeColon); + report(node, "value", whitespace.afterColon, afterColon); + } + } + + /** + * Verifies spacing of each property in a list. + * @param {ASTNode[]} properties List of Property AST nodes. + * @returns {void} + */ + function verifyListSpacing(properties) { + var length = properties.length; + + for (var i = 0; i < length; i++) { + verifySpacing(properties[i]); + } + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + if (align) { // Verify vertical alignment + + return { + "ObjectExpression": function(node) { + if (isSingleLine(node)) { + verifyListSpacing(node.properties); + } else { + verifyAlignment(node); + } + } + }; + + } else { // Strictly obey beforeColon and afterColon in each property + + return { + "Property": function (node) { + verifySpacing(node); + } + }; + + } + +}; diff --git a/tools/eslint/lib/rules/max-depth.js b/tools/eslint/lib/rules/max-depth.js new file mode 100644 index 0000000000..cc7e366257 --- /dev/null +++ b/tools/eslint/lib/rules/max-depth.js @@ -0,0 +1,83 @@ +/** + * @fileoverview A rule to set the maximum depth block can be nested in a function. + * @author Ian Christian Myers + * @copyright 2013 Ian Christian Myers. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + var functionStack = [], + maxDepth = context.options[0] || 4; + + function startFunction() { + functionStack.push(0); + } + + function endFunction() { + functionStack.pop(); + } + + function pushBlock(node) { + var len = ++functionStack[functionStack.length - 1]; + + if (len > maxDepth) { + context.report(node, "Blocks are nested too deeply ({{depth}}).", + { depth: len }); + } + } + + function popBlock() { + functionStack[functionStack.length - 1]--; + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + "Program": startFunction, + "FunctionDeclaration": startFunction, + "FunctionExpression": startFunction, + "ArrowFunctionExpression": startFunction, + + "IfStatement": function(node) { + if (node.parent.type !== "IfStatement") { + pushBlock(node); + } + }, + "SwitchStatement": pushBlock, + "TryStatement": pushBlock, + "DoWhileStatement": pushBlock, + "WhileStatement": pushBlock, + "WithStatement": pushBlock, + "ForStatement": pushBlock, + "ForInStatement": pushBlock, + "ForOfStatement": pushBlock, + + "IfStatement:exit": popBlock, + "SwitchStatement:exit": popBlock, + "TryStatement:exit": popBlock, + "DoWhileStatement:exit": popBlock, + "WhileStatement:exit": popBlock, + "WithStatement:exit": popBlock, + "ForStatement:exit": popBlock, + "ForInStatement:exit": popBlock, + "ForOfStatement:exit": popBlock, + + "FunctionDeclaration:exit": endFunction, + "FunctionExpression:exit": endFunction, + "ArrowFunctionExpression:exit": endFunction, + "Program:exit": endFunction + }; + +}; diff --git a/tools/eslint/lib/rules/max-len.js b/tools/eslint/lib/rules/max-len.js new file mode 100644 index 0000000000..a36dd73b1b --- /dev/null +++ b/tools/eslint/lib/rules/max-len.js @@ -0,0 +1,65 @@ +/** + * @fileoverview Rule to check for max length on a line. + * @author Matt DuVall <http://www.mattduvall.com> + * @copyright 2013 Matt DuVall. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + /** + * Creates a string that is made up of repeating a given string a certain + * number of times. This uses exponentiation of squares to achieve significant + * performance gains over the more traditional implementation of such + * functionality. + * @param {string} str The string to repeat. + * @param {int} num The number of times to repeat the string. + * @returns {string} The created string. + * @private + */ + function stringRepeat(str, num) { + var result = ""; + for (num |= 0; num > 0; num >>>= 1, str += str) { + if (num & 1) { + result += str; + } + } + return result; + } + + var tabWidth = context.options[1]; + + var maxLength = context.options[0], + tabString = stringRepeat(" ", tabWidth); + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + function checkProgramForMaxLength(node) { + var lines = context.getSourceLines(); + + // Replace the tabs + // Split (honors line-ending) + // Iterate + lines.forEach(function(line, i) { + if (line.replace(/\t/g, tabString).length > maxLength) { + context.report(node, { line: i + 1, col: 1 }, "Line " + (i + 1) + " exceeds the maximum line length of " + maxLength + "."); + } + }); + } + + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + "Program": checkProgramForMaxLength + }; + +}; diff --git a/tools/eslint/lib/rules/max-nested-callbacks.js b/tools/eslint/lib/rules/max-nested-callbacks.js new file mode 100644 index 0000000000..590274ffd1 --- /dev/null +++ b/tools/eslint/lib/rules/max-nested-callbacks.js @@ -0,0 +1,67 @@ +/** + * @fileoverview Rule to enforce a maximum number of nested callbacks. + * @author Ian Christian Myers + * @copyright 2013 Ian Christian Myers. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + //-------------------------------------------------------------------------- + // Constants + //-------------------------------------------------------------------------- + + var THRESHOLD = context.options[0]; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + var callbackStack = []; + + /** + * Checks a given function node for too many callbacks. + * @param {ASTNode} node The node to check. + * @returns {void} + * @private + */ + function checkFunction(node) { + var parent = node.parent; + + if (parent.type === "CallExpression") { + callbackStack.push(node); + } + + if (callbackStack.length > THRESHOLD) { + var opts = {num: callbackStack.length, max: THRESHOLD}; + context.report(node, "Too many nested callbacks ({{num}}). Maximum allowed is {{max}}.", opts); + } + } + + /** + * Pops the call stack. + * @returns {void} + * @private + */ + function popStack() { + callbackStack.pop(); + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + "ArrowFunctionExpression": checkFunction, + "ArrowFunctionExpression:exit": popStack, + + "FunctionExpression": checkFunction, + "FunctionExpression:exit": popStack + }; + +}; diff --git a/tools/eslint/lib/rules/max-params.js b/tools/eslint/lib/rules/max-params.js new file mode 100644 index 0000000000..931548932f --- /dev/null +++ b/tools/eslint/lib/rules/max-params.js @@ -0,0 +1,39 @@ +/** + * @fileoverview Rule to flag when a function has too many parameters + * @author Ilya Volodin + * @copyright 2014 Nicholas C. Zakas. All rights reserved. + * @copyright 2013 Ilya Volodin. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var numParams = context.options[0] || 3; + + /** + * Checks a function to see if it has too many parameters. + * @param {ASTNode} node The node to check. + * @returns {void} + * @private + */ + function checkFunction(node) { + if (node.params.length > numParams) { + context.report(node, "This function has too many parameters ({{count}}). Maximum allowed is {{max}}.", { + count: node.params.length, + max: numParams + }); + } + } + + return { + "FunctionDeclaration": checkFunction, + "ArrowFunctionExpression": checkFunction, + "FunctionExpression": checkFunction + }; + +}; diff --git a/tools/eslint/lib/rules/max-statements.js b/tools/eslint/lib/rules/max-statements.js new file mode 100644 index 0000000000..9fe11bfd91 --- /dev/null +++ b/tools/eslint/lib/rules/max-statements.js @@ -0,0 +1,55 @@ +/** + * @fileoverview A rule to set the maximum number of statements in a function. + * @author Ian Christian Myers + * @copyright 2013 Ian Christian Myers. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + var functionStack = [], + maxStatements = context.options[0] || 10; + + function startFunction() { + functionStack.push(0); + } + + function endFunction(node) { + var count = functionStack.pop(); + + if (count > maxStatements) { + context.report(node, "This function has too many statements ({{count}}). Maximum allowed is {{max}}.", + { count: count, max: maxStatements }); + } + } + + function countStatements(node) { + functionStack[functionStack.length - 1] += node.body.length; + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + "FunctionDeclaration": startFunction, + "FunctionExpression": startFunction, + "ArrowFunctionExpression": startFunction, + + "BlockStatement": countStatements, + + "FunctionDeclaration:exit": endFunction, + "FunctionExpression:exit": endFunction, + "ArrowFunctionExpression:exit": endFunction + }; + +}; diff --git a/tools/eslint/lib/rules/new-cap.js b/tools/eslint/lib/rules/new-cap.js new file mode 100644 index 0000000000..1474fc8028 --- /dev/null +++ b/tools/eslint/lib/rules/new-cap.js @@ -0,0 +1,197 @@ +/** + * @fileoverview Rule to flag use of constructors without capital letters + * @author Nicholas C. Zakas + * @copyright 2014 Jordan Harband. All rights reserved. + * @copyright 2013-2014 Nicholas C. Zakas. All rights reserved. + */ + +"use strict"; + +var CAPS_ALLOWED = [ + "Array", + "Boolean", + "Date", + "Error", + "Function", + "Number", + "Object", + "RegExp", + "String", + "Symbol" +]; + +/** + * Ensure that if the key is provided, it must be an array. + * @param {Object} obj Object to check with `key`. + * @param {string} key Object key to check on `obj`. + * @param {*} fallback If obj[key] is not present, this will be returned. + * @returns {string[]} Returns obj[key] if it's an Array, otherwise `fallback` + */ +function checkArray(obj, key, fallback) { + if (Object.prototype.hasOwnProperty.call(obj, key) && !Array.isArray(obj[key])) { + throw new TypeError(key + ", if provided, must be an Array"); + } + return obj[key] || fallback; +} + +/** + * A reducer function to invert an array to an Object mapping the string form of the key, to `true`. + * @param {Object} map Accumulator object for the reduce. + * @param {string} key Object key to set to `true`. + * @returns {Object} Returns the updated Object for further reduction. + */ +function invert(map, key) { + map[key] = true; + return map; +} + +/** + * Creates an object with the cap is new exceptions as its keys and true as their values. + * @param {Object} config Rule configuration + * @returns {Object} Object with cap is new exceptions. + */ +function calculateCapIsNewExceptions(config) { + var capIsNewExceptions = checkArray(config, "capIsNewExceptions", CAPS_ALLOWED); + + if (capIsNewExceptions !== CAPS_ALLOWED) { + capIsNewExceptions = capIsNewExceptions.concat(CAPS_ALLOWED); + } + + return capIsNewExceptions.reduce(invert, {}); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var config = context.options[0] || {}; + config.newIsCap = config.newIsCap === false ? false : true; + config.capIsNew = config.capIsNew === false ? false : true; + + var newIsCapExceptions = checkArray(config, "newIsCapExceptions", []).reduce(invert, {}); + + var capIsNewExceptions = calculateCapIsNewExceptions(config); + + var listeners = {}; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Get exact callee name from expression + * @param {ASTNode} node CallExpression or NewExpression node + * @returns {string} name + */ + function extractNameFromExpression(node) { + + var name = "", + property; + + if (node.callee.type === "MemberExpression") { + property = node.callee.property; + + if (property.type === "Literal" && (typeof property.value === "string")) { + name = property.value; + } else if (property.type === "Identifier" && !node.callee.computed) { + name = property.name; + } + } else { + name = node.callee.name; + } + return name; + } + + /** + * Returns the capitalization state of the string - + * Whether the first character is uppercase, lowercase, or non-alphabetic + * @param {string} str String + * @returns {string} capitalization state: "non-alpha", "lower", or "upper" + */ + function getCap(str) { + var firstChar = str.charAt(0); + + var firstCharLower = firstChar.toLowerCase(); + var firstCharUpper = firstChar.toUpperCase(); + + if (firstCharLower === firstCharUpper) { + // char has no uppercase variant, so it's non-alphabetic + return "non-alpha"; + } else if (firstChar === firstCharLower) { + return "lower"; + } else { + return "upper"; + } + } + + /** + * Check if capitalization is allowed for a CallExpression + * @param {Object} allowedMap Object mapping calleeName to a Boolean + * @param {ASTNode} node CallExpression node + * @param {string} calleeName Capitalized callee name from a CallExpression + * @returns {Boolean} Returns true if the callee may be capitalized + */ + function isCapAllowed(allowedMap, node, calleeName) { + if (allowedMap[calleeName]) { + return true; + } + if (calleeName === "UTC" && node.callee.type === "MemberExpression") { + // allow if callee is Date.UTC + return node.callee.object.type === "Identifier" && + node.callee.object.name === "Date"; + } + return false; + } + + /** + * Reports the given message for the given node. The location will be the start of the property or the callee. + * @param {ASTNode} node CallExpression or NewExpression node. + * @param {string} message The message to report. + * @returns {void} + */ + function report(node, message) { + var callee = node.callee; + + if (callee.type === "MemberExpression") { + callee = callee.property; + } + + context.report(node, callee.loc.start, message); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + if (config.newIsCap) { + listeners.NewExpression = function(node) { + + var constructorName = extractNameFromExpression(node); + if (constructorName) { + var capitalization = getCap(constructorName); + var isAllowed = capitalization !== "lower" || isCapAllowed(newIsCapExceptions, node, constructorName); + if (!isAllowed) { + report(node, "A constructor name should not start with a lowercase letter."); + } + } + }; + } + + if (config.capIsNew) { + listeners.CallExpression = function(node) { + + var calleeName = extractNameFromExpression(node); + if (calleeName) { + var capitalization = getCap(calleeName); + var isAllowed = capitalization !== "upper" || isCapAllowed(capIsNewExceptions, node, calleeName); + if (!isAllowed) { + report(node, "A function with a name starting with an uppercase letter should only be used as a constructor."); + } + } + }; + } + + return listeners; +}; diff --git a/tools/eslint/lib/rules/new-parens.js b/tools/eslint/lib/rules/new-parens.js new file mode 100644 index 0000000000..adc2f70c92 --- /dev/null +++ b/tools/eslint/lib/rules/new-parens.js @@ -0,0 +1,27 @@ +/** + * @fileoverview Rule to flag when using constructor without parentheses + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "NewExpression": function(node) { + var tokens = context.getTokens(node); + var prenticesTokens = tokens.filter(function(token) { + return token.value === "(" || token.value === ")"; + }); + if (prenticesTokens.length < 2) { + context.report(node, "Missing '()' invoking a constructor"); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/newline-after-var.js b/tools/eslint/lib/rules/newline-after-var.js new file mode 100644 index 0000000000..7a6c3cff2f --- /dev/null +++ b/tools/eslint/lib/rules/newline-after-var.js @@ -0,0 +1,121 @@ +/** + * @fileoverview Rule to check empty newline after "var" statement + * @author Gopal Venkatesan + * @copyright 2015 Gopal Venkatesan. All rights reserved. + * @copyright 2015 Casey Visco. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var ALWAYS_MESSAGE = "Expected blank line after variable declarations.", + NEVER_MESSAGE = "Unexpected blank line after variable declarations."; + + // Default `mode` to "always". This means that invalid options will also + // be treated as "always" and the only special case is "never" + var mode = context.options[0] === "never" ? "never" : "always"; + + // Cache line numbers of comments for faster lookup + var comments = context.getAllComments().map(function (token) { + return token.loc.start.line; + }); + + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Determine if provided keyword is a variable declaration + * @private + * @param {string} keyword - keyword to test + * @returns {boolean} True if `keyword` is a type of var + */ + function isVar(keyword) { + return keyword === "var" || keyword === "let" || keyword === "const"; + } + + /** + * Determine if provided keyword is a variant of for specifiers + * @private + * @param {string} keyword - keyword to test + * @returns {boolean} True if `keyword` is a variant of for specifier + */ + function isForTypeSpecifier(keyword) { + return keyword === "ForStatement" || keyword === "ForInStatement" || keyword === "ForOfStatement"; + } + + /** + * Determine if provided keyword is an export specifiers + * @private + * @param {string} nodeType - nodeType to test + * @returns {boolean} True if `nodeType` is an export specifier + */ + function isExportSpecifier(nodeType) { + return nodeType === "ExportNamedDeclaration" || nodeType === "ExportSpecifier" || + nodeType === "ExportDefaultDeclaration" || nodeType === "ExportAllDeclaration"; + } + + /** + * Checks that a blank line exists after a variable declaration when mode is + * set to "always", or checks that there is no blank line when mode is set + * to "never" + * @private + * @param {ASTNode} node - `VariableDeclaration` node to test + * @returns {void} + */ + function checkForBlankLine(node) { + var lastToken = context.getLastToken(node), + nextToken = context.getTokenAfter(node), + nextLineNum = lastToken.loc.end.line + 1, + noNextLineToken, + hasNextLineComment; + + // Ignore if there is no following statement + if (!nextToken) { + return; + } + + // Ignore if parent of node is a for variant + if (isForTypeSpecifier(node.parent.type)) { + return; + } + + // Ignore if parent of node is an export specifier + if (isExportSpecifier(node.parent.type)) { + return; + } + + // Some coding styles use multiple `var` statements, so do nothing if + // the next token is a `var` statement. + if (nextToken.type === "Keyword" && isVar(nextToken.value)) { + return; + } + + // Next statement is not a `var`... + noNextLineToken = nextToken.loc.start.line > nextLineNum; + hasNextLineComment = comments.indexOf(nextLineNum) >= 0; + + if (mode === "never" && noNextLineToken && !hasNextLineComment) { + context.report(node, NEVER_MESSAGE, { identifier: node.name }); + } + + if (mode === "always" && (!noNextLineToken || hasNextLineComment)) { + context.report(node, ALWAYS_MESSAGE, { identifier: node.name }); + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + "VariableDeclaration": checkForBlankLine + }; + +}; diff --git a/tools/eslint/lib/rules/no-alert.js b/tools/eslint/lib/rules/no-alert.js new file mode 100644 index 0000000000..1f14b533d7 --- /dev/null +++ b/tools/eslint/lib/rules/no-alert.js @@ -0,0 +1,54 @@ +/** + * @fileoverview Rule to flag use of alert, confirm, prompt + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +function matchProhibited(name) { + return name.match(/^(alert|confirm|prompt)$/); +} + +function report(context, node, result) { + context.report(node, "Unexpected {{name}}.", { name: result[1] }); +} + + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "CallExpression": function(node) { + + var result; + + // without window. + if (node.callee.type === "Identifier") { + + result = matchProhibited(node.callee.name); + + if (result) { + report(context, node, result); + } + + } else if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier") { + + result = matchProhibited(node.callee.property.name); + + if (result && node.callee.object.name === "window") { + report(context, node, result); + } + + } + + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-array-constructor.js b/tools/eslint/lib/rules/no-array-constructor.js new file mode 100644 index 0000000000..a37d674c80 --- /dev/null +++ b/tools/eslint/lib/rules/no-array-constructor.js @@ -0,0 +1,29 @@ +/** + * @fileoverview Disallow construction of dense arrays using the Array constructor + * @author Matt DuVall <http://www.mattduvall.com/> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + function check(node) { + if ( + node.arguments.length !== 1 && + node.callee.type === "Identifier" && + node.callee.name === "Array" + ) { + context.report(node, "The array literal notation [] is preferrable."); + } + } + + return { + "CallExpression": check, + "NewExpression": check + }; + +}; diff --git a/tools/eslint/lib/rules/no-bitwise.js b/tools/eslint/lib/rules/no-bitwise.js new file mode 100644 index 0000000000..942317f3d0 --- /dev/null +++ b/tools/eslint/lib/rules/no-bitwise.js @@ -0,0 +1,55 @@ +/** + * @fileoverview Rule to flag bitwise identifiers + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var BITWISE_OPERATORS = [ + "^", "|", "&", "<<", ">>", ">>>", + "^=", "|=", "&=", "<<=", ">>=", ">>>=", + "~" + ]; + + /** + * Reports an unexpected use of a bitwise operator. + * @param {ASTNode} node Node which contains the bitwise operator. + * @returns {void} + */ + function report(node) { + context.report(node, "Unexpected use of '{{operator}}'.", { operator: node.operator }); + } + + /** + * Checks if the given node has a bitwise operator. + * @param {ASTNode} node The node to check. + * @returns {boolean} Whether or not the node has a bitwise operator. + */ + function hasBitwiseOperator(node) { + return BITWISE_OPERATORS.indexOf(node.operator) !== -1; + } + + /** + * Report if the given node contains a bitwise operator. + * @param {ASTNode} node The node to check. + * @returns {void} + */ + function checkNodeForBitwiseOperator(node) { + if (hasBitwiseOperator(node)) { + report(node); + } + } + + return { + "AssignmentExpression": checkNodeForBitwiseOperator, + "BinaryExpression": checkNodeForBitwiseOperator, + "UnaryExpression": checkNodeForBitwiseOperator + }; + +}; diff --git a/tools/eslint/lib/rules/no-caller.js b/tools/eslint/lib/rules/no-caller.js new file mode 100644 index 0000000000..b489d79c54 --- /dev/null +++ b/tools/eslint/lib/rules/no-caller.js @@ -0,0 +1,27 @@ +/** + * @fileoverview Rule to flag use of arguments.callee and arguments.caller. + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "MemberExpression": function(node) { + var objectName = node.object.name, + propertyName = node.property.name; + + if (objectName === "arguments" && !node.computed && propertyName && propertyName.match(/^calle[er]$/)) { + context.report(node, "Avoid arguments.{{property}}.", { property: propertyName }); + } + + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-catch-shadow.js b/tools/eslint/lib/rules/no-catch-shadow.js new file mode 100644 index 0000000000..af07c46292 --- /dev/null +++ b/tools/eslint/lib/rules/no-catch-shadow.js @@ -0,0 +1,50 @@ +/** + * @fileoverview Rule to flag variable leak in CatchClauses in IE 8 and earlier + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + function paramIsShadowing(scope, name) { + var found = scope.variables.some(function(variable) { + return variable.name === name; + }); + + if (found) { + return true; + } + + if (scope.upper) { + return paramIsShadowing(scope.upper, name); + } + + return false; + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + + "CatchClause": function(node) { + var scope = context.getScope(); + + if (paramIsShadowing(scope, node.param.name)) { + context.report(node, "Value of '{{name}}' may be overwritten in IE 8 and earlier.", + { name: node.param.name }); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-comma-dangle.js b/tools/eslint/lib/rules/no-comma-dangle.js new file mode 100644 index 0000000000..32b2a73658 --- /dev/null +++ b/tools/eslint/lib/rules/no-comma-dangle.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Rule to flag trailing commas in object literals. + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + //------------------------------------------------------------------------- + // Helpers + //------------------------------------------------------------------------- + + function checkForTrailingComma(node) { + var items = node.properties || node.elements, + length = items.length, + lastItem, penultimateToken; + + if (length) { + lastItem = items[length - 1]; + if (lastItem) { + penultimateToken = context.getLastToken(node, 1); + if (penultimateToken.value === ",") { + context.report(lastItem, penultimateToken.loc.start, "Trailing comma."); + } + } + } + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + "ObjectExpression": checkForTrailingComma, + "ArrayExpression": checkForTrailingComma + }; + +}; diff --git a/tools/eslint/lib/rules/no-cond-assign.js b/tools/eslint/lib/rules/no-cond-assign.js new file mode 100644 index 0000000000..9b00fe4e32 --- /dev/null +++ b/tools/eslint/lib/rules/no-cond-assign.js @@ -0,0 +1,117 @@ +/** + * @fileoverview Rule to flag assignment in a conditional statement's test expression + * @author Stephen Murray <spmurrayzzz> + */ +"use strict"; + +var NODE_DESCRIPTIONS = { + "DoWhileStatement": "a 'do...while' statement", + "ForStatement": "a 'for' statement", + "IfStatement": "an 'if' statement", + "WhileStatement": "a 'while' statement" +}; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var prohibitAssign = (context.options[0] || "except-parens"); + + /** + * Check whether an AST node is the test expression for a conditional statement. + * @param {!Object} node The node to test. + * @returns {boolean} `true` if the node is the text expression for a conditional statement; otherwise, `false`. + */ + function isConditionalTestExpression(node) { + return node.parent && + node.parent.test && + node === node.parent.test; + } + + /** + * Given an AST node, perform a bottom-up search for the first ancestor that represents a conditional statement. + * @param {!Object} node The node to use at the start of the search. + * @returns {?Object} The closest ancestor node that represents a conditional statement. + */ + function findConditionalAncestor(node) { + var currentAncestor = node; + + while ((currentAncestor = currentAncestor.parent)) { + if (isConditionalTestExpression(currentAncestor)) { + return currentAncestor.parent; + } + } + + return null; + } + + /** + * Check whether the code represented by an AST node is enclosed in parentheses. + * @param {!Object} node The node to test. + * @returns {boolean} `true` if the code is enclosed in parentheses; otherwise, `false`. + */ + function isParenthesised(node) { + var previousToken = context.getTokenBefore(node), + nextToken = context.getTokenAfter(node); + + return previousToken.value === "(" && previousToken.range[1] <= node.range[0] && + nextToken.value === ")" && nextToken.range[0] >= node.range[1]; + } + + /** + * Check whether the code represented by an AST node is enclosed in two sets of parentheses. + * @param {!Object} node The node to test. + * @returns {boolean} `true` if the code is enclosed in two sets of parentheses; otherwise, `false`. + */ + function isParenthesisedTwice(node) { + var previousToken = context.getTokenBefore(node, 1), + nextToken = context.getTokenAfter(node, 1); + + return isParenthesised(node) && + previousToken.value === "(" && previousToken.range[1] <= node.range[0] && + nextToken.value === ")" && nextToken.range[0] >= node.range[1]; + } + + /** + * Check a conditional statement's test expression for top-level assignments that are not enclosed in parentheses. + * @param {!Object} node The node for the conditional statement. + * @returns {void} + */ + function testForAssign(node) { + if (node.test && (node.test.type === "AssignmentExpression") && !isParenthesisedTwice(node.test)) { + // must match JSHint's error message + context.report(node, "Expected a conditional expression and instead saw an assignment."); + } + } + + /** + * Check whether an assignment expression is descended from a conditional statement's test expression. + * @param {!Object} node The node for the assignment expression. + * @returns {void} + */ + function testForConditionalAncestor(node) { + var ancestor = findConditionalAncestor(node); + + if (ancestor) { + context.report(ancestor, "Unexpected assignment within {{type}}.", { + type: NODE_DESCRIPTIONS[ancestor.type] || ancestor.type + }); + } + } + + if (prohibitAssign === "always") { + return { + "AssignmentExpression": testForConditionalAncestor + }; + } + + return { + "DoWhileStatement": testForAssign, + "ForStatement": testForAssign, + "IfStatement": testForAssign, + "WhileStatement": testForAssign + }; + +}; diff --git a/tools/eslint/lib/rules/no-console.js b/tools/eslint/lib/rules/no-console.js new file mode 100644 index 0000000000..929f00045e --- /dev/null +++ b/tools/eslint/lib/rules/no-console.js @@ -0,0 +1,25 @@ +/** + * @fileoverview Rule to flag use of console object + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "MemberExpression": function(node) { + + if (node.object.name === "console") { + context.report(node, "Unexpected console statement."); + } + + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-constant-condition.js b/tools/eslint/lib/rules/no-constant-condition.js new file mode 100644 index 0000000000..394a9c9bb8 --- /dev/null +++ b/tools/eslint/lib/rules/no-constant-condition.js @@ -0,0 +1,71 @@ +/** + * @fileoverview Rule to flag use constant conditions + * @author Christian Schulz <http://rndm.de> + * @copyright 2014 Christian Schulz. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Checks if a node has a constant truthiness value. + * @param {ASTNode} node The AST node to check. + * @returns {Bool} true when node's truthiness is constant + * @private + */ + function isConstant(node) { + switch (node.type) { + case "Literal": + case "ArrowFunctionExpression": + case "FunctionExpression": + case "ObjectExpression": + case "ArrayExpression": + return true; + case "UnaryExpression": + return isConstant(node.argument); + case "BinaryExpression": + case "LogicalExpression": + return isConstant(node.left) && isConstant(node.right); + case "AssignmentExpression": + return (node.operator === "=") && isConstant(node.right); + case "SequenceExpression": + return isConstant(node.expressions[node.expressions.length - 1]); + // no default + } + return false; + } + + /** + * Reports when the given node contains a constant condition. + * @param {ASTNode} node The AST node to check. + * @returns {void} + * @private + */ + function checkConstantCondition(node) { + if (node.test && isConstant(node.test)) { + context.report(node, "Unexpected constant condition."); + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + "ConditionalExpression": checkConstantCondition, + "IfStatement": checkConstantCondition, + "WhileStatement": checkConstantCondition, + "DoWhileStatement": checkConstantCondition, + "ForStatement": checkConstantCondition + }; + +}; diff --git a/tools/eslint/lib/rules/no-continue.js b/tools/eslint/lib/rules/no-continue.js new file mode 100644 index 0000000000..b481013005 --- /dev/null +++ b/tools/eslint/lib/rules/no-continue.js @@ -0,0 +1,21 @@ +/** + * @fileoverview Rule to flag use of continue statement + * @author Borislav Zhivkov + * @copyright 2015 Borislav Zhivkov. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + "ContinueStatement": function(node) { + context.report(node, "Unexpected use of continue statement"); + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-control-regex.js b/tools/eslint/lib/rules/no-control-regex.js new file mode 100644 index 0000000000..a800acc87f --- /dev/null +++ b/tools/eslint/lib/rules/no-control-regex.js @@ -0,0 +1,56 @@ +/** + * @fileoverview Rule to forbid control charactes from regular expressions. + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + function getRegExp(node) { + + if (node.value instanceof RegExp) { + return node.value; + } else if (typeof node.value === "string") { + + var parent = context.getAncestors().pop(); + if ((parent.type === "NewExpression" || parent.type === "CallExpression") && + parent.callee.type === "Identifier" && parent.callee.name === "RegExp") { + + // there could be an invalid regular expression string + try { + return new RegExp(node.value); + } catch (ex) { + return null; + } + + } + } else { + return null; + } + + } + + + + return { + + "Literal": function(node) { + + var computedValue, + regex = getRegExp(node); + + if (regex) { + computedValue = regex.toString(); + if (/[\x00-\x1f]/.test(computedValue)) { + context.report(node, "Unexpected control character in regular expression."); + } + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-debugger.js b/tools/eslint/lib/rules/no-debugger.js new file mode 100644 index 0000000000..1a9a7e7d70 --- /dev/null +++ b/tools/eslint/lib/rules/no-debugger.js @@ -0,0 +1,20 @@ +/** + * @fileoverview Rule to flag use of a debugger statement + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + "DebuggerStatement": function(node) { + context.report(node, "Unexpected 'debugger' statement."); + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-delete-var.js b/tools/eslint/lib/rules/no-delete-var.js new file mode 100644 index 0000000000..a863d6897a --- /dev/null +++ b/tools/eslint/lib/rules/no-delete-var.js @@ -0,0 +1,23 @@ +/** + * @fileoverview Rule to flag when deleting variables + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "UnaryExpression": function(node) { + if (node.operator === "delete" && node.argument.type === "Identifier") { + context.report(node, "Variables should not be deleted."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-div-regex.js b/tools/eslint/lib/rules/no-div-regex.js new file mode 100644 index 0000000000..17347480c6 --- /dev/null +++ b/tools/eslint/lib/rules/no-div-regex.js @@ -0,0 +1,25 @@ +/** + * @fileoverview Rule to check for ambiguous div operator in regexes + * @author Matt DuVall <http://www.mattduvall.com> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "Literal": function(node) { + var token = context.getFirstToken(node); + + if (token.type === "RegularExpression" && token.value[1] === "=") { + context.report(node, "A regular expression literal can be confused with '/='."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-dupe-args.js b/tools/eslint/lib/rules/no-dupe-args.js new file mode 100644 index 0000000000..458622e5e0 --- /dev/null +++ b/tools/eslint/lib/rules/no-dupe-args.js @@ -0,0 +1,83 @@ +/** + * @fileoverview Rule to flag duplicate arguments + * @author Jamund Ferguson + * @copyright 2015 Jamund Ferguson. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Determines if a given node has duplicate parameters. + * @param {ASTNode} node The node to check. + * @returns {void} + * @private + */ + function checkParams(node) { + var params = {}, + dups = {}; + + + /** + * Marks a given param as either seen or duplicated. + * @param {string} name The name of the param to mark. + * @returns {void} + * @private + */ + function markParam(name) { + if (params.hasOwnProperty(name)) { + dups[name] = 1; + } else { + params[name] = 1; + } + } + + // loop through and find each duplicate param + node.params.forEach(function(param) { + + switch (param.type) { + case "Identifier": + markParam(param.name); + break; + + case "ObjectPattern": + param.properties.forEach(function(property) { + markParam(property.key.name); + }); + break; + + case "ArrayPattern": + param.elements.forEach(function(element) { + markParam(element.name); + }); + break; + + // no default + } + }); + + // log an error for each duplicate (not 2 for each) + Object.keys(dups).forEach(function(currentParam) { + context.report(node, "Duplicate param '{{key}}'.", { key: currentParam }); + }); + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + "FunctionDeclaration": checkParams, + "FunctionExpression": checkParams + }; + +}; diff --git a/tools/eslint/lib/rules/no-dupe-keys.js b/tools/eslint/lib/rules/no-dupe-keys.js new file mode 100644 index 0000000000..522f6ace6d --- /dev/null +++ b/tools/eslint/lib/rules/no-dupe-keys.js @@ -0,0 +1,41 @@ +/** + * @fileoverview Rule to flag use of duplicate keys in an object. + * @author Ian Christian Myers + * @copyright 2013 Ian Christian Myers. All rights reserved. + * @copyright 2013 Nicholas C. Zakas. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "ObjectExpression": function(node) { + + // Object that will be a map of properties--safe because we will + // prefix all of the keys. + var nodeProps = Object.create(null); + + node.properties.forEach(function(property) { + var keyName = property.key.name || property.key.value, + key = property.kind + "-" + keyName, + checkProperty = (!property.computed || property.key.type === "Literal"); + + if (checkProperty) { + if (nodeProps[key]) { + context.report(node, "Duplicate key '{{key}}'.", { key: keyName }); + } else { + nodeProps[key] = true; + } + } + }); + + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-duplicate-case.js b/tools/eslint/lib/rules/no-duplicate-case.js new file mode 100644 index 0000000000..89de7174fc --- /dev/null +++ b/tools/eslint/lib/rules/no-duplicate-case.js @@ -0,0 +1,59 @@ +/** + * @fileoverview Rule to disallow a duplicate case label. + * @author Dieter Oberkofler + * @copyright 2015 Dieter Oberkofler. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + /** + * Get a hash value for the node + * @param {ASTNode} node The node. + * @returns {string} A hash value for the node. + * @private + */ + function getHash(node) { + if (node.type === "Literal") { + return node.type + typeof node.value + node.value; + } else if (node.type === "Identifier") { + return node.type + typeof node.name + node.name; + } else if (node.type === "MemberExpression") { + return node.type + getHash(node.object) + getHash(node.property); + } + } + + var switchStatement = []; + + return { + + "SwitchStatement": function(/*node*/) { + switchStatement.push({}); + }, + + "SwitchStatement:exit": function(/*node*/) { + switchStatement.pop(); + }, + + "SwitchCase": function(node) { + var currentSwitch = switchStatement[switchStatement.length - 1], + hashValue; + + if (node.test) { + hashValue = getHash(node.test); + if (typeof hashValue !== "undefined" && currentSwitch.hasOwnProperty(hashValue)) { + context.report(node, "Duplicate case label."); + } else { + currentSwitch[hashValue] = true; + } + } + } + + }; + +}; diff --git a/tools/eslint/lib/rules/no-else-return.js b/tools/eslint/lib/rules/no-else-return.js new file mode 100644 index 0000000000..2496095365 --- /dev/null +++ b/tools/eslint/lib/rules/no-else-return.js @@ -0,0 +1,123 @@ +/** + * @fileoverview Rule to flag `else` after a `return` in `if` + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Display the context report if rule is violated + * + * @param {Node} node The 'else' node + * @returns {void} + */ + function displayReport(node) { + context.report(node, "Unexpected 'else' after 'return'."); + } + + /** + * Check to see if the node is a ReturnStatement + * + * @param {Node} node The node being evaluated + * @returns {boolean} True if node is a return + */ + function checkForReturn(node) { + return node.type === "ReturnStatement"; + } + + /** + * Naive return checking, does not iterate through the whole + * BlockStatement because we make the assumption that the ReturnStatement + * will be the last node in the body of the BlockStatement. + * + * @param {Node} node The consequent/alternate node + * @returns {boolean} True if it has a return + */ + function naiveHasReturn(node) { + if (node.type === "BlockStatement") { + var body = node.body, + lastChildNode = body[body.length - 1]; + + return lastChildNode && checkForReturn(lastChildNode); + } + return checkForReturn(node); + } + + /** + * Check to see if the node is valid for evaluation, + * meaning it has an else and not an else-if + * + * @param {Node} node The node being evaluated + * @returns {boolean} True if the node is valid + */ + function hasElse(node) { + return node.alternate && node.consequent && node.alternate.type !== "IfStatement"; + } + + /** + * If the consequent is an IfStatement, check to see if it has an else + * and both its consequent and alternate path return, meaning this is + * a nested case of rule violation. If-Else not considered currently. + * + * @param {Node} node The consequent node + * @returns {boolean} True if this is a nested rule violation + */ + function checkForIf(node) { + return node.type === "IfStatement" && hasElse(node) && + naiveHasReturn(node.alternate) && naiveHasReturn(node.consequent); + } + + /** + * Check the consequent/body node to make sure it is not + * a ReturnStatement or an IfStatement that returns on both + * code paths. If it is, display the context report. + * + * @param {Node} node The consequent or body node + * @param {Node} alternate The alternate node + * @returns {void} + */ + function checkForReturnOrIf(node, alternate) { + if (checkForReturn(node) || checkForIf(node)) { + displayReport(alternate); + } + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + + "IfStatement": function (node) { + // Don't bother finding a ReturnStatement, if there's no `else` + // or if the alternate is also an if (indicating an else if). + if (hasElse(node)) { + var consequent = node.consequent, + alternate = node.alternate; + // If we have a BlockStatement, check each consequent body node. + if (consequent.type === "BlockStatement") { + var body = consequent.body; + body.forEach(function (bodyNode) { + checkForReturnOrIf(bodyNode, alternate); + }); + // If not a block statement, make sure the consequent isn't a ReturnStatement + // or an IfStatement with returns on both paths + } else { + checkForReturnOrIf(consequent, alternate); + } + } + } + + }; + +}; diff --git a/tools/eslint/lib/rules/no-empty-class.js b/tools/eslint/lib/rules/no-empty-class.js new file mode 100644 index 0000000000..009c1446b4 --- /dev/null +++ b/tools/eslint/lib/rules/no-empty-class.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Rule to flag the use of empty character classes in regular expressions + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/* +plain-English description of the following regexp: +0. `^` fix the match at the beginning of the string +1. `\/`: the `/` that begins the regexp +2. `([^\\[]|\\.|\[([^\\\]]|\\.)+\])*`: regexp contents; 0 or more of the following + 2.0. `[^\\[]`: any character that's not a `\` or a `[` (anything but escape sequences and character classes) + 2.1. `\\.`: an escape sequence + 2.2. `\[([^\\\]]|\\.)+\]`: a character class that isn't empty +3. `\/` the `/` that ends the regexp +4. `[gimy]*`: optional regexp flags +5. `$`: fix the match at the end of the string +*/ +var regex = /^\/([^\\[]|\\.|\[([^\\\]]|\\.)+\])*\/[gimy]*$/; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "Literal": function(node) { + var token = context.getFirstToken(node); + if (token.type === "RegularExpression" && !regex.test(token.value)) { + context.report(node, "Empty class."); + } + } + + }; + +}; diff --git a/tools/eslint/lib/rules/no-empty-label.js b/tools/eslint/lib/rules/no-empty-label.js new file mode 100644 index 0000000000..371c629a82 --- /dev/null +++ b/tools/eslint/lib/rules/no-empty-label.js @@ -0,0 +1,25 @@ +/** + * @fileoverview Rule to flag when label is not used for a loop or switch + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "LabeledStatement": function(node) { + var type = node.body.type; + + if (type !== "ForStatement" && type !== "WhileStatement" && type !== "DoWhileStatement" && type !== "SwitchStatement" && type !== "ForInStatement" && type !== "ForOfStatement") { + context.report(node, "Unexpected label {{l}}", {l: node.label.name}); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-empty.js b/tools/eslint/lib/rules/no-empty.js new file mode 100644 index 0000000000..1c2307d63f --- /dev/null +++ b/tools/eslint/lib/rules/no-empty.js @@ -0,0 +1,47 @@ +/** + * @fileoverview Rule to flag use of an empty block statement + * @author Nicholas C. Zakas + * @copyright Nicholas C. Zakas. All rights reserved. + * @copyright 2015 Dieter Oberkofler. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "BlockStatement": function(node) { + var parent = node.parent, + parentType = parent.type; + + // if the body is not empty, we can just return immediately + if (node.body.length !== 0) { + return; + } + + // a function is generally allowed to be empty + if (parentType === "FunctionDeclaration" || parentType === "FunctionExpression" || parentType === "ArrowFunctionExpression") { + return; + } + + // any other block is only allowed to be empty, if it contains a comment + if (context.getComments(node).trailing.length > 0) { + return; + } + + context.report(node, "Empty block statement."); + }, + + "SwitchStatement": function(node) { + + if (typeof node.cases === "undefined" || node.cases.length === 0) { + context.report(node, "Empty switch statement."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-eq-null.js b/tools/eslint/lib/rules/no-eq-null.js new file mode 100644 index 0000000000..deee40c36a --- /dev/null +++ b/tools/eslint/lib/rules/no-eq-null.js @@ -0,0 +1,27 @@ +/** + * @fileoverview Rule to flag comparisons to null without a type-checking + * operator. + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "BinaryExpression": function(node) { + var badOperator = node.operator === "==" || node.operator === "!="; + + if (node.right.type === "Literal" && node.right.raw === "null" && badOperator || + node.left.type === "Literal" && node.left.raw === "null" && badOperator) { + context.report(node, "Use ‘===’ to compare with ‘null’."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-eval.js b/tools/eslint/lib/rules/no-eval.js new file mode 100644 index 0000000000..8eced727db --- /dev/null +++ b/tools/eslint/lib/rules/no-eval.js @@ -0,0 +1,24 @@ +/** + * @fileoverview Rule to flag use of eval() statement + * @author Nicholas C. Zakas + * @copyright 2015 Mathias Schreck. All rights reserved. + * @copyright 2013 Nicholas C. Zakas. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + "CallExpression": function(node) { + if (node.callee.name === "eval") { + context.report(node, "eval can be harmful."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-ex-assign.js b/tools/eslint/lib/rules/no-ex-assign.js new file mode 100644 index 0000000000..6ea5a22360 --- /dev/null +++ b/tools/eslint/lib/rules/no-ex-assign.js @@ -0,0 +1,40 @@ +/** + * @fileoverview Rule to flag assignment of the exception parameter + * @author Stephen Murray <spmurrayzzz> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var catchStack = []; + + return { + + "CatchClause": function(node) { + catchStack.push(node.param.name); + }, + + "CatchClause:exit": function() { + catchStack.pop(); + }, + + "AssignmentExpression": function(node) { + + if (catchStack.length > 0) { + + var exceptionName = catchStack[catchStack.length - 1]; + + if (node.left.name && node.left.name === exceptionName) { + context.report(node, "Do not assign to the exception parameter."); + } + } + } + + }; + +}; diff --git a/tools/eslint/lib/rules/no-extend-native.js b/tools/eslint/lib/rules/no-extend-native.js new file mode 100644 index 0000000000..a600de09be --- /dev/null +++ b/tools/eslint/lib/rules/no-extend-native.js @@ -0,0 +1,77 @@ +/** + * @fileoverview Rule to flag adding properties to native object's prototypes. + * @author David Nelson + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var BUILTINS = [ + "Object", "Function", "Array", "String", "Boolean", "Number", "Date", + "RegExp", "Error", "EvalError", "RangeError", "ReferenceError", + "SyntaxError", "TypeError", "URIError" +]; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + // handle the Array.prototype.extra style case + "AssignmentExpression": function(node) { + var lhs = node.left, affectsProto; + + if (lhs.type !== "MemberExpression" || lhs.object.type !== "MemberExpression") { + return; + } + + affectsProto = lhs.object.computed ? + lhs.object.property.type === "Literal" && lhs.object.property.value === "prototype" : + lhs.object.property.name === "prototype"; + + if (!affectsProto) { + return; + } + + BUILTINS.forEach(function(builtin) { + if (lhs.object.object.name === builtin) { + context.report(node, builtin + " prototype is read only, properties should not be added."); + } + }); + }, + + // handle the Object.defineProperty(Array.prototype) case + "CallExpression": function(node) { + + var callee = node.callee, + subject, + object; + + // only worry about Object.defineProperty + if (callee.type === "MemberExpression" && + callee.object.name === "Object" && + callee.property.name === "defineProperty") { + + // verify the object being added to is a native prototype + subject = node.arguments[0]; + object = subject.object; + + if (object && + object.type === "Identifier" && + (BUILTINS.indexOf(object.name) > -1) && + subject.property.name === "prototype") { + + context.report(node, object.name + " prototype is read only, properties should not be added."); + } + } + + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-extra-bind.js b/tools/eslint/lib/rules/no-extra-bind.js new file mode 100644 index 0000000000..b585730633 --- /dev/null +++ b/tools/eslint/lib/rules/no-extra-bind.js @@ -0,0 +1,79 @@ +/** + * @fileoverview Rule to flag unnecessary bind calls + * @author Bence Dányi <bence@danyi.me> + * @copyright 2014 Bence Dányi. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var scope = [{ + depth: -1, + found: 0 + }]; + + /** + * Get the topmost scope + * @returns {Object} The topmost scope + */ + function getTopScope() { + return scope[scope.length - 1]; + } + + /** + * Increment the depth of the top scope + * @returns {void} + */ + function incrementScopeDepth() { + var top = getTopScope(); + top.depth++; + } + + /** + * Decrement the depth of the top scope + * @returns {void} + */ + function decrementScopeDepth() { + var top = getTopScope(); + top.depth--; + } + + return { + "CallExpression": function(node) { + if (node.arguments.length === 1 && + node.callee.type === "MemberExpression" && + node.callee.property.name === "bind" && + /FunctionExpression$/.test(node.callee.object.type)) { + scope.push({ + call: node, + depth: -1, + found: 0 + }); + } + }, + "CallExpression:exit": function(node) { + var top = getTopScope(); + if (top.call === node && top.found === 0) { + context.report(node, "The function binding is unnecessary."); + scope.pop(); + } + }, + "ArrowFunctionExpression": incrementScopeDepth, + "ArrowFunctionExpression:exit": decrementScopeDepth, + "FunctionExpression": incrementScopeDepth, + "FunctionExpression:exit": decrementScopeDepth, + "FunctionDeclaration": incrementScopeDepth, + "FunctionDeclaration:exit": decrementScopeDepth, + "ThisExpression": function() { + var top = getTopScope(); + if (top.depth === 0) { + top.found++; + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-extra-boolean-cast.js b/tools/eslint/lib/rules/no-extra-boolean-cast.js new file mode 100644 index 0000000000..605d7b1210 --- /dev/null +++ b/tools/eslint/lib/rules/no-extra-boolean-cast.js @@ -0,0 +1,69 @@ +/** + * @fileoverview Rule to flag unnecessary double negation in Boolean contexts + * @author Brandon Mills + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + "UnaryExpression": function (node) { + var ancestors = context.getAncestors(), + parent = ancestors.pop(), + grandparent = ancestors.pop(); + + // Exit early if it's guaranteed not to match + if (node.operator !== "!" || + parent.type !== "UnaryExpression" || + parent.operator !== "!") { + return; + } + + // if (<bool>) ... + if (grandparent.type === "IfStatement") { + context.report(node, "Redundant double negation in an if statement condition."); + + // do ... while (<bool>) + } else if (grandparent.type === "DoWhileStatement") { + context.report(node, "Redundant double negation in a do while loop condition."); + + // while (<bool>) ... + } else if (grandparent.type === "WhileStatement") { + context.report(node, "Redundant double negation in a while loop condition."); + + // <bool> ? ... : ... + } else if ((grandparent.type === "ConditionalExpression" && + parent === grandparent.test)) { + context.report(node, "Redundant double negation in a ternary condition."); + + // for (...; <bool>; ...) ... + } else if ((grandparent.type === "ForStatement" && + parent === grandparent.test)) { + context.report(node, "Redundant double negation in a for loop condition."); + + // !<bool> + } else if ((grandparent.type === "UnaryExpression" && + grandparent.operator === "!")) { + context.report(node, "Redundant multiple negation."); + + // Boolean(<bool>) + } else if ((grandparent.type === "CallExpression" && + grandparent.callee.type === "Identifier" && + grandparent.callee.name === "Boolean")) { + context.report(node, "Redundant double negation in call to Boolean()."); + + // new Boolean(<bool>) + } else if ((grandparent.type === "NewExpression" && + grandparent.callee.type === "Identifier" && + grandparent.callee.name === "Boolean")) { + context.report(node, "Redundant double negation in Boolean constructor call."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-extra-parens.js b/tools/eslint/lib/rules/no-extra-parens.js new file mode 100644 index 0000000000..2ec8647a68 --- /dev/null +++ b/tools/eslint/lib/rules/no-extra-parens.js @@ -0,0 +1,299 @@ +/** + * @fileoverview Disallow parenthesesisng higher precedence subexpressions. + * @author Michael Ficarra + * @copyright 2014 Michael Ficarra. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + function isParenthesised(node) { + var previousToken = context.getTokenBefore(node), + nextToken = context.getTokenAfter(node); + + return previousToken && nextToken && + previousToken.value === "(" && previousToken.range[1] <= node.range[0] && + nextToken.value === ")" && nextToken.range[0] >= node.range[1]; + } + + function isParenthesisedTwice(node) { + var previousToken = context.getTokenBefore(node, 1), + nextToken = context.getTokenAfter(node, 1); + + return isParenthesised(node) && previousToken && nextToken && + previousToken.value === "(" && previousToken.range[1] <= node.range[0] && + nextToken.value === ")" && nextToken.range[0] >= node.range[1]; + } + + function precedence(node) { + + switch (node.type) { + case "SequenceExpression": + return 0; + + case "AssignmentExpression": + case "ArrowFunctionExpression": + case "YieldExpression": + return 1; + + case "ConditionalExpression": + return 3; + + case "LogicalExpression": + switch (node.operator) { + case "||": + return 4; + case "&&": + return 5; + // no default + } + + /* falls through */ + case "BinaryExpression": + switch (node.operator) { + case "|": + return 6; + case "^": + return 7; + case "&": + return 8; + case "==": + case "!=": + case "===": + case "!==": + return 9; + case "<": + case "<=": + case ">": + case ">=": + case "in": + case "instanceof": + return 10; + case "<<": + case ">>": + case ">>>": + return 11; + case "+": + case "-": + return 12; + case "*": + case "/": + case "%": + return 13; + // no default + } + /* falls through */ + case "UnaryExpression": + return 14; + case "UpdateExpression": + return 15; + case "CallExpression": + // IIFE is allowed to have parens in any position (#655) + if (node.callee.type === "FunctionExpression") { + return -1; + } + return 16; + case "NewExpression": + return 17; + // no default + } + return 18; + } + + function report(node) { + var previousToken = context.getTokenBefore(node); + context.report(node, previousToken.loc.start, "Gratuitous parentheses around expression."); + } + + function dryUnaryUpdate(node) { + if (isParenthesised(node.argument) && precedence(node.argument) >= precedence(node)) { + report(node.argument); + } + } + + function dryCallNew(node) { + if (isParenthesised(node.callee) && precedence(node.callee) >= precedence(node) && + !(node.type === "CallExpression" && node.callee.type === "FunctionExpression")) { + report(node.callee); + } + if (node.arguments.length === 1) { + if (isParenthesisedTwice(node.arguments[0]) && precedence(node.arguments[0]) >= precedence({type: "AssignmentExpression"})) { + report(node.arguments[0]); + } + } else { + [].forEach.call(node.arguments, function(arg) { + if (isParenthesised(arg) && precedence(arg) >= precedence({type: "AssignmentExpression"})) { + report(arg); + } + }); + } + } + + function dryBinaryLogical(node) { + var prec = precedence(node); + if (isParenthesised(node.left) && precedence(node.left) >= prec) { + report(node.left); + } + if (isParenthesised(node.right) && precedence(node.right) > prec) { + report(node.right); + } + } + + return { + "ArrayExpression": function(node) { + [].forEach.call(node.elements, function(e) { + if (e && isParenthesised(e) && precedence(e) >= precedence({type: "AssignmentExpression"})) { + report(e); + } + }); + }, + "ArrowFunctionExpression": function(node) { + if (node.body.type !== "BlockStatement" && isParenthesised(node.body) && precedence(node.body) >= precedence({type: "AssignmentExpression"})) { + report(node.body); + } + }, + "AssignmentExpression": function(node) { + if (isParenthesised(node.right) && precedence(node.right) >= precedence(node)) { + report(node.right); + } + }, + "BinaryExpression": dryBinaryLogical, + "CallExpression": dryCallNew, + "ConditionalExpression": function(node) { + if (isParenthesised(node.test) && precedence(node.test) >= precedence({type: "LogicalExpression", operator: "||"})) { + report(node.test); + } + if (isParenthesised(node.consequent) && precedence(node.consequent) >= precedence({type: "AssignmentExpression"})) { + report(node.consequent); + } + if (isParenthesised(node.alternate) && precedence(node.alternate) >= precedence({type: "AssignmentExpression"})) { + report(node.alternate); + } + }, + "DoWhileStatement": function(node) { + if (isParenthesisedTwice(node.test)) { + report(node.test); + } + }, + "ExpressionStatement": function(node) { + var firstToken; + if (isParenthesised(node.expression)) { + firstToken = context.getFirstToken(node.expression); + if (firstToken.value !== "function" && firstToken.value !== "{") { + report(node.expression); + } + } + }, + "ForInStatement": function(node) { + if (isParenthesised(node.right)) { + report(node.right); + } + }, + "ForOfStatement": function(node) { + if (isParenthesised(node.right)) { + report(node.right); + } + }, + "ForStatement": function(node) { + if (node.init && isParenthesised(node.init)) { + report(node.init); + } + + if (node.test && isParenthesised(node.test)) { + report(node.test); + } + + if (node.update && isParenthesised(node.update)) { + report(node.update); + } + }, + "IfStatement": function(node) { + if (isParenthesisedTwice(node.test)) { + report(node.test); + } + }, + "LogicalExpression": dryBinaryLogical, + "MemberExpression": function(node) { + if ( + isParenthesised(node.object) && + precedence(node.object) >= precedence(node) && + ( + node.computed || + !( + (node.object.type === "Literal" && + typeof node.object.value === "number" && + /^[0-9]+$/.test(context.getFirstToken(node.object).value)) + || + // RegExp literal is allowed to have parens (#1589) + (node.object.type === "Literal" && node.object.regex) + ) + ) + ) { + report(node.object); + } + }, + "NewExpression": dryCallNew, + "ObjectExpression": function(node) { + [].forEach.call(node.properties, function(e) { + var v = e.value; + if (v && isParenthesised(v) && precedence(v) >= precedence({type: "AssignmentExpression"})) { + report(v); + } + }); + }, + "ReturnStatement": function(node) { + if (node.argument && isParenthesised(node.argument) && + // RegExp literal is allowed to have parens (#1589) + !(node.argument.type === "Literal" && node.argument.regex)) { + report(node.argument); + } + }, + "SequenceExpression": function(node) { + [].forEach.call(node.expressions, function(e) { + if (isParenthesised(e) && precedence(e) >= precedence(node)) { + report(e); + } + }); + }, + "SwitchCase": function(node) { + if (node.test && isParenthesised(node.test)) { + report(node.test); + } + }, + "SwitchStatement": function(node) { + if (isParenthesisedTwice(node.discriminant)) { + report(node.discriminant); + } + }, + "ThrowStatement": function(node) { + if (isParenthesised(node.argument)) { + report(node.argument); + } + }, + "UnaryExpression": dryUnaryUpdate, + "UpdateExpression": dryUnaryUpdate, + "VariableDeclarator": function(node) { + if (node.init && isParenthesised(node.init) && + precedence(node.init) >= precedence({type: "AssignmentExpression"}) && + // RegExp literal is allowed to have parens (#1589) + !(node.init.type === "Literal" && node.init.regex)) { + report(node.init); + } + }, + "WhileStatement": function(node) { + if (isParenthesisedTwice(node.test)) { + report(node.test); + } + }, + "WithStatement": function(node) { + if (isParenthesisedTwice(node.object)) { + report(node.object); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-extra-semi.js b/tools/eslint/lib/rules/no-extra-semi.js new file mode 100644 index 0000000000..e155d982e5 --- /dev/null +++ b/tools/eslint/lib/rules/no-extra-semi.js @@ -0,0 +1,21 @@ +/** + * @fileoverview Rule to flag use of unnecessary semicolons + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "EmptyStatement": function(node) { + context.report(node, "Unnecessary semicolon."); + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-extra-strict.js b/tools/eslint/lib/rules/no-extra-strict.js new file mode 100644 index 0000000000..4c6ac16a42 --- /dev/null +++ b/tools/eslint/lib/rules/no-extra-strict.js @@ -0,0 +1,84 @@ +/** + * @fileoverview Rule to flag unnecessary strict directives. + * @author Ian Christian Myers + * @copyright 2014 Ian Christian Myers. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + function directives(block) { + var ds = [], body = block.body, e, i, l; + + if (body) { + for (i = 0, l = body.length; i < l; ++i) { + e = body[i]; + + if ( + e.type === "ExpressionStatement" && + e.expression.type === "Literal" && + typeof e.expression.value === "string" + ) { + ds.push(e.expression); + } else { + break; + } + } + } + + return ds; + } + + function isStrict(directive) { + return directive.value === "use strict"; + } + + function checkForUnnecessaryUseStrict(node) { + var useStrictDirectives = directives(node).filter(isStrict), + scope, + upper; + + switch (useStrictDirectives.length) { + case 0: + break; + + case 1: + scope = context.getScope(); + upper = scope.upper; + + if (upper && upper.functionExpressionScope) { + upper = upper.upper; + } + + if (upper && upper.isStrict) { + context.report(useStrictDirectives[0], "Unnecessary 'use strict'."); + } + break; + + default: + context.report(useStrictDirectives[1], "Multiple 'use strict' directives."); + } + } + + return { + + "Program": checkForUnnecessaryUseStrict, + + "ArrowFunctionExpression": function(node) { + checkForUnnecessaryUseStrict(node.body); + }, + + "FunctionExpression": function(node) { + checkForUnnecessaryUseStrict(node.body); + }, + + "FunctionDeclaration": function(node) { + checkForUnnecessaryUseStrict(node.body); + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-fallthrough.js b/tools/eslint/lib/rules/no-fallthrough.js new file mode 100644 index 0000000000..a137fb3f80 --- /dev/null +++ b/tools/eslint/lib/rules/no-fallthrough.js @@ -0,0 +1,95 @@ +/** + * @fileoverview Rule to flag fall-through cases in switch statements. + * @author Matt DuVall <http://mattduvall.com/> + */ +"use strict"; + + +var FALLTHROUGH_COMMENT = /falls\sthrough/; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var switches = []; + + return { + + "SwitchCase": function(node) { + + var consequent = node.consequent, + switchData = switches[switches.length - 1], + i, + comments, + comment; + + /* + * Some developers wrap case bodies in blocks, so if there is just one + * node and it's a block statement, check inside. + */ + if (consequent.length === 1 && consequent[0].type === "BlockStatement") { + consequent = consequent[0]; + } + + // checking on previous case + if (!switchData.lastCaseClosed) { + + // a fall through comment will be the last trailing comment of the last case + comments = context.getComments(switchData.lastCase).trailing; + comment = comments[comments.length - 1]; + + // unless the user doesn't like semicolons, in which case it's first leading comment of this case + if (!comment) { + comments = context.getComments(node).leading; + comment = comments[comments.length - 1]; + } + + // check for comment + if (!comment || !FALLTHROUGH_COMMENT.test(comment.value)) { + + context.report(switchData.lastCase, + "Expected a \"break\" statement before \"{{code}}\".", + { code: node.test ? "case" : "default" }); + } + } + + // now dealing with the current case + switchData.lastCaseClosed = false; + switchData.lastCase = node; + + // try to verify using statements - go backwards as a fast path for the search + if (consequent.length) { + for (i = consequent.length - 1; i >= 0; i--) { + if (/(?:Break|Continue|Return|Throw)Statement/.test(consequent[i].type)) { + switchData.lastCaseClosed = true; + break; + } + } + } else { + // the case statement has no statements, so it must logically fall through + switchData.lastCaseClosed = true; + } + + /* + * Any warnings are triggered when the next SwitchCase occurs. + * There is no need to warn on the last SwitchCase, since it can't + * fall through to anything. + */ + }, + + "SwitchStatement": function(node) { + switches.push({ + node: node, + lastCaseClosed: true, + lastCase: null + }); + }, + + "SwitchStatement:exit": function() { + switches.pop(); + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-floating-decimal.js b/tools/eslint/lib/rules/no-floating-decimal.js new file mode 100644 index 0000000000..c6380d5460 --- /dev/null +++ b/tools/eslint/lib/rules/no-floating-decimal.js @@ -0,0 +1,28 @@ +/** + * @fileoverview Rule to flag use of a leading/trailing decimal point in a numeric literal + * @author James Allardice + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + "Literal": function(node) { + + if (typeof node.value === "number") { + if (node.raw.indexOf(".") === 0) { + context.report(node, "A leading decimal point can be confused with a dot."); + } + if (node.raw.indexOf(".") === node.raw.length - 1) { + context.report(node, "A trailing decimal point can be confused with a dot."); + } + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-func-assign.js b/tools/eslint/lib/rules/no-func-assign.js new file mode 100644 index 0000000000..a9f2858126 --- /dev/null +++ b/tools/eslint/lib/rules/no-func-assign.js @@ -0,0 +1,81 @@ +/** + * @fileoverview Rule to flag use of function declaration identifiers as variables. + * @author Ian Christian Myers + * @copyright 2013 Ian Christian Myers. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /* + * Walk the scope chain looking for either a FunctionDeclaration or a + * VariableDeclaration with the same name as left-hand side of the + * AssignmentExpression. If we find the FunctionDeclaration first, then we + * warn, because a FunctionDeclaration is trying to become a Variable or a + * FunctionExpression. If we find a VariableDeclaration first, then we have + * a legitimate shadow variable. + */ + function checkIfIdentifierIsFunction(scope, name) { + var variable, + def, + i, + j; + + // Loop over all of the identifiers available in scope. + for (i = 0; i < scope.variables.length; i++) { + variable = scope.variables[i]; + + // For each identifier, see if it was defined in _this_ scope. + for (j = 0; j < variable.defs.length; j++) { + def = variable.defs[j]; + + // Identifier is a function and was declared in this scope + if (def.type === "FunctionName" && def.name.name === name) { + return true; + } + + // Identifier is a variable and was declared in this scope. This + // is a legitimate shadow variable. + if (def.name && def.name.name === name) { + return false; + } + } + } + + // Check the upper scope. + if (scope.upper) { + return checkIfIdentifierIsFunction(scope.upper, name); + } + + // We've reached the global scope and haven't found anything. + return false; + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + + "AssignmentExpression": function(node) { + var scope = context.getScope(), + name = node.left.name; + + if (checkIfIdentifierIsFunction(scope, name)) { + context.report(node, "'{{name}}' is a function.", { name: name }); + } + + } + + }; + +}; diff --git a/tools/eslint/lib/rules/no-implied-eval.js b/tools/eslint/lib/rules/no-implied-eval.js new file mode 100644 index 0000000000..f798dcdade --- /dev/null +++ b/tools/eslint/lib/rules/no-implied-eval.js @@ -0,0 +1,74 @@ +/** + * @fileoverview Rule to flag use of implied eval via setTimeout and setInterval + * @author James Allardice + * @copyright 2015 Mathias Schreck. All rights reserved. + * @copyright 2013 James Allardice. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + var IMPLIED_EVAL = /set(?:Timeout|Interval)/; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Checks if the first argument of a given CallExpression node is a string literal. + * @param {ASTNode} node The CallExpression node the check. + * @returns {boolean} True if the first argument is a string literal, false if not. + */ + function hasStringLiteralArgument(node) { + var firstArgument = node.arguments[0]; + + return firstArgument && firstArgument.type === "Literal" && typeof firstArgument.value === "string"; + } + + /** + * Checks if the given MemberExpression node is window.setTimeout or window.setInterval. + * @param {ASTNode} node The MemberExpression node to check. + * @returns {boolean} Whether or not the given node is window.set*. + */ + function isSetMemberExpression(node) { + var object = node.object, + property = node.property, + hasSetPropertyName = IMPLIED_EVAL.test(property.name) || IMPLIED_EVAL.test(property.value); + + return object.name === "window" && hasSetPropertyName; + + } + + /** + * Determines if a node represents a call to setTimeout/setInterval with + * a string argument. + * @param {ASTNode} node The node to check. + * @returns {boolean} True if the node matches, false if not. + * @private + */ + function isImpliedEval(node) { + var isMemberExpression = (node.callee.type === "MemberExpression"), + isIdentifier = (node.callee.type === "Identifier"), + isSetMethod = (isIdentifier && IMPLIED_EVAL.test(node.callee.name)) || + (isMemberExpression && isSetMemberExpression(node.callee)); + + return isSetMethod && hasStringLiteralArgument(node); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + "CallExpression": function(node) { + if (isImpliedEval(node)) { + context.report(node, "Implied eval. Consider passing a function instead of a string."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-inline-comments.js b/tools/eslint/lib/rules/no-inline-comments.js new file mode 100644 index 0000000000..091ad9bee4 --- /dev/null +++ b/tools/eslint/lib/rules/no-inline-comments.js @@ -0,0 +1,47 @@ +/** + * @fileoverview Enforces or disallows inline comments. + * @author Greg Cochard + * @copyright 2014 Greg Cochard. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + /** + * Will check that comments are not on lines starting with or ending with code + * @param {ASTNode} node The comment node to check + * @private + * @returns {void} + */ + function testCodeAroundComment(node) { + + // Get the whole line and cut it off at the start of the comment + var startLine = String(context.getSourceLines()[node.loc.start.line - 1]); + var endLine = String(context.getSourceLines()[node.loc.end.line - 1]); + + var preamble = startLine.slice(0, node.loc.start.column).trim(); + + // Also check after the comment + var postamble = endLine.slice(node.loc.end.column).trim(); + + // Should be empty if there was only whitespace around the comment + if (preamble || postamble) { + context.report(node, "Unexpected comment inline with code."); + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + "LineComment": testCodeAroundComment, + "BlockComment": testCodeAroundComment + + }; +}; diff --git a/tools/eslint/lib/rules/no-inner-declarations.js b/tools/eslint/lib/rules/no-inner-declarations.js new file mode 100644 index 0000000000..664639d575 --- /dev/null +++ b/tools/eslint/lib/rules/no-inner-declarations.js @@ -0,0 +1,72 @@ +/** + * @fileoverview Rule to enforce declarations in program or function body root. + * @author Brandon Mills + * @copyright 2014 Brandon Mills. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + /** + * Find the nearest Program or Function ancestor node. + * @returns {Object} Ancestor's type and distance from node. + */ + function nearestBody() { + var ancestors = context.getAncestors(), + ancestor = ancestors.pop(), + generation = 1; + + while (ancestor && ["Program", "FunctionDeclaration", + "FunctionExpression", "ArrowFunctionExpression" + ].indexOf(ancestor.type) < 0) { + generation += 1; + ancestor = ancestors.pop(); + } + + return { + // Type of containing ancestor + type: ancestor.type, + // Separation between ancestor and node + distance: generation + }; + } + + /** + * Ensure that a given node is at a program or function body's root. + * @param {ASTNode} node Declaration node to check. + * @returns {void} + */ + function check(node) { + var body = nearestBody(node), + valid = ((body.type === "Program" && body.distance === 1) || + body.distance === 2); + + if (!valid) { + context.report(node, "Move {{type}} declaration to {{body}} root.", + { + type: (node.type === "FunctionDeclaration" ? + "function" : "variable"), + body: (body.type === "Program" ? + "program" : "function body") + } + ); + } + } + + return { + + "FunctionDeclaration": check, + "VariableDeclaration": function(node) { + if (context.options[0] === "both" && node.kind === "var") { + check(node); + } + } + + }; + +}; diff --git a/tools/eslint/lib/rules/no-invalid-regexp.js b/tools/eslint/lib/rules/no-invalid-regexp.js new file mode 100644 index 0000000000..cea5372f15 --- /dev/null +++ b/tools/eslint/lib/rules/no-invalid-regexp.js @@ -0,0 +1,51 @@ +/** + * @fileoverview Validate strings passed to the RegExp constructor + * @author Michael Ficarra + * @copyright 2014 Michael Ficarra. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var espree = require("espree"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + function isString(node) { + return node && node.type === "Literal" && typeof node.value === "string"; + } + + function check(node) { + if (node.callee.type === "Identifier" && node.callee.name === "RegExp" && isString(node.arguments[0])) { + var flags = isString(node.arguments[1]) ? node.arguments[1].value : ""; + + try { + void new RegExp(node.arguments[0].value); + } catch(e) { + context.report(node, e.message); + } + + if (flags) { + + try { + espree.parse("/./" + flags, { ecmaFeatures: context.ecmaFeatures }); + } catch (ex) { + context.report(node, "Invalid flags supplied to RegExp constructor '" + flags + "'"); + } + } + + } + } + + return { + "CallExpression": check, + "NewExpression": check + }; + +}; diff --git a/tools/eslint/lib/rules/no-irregular-whitespace.js b/tools/eslint/lib/rules/no-irregular-whitespace.js new file mode 100644 index 0000000000..3599f0cecd --- /dev/null +++ b/tools/eslint/lib/rules/no-irregular-whitespace.js @@ -0,0 +1,92 @@ +/** + * @fileoverview Rule to disalow whitespace that is not a tab or space, whitespace inside strings and comments are allowed + * @author Jonathan Kingston + * @copyright 2014 Jonathan Kingston. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var irregularWhitespace = /[\u0085\u00A0\ufeff\f\v\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u202f\u205f\u3000]+/mg; + + // Module store of errors that we have found + var errors = []; + + /** + * Removes errors that occur inside a string node + * @param {ASTNode} node to check for matching errors. + * @returns {void} + * @private + */ + function removeStringError(node) { + var locStart = node.loc.start; + var locEnd = node.loc.end; + + errors = errors.filter(function (error) { + var errorLoc = error[1]; + if (errorLoc.line >= locStart.line && errorLoc.line <= locEnd.line) { + if (errorLoc.column >= locStart.column && errorLoc.column <= locEnd.column) { + return false; + } + } + return true; + }); + } + + /** + * Checks nodes for errors that we are choosing to ignore and calls the relevent methods to remove the errors + * @param {ASTNode} node to check for matching errors. + * @returns {void} + * @private + */ + function removeInvalidNodeErrors(node) { + if (typeof node.value === "string") { + // If we have irregular characters remove them from the errors list + if (node.value.match(irregularWhitespace)) { + removeStringError(node); + } + } + } + + return { + "Program": function (node) { + /** + * As we can easily fire warnings for all white space issues with all the source its simpler to fire them here + * This means we can check all the application code without having to worry about issues caused in the parser tokens + * When writing this code also evaluating per node was missing out connecting tokens in some cases + * We can later filter the errors when they are found to be not an issue in nodes we don't care about + */ + var sourceLines = context.getSourceLines(); + + sourceLines.forEach(function (sourceLine, lineIndex) { + var location, + match = irregularWhitespace.exec(sourceLine); + + if (match !== null) { + location = { + line: lineIndex + 1, + column: match.index + }; + + errors.push([node, location, "Irregular whitespace not allowed"]); + } + }); + }, + "Identifier": removeInvalidNodeErrors, + "Literal": removeInvalidNodeErrors, + "Statement": removeInvalidNodeErrors, + "Expression": removeInvalidNodeErrors, + "Program:exit": function () { + + // If we have any errors remaining report on them + errors.forEach(function (error) { + context.report.apply(this, error); + }); + } + }; +}; diff --git a/tools/eslint/lib/rules/no-iterator.js b/tools/eslint/lib/rules/no-iterator.js new file mode 100644 index 0000000000..564c09abfa --- /dev/null +++ b/tools/eslint/lib/rules/no-iterator.js @@ -0,0 +1,26 @@ +/** + * @fileoverview Rule to flag usage of __iterator__ property + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "MemberExpression": function(node) { + + if (node.property && + (node.property.type === "Identifier" && node.property.name === "__iterator__" && !node.computed) || + (node.property.type === "Literal" && node.property.value === "__iterator__")) { + context.report(node, "Reserved name '__iterator__'."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-label-var.js b/tools/eslint/lib/rules/no-label-var.js new file mode 100644 index 0000000000..b82bd0af60 --- /dev/null +++ b/tools/eslint/lib/rules/no-label-var.js @@ -0,0 +1,62 @@ +/** + * @fileoverview Rule to flag labels that are the same as an identifier + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + function findIdentifier(scope, identifier) { + var found = false; + + scope.variables.forEach(function(variable) { + if (variable.name === identifier) { + found = true; + } + }); + + scope.references.forEach(function(reference) { + if (reference.identifier.name === identifier) { + found = true; + } + }); + + // If we have not found the identifier in this scope, check the parent + // scope. + if (scope.upper && !found) { + return findIdentifier(scope.upper, identifier); + } + + return found; + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + + "LabeledStatement": function(node) { + + // Fetch the innermost scope. + var scope = context.getScope(); + + // Recursively find the identifier walking up the scope, starting + // with the innermost scope. + if (findIdentifier(scope, node.label.name)) { + context.report(node, "Found identifier with same name as label."); + } + } + + }; + +}; diff --git a/tools/eslint/lib/rules/no-labels.js b/tools/eslint/lib/rules/no-labels.js new file mode 100644 index 0000000000..8b5086a295 --- /dev/null +++ b/tools/eslint/lib/rules/no-labels.js @@ -0,0 +1,42 @@ +/** + * @fileoverview Disallow Labeled Statements + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + "LabeledStatement": function(node) { + context.report(node, "Unexpected labeled statement."); + }, + + "BreakStatement": function(node) { + + if (node.label) { + context.report(node, "Unexpected label in break statement."); + } + + }, + + "ContinueStatement": function(node) { + + if (node.label) { + context.report(node, "Unexpected label in continue statement."); + } + + } + + + }; + +}; diff --git a/tools/eslint/lib/rules/no-lone-blocks.js b/tools/eslint/lib/rules/no-lone-blocks.js new file mode 100644 index 0000000000..25d8c34f00 --- /dev/null +++ b/tools/eslint/lib/rules/no-lone-blocks.js @@ -0,0 +1,25 @@ +/** + * @fileoverview Rule to flag blocks with no reason to exist + * @author Brandon Mills + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + "BlockStatement": function (node) { + // Check for any occurrence of BlockStatement > BlockStatement or + // Program > BlockStatement + var parent = context.getAncestors().pop(); + if (parent.type === "BlockStatement" || parent.type === "Program") { + context.report(node, "Block is nested inside another block."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-lonely-if.js b/tools/eslint/lib/rules/no-lonely-if.js new file mode 100644 index 0000000000..59d807da30 --- /dev/null +++ b/tools/eslint/lib/rules/no-lonely-if.js @@ -0,0 +1,28 @@ +/** + * @fileoverview Rule to disallow if as the only statmenet in an else block + * @author Brandon Mills + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + "IfStatement": function(node) { + var ancestors = context.getAncestors(), + parent = ancestors.pop(), + grandparent = ancestors.pop(); + + if (parent && parent.type === "BlockStatement" && + parent.body.length === 1 && grandparent && + grandparent.type === "IfStatement" && + parent === grandparent.alternate) { + context.report(node, "Unexpected if as the only statement in an else block."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-loop-func.js b/tools/eslint/lib/rules/no-loop-func.js new file mode 100644 index 0000000000..ec68611b72 --- /dev/null +++ b/tools/eslint/lib/rules/no-loop-func.js @@ -0,0 +1,49 @@ +/** + * @fileoverview Rule to flag creation of function inside a loop + * @author Ilya Volodin + * @copyright 2013 Ilya Volodin. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + var loopNodeTypes = [ + "ForStatement", + "WhileStatement", + "ForInStatement", + "ForOfStatement", + "DoWhileStatement" + ]; + + /** + * Checks if the given node is a loop. + * @param {ASTNode} node The AST node to check. + * @returns {boolean} Whether or not the node is a loop. + */ + function isLoop(node) { + return loopNodeTypes.indexOf(node.type) > -1; + } + + /** + * Reports if the given node has an ancestor node which is a loop. + * @param {ASTNode} node The AST node to check. + * @returns {boolean} Whether or not the node is within a loop. + */ + function checkForLoops(node) { + var ancestors = context.getAncestors(); + + if (ancestors.some(isLoop)) { + context.report(node, "Don't make functions within a loop"); + } + } + + return { + "ArrowFunctionExpression": checkForLoops, + "FunctionExpression": checkForLoops, + "FunctionDeclaration": checkForLoops + }; +}; diff --git a/tools/eslint/lib/rules/no-mixed-requires.js b/tools/eslint/lib/rules/no-mixed-requires.js new file mode 100644 index 0000000000..d75fc56abb --- /dev/null +++ b/tools/eslint/lib/rules/no-mixed-requires.js @@ -0,0 +1,159 @@ +/** + * @fileoverview Rule to enforce grouped require statements for Node.JS + * @author Raphael Pigulla + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + /** + * Returns the list of built-in modules. + * + * @returns {string[]} An array of built-in Node.js modules. + */ + function getBuiltinModules() { + // This list is generated using `require("repl")._builtinLibs.concat('repl').sort()` + // This particular list is as per nodejs v0.12.2 and iojs v0.7.1 + return [ + "assert", "buffer", "child_process", "cluster", "crypto", + "dgram", "dns", "domain", "events", "fs", "http", "https", + "net", "os", "path", "punycode", "querystring", "readline", + "repl", "smalloc", "stream", "string_decoder", "tls", "tty", + "url", "util", "v8", "vm", "zlib" + ]; + } + + var BUILTIN_MODULES = getBuiltinModules(); + + var DECL_REQUIRE = "require", + DECL_UNINITIALIZED = "uninitialized", + DECL_OTHER = "other"; + + var REQ_CORE = "core", + REQ_FILE = "file", + REQ_MODULE = "module", + REQ_COMPUTED = "computed"; + + /** + * Determines the type of a declaration statement. + * @param {ASTNode} initExpression The init node of the VariableDeclarator. + * @returns {string} The type of declaration represented by the expression. + */ + function getDeclarationType(initExpression) { + if (!initExpression) { + // "var x;" + return DECL_UNINITIALIZED; + } + + if (initExpression.type === "CallExpression" && + initExpression.callee.type === "Identifier" && + initExpression.callee.name === "require" + ) { + // "var x = require('util');" + return DECL_REQUIRE; + } else if (initExpression.type === "MemberExpression") { + // "var x = require('glob').Glob;" + return getDeclarationType(initExpression.object); + } + + // "var x = 42;" + return DECL_OTHER; + } + + /** + * Determines the type of module that is loaded via require. + * @param {ASTNode} initExpression The init node of the VariableDeclarator. + * @returns {string} The module type. + */ + function inferModuleType(initExpression) { + if (initExpression.type === "MemberExpression") { + // "var x = require('glob').Glob;" + return inferModuleType(initExpression.object); + } else if (initExpression.arguments.length === 0) { + // "var x = require();" + return REQ_COMPUTED; + } + + var arg = initExpression.arguments[0]; + + if (arg.type !== "Literal" || typeof arg.value !== "string") { + // "var x = require(42);" + return REQ_COMPUTED; + } + + if (BUILTIN_MODULES.indexOf(arg.value) !== -1) { + // "var fs = require('fs');" + return REQ_CORE; + } else if (/^\.{0,2}\//.test(arg.value)) { + // "var utils = require('./utils');" + return REQ_FILE; + } else { + // "var async = require('async');" + return REQ_MODULE; + } + } + + /** + * Check if the list of variable declarations is mixed, i.e. whether it + * contains both require and other declarations. + * @param {ASTNode} declarations The list of VariableDeclarators. + * @returns {boolean} True if the declarations are mixed, false if not. + */ + function isMixed(declarations) { + var contains = {}; + + declarations.forEach(function (declaration) { + var type = getDeclarationType(declaration.init); + contains[type] = true; + }); + + return !!( + contains[DECL_REQUIRE] && + (contains[DECL_UNINITIALIZED] || contains[DECL_OTHER]) + ); + } + + /** + * Check if all require declarations in the given list are of the same + * type. + * @param {ASTNode} declarations The list of VariableDeclarators. + * @returns {boolean} True if the declarations are grouped, false if not. + */ + function isGrouped(declarations) { + var found = {}; + + declarations.forEach(function (declaration) { + if (getDeclarationType(declaration.init) === DECL_REQUIRE) { + found[inferModuleType(declaration.init)] = true; + } + }); + + return Object.keys(found).length <= 1; + } + + + return { + + "VariableDeclaration": function(node) { + var grouping = !!context.options[0]; + + if (isMixed(node.declarations)) { + context.report( + node, + "Do not mix 'require' and other declarations." + ); + } else if (grouping && !isGrouped(node.declarations)) { + context.report( + node, + "Do not mix core, module, file and computed requires." + ); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-mixed-spaces-and-tabs.js b/tools/eslint/lib/rules/no-mixed-spaces-and-tabs.js new file mode 100644 index 0000000000..7b76d76db9 --- /dev/null +++ b/tools/eslint/lib/rules/no-mixed-spaces-and-tabs.js @@ -0,0 +1,68 @@ +/** + * @fileoverview Disallow mixed spaces and tabs for indentation + * @author Jary Niebur + * @copyright 2014 Nicholas C. Zakas. All rights reserved. + * @copyright 2014 Jary Niebur. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var smartTabs; + + switch (context.options[0]) { + case true: // Support old syntax, maybe add deprecation warning here + case "smart-tabs": + smartTabs = true; + break; + default: + smartTabs = false; + } + + var COMMENT_START = /^\s*\/\*/, + MAYBE_COMMENT = /^\s*\*/; + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + "Program": function(node) { + /* + * At least one space followed by a tab + * or the reverse before non-tab/-space + * characters begin. + */ + var regex = /^(?=[\t ]*(\t | \t))/, + match, + lines = context.getSourceLines(); + + if (smartTabs) { + /* + * At least one space followed by a tab + * before non-tab/-space characters begin. + */ + regex = /^(?=[\t ]* \t)/; + } + + lines.forEach(function(line, i) { + match = regex.exec(line); + + if (match) { + + if (!MAYBE_COMMENT.test(line) && !COMMENT_START.test(lines[i - 1])) { + context.report(node, { line: i + 1, column: match.index + 1 }, "Mixed spaces and tabs."); + } + + } + }); + } + + }; + +}; diff --git a/tools/eslint/lib/rules/no-multi-spaces.js b/tools/eslint/lib/rules/no-multi-spaces.js new file mode 100644 index 0000000000..9e247c1533 --- /dev/null +++ b/tools/eslint/lib/rules/no-multi-spaces.js @@ -0,0 +1,101 @@ +/** + * @fileoverview Disallow use of multiple spaces. + * @author Nicholas C. Zakas + * @copyright 2015 Brandon Mills. All rights reserved. + * @copyright 2015 Nicholas C. Zakas. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + // the index of the last comment that was checked + var exceptions = { "Property": true }, + hasExceptions = true, + options = context.options[0], + lastCommentIndex = 0; + + if (options && options.exceptions) { + Object.keys(options.exceptions).forEach(function (key) { + if (options.exceptions[key]) { + exceptions[key] = true; + } else { + delete exceptions[key]; + } + }); + hasExceptions = Object.keys(exceptions).length > 0; + } + + /** + * Determines if a given source index is in a comment or not by checking + * the index against the comment range. Since the check goes straight + * through the file, once an index is passed a certain comment, we can + * go to the next comment to check that. + * @param {int} index The source index to check. + * @param {ASTNode[]} comments An array of comment nodes. + * @returns {boolean} True if the index is within a comment, false if not. + * @private + */ + function isIndexInComment(index, comments) { + + var comment; + + while (lastCommentIndex < comments.length) { + + comment = comments[lastCommentIndex]; + + if (comment.range[0] <= index && index < comment.range[1]) { + return true; + } else if (index > comment.range[1]) { + lastCommentIndex++; + } else { + break; + } + + } + + return false; + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + "Program": function() { + + var source = context.getSource(), + allComments = context.getAllComments(), + pattern = /[^\n\r\u2028\u2029 ] {2,}/g, // note: repeating space + token, + parent; + + while (pattern.test(source)) { + + // do not flag anything inside of comments + if (!isIndexInComment(pattern.lastIndex, allComments)) { + + token = context.getTokenByRangeStart(pattern.lastIndex); + + if (token) { + if (hasExceptions) { + parent = context.getNodeByRangeIndex(pattern.lastIndex - 1); + } + + if (!parent || !exceptions[parent.type]) { + context.report(token, token.loc.start, + "Multiple spaces found before '{{value}}'.", + { value: token.value }); + } + } + + } + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-multi-str.js b/tools/eslint/lib/rules/no-multi-str.js new file mode 100644 index 0000000000..1d4810b97d --- /dev/null +++ b/tools/eslint/lib/rules/no-multi-str.js @@ -0,0 +1,41 @@ +/** + * @fileoverview Rule to flag when using multiline strings + * @author Ilya Volodin + * @copyright 2014 Nicholas C. Zakas. All rights reserved. + * @copyright 2013 Ilya Volodin. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + /** + * Determines if a given node is part of JSX syntax. + * @param {ASTNode} node The node to check. + * @returns {boolean} True if the node is a JSX node, false if not. + * @private + */ + function isJSXElement(node) { + return node.type.indexOf("JSX") === 0; + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + + "Literal": function(node) { + var lineBreak = /\n/; + + if (lineBreak.test(node.raw) && !isJSXElement(node.parent)) { + context.report(node, "Multiline support is limited to browsers supporting ES5 only."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-multiple-empty-lines.js b/tools/eslint/lib/rules/no-multiple-empty-lines.js new file mode 100644 index 0000000000..9cfea63daa --- /dev/null +++ b/tools/eslint/lib/rules/no-multiple-empty-lines.js @@ -0,0 +1,60 @@ +/** + * @fileoverview Disallows multiple blank lines. + * implementation adapted from the no-trailing-spaces rule. + * @author Greg Cochard + * @copyright 2014 Greg Cochard. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + // Use options.max or 2 as default + var numLines = 2; + + if (context.options.length) { + numLines = context.options[0].max; + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + "Program": function checkBlankLines(node) { + var lines = context.getSourceLines(), + currentLocation = -1, + lastLocation, + blankCounter = 0, + location, + trimmedLines = lines.map(function(str) { + return str.trim(); + }); + + // Aggregate and count blank lines + do { + lastLocation = currentLocation; + currentLocation = trimmedLines.indexOf("", currentLocation + 1); + if (lastLocation === currentLocation - 1) { + blankCounter++; + } else { + if (blankCounter >= numLines) { + location = { + line: lastLocation + 1, + column: lines[lastLocation].length + }; + context.report(node, location, "Multiple blank lines not allowed."); + } + + // Finally, reset the blank counter + blankCounter = 0; + } + } while (currentLocation !== -1); + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-native-reassign.js b/tools/eslint/lib/rules/no-native-reassign.js new file mode 100644 index 0000000000..29ff4a3836 --- /dev/null +++ b/tools/eslint/lib/rules/no-native-reassign.js @@ -0,0 +1,37 @@ +/** + * @fileoverview Rule to flag when re-assigning native objects + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var nativeObjects = ["Array", "Boolean", "Date", "decodeURI", + "decodeURIComponent", "encodeURI", "encodeURIComponent", + "Error", "eval", "EvalError", "Function", "isFinite", + "isNaN", "JSON", "Math", "Number", "Object", "parseInt", + "parseFloat", "RangeError", "ReferenceError", "RegExp", + "String", "SyntaxError", "TypeError", "URIError", + "Map", "NaN", "Set", "WeakMap", "Infinity", "undefined"]; + + return { + + "AssignmentExpression": function(node) { + if (nativeObjects.indexOf(node.left.name) >= 0) { + context.report(node, node.left.name + " is a read-only native object."); + } + }, + + "VariableDeclarator": function(node) { + if (nativeObjects.indexOf(node.id.name) >= 0) { + context.report(node, "Redefinition of '{{nativeObject}}'.", { nativeObject: node.id.name }); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-negated-in-lhs.js b/tools/eslint/lib/rules/no-negated-in-lhs.js new file mode 100644 index 0000000000..75bdf73192 --- /dev/null +++ b/tools/eslint/lib/rules/no-negated-in-lhs.js @@ -0,0 +1,23 @@ +/** + * @fileoverview A rule to disallow negated left operands of the `in` operator + * @author Michael Ficarra + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "BinaryExpression": function(node) { + if (node.operator === "in" && node.left.type === "UnaryExpression" && node.left.operator === "!") { + context.report(node, "The `in` expression's left operand is negated"); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-nested-ternary.js b/tools/eslint/lib/rules/no-nested-ternary.js new file mode 100644 index 0000000000..10b0683f95 --- /dev/null +++ b/tools/eslint/lib/rules/no-nested-ternary.js @@ -0,0 +1,22 @@ +/** + * @fileoverview Rule to flag nested ternary expressions + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + "ConditionalExpression": function(node) { + if (node.alternate.type === "ConditionalExpression" || + node.consequent.type === "ConditionalExpression") { + context.report(node, "Do not nest ternary expressions"); + } + } + }; +}; diff --git a/tools/eslint/lib/rules/no-new-func.js b/tools/eslint/lib/rules/no-new-func.js new file mode 100644 index 0000000000..c0e64350ed --- /dev/null +++ b/tools/eslint/lib/rules/no-new-func.js @@ -0,0 +1,23 @@ +/** + * @fileoverview Rule to flag when using new Function + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "NewExpression": function(node) { + if (node.callee.name === "Function") { + context.report(node, "The Function constructor is eval."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-new-object.js b/tools/eslint/lib/rules/no-new-object.js new file mode 100644 index 0000000000..426d7129da --- /dev/null +++ b/tools/eslint/lib/rules/no-new-object.js @@ -0,0 +1,23 @@ +/** + * @fileoverview A rule to disallow calls to the Object constructor + * @author Matt DuVall <http://www.mattduvall.com/> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "NewExpression": function(node) { + if (node.callee.name === "Object") { + context.report(node, "The object literal notation {} is preferrable."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-new-require.js b/tools/eslint/lib/rules/no-new-require.js new file mode 100644 index 0000000000..27d25d22af --- /dev/null +++ b/tools/eslint/lib/rules/no-new-require.js @@ -0,0 +1,23 @@ +/** + * @fileoverview Rule to disallow use of new operator with the `require` function + * @author Wil Moore III + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "NewExpression": function(node) { + if (node.callee.type === "Identifier" && node.callee.name === "require") { + context.report(node, "Unexpected use of new with require."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-new-wrappers.js b/tools/eslint/lib/rules/no-new-wrappers.js new file mode 100644 index 0000000000..4dbc60d5b4 --- /dev/null +++ b/tools/eslint/lib/rules/no-new-wrappers.js @@ -0,0 +1,24 @@ +/** + * @fileoverview Rule to flag when using constructor for wrapper objects + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "NewExpression": function(node) { + var wrapperObjects = ["String", "Number", "Boolean", "Math", "JSON"]; + if (wrapperObjects.indexOf(node.callee.name) > -1) { + context.report(node, "Do not use {{fn}} as a constructor.", { fn: node.callee.name }); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-new.js b/tools/eslint/lib/rules/no-new.js new file mode 100644 index 0000000000..914a649303 --- /dev/null +++ b/tools/eslint/lib/rules/no-new.js @@ -0,0 +1,25 @@ +/** + * @fileoverview Rule to flag statements with function invocation preceded by + * "new" and not part of assignment + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "ExpressionStatement": function(node) { + + if (node.expression.type === "NewExpression") { + context.report(node, "Do not use 'new' for side effects."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-obj-calls.js b/tools/eslint/lib/rules/no-obj-calls.js new file mode 100644 index 0000000000..246720c2c4 --- /dev/null +++ b/tools/eslint/lib/rules/no-obj-calls.js @@ -0,0 +1,26 @@ +/** + * @fileoverview Rule to flag use of an object property of the global object (Math and JSON) as a function + * @author James Allardice + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + "CallExpression": function(node) { + + if (node.callee.type === "Identifier") { + var name = node.callee.name; + if (name === "Math" || name === "JSON") { + context.report(node, "'{{name}}' is not a function.", { name: name }); + } + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-octal-escape.js b/tools/eslint/lib/rules/no-octal-escape.js new file mode 100644 index 0000000000..d6c75a868d --- /dev/null +++ b/tools/eslint/lib/rules/no-octal-escape.js @@ -0,0 +1,37 @@ +/** + * @fileoverview Rule to flag octal escape sequences in string literals. + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "Literal": function(node) { + if (typeof node.value !== "string") { + return; + } + + var match = node.raw.match(/^([^\\]|\\[^0-7])*\\([0-3][0-7]{1,2}|[4-7][0-7]|[0-7])/), + octalDigit; + + if (match) { + octalDigit = match[2]; + + // \0 is actually not considered an octal + if (match[2] !== "0" || typeof match[3] !== "undefined") { + context.report(node, "Don't use octal: '\\{{octalDigit}}'. Use '\\u....' instead.", + { octalDigit: octalDigit }); + } + } + } + + }; + +}; diff --git a/tools/eslint/lib/rules/no-octal.js b/tools/eslint/lib/rules/no-octal.js new file mode 100644 index 0000000000..dae4f7fe94 --- /dev/null +++ b/tools/eslint/lib/rules/no-octal.js @@ -0,0 +1,23 @@ +/** + * @fileoverview Rule to flag when initializing octal literal + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "Literal": function(node) { + if (typeof node.value === "number" && /^0[0-7]/.test(node.raw)) { + context.report(node, "Octal literals should not be used."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-param-reassign.js b/tools/eslint/lib/rules/no-param-reassign.js new file mode 100644 index 0000000000..c38f0ef6f4 --- /dev/null +++ b/tools/eslint/lib/rules/no-param-reassign.js @@ -0,0 +1,85 @@ +/** + * @fileoverview Disallow reassignment of function parameters. + * @author Nat Burns + * @copyright 2014 Nat Burns. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Finds the declaration for a given variable by name, searching up the scope tree. + * @param {Scope} scope The scope in which to search. + * @param {String} name The name of the variable. + * @returns {Variable} The declaration information for the given variable, or null if no declaration was found. + */ + function findDeclaration(scope, name) { + var variables = scope.variables; + + for (var i = 0; i < variables.length; i++) { + if (variables[i].name === name) { + return variables[i]; + } + } + + if (scope.upper) { + return findDeclaration(scope.upper, name); + } else { + return null; + } + } + + /** + * Determines if a given variable is declared as a function parameter. + * @param {Variable} variable The variable declaration. + * @returns {boolean} True if the variable is a function parameter, false otherwise. + */ + function isParameter(variable) { + var defs = variable.defs; + + for (var i = 0; i < defs.length; i++) { + if (defs[i].type === "Parameter") { + return true; + } + } + + return false; + } + + /** + * Checks whether a given node is an assignment to a function parameter. + * If so, a linting error will be reported. + * @param {ASTNode} node The node to check. + * @param {String} name The name of the variable being assigned to. + * @returns {void} + */ + function checkParameter(node, name) { + var declaration = findDeclaration(context.getScope(), name); + + if (declaration && isParameter(declaration)) { + context.report(node, "Assignment to function parameter '{{name}}'.", { name: name }); + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + "AssignmentExpression": function(node) { + checkParameter(node, node.left.name); + }, + + "UpdateExpression": function(node) { + checkParameter(node, node.argument.name); + } + }; +}; diff --git a/tools/eslint/lib/rules/no-path-concat.js b/tools/eslint/lib/rules/no-path-concat.js new file mode 100644 index 0000000000..b555589157 --- /dev/null +++ b/tools/eslint/lib/rules/no-path-concat.js @@ -0,0 +1,37 @@ +/** + * @fileoverview Disallow string concatenation when using __dirname and __filename + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var MATCHER = /^__(?:dir|file)name$/; + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + "BinaryExpression": function(node) { + + var left = node.left, + right = node.right; + + if (node.operator === "+" && + ((left.type === "Identifier" && MATCHER.test(left.name)) || + (right.type === "Identifier" && MATCHER.test(right.name))) + ) { + + context.report(node, "Use path.join() or path.resolve() instead of + to create paths."); + } + } + + }; + +}; diff --git a/tools/eslint/lib/rules/no-plusplus.js b/tools/eslint/lib/rules/no-plusplus.js new file mode 100644 index 0000000000..268af7ee64 --- /dev/null +++ b/tools/eslint/lib/rules/no-plusplus.js @@ -0,0 +1,22 @@ +/** + * @fileoverview Rule to flag use of unary increment and decrement operators. + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "UpdateExpression": function(node) { + context.report(node, "Unary operator '" + node.operator + "' used."); + } + + }; + +}; diff --git a/tools/eslint/lib/rules/no-process-env.js b/tools/eslint/lib/rules/no-process-env.js new file mode 100644 index 0000000000..54a0cb4928 --- /dev/null +++ b/tools/eslint/lib/rules/no-process-env.js @@ -0,0 +1,28 @@ +/** + * @fileoverview Disallow the use of process.env() + * @author Vignesh Anand + * @copyright 2014 Vignesh Anand. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "MemberExpression": function(node) { + var objectName = node.object.name, + propertyName = node.property.name; + + if (objectName === "process" && !node.computed && propertyName && propertyName === "env") { + context.report(node, "Unexpected use of process.env."); + } + + } + + }; + +}; diff --git a/tools/eslint/lib/rules/no-process-exit.js b/tools/eslint/lib/rules/no-process-exit.js new file mode 100644 index 0000000000..84006f2dbd --- /dev/null +++ b/tools/eslint/lib/rules/no-process-exit.js @@ -0,0 +1,31 @@ +/** + * @fileoverview Disallow the use of process.exit() + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + "CallExpression": function(node) { + var callee = node.callee; + + if (callee.type === "MemberExpression" && callee.object.name === "process" && + callee.property.name === "exit" + ) { + context.report(node, "Don't use process.exit(); throw an error instead."); + } + } + + }; + +}; diff --git a/tools/eslint/lib/rules/no-proto.js b/tools/eslint/lib/rules/no-proto.js new file mode 100644 index 0000000000..0ab434c1f5 --- /dev/null +++ b/tools/eslint/lib/rules/no-proto.js @@ -0,0 +1,26 @@ +/** + * @fileoverview Rule to flag usage of __proto__ property + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "MemberExpression": function(node) { + + if (node.property && + (node.property.type === "Identifier" && node.property.name === "__proto__" && !node.computed) || + (node.property.type === "Literal" && node.property.value === "__proto__")) { + context.report(node, "The '__proto__' property is deprecated."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-redeclare.js b/tools/eslint/lib/rules/no-redeclare.js new file mode 100644 index 0000000000..da1b7bff4c --- /dev/null +++ b/tools/eslint/lib/rules/no-redeclare.js @@ -0,0 +1,58 @@ +/** + * @fileoverview Rule to flag when the same variable is declared more then once. + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + /** + * Find variables in a given scope and flag redeclared ones. + * @param {Scope} scope An escope scope object. + * @returns {void} + * @private + */ + function findVariablesInScope(scope) { + scope.variables.forEach(function(variable) { + + if (variable.identifiers && variable.identifiers.length > 1) { + variable.identifiers.sort(function(a, b) { + return a.range[1] - b.range[1]; + }); + + for (var i = 1, l = variable.identifiers.length; i < l; i++) { + context.report(variable.identifiers[i], "{{a}} is already defined", {a: variable.name}); + } + } + }); + + } + + /** + * Find variables in a given node's associated scope. + * @param {ASTNode} node The node to check. + * @returns {void} + * @private + */ + function findVariables(node) { + var scope = context.getScope(); + + findVariablesInScope(scope); + + // globalReturn means one extra scope to check + if (node.type === "Program" && context.ecmaFeatures.globalReturn) { + findVariablesInScope(scope.childScopes[0]); + } + } + + return { + "Program": findVariables, + "FunctionExpression": findVariables, + "FunctionDeclaration": findVariables + }; +}; diff --git a/tools/eslint/lib/rules/no-regex-spaces.js b/tools/eslint/lib/rules/no-regex-spaces.js new file mode 100644 index 0000000000..edbf76d513 --- /dev/null +++ b/tools/eslint/lib/rules/no-regex-spaces.js @@ -0,0 +1,33 @@ +/** + * @fileoverview Rule to count multiple spaces in regular expressions + * @author Matt DuVall <http://www.mattduvall.com/> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "Literal": function(node) { + var token = context.getFirstToken(node), + nodeType = token.type, + nodeValue = token.value, + multipleSpacesRegex = /( {2,})+?/, + regexResults; + + if (nodeType === "RegularExpression") { + regexResults = multipleSpacesRegex.exec(nodeValue); + + if (regexResults !== null) { + context.report(node, "Spaces are hard to count. Use {" + regexResults[0].length + "}."); + } + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-reserved-keys.js b/tools/eslint/lib/rules/no-reserved-keys.js new file mode 100644 index 0000000000..d94dcffb97 --- /dev/null +++ b/tools/eslint/lib/rules/no-reserved-keys.js @@ -0,0 +1,54 @@ +/** + * @fileoverview Rule to disallow reserved words being used as keys + * @author Emil Bay + * @copyright 2014 Emil Bay. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var MESSAGE = "Reserved word '{{key}}' used as key."; + + var reservedWords = [ + "abstract", + "boolean", "break", "byte", + "case", "catch", "char", "class", "const", "continue", + "debugger", "default", "delete", "do", "double", + "else", "enum", "export", "extends", + "final", "finally", "float", "for", "function", + "goto", + "if", "implements", "import", "in", "instanceof", "int", "interface", + "long", + "native", "new", + "package", "private", "protected", "public", + "return", + "short", "static", "super", "switch", "synchronized", + "this", "throw", "throws", "transient", "try", "typeof", + "var", "void", "volatile", + "while", "with" + ]; + + return { + + "ObjectExpression": function(node) { + node.properties.forEach(function(property) { + + if (property.key.type === "Identifier") { + var keyName = property.key.name; + + if (reservedWords.indexOf("" + keyName) !== -1) { + context.report(node, MESSAGE, { key: keyName }); + } + } + + }); + + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-restricted-modules.js b/tools/eslint/lib/rules/no-restricted-modules.js new file mode 100644 index 0000000000..a4c13517df --- /dev/null +++ b/tools/eslint/lib/rules/no-restricted-modules.js @@ -0,0 +1,72 @@ +/** + * @fileoverview Restrict usage of specified node modules. + * @author Christian Schulz + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function (context) { + // trim restricted module names + var restrictedModules = context.options; + + // if no modules are restricted we don't need to check the CallExpressions + if (restrictedModules.length === 0) { + return {}; + } + + /** + * Function to check if a node is a string literal. + * @param {ASTNode} node The node to check. + * @returns {boolean} If the node is a string literal. + */ + function isString(node) { + return node && node.type === "Literal" && typeof node.value === "string"; + } + + /** + * Function to check if a node is a require call. + * @param {ASTNode} node The node to check. + * @returns {boolean} If the node is a require call. + */ + function isRequireCall(node) { + return node.callee.type === "Identifier" && node.callee.name === "require"; + } + + /** + * Function to check if a node has an argument that is an restricted module and return its name. + * @param {ASTNode} node The node to check + * @returns {undefined|String} restricted module name or undefined if node argument isn't restricted. + */ + function getRestrictedModuleName(node) { + var moduleName; + + // node has arguments and first argument is string + if (node.arguments.length && isString(node.arguments[0])) { + var argumentValue = node.arguments[0].value.trim(); + + // check if argument value is in restricted modules array + if (restrictedModules.indexOf(argumentValue) !== -1) { + moduleName = argumentValue; + } + } + + return moduleName; + } + + return { + "CallExpression": function (node) { + if (isRequireCall(node)) { + var restrictedModuleName = getRestrictedModuleName(node); + + if (restrictedModuleName) { + context.report(node, "'{{moduleName}}' module is restricted from being used.", { + moduleName: restrictedModuleName + }); + } + } + } + }; +}; diff --git a/tools/eslint/lib/rules/no-return-assign.js b/tools/eslint/lib/rules/no-return-assign.js new file mode 100644 index 0000000000..65a751c0e8 --- /dev/null +++ b/tools/eslint/lib/rules/no-return-assign.js @@ -0,0 +1,22 @@ +/** + * @fileoverview Rule to flag when return statement contains assignment + * @author Ilya Volodin + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "ReturnStatement": function(node) { + if (node.argument && node.argument.type === "AssignmentExpression") { + context.report(node, "Return statement should not contain assignment."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-script-url.js b/tools/eslint/lib/rules/no-script-url.js new file mode 100644 index 0000000000..9ae9ba1a3a --- /dev/null +++ b/tools/eslint/lib/rules/no-script-url.js @@ -0,0 +1,32 @@ +/** + * @fileoverview Rule to flag when using javascript: urls + * @author Ilya Volodin + */ +/*jshint scripturl: true */ +/*eslint no-script-url: 0*/ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "Literal": function(node) { + + var value; + + if (node.value && typeof node.value === "string") { + value = node.value.toLowerCase(); + + if (value.indexOf("javascript:") === 0) { + context.report(node, "Script URL is a form of eval."); + } + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-self-compare.js b/tools/eslint/lib/rules/no-self-compare.js new file mode 100644 index 0000000000..dc68c50046 --- /dev/null +++ b/tools/eslint/lib/rules/no-self-compare.js @@ -0,0 +1,27 @@ +/** + * @fileoverview Rule to flag comparison where left part is the same as the right + * part. + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "BinaryExpression": function(node) { + var operators = ["===", "==", "!==", "!=", ">", "<", ">=", "<="]; + if (operators.indexOf(node.operator) > -1 && + (node.left.type === "Identifier" && node.right.type === "Identifier" && node.left.name === node.right.name || + node.left.type === "Literal" && node.right.type === "Literal" && node.left.value === node.right.value)) { + context.report(node, "Comparing to itself is potentially pointless."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-sequences.js b/tools/eslint/lib/rules/no-sequences.js new file mode 100644 index 0000000000..7edcd40d70 --- /dev/null +++ b/tools/eslint/lib/rules/no-sequences.js @@ -0,0 +1,92 @@ +/** + * @fileoverview Rule to flag use of comma operator + * @author Brandon Mills + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + /** + * Parts of the grammar that are required to have parens. + */ + var parenthesized = { + "DoWhileStatement": "test", + "IfStatement": "test", + "SwitchStatement": "discriminant", + "WhileStatement": "test", + "WithStatement": "object" + + // Omitting CallExpression - commas are parsed as argument separators + // Omitting NewExpression - commas are parsed as argument separators + // Omitting ForInStatement - parts aren't individually parenthesised + // Omitting ForStatement - parts aren't individually parenthesised + }; + + /** + * Determines whether a node is required by the grammar to be wrapped in + * parens, e.g. the test of an if statement. + * @param {ASTNode} node - The AST node + * @returns {boolean} True if parens around node belong to parent node. + */ + function requiresExtraParens(node) { + return node.parent && parenthesized[node.parent.type] != null && + node === node.parent[parenthesized[node.parent.type]]; + } + + /** + * Check if a node is wrapped in parens. + * @param {ASTNode} node - The AST node + * @returns {boolean} True if the node has a paren on each side. + */ + function isParenthesised(node) { + var previousToken = context.getTokenBefore(node), + nextToken = context.getTokenAfter(node); + + return previousToken && nextToken && + previousToken.value === "(" && previousToken.range[1] <= node.range[0] && + nextToken.value === ")" && nextToken.range[0] >= node.range[1]; + } + + /** + * Check if a node is wrapped in two levels of parens. + * @param {ASTNode} node - The AST node + * @returns {boolean} True if two parens surround the node on each side. + */ + function isParenthesisedTwice(node) { + var previousToken = context.getTokenBefore(node, 1), + nextToken = context.getTokenAfter(node, 1); + + return isParenthesised(node) && previousToken && nextToken && + previousToken.value === "(" && previousToken.range[1] <= node.range[0] && + nextToken.value === ")" && nextToken.range[0] >= node.range[1]; + } + + return { + "SequenceExpression": function(node) { + // Always allow sequences in for statement update + if (node.parent.type === "ForStatement" && + (node === node.parent.init || node === node.parent.update)) { + return; + } + + // Wrapping a sequence in extra parens indicates intent + if (requiresExtraParens(node)) { + if (isParenthesisedTwice(node)) { + return; + } + } else { + if (isParenthesised(node)) { + return; + } + } + + context.report(node, "Unexpected use of comma operator."); + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-shadow-restricted-names.js b/tools/eslint/lib/rules/no-shadow-restricted-names.js new file mode 100644 index 0000000000..e720a0a153 --- /dev/null +++ b/tools/eslint/lib/rules/no-shadow-restricted-names.js @@ -0,0 +1,49 @@ +/** + * @fileoverview Disallow shadowing of NaN, undefined, and Infinity (ES5 section 15.1.1) + * @author Michael Ficarra + * @copyright 2013 Michael Ficarra. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var RESTRICTED = ["undefined", "NaN", "Infinity", "arguments", "eval"]; + + function checkForViolation(id) { + if (RESTRICTED.indexOf(id.name) > -1) { + context.report(id, "Shadowing of global property \"" + id.name + "\"."); + } + } + + return { + "VariableDeclarator": function(node) { + checkForViolation(node.id); + }, + "ArrowFunctionExpression": function(node) { + if (node.id) { + checkForViolation(node.id); + } + [].map.call(node.params, checkForViolation); + }, + "FunctionExpression": function(node) { + if (node.id) { + checkForViolation(node.id); + } + [].map.call(node.params, checkForViolation); + }, + "FunctionDeclaration": function(node) { + if (node.id) { + checkForViolation(node.id); + [].map.call(node.params, checkForViolation); + } + }, + "CatchClause": function(node) { + checkForViolation(node.param); + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-shadow.js b/tools/eslint/lib/rules/no-shadow.js new file mode 100644 index 0000000000..4906fa58f2 --- /dev/null +++ b/tools/eslint/lib/rules/no-shadow.js @@ -0,0 +1,72 @@ +/** + * @fileoverview Rule to flag on declaring variables already declared in the outer scope + * @author Ilya Volodin + * @copyright 2013 Ilya Volodin. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + /** + * Checks if a variable is contained in the list of given scope variables. + * @param {Object} variable The variable to check. + * @param {Array} scopeVars The scope variables to look for. + * @returns {boolean} Whether or not the variable is contains in the list of scope variables. + */ + function isContainedInScopeVars(variable, scopeVars) { + return scopeVars.some(function (scopeVar) { + if (scopeVar.identifiers.length > 0) { + return variable.name === scopeVar.name; + } + return false; + }); + } + + /** + * Checks if the given variables are shadowed in the given scope. + * @param {Array} variables The variables to look for + * @param {Object} scope The scope to be checked. + * @returns {void} + */ + function checkShadowsInScope(variables, scope) { + variables.forEach(function (variable) { + if (isContainedInScopeVars(variable, scope.variables) && + // "arguments" is a special case that has no identifiers (#1759) + variable.identifiers.length > 0 && + + // function names are exempt + variable.defs.length && variable.defs[0].type !== "FunctionName" + ) { + context.report(variable.identifiers[0], "{{a}} is already declared in the upper scope.", {a: variable.name}); + } + }); + } + + /** + * Checks the current context for shadowed variables. + * @returns {void} + */ + function checkForShadows() { + var scope = context.getScope(), + variables = scope.variables; + + // iterate through the array of variables and find duplicates with the upper scope + var upper = scope.upper; + while (upper) { + checkShadowsInScope(variables, upper); + upper = upper.upper; + } + } + + return { + "FunctionDeclaration": checkForShadows, + "FunctionExpression": checkForShadows, + "ArrowFunctionExpression": checkForShadows + }; + +}; diff --git a/tools/eslint/lib/rules/no-space-before-semi.js b/tools/eslint/lib/rules/no-space-before-semi.js new file mode 100644 index 0000000000..33926741e0 --- /dev/null +++ b/tools/eslint/lib/rules/no-space-before-semi.js @@ -0,0 +1,96 @@ +/** + * @fileoverview Rule to disallow whitespace before the semicolon + * @author Jonathan Kingston + * @copyright 2015 Mathias Schreck + * @copyright 2014 Jonathan Kingston + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + /** + * Determines whether two adjacent tokens are have whitespace between them. + * @param {Object} left - The left token object. + * @param {Object} right - The right token object. + * @returns {boolean} Whether or not there is space between the tokens. + */ + function isSpaced(left, right) { + return left.range[1] < right.range[0]; + } + + /** + * Checks whether two tokens are on the same line. + * @param {Object} left The leftmost token. + * @param {Object} right The rightmost token. + * @returns {boolean} True if the tokens are on the same line, false if not. + * @private + */ + function isSameLine(left, right) { + return left.loc.end.line === right.loc.start.line; + } + + /** + * Checks if a given token has leading whitespace. + * @param {Object} token The token to check. + * @returns {boolean} True if the given token has leading space, false if not. + */ + function hasLeadingSpace(token) { + var tokenBefore = context.getTokenBefore(token); + return isSameLine(tokenBefore, token) && isSpaced(tokenBefore, token); + } + + /** + * Checks if the given token is a semicolon. + * @param {Token} token The token to check. + * @returns {boolean} Whether or not the given token is a semicolon. + */ + function isSemicolon(token) { + return token.type === "Punctuator" && token.value === ";"; + } + + /** + * Reports if the given token has leading space. + * @param {Token} token The semicolon token to check. + * @param {ASTNode} node The corresponding node of the token. + * @returns {void} + */ + function checkSemiTokenForLeadingSpace(token, node) { + if (isSemicolon(token) && hasLeadingSpace(token)) { + context.report(node, token.loc.start, "Unexpected whitespace before semicolon."); + } + } + + /** + * Checks leading space before the semicolon with the assumption that the last token is the semicolon. + * @param {ASTNode} node The node to check. + * @returns {void} + */ + function checkNode(node) { + var token = context.getLastToken(node); + checkSemiTokenForLeadingSpace(token, node); + } + + return { + "VariableDeclaration": checkNode, + "ExpressionStatement": checkNode, + "BreakStatement": checkNode, + "ContinueStatement": checkNode, + "DebuggerStatement": checkNode, + "ReturnStatement": checkNode, + "ThrowStatement": checkNode, + "ForStatement": function (node) { + if (node.init) { + checkSemiTokenForLeadingSpace(context.getTokenAfter(node.init), node); + } + + if (node.test) { + checkSemiTokenForLeadingSpace(context.getTokenAfter(node.test), node); + } + } + }; +}; diff --git a/tools/eslint/lib/rules/no-spaced-func.js b/tools/eslint/lib/rules/no-spaced-func.js new file mode 100644 index 0000000000..7fb9fc2864 --- /dev/null +++ b/tools/eslint/lib/rules/no-spaced-func.js @@ -0,0 +1,35 @@ +/** + * @fileoverview Rule to check that spaced function application + * @author Matt DuVall <http://www.mattduvall.com> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + function detectOpenSpaces(node) { + var lastCalleeToken = context.getLastToken(node.callee); + var tokens = context.getTokens(node); + var i = tokens.indexOf(lastCalleeToken), l = tokens.length; + while (i < l && tokens[i].value !== "(") { + ++i; + } + if (i >= l) { + return; + } + // look for a space between the callee and the open paren + if (tokens[i - 1].range[1] !== tokens[i].range[0]) { + context.report(node, "Unexpected space between function name and paren."); + } + } + + return { + "CallExpression": detectOpenSpaces, + "NewExpression": detectOpenSpaces + }; + +}; diff --git a/tools/eslint/lib/rules/no-sparse-arrays.js b/tools/eslint/lib/rules/no-sparse-arrays.js new file mode 100644 index 0000000000..143097713f --- /dev/null +++ b/tools/eslint/lib/rules/no-sparse-arrays.js @@ -0,0 +1,31 @@ +/** + * @fileoverview Disallow sparse arrays + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + "ArrayExpression": function(node) { + + var emptySpot = node.elements.indexOf(null) > -1; + + if (emptySpot) { + context.report(node, "Unexpected comma in middle of array."); + } + } + + }; + +}; diff --git a/tools/eslint/lib/rules/no-sync.js b/tools/eslint/lib/rules/no-sync.js new file mode 100644 index 0000000000..1a9d27d637 --- /dev/null +++ b/tools/eslint/lib/rules/no-sync.js @@ -0,0 +1,28 @@ +/** + * @fileoverview Rule to check for properties whose identifier ends with the string Sync + * @author Matt DuVall<http://mattduvall.com/> + */ + +/*jshint node:true*/ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "MemberExpression": function(node) { + var propertyName = node.property.name, + syncRegex = /.*Sync$/; + + if (syncRegex.exec(propertyName) !== null) { + context.report(node, "Unexpected sync method: '" + propertyName + "'."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-ternary.js b/tools/eslint/lib/rules/no-ternary.js new file mode 100644 index 0000000000..6ae8e9a272 --- /dev/null +++ b/tools/eslint/lib/rules/no-ternary.js @@ -0,0 +1,22 @@ +/** + * @fileoverview Rule to flag use of ternary operators. + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "ConditionalExpression": function(node) { + context.report(node, "Ternary operator used."); + } + + }; + +}; diff --git a/tools/eslint/lib/rules/no-throw-literal.js b/tools/eslint/lib/rules/no-throw-literal.js new file mode 100644 index 0000000000..d02e7786b6 --- /dev/null +++ b/tools/eslint/lib/rules/no-throw-literal.js @@ -0,0 +1,31 @@ +/** + * @fileoverview Rule to restrict what can be thrown as an exception. + * @author Dieter Oberkofler + * @copyright 2015 Dieter Oberkofler. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "ThrowStatement": function(node) { + + if (node.argument.type === "Literal") { + context.report(node, "Do not throw a literal."); + } else if (node.argument.type === "Identifier") { + if (node.argument.name === "undefined") { + context.report(node, "Do not throw undefined."); + } + } + + } + + }; + +}; diff --git a/tools/eslint/lib/rules/no-trailing-spaces.js b/tools/eslint/lib/rules/no-trailing-spaces.js new file mode 100644 index 0000000000..d299eb5151 --- /dev/null +++ b/tools/eslint/lib/rules/no-trailing-spaces.js @@ -0,0 +1,46 @@ +/** + * @fileoverview Disallow trailing spaces at the end of lines. + * @author Nodeca Team <https://github.com/nodeca> + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var TRAILER = "[ \t\u00a0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000]+$"; + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + "Program": function checkTrailingSpaces(node) { + + // Let's hack. Since Esprima does not return whitespace nodes, + // fetch the source code and do black magic via regexps. + + var src = context.getSource(), + re = new RegExp(TRAILER, "mg"), + match, lines, location; + + while ((match = re.exec(src)) !== null) { + lines = src.slice(0, re.lastIndex).split(/\r?\n/g); + + location = { + line: lines.length, + column: lines[lines.length - 1].length - match[0].length + 1 + }; + + // Passing node is a bit dirty, because message data will contain + // big text in `source`. But... who cares :) ? + // One more kludge will not make worse the bloody wizardry of this plugin. + context.report(node, location, "Trailing spaces not allowed."); + } + } + + }; +}; diff --git a/tools/eslint/lib/rules/no-undef-init.js b/tools/eslint/lib/rules/no-undef-init.js new file mode 100644 index 0000000000..60ad5700f8 --- /dev/null +++ b/tools/eslint/lib/rules/no-undef-init.js @@ -0,0 +1,26 @@ +/** + * @fileoverview Rule to flag when initializing to undefined + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "VariableDeclarator": function(node) { + var name = node.id.name; + var init = node.init && node.init.name; + + if (init === "undefined") { + context.report(node, "It's not necessary to initialize '{{name}}' to undefined.", { name: name }); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-undef.js b/tools/eslint/lib/rules/no-undef.js new file mode 100644 index 0000000000..5ee07f3124 --- /dev/null +++ b/tools/eslint/lib/rules/no-undef.js @@ -0,0 +1,90 @@ +/** + * @fileoverview Rule to flag references to undeclared variables. + * @author Mark Macdonald + * @copyright 2015 Nicholas C. Zakas. All rights reserved. + * @copyright 2013 Mark Macdonald. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +// none! + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +function isImplicitGlobal(variable) { + return variable.defs.every(function(def) { + return def.type === "ImplicitGlobalVariable"; + }); +} + +/** + * Gets the declared variable, defined in `scope`, that `ref` refers to. + * @param {Scope} scope The scope in which to search. + * @param {Reference} ref The reference to find in the scope. + * @returns {Variable} The variable, or null if ref refers to an undeclared variable. + */ +function getDeclaredGlobalVariable(scope, ref) { + var declaredGlobal = null; + scope.variables.some(function(variable) { + if (variable.name === ref.identifier.name) { + // If it's an implicit global, it must have a `writeable` field (indicating it was declared) + if (!isImplicitGlobal(variable) || {}.hasOwnProperty.call(variable, "writeable")) { + declaredGlobal = variable; + return true; + } + } + return false; + }); + return declaredGlobal; +} + +/** + * Checks if the given node is the argument of a typeof operator. + * @param {ASTNode} node The AST node being checked. + * @returns {boolean} Whether or not the node is the argument of a typeof operator. + */ +function hasTypeOfOperator(node) { + var parent = node.parent; + return parent.type === "UnaryExpression" && parent.operator === "typeof"; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var NOT_DEFINED_MESSAGE = "\"{{name}}\" is not defined.", + READ_ONLY_MESSAGE = "\"{{name}}\" is read only."; + + return { + + "Program:exit": function(/*node*/) { + + var globalScope = context.getScope(); + + globalScope.through.forEach(function(ref) { + var variable = getDeclaredGlobalVariable(globalScope, ref), + name = ref.identifier.name; + + if (hasTypeOfOperator(ref.identifier)) { + return; + } + + if (!variable) { + context.report(ref.identifier, NOT_DEFINED_MESSAGE, { name: name }); + } else if (ref.isWrite() && variable.writeable === false) { + context.report(ref.identifier, READ_ONLY_MESSAGE, { name: name }); + } + }); + + } + + }; + +}; diff --git a/tools/eslint/lib/rules/no-undefined.js b/tools/eslint/lib/rules/no-undefined.js new file mode 100644 index 0000000000..4d96eb3e92 --- /dev/null +++ b/tools/eslint/lib/rules/no-undefined.js @@ -0,0 +1,25 @@ +/** + * @fileoverview Rule to flag references to the undefined variable. + * @author Michael Ficarra + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "Identifier": function(node) { + if (node.name === "undefined") { + var parent = context.getAncestors().pop(); + if (!parent || parent.type !== "MemberExpression" || node !== parent.property || parent.computed) { + context.report(node, "Unexpected use of undefined."); + } + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-underscore-dangle.js b/tools/eslint/lib/rules/no-underscore-dangle.js new file mode 100644 index 0000000000..354c1e4cda --- /dev/null +++ b/tools/eslint/lib/rules/no-underscore-dangle.js @@ -0,0 +1,71 @@ +/** + * @fileoverview Rule to flag trailing underscores in variable declarations. + * @author Matt DuVall <http://www.mattduvall.com> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + //------------------------------------------------------------------------- + // Helpers + //------------------------------------------------------------------------- + + function hasTrailingUnderscore(identifier) { + var len = identifier.length; + + return identifier !== "_" && (identifier[0] === "_" || identifier[len - 1] === "_"); + } + + function isSpecialCaseIdentifierForMemberExpression(identifier) { + return identifier === "__proto__"; + } + + function isSpecialCaseIdentifierInVariableExpression(identifier) { + // Checks for the underscore library usage here + return identifier === "_"; + } + + function checkForTrailingUnderscoreInFunctionDeclaration(node) { + if (node.id) { + var identifier = node.id.name; + + if (typeof identifier !== "undefined" && hasTrailingUnderscore(identifier)) { + context.report(node, "Unexpected dangling \"_\" in \"" + identifier + "\"."); + } + } + } + + function checkForTrailingUnderscoreInVariableExpression(node) { + var identifier = node.id.name; + + if (typeof identifier !== "undefined" && hasTrailingUnderscore(identifier) && + !isSpecialCaseIdentifierInVariableExpression(identifier)) { + context.report(node, "Unexpected dangling \"_\" in \"" + identifier + "\"."); + } + } + + function checkForTrailingUnderscoreInMemberExpression(node) { + var identifier = node.property.name; + + if (typeof identifier !== "undefined" && hasTrailingUnderscore(identifier) && + !isSpecialCaseIdentifierForMemberExpression(identifier)) { + context.report(node, "Unexpected dangling \"_\" in \"" + identifier + "\"."); + } + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + "FunctionDeclaration": checkForTrailingUnderscoreInFunctionDeclaration, + "VariableDeclarator": checkForTrailingUnderscoreInVariableExpression, + "MemberExpression": checkForTrailingUnderscoreInMemberExpression + }; + +}; diff --git a/tools/eslint/lib/rules/no-unreachable.js b/tools/eslint/lib/rules/no-unreachable.js new file mode 100644 index 0000000000..6606b6adae --- /dev/null +++ b/tools/eslint/lib/rules/no-unreachable.js @@ -0,0 +1,96 @@ +/** + * @fileoverview Checks for unreachable code due to return, throws, break, and continue. + * @author Joel Feenstra + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + + +function report(context, node, unreachableType) { + var keyword; + switch (unreachableType) { + case "BreakStatement": + keyword = "break"; + break; + case "ContinueStatement": + keyword = "continue"; + break; + case "ReturnStatement": + keyword = "return"; + break; + case "ThrowStatement": + keyword = "throw"; + break; + default: + return; + } + context.report(node, "Found unexpected statement after a {{type}}.", { type: keyword }); +} + + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + /** + * Checks if a node is an exception for no-unreachable because of variable/function hoisting + * @param {ASTNode} node The AST node to check. + * @returns {boolean} if the node doesn't trigger unreachable + * @private + */ + function isUnreachableAllowed(node) { + return node.type === "FunctionDeclaration" || + node.type === "VariableDeclaration" && + node.declarations.every(function(declaration) { + return declaration.type === "VariableDeclarator" && declaration.init === null; + }); + } + + /* + * Verifies that the given node is the last node or followed exclusively by + * hoisted declarations + * @param {ASTNode} node Node that should be the last node + * @returns {void} + * @private + */ + function checkNode(node) { + var parent = context.getAncestors().pop(); + var field, i, sibling; + + switch (parent.type) { + case "SwitchCase": + field = "consequent"; + break; + case "Program": + case "BlockStatement": + field = "body"; + break; + default: + return; + } + + for (i = parent[field].length - 1; i >= 0; i--) { + sibling = parent[field][i]; + if (sibling === node) { + return; // Found the last reachable statement, all done + } + + if (!isUnreachableAllowed(sibling)) { + report(context, sibling, node.type); + } + } + } + + return { + "ReturnStatement": checkNode, + "ThrowStatement": checkNode, + "ContinueStatement": checkNode, + "BreakStatement": checkNode + }; + +}; diff --git a/tools/eslint/lib/rules/no-unused-expressions.js b/tools/eslint/lib/rules/no-unused-expressions.js new file mode 100644 index 0000000000..7036f90e7c --- /dev/null +++ b/tools/eslint/lib/rules/no-unused-expressions.js @@ -0,0 +1,74 @@ +/** + * @fileoverview Flag expressions in statement position that do not side effect + * @author Michael Ficarra + * @copyright 2013 Michael Ficarra. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + /** + * @param {ASTNode} node - any node + * @returns {Boolean} whether the given node structurally represents a directive + */ + function looksLikeDirective(node) { + return node.type === "ExpressionStatement" && + node.expression.type === "Literal" && typeof node.expression.value === "string"; + } + + /** + * @param {Function} predicate - ([a] -> Boolean) the function used to make the determination + * @param {a[]} list - the input list + * @returns {a[]} the leading sequence of members in the given list that pass the given predicate + */ + function takeWhile(predicate, list) { + for (var i = 0, l = list.length; i < l; ++i) { + if (!predicate(list[i])) { + break; + } + } + return [].slice.call(list, 0, i); + } + + /** + * @param {ASTNode} node - a Program or BlockStatement node + * @returns {ASTNode[]} the leading sequence of directive nodes in the given node's body + */ + function directives(node) { + return takeWhile(looksLikeDirective, node.body); + } + + /** + * @param {ASTNode} node - any node + * @param {ASTNode[]} ancestors - the given node's ancestors + * @returns {Boolean} whether the given node is considered a directive in its current position + */ + function isDirective(node, ancestors) { + var parent = ancestors[ancestors.length - 1], + grandparent = ancestors[ancestors.length - 2]; + return (parent.type === "Program" || parent.type === "BlockStatement" && + (/Function/.test(grandparent.type))) && + directives(parent).indexOf(node) >= 0; + } + + return { + "ExpressionStatement": function(node) { + + var type = node.expression.type, + ancestors = context.getAncestors(); + + if ( + !/^(?:Assignment|Call|New|Update|Yield)Expression$/.test(type) && + (type !== "UnaryExpression" || ["delete", "void"].indexOf(node.expression.operator) < 0) && + !isDirective(node, ancestors) + ) { + context.report(node, "Expected an assignment or function call and instead saw an expression."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-unused-vars.js b/tools/eslint/lib/rules/no-unused-vars.js new file mode 100644 index 0000000000..d6ec43bf1d --- /dev/null +++ b/tools/eslint/lib/rules/no-unused-vars.js @@ -0,0 +1,180 @@ +/** + * @fileoverview Rule to flag declared but unused variables + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var MESSAGE = "{{name}} is defined but never used"; + + var config = { + vars: "all", + args: "after-used" + }; + + if (context.options[0]) { + if (typeof context.options[0] === "string") { + config.vars = context.options[0]; + } else { + config.vars = context.options[0].vars || config.vars; + config.args = context.options[0].args || config.args; + } + } + + /** + * Determines if a given variable is being exported from a module. + * @param {Variable} variable EScope variable object. + * @returns {boolean} True if the variable is exported, false if not. + * @private + */ + function isExported(variable) { + + var definition = variable.defs[0]; + + if (definition) { + + definition = definition.node; + if (definition.type === "VariableDeclarator") { + definition = definition.parent; + } + + return definition.parent.type.indexOf("Export") === 0; + } else { + return false; + } + } + + /** + * Determines if a reference is a read operation. + * @param {Reference} ref - an escope Reference + * @returns {Boolean} whether the given reference represents a read operation + * @private + */ + function isReadRef(ref) { + return ref.isRead(); + } + + /** + * Determine if an identifier is referencing the enclosing function name. + * @param {Reference} ref The reference to check. + * @returns {boolean} True if it's a self-reference, false if not. + * @private + */ + function isSelfReference(ref) { + + if (ref.from.type === "function" && ref.from.block.id) { + return ref.identifier.name === ref.from.block.id.name; + } + + return false; + } + + /** + * Determines if a reference should be counted as a read. A reference should + * be counted only if it's a read and it's not a reference to the containing + * function declaration name. + * @param {Reference} ref The reference to check. + * @returns {boolean} True if it's a value read reference, false if not. + * @private + */ + function isValidReadRef(ref) { + return isReadRef(ref) && !isSelfReference(ref); + } + + /** + * Gets an array of local variables without read references. + * @param {Scope} scope - an escope Scope object + * @returns {Variable[]} most of the local variables with no read references + * @private + */ + function getUnusedLocals(scope) { + var unused = []; + var variables = scope.variables; + + if (scope.type !== "global" && scope.type !== "TDZ") { + for (var i = 0, l = variables.length; i < l; ++i) { + + // skip function expression names + if (scope.functionExpressionScope || variables[i].eslintUsed) { + continue; + } + // skip implicit "arguments" variable + if (scope.type === "function" && variables[i].name === "arguments" && variables[i].identifiers.length === 0) { + continue; + } + + var def = variables[i].defs[0], + type = def.type; + + // skip catch variables + if (type === "CatchClause") { + continue; + } + + // skip any setter argument + if (type === "Parameter" && def.node.parent.type === "Property" && def.node.parent.kind === "set") { + continue; + } + + // if "args" option is "none", skip any parameter + if (config.args === "none" && type === "Parameter") { + continue; + } + + // if "args" option is "after-used", skip all but the last parameter + if (config.args === "after-used" && type === "Parameter" && variables[i].defs[0].index < variables[i].defs[0].node.params.length - 1) { + continue; + } + + if (variables[i].references.filter(isValidReadRef).length === 0 && !isExported(variables[i])) { + unused.push(variables[i]); + } + } + } + + return [].concat.apply(unused, scope.childScopes.map(getUnusedLocals)); + } + + return { + "Program:exit": function(programNode) { + var globalScope = context.getScope(); + var unused = getUnusedLocals(globalScope); + var i, l; + + // determine unused globals + if (config.vars === "all") { + var unresolvedRefs = globalScope.through.filter(isValidReadRef).map(function(ref) { + return ref.identifier.name; + }); + + for (i = 0, l = globalScope.variables.length; i < l; ++i) { + if (unresolvedRefs.indexOf(globalScope.variables[i].name) < 0 && + !globalScope.variables[i].eslintUsed && !isExported(globalScope.variables[i])) { + unused.push(globalScope.variables[i]); + } + } + } + + for (i = 0, l = unused.length; i < l; ++i) { + if (unused[i].eslintExplicitGlobal) { + context.report(programNode, MESSAGE, unused[i]); + } else if (unused[i].defs.length > 0) { + + // TODO: Remove when https://github.com/estools/escope/issues/49 is resolved + if (unused[i].defs[0].type === "ClassName") { + continue; + } + + context.report(unused[i].identifiers[0], MESSAGE, unused[i]); + } + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-use-before-define.js b/tools/eslint/lib/rules/no-use-before-define.js new file mode 100644 index 0000000000..3f175dc78b --- /dev/null +++ b/tools/eslint/lib/rules/no-use-before-define.js @@ -0,0 +1,67 @@ +/** + * @fileoverview Rule to flag use of variables before they are defined + * @author Ilya Volodin + * @copyright 2013 Ilya Volodin. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Constants +//------------------------------------------------------------------------------ + +var NO_FUNC = "nofunc"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + function findDeclaration(name, scope) { + // try searching in the current scope first + for (var i = 0, l = scope.variables.length; i < l; i++) { + if (scope.variables[i].name === name) { + return scope.variables[i]; + } + } + // check if there's upper scope and call recursivly till we find the variable + if (scope.upper) { + return findDeclaration(name, scope.upper); + } + } + + function findVariables() { + var scope = context.getScope(); + var typeOption = context.options[0]; + + function checkLocationAndReport(reference, declaration) { + if (typeOption !== NO_FUNC || declaration.defs[0].type !== "FunctionName") { + if (declaration.identifiers[0].range[1] > reference.identifier.range[1]) { + context.report(reference.identifier, "{{a}} was used before it was defined", {a: reference.identifier.name}); + } + } + } + + scope.references.forEach(function(reference) { + // if the reference is resolved check for declaration location + // if not, it could be function invocation, try to find manually + if (reference.resolved && reference.resolved.identifiers.length > 0) { + checkLocationAndReport(reference, reference.resolved); + } else { + var declaration = findDeclaration(reference.identifier.name, scope); + // if there're no identifiers, this is a global environment variable + if (declaration && declaration.identifiers.length !== 0) { + checkLocationAndReport(reference, declaration); + } + } + }); + } + + return { + "Program": findVariables, + "FunctionExpression": findVariables, + "FunctionDeclaration": findVariables, + "ArrowFunctionExpression": findVariables + }; +}; diff --git a/tools/eslint/lib/rules/no-var.js b/tools/eslint/lib/rules/no-var.js new file mode 100644 index 0000000000..6b47b3dab0 --- /dev/null +++ b/tools/eslint/lib/rules/no-var.js @@ -0,0 +1,24 @@ +/** + * @fileoverview Rule to check for the usage of var. + * @author Jamund Ferguson + * @copyright 2014 Jamund Ferguson. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + "VariableDeclaration": function (node) { + if (node.kind === "var") { + context.report(node, "Unexpected var, use let or const instead."); + } + } + + }; + +}; diff --git a/tools/eslint/lib/rules/no-void.js b/tools/eslint/lib/rules/no-void.js new file mode 100644 index 0000000000..11c54af7fc --- /dev/null +++ b/tools/eslint/lib/rules/no-void.js @@ -0,0 +1,26 @@ +/** + * @fileoverview Rule to disallow use of void operator. + * @author Mike Sidorov + * @copyright 2014 Mike Sidorov. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + "UnaryExpression": function(node) { + if (node.operator === "void") { + context.report(node, "Expected 'undefined' and instead saw 'void'."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-warning-comments.js b/tools/eslint/lib/rules/no-warning-comments.js new file mode 100644 index 0000000000..58b6764ff1 --- /dev/null +++ b/tools/eslint/lib/rules/no-warning-comments.js @@ -0,0 +1,84 @@ +/** + * @fileoverview Rule that warns about used warning comments + * @author Alexander Schmidt <https://github.com/lxanders> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function (context) { + + var configuration = context.options[0] || {}, + warningTerms = configuration.terms || ["todo", "fixme", "xxx"], + location = configuration.location || "start", + warningRegExps; + + /** + * Convert a warning term into a RegExp which will match a comment containing that whole word in the specified + * location ("start" or "anywhere"). If the term starts or ends with non word characters, then the match will not + * require word boundaries on that side. + * + * @param {String} term A term to convert to a RegExp + * @returns {RegExp} The term converted to a RegExp + */ + function convertToRegExp(term) { + var escaped = term.replace(/[-\/\\$\^*+?.()|\[\]{}]/g, "\\$&"), + // If the term ends in a word character (a-z0-9_), ensure a word boundary at the end, so that substrings do + // not get falsely matched. eg "todo" in a string such as "mastodon". + // If the term ends in a non-word character, then \b won't match on the boundary to the next non-word + // character, which would likely be a space. For example `/\bFIX!\b/.test('FIX! blah') === false`. + // In these cases, use no bounding match. Same applies for the prefix, handled below. + suffix = /\w$/.test(term) ? "\\b" : "", + prefix; + + if (location === "start") { + // When matching at the start, ignore leading whitespace, and there's no need to worry about word boundaries + prefix = "^\\s*"; + } else if (/^\w/.test(term)) { + prefix = "\\b"; + } else { + prefix = ""; + } + + return new RegExp(prefix + escaped + suffix, "i"); + } + + /** + * Checks the specified comment for matches of the configured warning terms and returns the matches. + * @param {String} comment The comment which is checked. + * @returns {Array} All matched warning terms for this comment. + */ + function commentContainsWarningTerm(comment) { + var matches = []; + + warningRegExps.forEach(function (regex, index) { + if (regex.test(comment)) { + matches.push(warningTerms[index]); + } + }); + + return matches; + } + + /** + * Checks the specified node for matching warning comments and reports them. + * @param {ASTNode} node The AST node being checked. + * @returns {void} undefined. + */ + function checkComment(node) { + var matches = commentContainsWarningTerm(node.value); + + matches.forEach(function (matchedTerm) { + context.report(node, "Unexpected " + matchedTerm + " comment."); + }); + } + + warningRegExps = warningTerms.map(convertToRegExp); + return { + "BlockComment": checkComment, + "LineComment": checkComment + }; +}; diff --git a/tools/eslint/lib/rules/no-with.js b/tools/eslint/lib/rules/no-with.js new file mode 100644 index 0000000000..1b889dc5f4 --- /dev/null +++ b/tools/eslint/lib/rules/no-with.js @@ -0,0 +1,20 @@ +/** + * @fileoverview Rule to flag use of with statement + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + "WithStatement": function(node) { + context.report(node, "Unexpected use of 'with' statement."); + } + }; + +}; diff --git a/tools/eslint/lib/rules/no-wrap-func.js b/tools/eslint/lib/rules/no-wrap-func.js new file mode 100644 index 0000000000..a99c09ee38 --- /dev/null +++ b/tools/eslint/lib/rules/no-wrap-func.js @@ -0,0 +1,44 @@ +/** + * @fileoverview Rule to flag wrapping non-iife in parens + * @author Ilya Volodin + * @copyright 2013 Ilya Volodin. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + /** + * Checks a function expression to see if its surrounded by parens. + * @param {ASTNode} node The node to check. + * @returns {void} + * @private + */ + function checkFunction(node) { + var previousToken, nextToken; + + if (node.type === "ArrowFunctionExpression" && + /(?:Call|New|Logical|Binary|Conditional|Update)Expression/.test(node.parent.type) + ) { + return; + } + + if (!/CallExpression|NewExpression/.test(node.parent.type)) { + previousToken = context.getTokenBefore(node); + nextToken = context.getTokenAfter(node); + if (previousToken.value === "(" && nextToken.value === ")") { + context.report(node, "Wrapping non-IIFE function literals in parens is unnecessary."); + } + } + } + + return { + "ArrowFunctionExpression": checkFunction, + "FunctionExpression": checkFunction + }; + +}; diff --git a/tools/eslint/lib/rules/object-shorthand.js b/tools/eslint/lib/rules/object-shorthand.js new file mode 100644 index 0000000000..548c9245d6 --- /dev/null +++ b/tools/eslint/lib/rules/object-shorthand.js @@ -0,0 +1,63 @@ +/** + * @fileoverview Rule to enforce concise object methods and properties. + * @author Jamund Ferguson + * @copyright 2015 Jamund Ferguson. All rights reserved. + */ + +"use strict"; + +var OPTIONS = { + always: "always", + never: "never", + methods: "methods", + properties: "properties" +}; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var APPLY = context.options[0] || OPTIONS.always; + var APPLY_TO_METHODS = APPLY === OPTIONS.methods || APPLY === OPTIONS.always; + var APPLY_TO_PROPS = APPLY === OPTIONS.properties || APPLY === OPTIONS.always; + var APPLY_NEVER = APPLY === OPTIONS.never; + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + "Property": function(node) { + var isConciseProperty = node.method || node.shorthand, + type; + + // if we're "never" and concise we should warn now + if (APPLY_NEVER && isConciseProperty) { + type = node.method ? "method" : "property"; + context.report(node, "Expected longform " + type + " syntax."); + } + + // at this point if we're concise or if we're "never" we can leave + if (APPLY_NEVER || isConciseProperty) { + return; + } + + if (node.value.type === "ArrowFunctionExpression" && APPLY_TO_METHODS) { + + // {x: ()=>{}} should be written as {x() {}} + context.report(node, "Expected method shorthand."); + } else if (node.value.type === "FunctionExpression" && APPLY_TO_METHODS) { + + // {x: function(){}} should be written as {x() {}} + context.report(node, "Expected method shorthand."); + } else if (node.key.name === node.value.name && APPLY_TO_PROPS) { + + // {x: x} should be written as {x} + context.report(node, "Expected property shorthand."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/one-var.js b/tools/eslint/lib/rules/one-var.js new file mode 100644 index 0000000000..fd28522f0f --- /dev/null +++ b/tools/eslint/lib/rules/one-var.js @@ -0,0 +1,175 @@ +/** + * @fileoverview A rule to ensure the use of a single variable declaration. + * @author Ian Christian Myers + * @copyright 2015 Joey Baker. All rights reserved. + * @copyright 2015 Danny Fritz. All rights reserved. + * @copyright 2013 Ian Christian Myers. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var MODE = context.options[0] || "always"; + var options = {}; + + // simple options configuration with just a string or no option + if (typeof context.options[0] === "string" || context.options[0] == null) { + options.var = MODE; + options.let = MODE; + options.const = MODE; + } else { + options = context.options[0]; + } + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + var functionStack = []; + var blockStack = []; + + /** + * Increments the blockStack counter. + * @returns {void} + * @private + */ + function startBlock() { + blockStack.push({let: false, const: false}); + } + + /** + * Increments the functionStack counter. + * @returns {void} + * @private + */ + function startFunction() { + functionStack.push(false); + startBlock(); + } + + /** + * Decrements the blockStack counter. + * @returns {void} + * @private + */ + function endBlock() { + blockStack.pop(); + } + + /** + * Decrements the functionStack counter. + * @returns {void} + * @private + */ + function endFunction() { + functionStack.pop(); + endBlock(); + } + + /** + * Determines if there is more than one var statement in the current scope. + * @returns {boolean} Returns true if it is the first var declaration, false if not. + * @private + */ + function hasOnlyOneVar() { + if (functionStack[functionStack.length - 1]) { + return true; + } else { + functionStack[functionStack.length - 1] = true; + return false; + } + } + + /** + * Determines if there is more than one let statement in the current scope. + * @returns {boolean} Returns true if it is the first let declaration, false if not. + * @private + */ + function hasOnlyOneLet() { + if (blockStack[blockStack.length - 1].let) { + return true; + } else { + blockStack[blockStack.length - 1].let = true; + return false; + } + } + + /** + * Determines if there is more than one const statement in the current scope. + * @returns {boolean} Returns true if it is the first const declaration, false if not. + * @private + */ + function hasOnlyOneConst() { + if (blockStack[blockStack.length - 1].const) { + return true; + } else { + blockStack[blockStack.length - 1].const = true; + return false; + } + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + "Program": startFunction, + "FunctionDeclaration": startFunction, + "FunctionExpression": startFunction, + "ArrowFunctionExpression": startFunction, + "BlockStatement": startBlock, + "ForStatement": startBlock, + "SwitchStatement": startBlock, + + "VariableDeclaration": function(node) { + var declarationCount = node.declarations.length; + + if (node.kind === "var") { + if (options.var === "never") { + if (declarationCount > 1) { + context.report(node, "Split 'var' declaration into multiple statements."); + } + } else { + if (hasOnlyOneVar()) { + context.report(node, "Combine this with the previous 'var' statement."); + } + } + } else if (node.kind === "let") { + if (options.let === "never") { + if (declarationCount > 1) { + context.report(node, "Split 'let' declaration into multiple statements."); + } + } else { + if (hasOnlyOneLet()) { + context.report(node, "Combine this with the previous 'let' statement."); + } + } + } else if (node.kind === "const") { + if (options.const === "never") { + if (declarationCount > 1) { + context.report(node, "Split 'const' declaration into multiple statements."); + } + } else { + if (hasOnlyOneConst()) { + context.report(node, "Combine this with the previous 'const' statement."); + } + } + + } + }, + + "ForStatement:exit": endBlock, + "SwitchStatement:exit": endBlock, + "BlockStatement:exit": endBlock, + "Program:exit": endFunction, + "FunctionDeclaration:exit": endFunction, + "FunctionExpression:exit": endFunction, + "ArrowFunctionExpression:exit": endFunction + }; + +}; diff --git a/tools/eslint/lib/rules/operator-assignment.js b/tools/eslint/lib/rules/operator-assignment.js new file mode 100644 index 0000000000..7ebb74d2ba --- /dev/null +++ b/tools/eslint/lib/rules/operator-assignment.js @@ -0,0 +1,112 @@ +/** + * @fileoverview Rule to replace assignment expressions with operator assignment + * @author Brandon Mills + * @copyright 2014 Brandon Mills. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether an operator is commutative and has an operator assignment + * shorthand form. + * @param {string} operator Operator to check. + * @returns {boolean} True if the operator is commutative and has a + * shorthand form. + */ +function isCommutativeOperatorWithShorthand(operator) { + return ["*", "&", "^", "|"].indexOf(operator) >= 0; +} + +/** + * Checks whether an operator is not commuatative and has an operator assignment + * shorthand form. + * @param {string} operator Operator to check. + * @returns {boolean} True if the operator is not commuatative and has + * a shorthand form. + */ +function isNonCommutativeOperatorWithShorthand(operator) { + return ["+", "-", "/", "%", "<<", ">>", ">>>"].indexOf(operator) >= 0; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +/** + * Checks whether two expressions reference the same value. For example: + * a = a + * a.b = a.b + * a[0] = a[0] + * a['b'] = a['b'] + * @param {ASTNode} a Left side of the comparison. + * @param {ASTNode} b Right side of the comparison. + * @returns {boolean} True if both sides match and reference the same value. + */ +function same(a, b) { + if (a.type !== b.type) { + return false; + } + + switch (a.type) { + case "Identifier": + return a.name === b.name; + case "Literal": + return a.value === b.value; + case "MemberExpression": + // x[0] = x[0] + // x[y] = x[y] + // x.y = x.y + return same(a.object, b.object) && same(a.property, b.property); + default: + return false; + } +} + +module.exports = function(context) { + + /** + * Ensures that an assignment uses the shorthand form where possible. + * @param {ASTNode} node An AssignmentExpression node. + * @returns {void} + */ + function verify(node) { + var expr, left, operator; + + if (node.operator !== "=" || node.right.type !== "BinaryExpression") { + return; + } + + left = node.left; + expr = node.right; + operator = expr.operator; + + if (isCommutativeOperatorWithShorthand(operator)) { + if (same(left, expr.left) || same(left, expr.right)) { + context.report(node, "Assignment can be replaced with operator assignment."); + } + } else if (isNonCommutativeOperatorWithShorthand(operator)) { + if (same(left, expr.left)) { + context.report(node, "Assignment can be replaced with operator assignment."); + } + } + } + + /** + * Warns if an assignment expression uses operator assignment shorthand. + * @param {ASTNode} node An AssignmentExpression node. + * @returns {void} + */ + function prohibit(node) { + if (node.operator !== "=") { + context.report(node, "Unexpected operator assignment shorthand."); + } + } + + return { + "AssignmentExpression": context.options[0] !== "never" ? verify : prohibit + }; + +}; diff --git a/tools/eslint/lib/rules/operator-linebreak.js b/tools/eslint/lib/rules/operator-linebreak.js new file mode 100644 index 0000000000..020c61544c --- /dev/null +++ b/tools/eslint/lib/rules/operator-linebreak.js @@ -0,0 +1,100 @@ +/** + * @fileoverview Operator linebreak - enforces operator linebreak style of two types: after and before + * @author Benoît Zugmeyer + * @copyright 2015 Benoît Zugmeyer. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var style = context.options[0] || "after"; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Checks whether two tokens are on the same line. + * @param {ASTNode} left The leftmost token. + * @param {ASTNode} right The rightmost token. + * @returns {boolean} True if the tokens are on the same line, false if not. + * @private + */ + function isSameLine(left, right) { + return left.loc.end.line === right.loc.start.line; + } + + /** + * Checks the operator placement + * @param {ASTNode} node The binary operator node to check + * @private + * @returns {void} + */ + function validateBinaryExpression(node) { + var leftToken = context.getLastToken(node.left || node.id); + var operatorToken = context.getTokenAfter(leftToken); + + // When the left part of a binary expression is a single expression wrapped in + // parentheses (ex: `(a) + b`), leftToken will be the last token of the expression + // and operatorToken will be the closing parenthesis. + // The leftToken should be the last closing parenthesis, and the operatorToken + // should be the token right after that. + while (operatorToken.value === ")") { + leftToken = operatorToken; + operatorToken = context.getTokenAfter(operatorToken); + } + + var rightToken = context.getTokenAfter(operatorToken); + var operator = operatorToken.value; + + // if single line + if (isSameLine(leftToken, operatorToken) && + isSameLine(operatorToken, rightToken)) { + + return; + + } else if (!isSameLine(leftToken, operatorToken) && + !isSameLine(operatorToken, rightToken)) { + + // lone operator + context.report(node, { + line: operatorToken.loc.end.line, + column: operatorToken.loc.end.column + }, "Bad line breaking before and after '" + operator + "'."); + + } else if (style === "before" && isSameLine(leftToken, operatorToken)) { + + context.report(node, { + line: operatorToken.loc.end.line, + column: operatorToken.loc.end.column + }, "'" + operator + "' should be placed at the beginning of the line."); + + } else if (style === "after" && isSameLine(operatorToken, rightToken)) { + + context.report(node, { + line: operatorToken.loc.end.line, + column: operatorToken.loc.end.column + }, "'" + operator + "' should be placed at the end of the line."); + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + "BinaryExpression": validateBinaryExpression, + "LogicalExpression": validateBinaryExpression, + "AssignmentExpression": validateBinaryExpression, + "VariableDeclarator": function (node) { + if (node.init) { + validateBinaryExpression(node); + } + } + }; +}; diff --git a/tools/eslint/lib/rules/padded-blocks.js b/tools/eslint/lib/rules/padded-blocks.js new file mode 100644 index 0000000000..9a5be4df0e --- /dev/null +++ b/tools/eslint/lib/rules/padded-blocks.js @@ -0,0 +1,92 @@ +/** + * @fileoverview A rule to ensure blank lines within blocks. + * @author Mathias Schreck <https://github.com/lo1tuma> + * @copyright 2014 Mathias Schreck. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function (context) { + var requirePadding = context.options[0] !== "never"; + + var ALWAYS_MESSAGE = "Block must be padded by blank lines.", + NEVER_MESSAGE = "Block must not be padded by blank lines."; + + /** + * Checks if the given non empty block node has a blank line before its first child node. + * @param {ASTNode} node The AST node of a BlockStatement. + * @returns {boolean} Whether or not the block starts with a blank line. + */ + function isBlockTopPadded(node) { + var blockStart = node.loc.start.line, + first = node.body[0], + firstLine = first.loc.start.line, + expectedFirstLine = blockStart + 2, + leadingComments = context.getComments(first).leading; + + if (leadingComments.length > 0) { + firstLine = leadingComments[0].loc.start.line; + } + + return expectedFirstLine <= firstLine; + } + + /** + * Checks if the given non empty block node has a blank line after its last child node. + * @param {ASTNode} node The AST node of a BlockStatement. + * @returns {boolean} Whether or not the block ends with a blank line. + */ + function isBlockBottomPadded(node) { + var blockEnd = node.loc.end.line, + last = node.body[node.body.length - 1], + lastLine = context.getLastToken(last).loc.end.line, + expectedLastLine = blockEnd - 2, + trailingComments = context.getComments(last).trailing; + + if (trailingComments.length > 0) { + lastLine = trailingComments[trailingComments.length - 1].loc.end.line; + } + + return lastLine <= expectedLastLine; + } + + /** + * Checks the given BlockStatement node to be padded if the block is not empty. + * @param {ASTNode} node The AST node of a BlockStatement. + * @returns {void} undefined. + */ + function checkPadding(node) { + if (node.body.length > 0) { + + var blockHasTopPadding = isBlockTopPadded(node), + blockHasBottomPadding = isBlockBottomPadded(node); + + if (requirePadding) { + if (!blockHasTopPadding) { + context.report(node, ALWAYS_MESSAGE); + } + + if (!blockHasBottomPadding) { + context.report(node, node.loc.end, ALWAYS_MESSAGE); + } + } else { + if (blockHasTopPadding) { + context.report(node, NEVER_MESSAGE); + } + + if (blockHasBottomPadding) { + context.report(node, node.loc.end, NEVER_MESSAGE); + } + } + } + } + + return { + "BlockStatement": checkPadding + }; + +}; diff --git a/tools/eslint/lib/rules/quote-props.js b/tools/eslint/lib/rules/quote-props.js new file mode 100644 index 0000000000..ccac947853 --- /dev/null +++ b/tools/eslint/lib/rules/quote-props.js @@ -0,0 +1,66 @@ +/** + * @fileoverview Rule to flag non-quoted property names in object literals. + * @author Mathias Bynens <http://mathiasbynens.be/> + * @copyright 2014 Brandon Mills. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var espree = require("espree"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var MODE = context.options[0]; + + /** + * Ensures that a property's key is quoted only when necessary + * @param {ASTNode} node Property AST node + * @returns {void} + */ + function asNeeded(node) { + var key = node.key, + tokens; + + if (key.type === "Literal" && typeof key.value === "string") { + try { + tokens = espree.tokenize(key.value); + } catch (e) { + return; + } + + if (tokens.length === 1 && + (["Identifier", "Null", "Boolean"].indexOf(tokens[0].type) >= 0 || + (tokens[0].type === "Numeric" && "" + +tokens[0].value === tokens[0].value)) + ) { + context.report(node, "Unnecessarily quoted property `{{value}}` found.", key); + } + } + } + + /** + * Ensures that a property's key is quoted + * @param {ASTNode} node Property AST node + * @returns {void} + */ + function always(node) { + var key = node.key; + + if (!node.method && !(key.type === "Literal" && typeof key.value === "string")) { + context.report(node, "Unquoted property `{{key}}` found.", { + key: key.name || key.value + }); + } + } + + return { + "Property": MODE === "as-needed" ? asNeeded : always + }; + +}; diff --git a/tools/eslint/lib/rules/quotes.js b/tools/eslint/lib/rules/quotes.js new file mode 100644 index 0000000000..2bb10ea5e4 --- /dev/null +++ b/tools/eslint/lib/rules/quotes.js @@ -0,0 +1,83 @@ +/** + * @fileoverview A rule to choose between single and double quote marks + * @author Matt DuVall <http://www.mattduvall.com/>, Brandon Payton + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Constants +//------------------------------------------------------------------------------ + +var QUOTE_SETTINGS = { + "double": { + quote: "\"", + alternateQuote: "'", + description: "doublequote" + }, + "single": { + quote: "'", + alternateQuote: "\"", + description: "singlequote" + }, + "backtick": { + quote: "`", + alternateQuote: "\"", + description: "backtick" + } +}; + +var AVOID_ESCAPE = "avoid-escape"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + /** + * Validate that a string passed in is surrounded by the specified character + * @param {string} val The text to check. + * @param {string} character The character to see if it's surrounded by. + * @returns {boolean} True if the text is surrounded by the character, false if not. + * @private + */ + function isSurroundedBy(val, character) { + return val[0] === character && val[val.length - 1] === character; + } + + /** + * Determines if a given node is part of JSX syntax. + * @param {ASTNode} node The node to check. + * @returns {boolean} True if the node is a JSX node, false if not. + * @private + */ + function isJSXElement(node) { + return node.type.indexOf("JSX") === 0; + } + + return { + + "Literal": function(node) { + var val = node.value, + rawVal = node.raw, + quoteOption = context.options[0], + settings = QUOTE_SETTINGS[quoteOption], + avoidEscape = context.options[1] === AVOID_ESCAPE, + isValid; + + if (settings && typeof val === "string") { + isValid = isJSXElement(node.parent) || isSurroundedBy(rawVal, settings.quote); + + if (!isValid && avoidEscape) { + isValid = isSurroundedBy(rawVal, settings.alternateQuote) && rawVal.indexOf(settings.quote) >= 0; + } + + if (!isValid) { + context.report(node, "Strings must use " + settings.description + "."); + } + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/radix.js b/tools/eslint/lib/rules/radix.js new file mode 100644 index 0000000000..a0c7acfb2e --- /dev/null +++ b/tools/eslint/lib/rules/radix.js @@ -0,0 +1,39 @@ +/** + * @fileoverview Rule to flag use of parseInt without a radix argument + * @author James Allardice + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + "CallExpression": function(node) { + + var radix; + + if (node.callee.name === "parseInt") { + + if (node.arguments.length < 2) { + context.report(node, "Missing radix parameter."); + } else { + + radix = node.arguments[1]; + + // don't allow non-numeric literals or undefined + if ((radix.type === "Literal" && typeof radix.value !== "number") || + (radix.type === "Identifier" && radix.name === "undefined") + ) { + context.report(node, "Invalid radix parameter."); + } + } + + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/semi-spacing.js b/tools/eslint/lib/rules/semi-spacing.js new file mode 100644 index 0000000000..05ced44593 --- /dev/null +++ b/tools/eslint/lib/rules/semi-spacing.js @@ -0,0 +1,152 @@ +/** + * @fileoverview Validates spacing before and after semicolon + * @author Mathias Schreck + * @copyright 2015 Mathias Schreck + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function (context) { + + var config = context.options[0], + requireSpaceBefore = false, + requireSpaceAfter = true; + + if (typeof config === "object") { + if (config.hasOwnProperty("before")) { + requireSpaceBefore = config.before; + } + if (config.hasOwnProperty("after")) { + requireSpaceAfter = config.after; + } + } + + /** + * Determines whether two adjacent tokens are have whitespace between them. + * @param {Object} left - The left token object. + * @param {Object} right - The right token object. + * @returns {boolean} Whether or not there is space between the tokens. + */ + function isSpaced(left, right) { + return left.range[1] < right.range[0]; + } + + /** + * Checks whether two tokens are on the same line. + * @param {Object} left The leftmost token. + * @param {Object} right The rightmost token. + * @returns {boolean} True if the tokens are on the same line, false if not. + * @private + */ + function isSameLine(left, right) { + return left.loc.end.line === right.loc.start.line; + } + + /** + * Checks if a given token has leading whitespace. + * @param {Object} token The token to check. + * @returns {boolean} True if the given token has leading space, false if not. + */ + function hasLeadingSpace(token) { + var tokenBefore = context.getTokenBefore(token); + return tokenBefore && isSameLine(tokenBefore, token) && isSpaced(tokenBefore, token); + } + + /** + * Checks if a given token has trailing whitespace. + * @param {Object} token The token to check. + * @returns {boolean} True if the given token has trailing space, false if not. + */ + function hasTrailingSpace(token) { + var tokenAfter = context.getTokenAfter(token); + return tokenAfter && isSameLine(token, tokenAfter) && isSpaced(token, tokenAfter); + } + + /** + * Checks if the given token is the last token in its line. + * @param {Token} token The token to check. + * @returns {boolean} Whether or not the token is the last in its line. + */ + function isLastTokenInCurrentLine(token) { + var tokenAfter = context.getTokenAfter(token); + return !(tokenAfter && isSameLine(token, tokenAfter)); + } + + /** + * Checks if the given token is a semicolon. + * @param {Token} token The token to check. + * @returns {boolean} Whether or not the given token is a semicolon. + */ + function isSemicolon(token) { + return token.type === "Punctuator" && token.value === ";"; + } + + /** + * Reports if the given token has invalid spacing. + * @param {Token} token The semicolon token to check. + * @param {ASTNode} node The corresponding node of the token. + * @returns {void} + */ + function checkSemicolonSpacing(token, node) { + var location; + + if (isSemicolon(token)) { + location = token.loc.start; + + if (hasLeadingSpace(token)) { + if (!requireSpaceBefore) { + context.report(node, location, "Unexpected whitespace before semicolon."); + } + } else { + if (requireSpaceBefore) { + context.report(node, location, "Missing whitespace before semicolon."); + } + } + + if (!isLastTokenInCurrentLine(token)) { + if (hasTrailingSpace(token)) { + if (!requireSpaceAfter) { + context.report(node, location, "Unexpected whitespace after semicolon."); + } + } else { + if (requireSpaceAfter) { + context.report(node, location, "Missing whitespace after semicolon."); + } + } + } + } + } + + /** + * Checks the spacing of the semicolon with the assumption that the last token is the semicolon. + * @param {ASTNode} node The node to check. + * @returns {void} + */ + function checkNode(node) { + var token = context.getLastToken(node); + checkSemicolonSpacing(token, node); + } + + return { + "VariableDeclaration": checkNode, + "ExpressionStatement": checkNode, + "BreakStatement": checkNode, + "ContinueStatement": checkNode, + "DebuggerStatement": checkNode, + "ReturnStatement": checkNode, + "ThrowStatement": checkNode, + "ForStatement": function (node) { + if (node.init) { + checkSemicolonSpacing(context.getTokenAfter(node.init), node); + } + + if (node.test) { + checkSemicolonSpacing(context.getTokenAfter(node.test), node); + } + } + }; +}; diff --git a/tools/eslint/lib/rules/semi.js b/tools/eslint/lib/rules/semi.js new file mode 100644 index 0000000000..a9f841cd73 --- /dev/null +++ b/tools/eslint/lib/rules/semi.js @@ -0,0 +1,130 @@ +/** + * @fileoverview Rule to flag missing semicolons. + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ +module.exports = function(context) { + + var OPT_OUT_PATTERN = /[\[\(\/\+\-]/; // One of [(/+- + + var always = context.options[0] !== "never"; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Reports a semicolon error with appropriate location and message. + * @param {ASTNode} node The node with an extra or missing semicolon. + * @returns {void} + */ + function report(node) { + var message = always ? "Missing semicolon." : "Extra semicolon."; + context.report(node, context.getLastToken(node).loc.end, message); + } + + /** + * Checks whether a token is a semicolon punctuator. + * @param {Token} token The token. + * @returns {boolean} True if token is a semicolon punctuator. + */ + function isSemicolon(token) { + return (token.type === "Punctuator" && token.value === ";"); + } + + /** + * Check if a semicolon is unnecessary, only true if: + * - next token is on a new line and is not one of the opt-out tokens + * - next token is a valid statement divider + * @param {Token} lastToken last token of current node. + * @returns {boolean} whether the semicolon is unnecessary. + */ + function isUnnecessarySemicolon(lastToken) { + var isDivider, isOptOutToken, lastTokenLine, nextToken, nextTokenLine; + + if (!isSemicolon(lastToken)) { + return false; + } + + nextToken = context.getTokenAfter(lastToken); + + if (!nextToken) { + return true; + } + + lastTokenLine = lastToken.loc.end.line; + nextTokenLine = nextToken.loc.start.line; + isOptOutToken = OPT_OUT_PATTERN.test(nextToken.value); + isDivider = (nextToken.value === "}" || nextToken.value === ";"); + + return (lastTokenLine !== nextTokenLine && !isOptOutToken) || isDivider; + } + + /** + * Checks a node to see if it's followed by a semicolon. + * @param {ASTNode} node The node to check. + * @returns {void} + */ + function checkForSemicolon(node) { + var lastToken = context.getLastToken(node); + + if (always) { + if (!isSemicolon(lastToken)) { + report(node); + } + } else { + if (isUnnecessarySemicolon(lastToken)) { + report(node); + } + } + } + + /** + * Checks to see if there's a semicolon after a variable declaration. + * @param {ASTNode} node The node to check. + * @returns {void} + */ + function checkForSemicolonForVariableDeclaration(node) { + var ancestors = context.getAncestors(), + parentIndex = ancestors.length - 1, + parent = ancestors[parentIndex]; + + if ((parent.type !== "ForStatement" || parent.init !== node) && + (!/^For(?:In|Of)Statement/.test(parent.type) || parent.left !== node) + ) { + checkForSemicolon(node); + } + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + + "VariableDeclaration": checkForSemicolonForVariableDeclaration, + "ExpressionStatement": checkForSemicolon, + "ReturnStatement": checkForSemicolon, + "ThrowStatement": checkForSemicolon, + "DebuggerStatement": checkForSemicolon, + "BreakStatement": checkForSemicolon, + "ContinueStatement": checkForSemicolon, + "ImportDeclaration": checkForSemicolon, + "ExportAllDeclaration": checkForSemicolon, + "ExportNamedDeclaration": function (node) { + if (!node.declaration) { + checkForSemicolon(node); + } + }, + "ExportDefaultDeclaration": function (node) { + if (!/(?:Class|Function)Declaration/.test(node.declaration.type)) { + checkForSemicolon(node); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/sort-vars.js b/tools/eslint/lib/rules/sort-vars.js new file mode 100644 index 0000000000..8d4b6a8980 --- /dev/null +++ b/tools/eslint/lib/rules/sort-vars.js @@ -0,0 +1,37 @@ +/** + * @fileoverview Rule to require sorting of variables within a single Variable Declaration block + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var configuration = context.options[0] || {}, + ignoreCase = configuration.ignoreCase || false; + + return { + "VariableDeclaration": function(node) { + node.declarations.reduce(function(memo, decl) { + var lastVariableName = memo.id.name, + currenVariableName = decl.id.name; + + if (ignoreCase) { + lastVariableName = lastVariableName.toLowerCase(); + currenVariableName = currenVariableName.toLowerCase(); + } + + if (currenVariableName < lastVariableName) { + context.report(decl, "Variables within the same declaration block should be sorted alphabetically"); + return memo; + } else { + return decl; + } + }, node.declarations[0]); + } + }; +}; diff --git a/tools/eslint/lib/rules/space-after-function-name.js b/tools/eslint/lib/rules/space-after-function-name.js new file mode 100644 index 0000000000..c477768c46 --- /dev/null +++ b/tools/eslint/lib/rules/space-after-function-name.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Rule to enforce consistent spacing after function names + * @author Roberto Vidal + * @copyright 2014 Roberto Vidal. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var requiresSpace = context.options[0] === "always"; + + /** + * Reports if the give named function node has the correct spacing after its name + * + * @param {ASTNode} node The node to which the potential problem belongs. + * @returns {void} + */ + function check(node) { + var tokens = context.getFirstTokens(node, 3), + hasSpace = tokens[1].range[1] < tokens[2].range[0]; + + if (hasSpace !== requiresSpace) { + context.report(node, "Function name \"{{name}}\" must {{not}}be followed by whitespace.", { + name: node.id.name, + not: requiresSpace ? "" : "not " + }); + } + } + + return { + "FunctionDeclaration": check, + "FunctionExpression": function (node) { + if (node.id) { + check(node); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/space-after-keywords.js b/tools/eslint/lib/rules/space-after-keywords.js new file mode 100644 index 0000000000..3ece8bb065 --- /dev/null +++ b/tools/eslint/lib/rules/space-after-keywords.js @@ -0,0 +1,76 @@ +/** + * @fileoverview Rule to enforce the number of spaces after certain keywords + * @author Nick Fisher + * @copyright 2014 Nick Fisher. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + // unless the first option is `"never"`, then a space is required + var requiresSpace = context.options[0] !== "never"; + + /** + * Check if the separation of two adjacent tokens meets the spacing rules, and report a problem if not. + * + * @param {ASTNode} node The node to which the potential problem belongs. + * @param {Token} left The first token. + * @param {Token} right The second token + * @returns {void} + */ + function checkTokens(node, left, right) { + var hasSpace = left.range[1] < right.range[0], + value = left.value; + + if (hasSpace !== requiresSpace) { + context.report(node, "Keyword \"{{value}}\" must {{not}}be followed by whitespace.", { + value: value, + not: requiresSpace ? "" : "not " + }); + } + } + + /** + * Check if the given node (`if`, `for`, `while`, etc), has the correct spacing after it. + * @param {ASTNode} node The node to check. + * @returns {void} + */ + function check(node) { + var tokens = context.getFirstTokens(node, 2); + checkTokens(node, tokens[0], tokens[1]); + } + + return { + "IfStatement": function (node) { + check(node); + // check the `else` + if (node.alternate && node.alternate.type !== "IfStatement") { + checkTokens(node.alternate, context.getTokenBefore(node.alternate), context.getFirstToken(node.alternate)); + } + }, + "ForStatement": check, + "ForOfStatement": check, + "ForInStatement": check, + "WhileStatement": check, + "DoWhileStatement": function (node) { + check(node); + // check the `while` + var whileTokens = context.getTokensBefore(node.test, 2); + checkTokens(node, whileTokens[0], whileTokens[1]); + }, + "SwitchStatement": check, + "TryStatement": function (node) { + check(node); + // check the `finally` + if (node.finalizer) { + checkTokens(node.finalizer, context.getTokenBefore(node.finalizer), context.getFirstToken(node.finalizer)); + } + }, + "CatchStatement": check, + "WithStatement": check + }; +}; diff --git a/tools/eslint/lib/rules/space-before-blocks.js b/tools/eslint/lib/rules/space-before-blocks.js new file mode 100644 index 0000000000..963ff42598 --- /dev/null +++ b/tools/eslint/lib/rules/space-before-blocks.js @@ -0,0 +1,85 @@ +/** + * @fileoverview A rule to ensure whitespace before blocks. + * @author Mathias Schreck <https://github.com/lo1tuma> + * @copyright 2014 Mathias Schreck. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function (context) { + var requireSpace = context.options[0] !== "never"; + + /** + * Determines whether two adjacent tokens are have whitespace between them. + * @param {Object} left - The left token object. + * @param {Object} right - The right token object. + * @returns {boolean} Whether or not there is space between the tokens. + */ + function isSpaced(left, right) { + return left.range[1] < right.range[0]; + } + + /** + * Determines whether two adjacent tokens are on the same line. + * @param {Object} left - The left token object. + * @param {Object} right - The right token object. + * @returns {boolean} Whether or not the tokens are on the same line. + */ + function isSameLine(left, right) { + return left.loc.start.line === right.loc.start.line; + } + + /** + * Checks the given BlockStatement node has a preceding space if it doesn’t start on a new line. + * @param {ASTNode|Token} node The AST node of a BlockStatement. + * @returns {void} undefined. + */ + function checkPrecedingSpace(node) { + var precedingToken = context.getTokenBefore(node), + hasSpace; + + if (precedingToken && isSameLine(precedingToken, node)) { + hasSpace = isSpaced(precedingToken, node); + + if (requireSpace) { + if (!hasSpace) { + context.report(node, "Missing space before opening brace."); + } + } else { + if (hasSpace) { + context.report(node, "Unexpected space before opening brace."); + } + } + } + } + + /** + * Checks if the CaseBlock of an given SwitchStatement node has a preceding space. + * @param {ASTNode} node The node of a SwitchStatement. + * @returns {void} undefined. + */ + function checkSpaceBeforeCaseBlock(node) { + var cases = node.cases, + firstCase, + openingBrace; + + if (cases.length > 0) { + firstCase = cases[0]; + openingBrace = context.getTokenBefore(firstCase); + } else { + openingBrace = context.getLastToken(node, 1); + } + + checkPrecedingSpace(openingBrace); + } + + return { + "BlockStatement": checkPrecedingSpace, + "SwitchStatement": checkSpaceBeforeCaseBlock + }; + +}; diff --git a/tools/eslint/lib/rules/space-before-function-paren.js b/tools/eslint/lib/rules/space-before-function-paren.js new file mode 100644 index 0000000000..b56409ada0 --- /dev/null +++ b/tools/eslint/lib/rules/space-before-function-paren.js @@ -0,0 +1,117 @@ +/** + * @fileoverview Rule to validate spacing before function paren. + * @author Mathias Schreck <https://github.com/lo1tuma> + * @copyright 2015 Mathias Schreck + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var configuration = context.options[0], + requireAnonymousFunctionSpacing = true, + requireNamedFunctionSpacing = true; + + if (typeof configuration === "object") { + requireAnonymousFunctionSpacing = configuration.anonymous !== "never"; + requireNamedFunctionSpacing = configuration.named !== "never"; + } else if (configuration === "never") { + requireAnonymousFunctionSpacing = false; + requireNamedFunctionSpacing = false; + } + + /** + * Determines whether two adjacent tokens are have whitespace between them. + * @param {Object} left - The left token object. + * @param {Object} right - The right token object. + * @returns {boolean} Whether or not there is space between the tokens. + */ + function isSpaced(left, right) { + return left.range[1] < right.range[0]; + } + + /** + * Determines whether a function has a name. + * @param {ASTNode} node The function node. + * @returns {boolean} Whether the function has a name. + */ + function isNamedFunction(node) { + var parent; + + if (node.id) { + return true; + } + + parent = context.getAncestors().pop(); + return parent.type === "MethodDefinition" || + (parent.type === "Property" && + ( + parent.kind === "get" || + parent.kind === "set" || + parent.method + ) + ); + } + + /** + * Validates the spacing before function parentheses. + * @param {ASTNode} node The node to be validated. + * @returns {void} + */ + function validateSpacingBeforeParentheses(node) { + var isNamed = isNamedFunction(node), + tokens, + leftToken, + rightToken, + location; + + if (node.generator && !isNamed) { + return; + } + + tokens = context.getTokens(node); + + if (node.generator) { + if (node.id) { + leftToken = tokens[2]; + rightToken = tokens[3]; + } else { + // Object methods are named but don't have an id + leftToken = context.getTokenBefore(node); + rightToken = tokens[0]; + } + } else if (isNamed) { + if (node.id) { + leftToken = tokens[1]; + rightToken = tokens[2]; + } else { + // Object methods are named but don't have an id + leftToken = context.getTokenBefore(node); + rightToken = tokens[0]; + } + } else { + leftToken = tokens[0]; + rightToken = tokens[1]; + } + + location = leftToken.loc.end; + + if (isSpaced(leftToken, rightToken)) { + if ((isNamed && !requireNamedFunctionSpacing) || (!isNamed && !requireAnonymousFunctionSpacing)) { + context.report(node, location, "Unexpected space before function parentheses."); + } + } else { + if ((isNamed && requireNamedFunctionSpacing) || (!isNamed && requireAnonymousFunctionSpacing)) { + context.report(node, location, "Missing space before function parentheses."); + } + } + } + + return { + "FunctionDeclaration": validateSpacingBeforeParentheses, + "FunctionExpression": validateSpacingBeforeParentheses + }; +}; diff --git a/tools/eslint/lib/rules/space-before-function-parentheses.js b/tools/eslint/lib/rules/space-before-function-parentheses.js new file mode 100644 index 0000000000..b40351d0b9 --- /dev/null +++ b/tools/eslint/lib/rules/space-before-function-parentheses.js @@ -0,0 +1,117 @@ +/** + * @fileoverview Rule to validate spacing before function parentheses. + * @author Mathias Schreck <https://github.com/lo1tuma> + * @copyright 2015 Mathias Schreck + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var configuration = context.options[0], + requireAnonymousFunctionSpacing = true, + requireNamedFunctionSpacing = true; + + if (typeof configuration === "object") { + requireAnonymousFunctionSpacing = configuration.anonymous !== "never"; + requireNamedFunctionSpacing = configuration.named !== "never"; + } else if (configuration === "never") { + requireAnonymousFunctionSpacing = false; + requireNamedFunctionSpacing = false; + } + + /** + * Determines whether two adjacent tokens are have whitespace between them. + * @param {Object} left - The left token object. + * @param {Object} right - The right token object. + * @returns {boolean} Whether or not there is space between the tokens. + */ + function isSpaced(left, right) { + return left.range[1] < right.range[0]; + } + + /** + * Determines whether a function has a name. + * @param {ASTNode} node The function node. + * @returns {boolean} Whether the function has a name. + */ + function isNamedFunction(node) { + var parent; + + if (node.id) { + return true; + } + + parent = context.getAncestors().pop(); + return parent.type === "MethodDefinition" || + (parent.type === "Property" && + ( + parent.kind === "get" || + parent.kind === "set" || + parent.method + ) + ); + } + + /** + * Validates the spacing before function parentheses. + * @param {ASTNode} node The node to be validated. + * @returns {void} + */ + function validateSpacingBeforeParentheses(node) { + var isNamed = isNamedFunction(node), + tokens, + leftToken, + rightToken, + location; + + if (node.generator && !isNamed) { + return; + } + + tokens = context.getTokens(node); + + if (node.generator) { + if (node.id) { + leftToken = tokens[2]; + rightToken = tokens[3]; + } else { + // Object methods are named but don't have an id + leftToken = context.getTokenBefore(node); + rightToken = tokens[0]; + } + } else if (isNamed) { + if (node.id) { + leftToken = tokens[1]; + rightToken = tokens[2]; + } else { + // Object methods are named but don't have an id + leftToken = context.getTokenBefore(node); + rightToken = tokens[0]; + } + } else { + leftToken = tokens[0]; + rightToken = tokens[1]; + } + + location = leftToken.loc.end; + + if (isSpaced(leftToken, rightToken)) { + if ((isNamed && !requireNamedFunctionSpacing) || (!isNamed && !requireAnonymousFunctionSpacing)) { + context.report(node, location, "Unexpected space before function parentheses."); + } + } else { + if ((isNamed && requireNamedFunctionSpacing) || (!isNamed && requireAnonymousFunctionSpacing)) { + context.report(node, location, "Missing space before function parentheses."); + } + } + } + + return { + "FunctionDeclaration": validateSpacingBeforeParentheses, + "FunctionExpression": validateSpacingBeforeParentheses + }; +}; diff --git a/tools/eslint/lib/rules/space-in-brackets.js b/tools/eslint/lib/rules/space-in-brackets.js new file mode 100644 index 0000000000..1b5fcec466 --- /dev/null +++ b/tools/eslint/lib/rules/space-in-brackets.js @@ -0,0 +1,272 @@ +/** + * @fileoverview Disallows or enforces spaces inside of brackets. + * @author Ian Christian Myers + * @copyright 2014 Brandyn Bennett. All rights reserved. + * @copyright 2014 Michael Ficarra. No rights reserved. + * @copyright 2014 Vignesh Anand. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + var spaced = context.options[0] === "always"; + + /** + * Determines whether an option is set, relative to the spacing option. + * If spaced is "always", then check whether option is set to false. + * If spaced is "never", then check whether option is set to true. + * @param {Object} option - The option to exclude. + * @returns {boolean} Whether or not the property is excluded. + */ + function isOptionSet(option) { + return context.options[1] != null ? context.options[1][option] === !spaced : false; + } + + var options = { + spaced: spaced, + singleElementException: isOptionSet("singleValue"), + objectsInArraysException: isOptionSet("objectsInArrays"), + arraysInArraysException: isOptionSet("arraysInArrays"), + arraysInObjectsException: isOptionSet("arraysInObjects"), + objectsInObjectsException: isOptionSet("objectsInObjects"), + propertyNameException: isOptionSet("propertyName") + }; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Determines whether two adjacent tokens are have whitespace between them. + * @param {Object} left - The left token object. + * @param {Object} right - The right token object. + * @returns {boolean} Whether or not there is space between the tokens. + */ + function isSpaced(left, right) { + return left.range[1] < right.range[0]; + } + + /** + * Determines whether two adjacent tokens are on the same line. + * @param {Object} left - The left token object. + * @param {Object} right - The right token object. + * @returns {boolean} Whether or not the tokens are on the same line. + */ + function isSameLine(left, right) { + return left.loc.start.line === right.loc.start.line; + } + + /** + * Reports that there shouldn't be a space after the first token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportNoBeginningSpace(node, token) { + context.report(node, token.loc.start, + "There should be no space after '" + token.value + "'"); + } + + /** + * Reports that there shouldn't be a space before the last token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportNoEndingSpace(node, token) { + context.report(node, token.loc.start, + "There should be no space before '" + token.value + "'"); + } + + /** + * Reports that there should be a space after the first token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportRequiredBeginningSpace(node, token) { + context.report(node, token.loc.start, + "A space is required after '" + token.value + "'"); + } + + /** + * Reports that there should be a space before the last token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportRequiredEndingSpace(node, token) { + context.report(node, token.loc.start, + "A space is required before '" + token.value + "'"); + } + + + /** + * Determines if spacing in curly braces is valid. + * @param {ASTNode} node The AST node to check. + * @param {Token} first The first token to check (should be the opening brace) + * @param {Token} second The second token to check (should be first after the opening brace) + * @param {Token} penultimate The penultimate token to check (should be last before closing brace) + * @param {Token} last The last token to check (should be closing brace) + * @returns {void} + */ + function validateBraceSpacing(node, first, second, penultimate, last) { + var closingCurlyBraceMustBeSpaced = + options.arraysInObjectsException && penultimate.value === "]" || + options.objectsInObjectsException && penultimate.value === "}" + ? !options.spaced : options.spaced; + + if (isSameLine(first, second)) { + if (options.spaced && !isSpaced(first, second)) { + reportRequiredBeginningSpace(node, first); + } + if (!options.spaced && isSpaced(first, second)) { + reportNoBeginningSpace(node, first); + } + } + + if (isSameLine(penultimate, last)) { + if (closingCurlyBraceMustBeSpaced && !isSpaced(penultimate, last)) { + reportRequiredEndingSpace(node, last); + } + if (!closingCurlyBraceMustBeSpaced && isSpaced(penultimate, last)) { + reportNoEndingSpace(node, last); + } + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + MemberExpression: function(node) { + if (!node.computed) { + return; + } + + var property = node.property, + before = context.getTokenBefore(property), + first = context.getFirstToken(property), + last = context.getLastToken(property), + after = context.getTokenAfter(property); + + var propertyNameMustBeSpaced = options.propertyNameException ? + !options.spaced : options.spaced; + + if (isSameLine(before, first)) { + if (propertyNameMustBeSpaced) { + if (!isSpaced(before, first) && isSameLine(before, first)) { + reportRequiredBeginningSpace(node, before); + } + } else { + if (isSpaced(before, first)) { + reportNoBeginningSpace(node, before); + } + } + } + + if (isSameLine(last, after)) { + if (propertyNameMustBeSpaced) { + if (!isSpaced(last, after) && isSameLine(last, after)) { + reportRequiredEndingSpace(node, after); + } + } else { + if (isSpaced(last, after)) { + reportNoEndingSpace(node, after); + } + } + } + }, + + ArrayExpression: function(node) { + if (node.elements.length === 0) { + return; + } + + var first = context.getFirstToken(node), + second = context.getFirstToken(node, 1), + penultimate = context.getLastToken(node, 1), + last = context.getLastToken(node); + + var openingBracketMustBeSpaced = + options.objectsInArraysException && second.value === "{" || + options.arraysInArraysException && second.value === "[" || + options.singleElementException && node.elements.length === 1 + ? !options.spaced : options.spaced; + + var closingBracketMustBeSpaced = + options.objectsInArraysException && penultimate.value === "}" || + options.arraysInArraysException && penultimate.value === "]" || + options.singleElementException && node.elements.length === 1 + ? !options.spaced : options.spaced; + + if (isSameLine(first, second)) { + if (openingBracketMustBeSpaced && !isSpaced(first, second)) { + reportRequiredBeginningSpace(node, first); + } + if (!openingBracketMustBeSpaced && isSpaced(first, second)) { + reportNoBeginningSpace(node, first); + } + } + + if (isSameLine(penultimate, last)) { + if (closingBracketMustBeSpaced && !isSpaced(penultimate, last)) { + reportRequiredEndingSpace(node, last); + } + if (!closingBracketMustBeSpaced && isSpaced(penultimate, last)) { + reportNoEndingSpace(node, last); + } + } + }, + + ImportDeclaration: function(node) { + + var firstSpecifier = node.specifiers[0], + lastSpecifier = node.specifiers[node.specifiers.length - 1]; + + // don't do anything for namespace or default imports + if (firstSpecifier.type === "ImportSpecifier" && lastSpecifier.type === "ImportSpecifier") { + var first = context.getTokenBefore(firstSpecifier), + second = context.getFirstToken(firstSpecifier), + penultimate = context.getLastToken(lastSpecifier), + last = context.getTokenAfter(lastSpecifier); + + validateBraceSpacing(node, first, second, penultimate, last); + } + + }, + + ExportNamedDeclaration: function(node) { + + var firstSpecifier = node.specifiers[0], + lastSpecifier = node.specifiers[node.specifiers.length - 1], + first = context.getTokenBefore(firstSpecifier), + second = context.getFirstToken(firstSpecifier), + penultimate = context.getLastToken(lastSpecifier), + last = context.getTokenAfter(lastSpecifier); + + validateBraceSpacing(node, first, second, penultimate, last); + + }, + + ObjectExpression: function(node) { + if (node.properties.length === 0) { + return; + } + + var first = context.getFirstToken(node), + second = context.getFirstToken(node, 1), + penultimate = context.getLastToken(node, 1), + last = context.getLastToken(node); + + validateBraceSpacing(node, first, second, penultimate, last); + } + + }; + +}; diff --git a/tools/eslint/lib/rules/space-in-parens.js b/tools/eslint/lib/rules/space-in-parens.js new file mode 100644 index 0000000000..ff124c1298 --- /dev/null +++ b/tools/eslint/lib/rules/space-in-parens.js @@ -0,0 +1,262 @@ +/** + * @fileoverview Disallows or enforces spaces inside of parentheses. + * @author Jonathan Rajavuori + * @copyright 2014 David Clark. All rights reserved. + * @copyright 2014 Jonathan Rajavuori. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var MISSING_SPACE_MESSAGE = "There must be a space inside this paren.", + REJECTED_SPACE_MESSAGE = "There should be no spaces inside this paren.", + exceptionsArray = (context.options.length === 2) ? context.options[1].exceptions : [], + options = {}, + rejectedSpaceRegExp, + missingSpaceRegExp, + spaceChecks; + + if (exceptionsArray && exceptionsArray.length) { + options.braceException = exceptionsArray.indexOf("{}") !== -1 || false; + options.bracketException = exceptionsArray.indexOf("[]") !== -1 || false; + options.parenException = exceptionsArray.indexOf("()") !== -1 || false; + options.empty = exceptionsArray.indexOf("empty") !== -1 || false; + } + + /** + * Used with the `never` option to produce, given the exception options, + * two regular expressions to check for missing and rejected spaces. + * @param {Object} opts The exception options + * @returns {Object} `missingSpace` and `rejectedSpace` regular expressions + * @private + */ + function getNeverChecks(opts) { + var missingSpaceOpeners = [], + missingSpaceClosers = [], + rejectedSpaceOpeners = ["\\s"], + rejectedSpaceClosers = ["\\s"], + missingSpaceCheck, + rejectedSpaceCheck; + + // Populate openers and closers + if (opts.braceException) { + missingSpaceOpeners.push("\\{"); + missingSpaceClosers.push("\\}"); + rejectedSpaceOpeners.push("\\{"); + rejectedSpaceClosers.push("\\}"); + } + if (opts.bracketException) { + missingSpaceOpeners.push("\\["); + missingSpaceClosers.push("\\]"); + rejectedSpaceOpeners.push("\\["); + rejectedSpaceClosers.push("\\]"); + } + if (opts.parenException) { + missingSpaceOpeners.push("\\("); + missingSpaceClosers.push("\\)"); + rejectedSpaceOpeners.push("\\("); + rejectedSpaceClosers.push("\\)"); + } + if (opts.empty) { + missingSpaceOpeners.push("\\)"); + missingSpaceClosers.push("\\("); + rejectedSpaceOpeners.push("\\)"); + rejectedSpaceClosers.push("\\("); + } + + if (missingSpaceOpeners.length) { + missingSpaceCheck = "\\((" + missingSpaceOpeners.join("|") + ")"; + if (missingSpaceClosers.length) { + missingSpaceCheck += "|"; + } + } + if (missingSpaceClosers.length) { + missingSpaceCheck += "(" + missingSpaceClosers.join("|") + ")\\)"; + } + + // compose the rejected regexp + rejectedSpaceCheck = "\\( +[^" + rejectedSpaceOpeners.join("") + "]"; + rejectedSpaceCheck += "|[^" + rejectedSpaceClosers.join("") + "] +\\)"; + + return { + // e.g. \((\{)|(\})\) --- where {} is an exception + missingSpace: missingSpaceCheck || ".^", + // e.g. \( +[^ \n\r\{]|[^ \n\r\}] +\) --- where {} is an exception + rejectedSpace: rejectedSpaceCheck + }; + } + + /** + * Used with the `always` option to produce, given the exception options, + * two regular expressions to check for missing and rejected spaces. + * @param {Object} opts The exception options + * @returns {Object} `missingSpace` and `rejectedSpace` regular expressions + * @private + */ + function getAlwaysChecks(opts) { + var missingSpaceOpeners = ["\\s", "\\)"], + missingSpaceClosers = ["\\s", "\\("], + rejectedSpaceOpeners = [], + rejectedSpaceClosers = [], + missingSpaceCheck, + rejectedSpaceCheck; + + // Populate openers and closers + if (opts.braceException) { + missingSpaceOpeners.push("\\{"); + missingSpaceClosers.push("\\}"); + rejectedSpaceOpeners.push(" \\{"); + rejectedSpaceClosers.push("\\} "); + } + if (opts.bracketException) { + missingSpaceOpeners.push("\\["); + missingSpaceClosers.push("\\]"); + rejectedSpaceOpeners.push(" \\["); + rejectedSpaceClosers.push("\\] "); + } + if (opts.parenException) { + missingSpaceOpeners.push("\\("); + missingSpaceClosers.push("\\)"); + rejectedSpaceOpeners.push(" \\("); + rejectedSpaceClosers.push("\\) "); + } + if (opts.empty) { + rejectedSpaceOpeners.push(" \\)"); + rejectedSpaceClosers.push("\\( "); + } + + // compose the allowed regexp + missingSpaceCheck = "\\([^" + missingSpaceOpeners.join("") + "]"; + missingSpaceCheck += "|[^" + missingSpaceClosers.join("") + "]\\)"; + + // compose the rejected regexp + if (rejectedSpaceOpeners.length) { + rejectedSpaceCheck = "\\((" + rejectedSpaceOpeners.join("|") + ")"; + if (rejectedSpaceClosers.length) { + rejectedSpaceCheck += "|"; + } + } + if (rejectedSpaceClosers.length) { + rejectedSpaceCheck += "(" + rejectedSpaceClosers.join("|") + ")\\)"; + } + + return { + // e.g. \([^ \)\r\n\{]|[^ \(\r\n\}]\) --- where {} is an exception + missingSpace: missingSpaceCheck, + // e.g. \(( \{})|(\} )\) --- where {} is an excpetion + rejectedSpace: rejectedSpaceCheck || ".^" + }; + } + + spaceChecks = (context.options[0] === "always") ? getAlwaysChecks(options) : getNeverChecks(options); + missingSpaceRegExp = new RegExp(spaceChecks.missingSpace, "mg"); + rejectedSpaceRegExp = new RegExp(spaceChecks.rejectedSpace, "mg"); + + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + var skipRanges = []; + + /** + * Adds the range of a node to the set to be skipped when checking parens + * @param {ASTNode} node The node to skip + * @returns {void} + * @private + */ + function addSkipRange(node) { + skipRanges.push(node.range); + } + + /** + * Sorts the skipRanges array. Must be called before shouldSkip + * @returns {void} + * @private + */ + function sortSkipRanges() { + skipRanges.sort(function (a, b) { + return a[0] - b[0]; + }); + } + + /** + * Checks if a certain position in the source should be skipped + * @param {Number} pos The 0-based index in the source + * @returns {boolean} whether the position should be skipped + * @private + */ + function shouldSkip(pos) { + var i, len, range; + for (i = 0, len = skipRanges.length; i < len; i += 1) { + range = skipRanges[i]; + if (pos < range[0]) { + break; + } else if (pos < range[1]) { + return true; + } + } + return false; + } + + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + "Program:exit": function checkParenSpaces(node) { + + var nextMatch, + nextLine, + column, + line = 1, + source = context.getSource(), + pos = 0; + + function checkMatch(match, message) { + if (source.charAt(match.index) !== "(") { + // Matched a closing paren pattern + match.index += 1; + } + + if (!shouldSkip(match.index)) { + while ((nextLine = source.indexOf("\n", pos)) !== -1 && nextLine < match.index) { + pos = nextLine + 1; + line += 1; + } + column = match.index - pos; + + context.report(node, { line: line, column: column }, message); + } + } + + sortSkipRanges(); + + while ((nextMatch = rejectedSpaceRegExp.exec(source)) !== null) { + checkMatch(nextMatch, REJECTED_SPACE_MESSAGE); + } + + while ((nextMatch = missingSpaceRegExp.exec(source)) !== null) { + checkMatch(nextMatch, MISSING_SPACE_MESSAGE); + } + + }, + + + // These nodes can contain parentheses that this rule doesn't care about + + LineComment: addSkipRange, + + BlockComment: addSkipRange, + + Literal: addSkipRange + + }; + +}; diff --git a/tools/eslint/lib/rules/space-infix-ops.js b/tools/eslint/lib/rules/space-infix-ops.js new file mode 100644 index 0000000000..a3f3c940ac --- /dev/null +++ b/tools/eslint/lib/rules/space-infix-ops.js @@ -0,0 +1,94 @@ +/** + * @fileoverview Require spaces around infix operators + * @author Michael Ficarra + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + var int32Hint = context.options[0] ? context.options[0].int32Hint === true : false; + + var OPERATORS = [ + "*", "/", "%", "+", "-", "<<", ">>", ">>>", "<", "<=", ">", ">=", "in", + "instanceof", "==", "!=", "===", "!==", "&", "^", "|", "&&", "||", "=", + "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "&=", "^=", "|=", + "?", ":", "," + ]; + + /** + * Returns the first token which violates the rule + * @param {ASTNode} left - The left node of the main node + * @param {ASTNode} right - The right node of the main node + * @returns {object} The violator token or null + * @private + */ + function getFirstNonSpacedToken(left, right) { + var op, tokens = context.getTokensBetween(left, right, 1); + for (var i = 1, l = tokens.length - 1; i < l; ++i) { + op = tokens[i]; + if ( + op.type === "Punctuator" && + OPERATORS.indexOf(op.value) >= 0 && + (tokens[i - 1].range[1] >= op.range[0] || op.range[1] >= tokens[i + 1].range[0]) + ) { + return op; + } + } + return null; + } + + /** + * Reports an AST node as a rule violation + * @param {ASTNode} mainNode - The node to report + * @param {object} culpritToken - The token which has a problem + * @returns {void} + * @private + */ + function report(mainNode, culpritToken) { + context.report(mainNode, culpritToken.loc.start, "Infix operators must be spaced."); + } + + function checkBinary(node) { + var nonSpacedNode = getFirstNonSpacedToken(node.left, node.right); + + if (nonSpacedNode) { + if (!(int32Hint && context.getSource(node).substr(-2) === "|0")) { + report(node, nonSpacedNode); + } + } + } + + function checkConditional(node) { + var nonSpacedConsequesntNode = getFirstNonSpacedToken(node.test, node.consequent); + var nonSpacedAlternateNode = getFirstNonSpacedToken(node.consequent, node.alternate); + + if (nonSpacedConsequesntNode) { + report(node, nonSpacedConsequesntNode); + } else if (nonSpacedAlternateNode) { + report(node, nonSpacedAlternateNode); + } + } + + function checkVar(node) { + var nonSpacedNode; + + if (node.init) { + nonSpacedNode = getFirstNonSpacedToken(node.id, node.init); + if (nonSpacedNode) { + report(node, nonSpacedNode); + } + } + } + + return { + "AssignmentExpression": checkBinary, + "BinaryExpression": checkBinary, + "LogicalExpression": checkBinary, + "ConditionalExpression": checkConditional, + "VariableDeclarator": checkVar + }; + +}; diff --git a/tools/eslint/lib/rules/space-return-throw-case.js b/tools/eslint/lib/rules/space-return-throw-case.js new file mode 100644 index 0000000000..27928adec7 --- /dev/null +++ b/tools/eslint/lib/rules/space-return-throw-case.js @@ -0,0 +1,36 @@ +/** + * @fileoverview Require spaces following return, throw, and case + * @author Michael Ficarra + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + function check(node) { + var tokens = context.getFirstTokens(node, 2), + value = tokens[0].value; + + if (tokens[0].range[1] >= tokens[1].range[0]) { + context.report(node, "Keyword \"" + value + "\" must be followed by whitespace."); + } + } + + return { + "ReturnStatement": function(node) { + if (node.argument) { + check(node); + } + }, + "SwitchCase": function(node) { + if (node.test) { + check(node); + } + }, + "ThrowStatement": check + }; + +}; diff --git a/tools/eslint/lib/rules/space-unary-ops.js b/tools/eslint/lib/rules/space-unary-ops.js new file mode 100644 index 0000000000..2fe8a3f406 --- /dev/null +++ b/tools/eslint/lib/rules/space-unary-ops.js @@ -0,0 +1,118 @@ +/** + * @fileoverview This rule shoud require or disallow spaces before or after unary operations. + * @author Marcin Kumorek + * @copyright 2014 Marcin Kumorek. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + var options = context.options && Array.isArray(context.options) && context.options[0] || { words: true, nonwords: false }; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Check if the parent unary operator is "!" in order to know if it's "!!" convert to Boolean or just "!" negation + * @param {ASTnode} node AST node + * @returns {boolean} Whether or not the parent is unary "!" operator + */ + function isParentUnaryBangExpression(node) { + return node && node.parent && node.parent.type === "UnaryExpression" && node.parent.operator === "!"; + } + + /** + * Checks if the type is a unary word expression + * @param {string} type value of AST token + * @returns {boolean} Whether the word is in the list of known words + */ + function isWordExpression(type) { + return ["delete", "new", "typeof", "void"].indexOf(type) !== -1; + } + + /** + * Check if the node's child argument is an "ObjectExpression" + * @param {ASTnode} node AST node + * @returns {boolean} Whether or not the argument's type is "ObjectExpression" + */ + function isArgumentObjectExpression(node) { + return node.argument && node.argument.type && node.argument.type === "ObjectExpression"; + } + + /** + * Check Unary Word Operators for spaces after the word operator + * @param {ASTnode} node AST node + * @param {object} firstToken first token from the AST node + * @param {object} secondToken second token from the AST node + * @returns {void} + */ + function checkUnaryWordOperatorForSpaces(node, firstToken, secondToken) { + if (options.words) { + if (secondToken.range[0] === firstToken.range[1]) { + context.report(node, "Unary word operator \"" + firstToken.value + "\" must be followed by whitespace."); + } + } + + if (!options.words && isArgumentObjectExpression(node)) { + if (secondToken.range[0] > firstToken.range[1]) { + context.report(node, "Unexpected space after unary word operator \"" + firstToken.value + "\"."); + } + } + } + + /** + * Checks UnaryExpression, UpdateExpression and NewExpression for spaces before and after the operator + * @param {ASTnode} node AST node + * @returns {void} + */ + function checkForSpaces(node) { + var tokens = context.getFirstTokens(node, 2), + firstToken = tokens[0], + secondToken = tokens[1]; + + if (isWordExpression(firstToken.value)) { + checkUnaryWordOperatorForSpaces(node, firstToken, secondToken); + return void 0; + } + + if (options.nonwords) { + if (node.prefix) { + if (isParentUnaryBangExpression(node)) { + return void 0; + } + if (firstToken.range[1] === secondToken.range[0]) { + context.report(node, "Unary operator \"" + firstToken.value + "\" must be followed by whitespace."); + } + } else { + if (firstToken.range[1] === secondToken.range[0]) { + context.report(node, "Space is required before unary expressions \"" + secondToken.value + "\"."); + } + } + } else { + if (node.prefix) { + if (secondToken.range[0] > firstToken.range[1]) { + context.report(node, "Unexpected space after unary operator \"" + firstToken.value + "\"."); + } + } else { + if (secondToken.range[0] > firstToken.range[1]) { + context.report(node, "Unexpected space before unary operator \"" + secondToken.value + "\"."); + } + } + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + "UnaryExpression": checkForSpaces, + "UpdateExpression": checkForSpaces, + "NewExpression": checkForSpaces + }; + +}; diff --git a/tools/eslint/lib/rules/spaced-line-comment.js b/tools/eslint/lib/rules/spaced-line-comment.js new file mode 100644 index 0000000000..af5ba8cdb6 --- /dev/null +++ b/tools/eslint/lib/rules/spaced-line-comment.js @@ -0,0 +1,71 @@ +/** + * @fileoverview Enforces or disallows a space beginning a single-line comment. + * @author Greg Cochard + * @copyright 2014 Greg Cochard. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + // Unless the first option is never, require a space + var requireSpace = context.options[0] !== "never"; + + // Default to match anything, so all will fail if there are no exceptions + var exceptionMatcher = new RegExp(" "); + + // Grab the exceptions array and build a RegExp matcher for it + var hasExceptions = context.options.length === 2; + var unescapedExceptions = hasExceptions ? context.options[1].exceptions : []; + var exceptions; + + if (unescapedExceptions.length) { + exceptions = unescapedExceptions.map(function(s) { + return s.replace(/([.*+?${}()|\^\[\]\/\\])/g, "\\$1"); + }); + exceptionMatcher = new RegExp("(^(" + exceptions.join(")+$)|(^(") + ")+$)"); + } + + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + "LineComment": function checkCommentForSpace(node) { + + if (requireSpace) { + + // If length is zero, ignore it + if (node.value.length === 0) { + return; + } + + // Space expected and not found + if (node.value.indexOf(" ") !== 0 && node.value.indexOf("\t") !== 0) { + + /* + * Do two tests; one for space starting the line, + * and one for a comment comprised only of exceptions + */ + if (hasExceptions && !exceptionMatcher.test(node.value)) { + context.report(node, "Expected exception block, space or tab after // in comment."); + } else if (!hasExceptions) { + context.report(node, "Expected space or tab after // in comment."); + } + } + + } else { + + if (node.value.indexOf(" ") === 0 || node.value.indexOf("\t") === 0) { + context.report(node, "Unexpected space or tab after // in comment."); + } + } + } + + }; +}; diff --git a/tools/eslint/lib/rules/strict.js b/tools/eslint/lib/rules/strict.js new file mode 100644 index 0000000000..16d8612891 --- /dev/null +++ b/tools/eslint/lib/rules/strict.js @@ -0,0 +1,236 @@ +/** + * @fileoverview Rule to control usage of strict mode directives. + * @author Brandon Mills + * @copyright 2015 Brandon Mills. All rights reserved. + * @copyright 2013-2014 Nicholas C. Zakas. All rights reserved. + * @copyright 2013 Ian Christian Myers. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +var messages = { + function: "Use the function form of \"use strict\".", + global: "Use the global form of \"use strict\".", + multiple: "Multiple \"use strict\" directives.", + never: "Strict mode is not permitted.", + unnecessary: "Unnecessary \"use strict\" directive." +}; + +/** + * Gets all of the Use Strict Directives in the Directive Prologue of a group of + * statements. + * @param {ASTNode[]} statements Statements in the program or function body. + * @returns {ASTNode[]} All of the Use Strict Directives. + */ +function getUseStrictDirectives(statements) { + var directives = [], + i, statement; + + for (i = 0; i < statements.length; i++) { + statement = statements[i]; + + if ( + statement.type === "ExpressionStatement" && + statement.expression.type === "Literal" && + statement.expression.value === "use strict" + ) { + directives[i] = statement; + } else { + break; + } + } + + return directives; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var mode = context.options[0], + isModule = context.ecmaFeatures.modules, + modes = {}, + scopes = []; + + /** + * Report a node or array of nodes with a given message. + * @param {(ASTNode|ASTNode[])} nodes Node or nodes to report. + * @param {string} message Message to display. + * @returns {void} + */ + function report(nodes, message) { + var i; + + if (Array.isArray(nodes)) { + for (i = 0; i < nodes.length; i++) { + context.report(nodes[i], message); + } + } else { + context.report(nodes, message); + } + } + + //-------------------------------------------------------------------------- + // "deprecated" mode (default) + //-------------------------------------------------------------------------- + + /** + * Determines if a given node is "use strict". + * @param {ASTNode} node The node to check. + * @returns {boolean} True if the node is a strict pragma, false if not. + * @void + */ + function isStrictPragma(node) { + return (node && node.type === "ExpressionStatement" && + node.expression.value === "use strict"); + } + + /** + * When you enter a scope, push the strict value from the previous scope + * onto the stack. + * @param {ASTNode} node The AST node being checked. + * @returns {void} + * @private + */ + function enterScope(node) { + + var isStrict = false, + isProgram = (node.type === "Program"), + isParentGlobal = scopes.length === 1, + isParentStrict = scopes.length ? scopes[scopes.length - 1] : false; + + // look for the "use strict" pragma + if (isModule) { + isStrict = true; + } else if (isProgram) { + isStrict = isStrictPragma(node.body[0]) || isParentStrict; + } else { + isStrict = node.body.body && isStrictPragma(node.body.body[0]) || isParentStrict; + } + + scopes.push(isStrict); + + // never warn if the parent is strict or the function is strict + if (!isParentStrict && !isStrict && isParentGlobal) { + context.report(node, "Missing \"use strict\" statement."); + } + } + + /** + * When you exit a scope, pop off the top scope and see if it's true or + * false. + * @returns {void} + * @private + */ + function exitScope() { + scopes.pop(); + } + + modes.deprecated = { + "Program": enterScope, + "FunctionDeclaration": enterScope, + "FunctionExpression": enterScope, + "ArrowFunctionExpression": enterScope, + + "Program:exit": exitScope, + "FunctionDeclaration:exit": exitScope, + "FunctionExpression:exit": exitScope, + "ArrowFunctionExpression:exit": exitScope + }; + + //-------------------------------------------------------------------------- + // "never" mode + //-------------------------------------------------------------------------- + + modes.never = { + "Program": function(node) { + report(getUseStrictDirectives(node.body), messages.never); + }, + "FunctionDeclaration": function(node) { + report(getUseStrictDirectives(node.body.body), messages.never); + }, + "FunctionExpression": function(node) { + report(getUseStrictDirectives(node.body.body), messages.never); + } + }; + + //-------------------------------------------------------------------------- + // "global" mode + //-------------------------------------------------------------------------- + + modes.global = { + "Program": function(node) { + var useStrictDirectives = getUseStrictDirectives(node.body); + + if (!isModule && node.body.length && useStrictDirectives.length < 1) { + report(node, messages.global); + } else if (isModule) { + report(useStrictDirectives, messages.unnecessary); + } else { + report(useStrictDirectives.slice(1), messages.multiple); + } + }, + "FunctionDeclaration": function(node) { + report(getUseStrictDirectives(node.body.body), messages.global); + }, + "FunctionExpression": function(node) { + report(getUseStrictDirectives(node.body.body), messages.global); + } + }; + + //-------------------------------------------------------------------------- + // "function" mode + //-------------------------------------------------------------------------- + + /** + * Entering a function pushes a new nested scope onto the stack. The new + * scope is true if the nested function is strict mode code. + * @param {ASTNode} node The function declaration or expression. + * @returns {void} + */ + function enterFunction(node) { + var useStrictDirectives = getUseStrictDirectives(node.body.body), + isParentGlobal = scopes.length === 0, + isParentStrict = isModule || (scopes.length && scopes[scopes.length - 1]), + isStrict = useStrictDirectives.length > 0 || isModule; + + if (isStrict) { + if (isParentStrict && useStrictDirectives.length) { + report(useStrictDirectives[0], messages.unnecessary); + } + + report(useStrictDirectives.slice(1), messages.multiple); + } else if (isParentGlobal && !isModule) { + report(node, messages.function); + } + + scopes.push(isParentStrict || isStrict); + } + + /** + * Exiting a function pops its scope off the stack. + * @returns {void} + */ + function exitFunction() { + scopes.pop(); + } + + modes.function = { + "Program": function(node) { + report(getUseStrictDirectives(node.body), messages.function); + }, + "FunctionDeclaration": enterFunction, + "FunctionExpression": enterFunction, + "FunctionDeclaration:exit": exitFunction, + "FunctionExpression:exit": exitFunction + }; + + return modes[mode || "deprecated"]; + +}; diff --git a/tools/eslint/lib/rules/use-isnan.js b/tools/eslint/lib/rules/use-isnan.js new file mode 100644 index 0000000000..d7a3f53768 --- /dev/null +++ b/tools/eslint/lib/rules/use-isnan.js @@ -0,0 +1,24 @@ +/** + * @fileoverview Rule to flag comparisons to the value NaN + * @author James Allardice + * @copyright 2014 Jordan Harband. All rights reserved. + * @copyright 2013 James Allardice. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function (context) { + + return { + "BinaryExpression": function (node) { + if (/^(?:[<>]|[!=]=)=?$/.test(node.operator) && (node.left.name === "NaN" || node.right.name === "NaN")) { + context.report(node, "Use the isNaN function to compare with NaN."); + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/valid-jsdoc.js b/tools/eslint/lib/rules/valid-jsdoc.js new file mode 100644 index 0000000000..a79bf7fea4 --- /dev/null +++ b/tools/eslint/lib/rules/valid-jsdoc.js @@ -0,0 +1,191 @@ +/** + * @fileoverview Validates JSDoc comments are syntactically correct + * @author Nicholas C. Zakas + * @copyright 2014 Nicholas C. Zakas. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var doctrine = require("doctrine"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var options = context.options[0] || {}, + prefer = options.prefer || {}, + + // these both default to true, so you have to explicitly make them false + requireReturn = options.requireReturn === false ? false : true, + requireParamDescription = options.requireParamDescription !== false, + requireReturnDescription = options.requireReturnDescription !== false; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + // Using a stack to store if a function returns or not (handling nested functions) + var fns = []; + + /** + * When parsing a new function, store it in our function stack. + * @returns {void} + * @private + */ + function startFunction() { + fns.push({returnPresent: false}); + } + + /** + * Indicate that return has been found in the current function. + * @param {ASTNode} node The return node. + * @returns {void} + * @private + */ + function addReturn(node) { + var functionState = fns[fns.length - 1]; + + if (functionState && node.argument !== null) { + functionState.returnPresent = true; + } + } + + /** + * Validate the JSDoc node and output warnings if anything is wrong. + * @param {ASTNode} node The AST node to check. + * @returns {void} + * @private + */ + function checkJSDoc(node) { + var jsdocNode = context.getJSDocComment(node), + functionData = fns.pop(), + hasReturns = false, + hasConstructor = false, + params = Object.create(null), + jsdoc; + + // make sure only to validate JSDoc comments + if (jsdocNode) { + + try { + jsdoc = doctrine.parse(jsdocNode.value, { + strict: true, + unwrap: true, + sloppy: true + }); + } catch (ex) { + + if (/braces/i.test(ex.message)) { + context.report(jsdocNode, "JSDoc type missing brace."); + } else { + context.report(jsdocNode, "JSDoc syntax error."); + } + + return; + } + + jsdoc.tags.forEach(function(tag) { + + switch (tag.title) { + + case "param": + if (!tag.type) { + context.report(jsdocNode, "Missing JSDoc parameter type for '{{name}}'.", { name: tag.name }); + } + + if (!tag.description && requireParamDescription) { + context.report(jsdocNode, "Missing JSDoc parameter description for '{{name}}'.", { name: tag.name }); + } + + if (params[tag.name]) { + context.report(jsdocNode, "Duplicate JSDoc parameter '{{name}}'.", { name: tag.name }); + } else if (tag.name.indexOf(".") === -1) { + params[tag.name] = 1; + } + break; + + case "return": + case "returns": + hasReturns = true; + + if (!requireReturn && !functionData.returnPresent && tag.type.name !== "void" && tag.type.name !== "undefined") { + context.report(jsdocNode, "Unexpected @" + tag.title + " tag; function has no return statement."); + } else { + if (!tag.type) { + context.report(jsdocNode, "Missing JSDoc return type."); + } + + if (tag.type.name !== "void" && !tag.description && requireReturnDescription) { + context.report(jsdocNode, "Missing JSDoc return description."); + } + } + + break; + + case "constructor": + case "class": + hasConstructor = true; + break; + + // no default + } + + // check tag preferences + if (prefer.hasOwnProperty(tag.title)) { + context.report(jsdocNode, "Use @{{name}} instead.", { name: prefer[tag.title] }); + } + + }); + + // check for functions missing @returns + if (!hasReturns && !hasConstructor) { + if (requireReturn || functionData.returnPresent) { + context.report(jsdocNode, "Missing JSDoc @returns for function."); + } + } + + // check the parameters + var jsdocParams = Object.keys(params); + + node.params.forEach(function(param, i) { + var name = param.name; + + // TODO(nzakas): Figure out logical things to do with destructured, default, rest params + if (param.type === "Identifier") { + if (jsdocParams[i] && (name !== jsdocParams[i])) { + context.report(jsdocNode, "Expected JSDoc for '{{name}}' but found '{{jsdocName}}'.", { + name: name, + jsdocName: jsdocParams[i] + }); + } else if (!params[name]) { + context.report(jsdocNode, "Missing JSDoc for parameter '{{name}}'.", { + name: name + }); + } + } + }); + + } + + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + "ArrowFunctionExpression": startFunction, + "FunctionExpression": startFunction, + "FunctionDeclaration": startFunction, + "ArrowFunctionExpression:exit": checkJSDoc, + "FunctionExpression:exit": checkJSDoc, + "FunctionDeclaration:exit": checkJSDoc, + "ReturnStatement": addReturn + }; + +}; diff --git a/tools/eslint/lib/rules/valid-typeof.js b/tools/eslint/lib/rules/valid-typeof.js new file mode 100644 index 0000000000..a108ab360f --- /dev/null +++ b/tools/eslint/lib/rules/valid-typeof.js @@ -0,0 +1,40 @@ +/** + * @fileoverview Ensures that the results of typeof are compared against a valid string + * @author Ian Christian Myers + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var VALID_TYPES = ["symbol", "undefined", "object", "boolean", "number", "string", "function"], + OPERATORS = ["==", "===", "!=", "!=="]; + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + "UnaryExpression": function (node) { + var parent, sibling; + + if (node.operator === "typeof") { + parent = context.getAncestors().pop(); + + if (parent.type === "BinaryExpression" && OPERATORS.indexOf(parent.operator) !== -1) { + sibling = parent.left === node ? parent.right : parent.left; + + if (sibling.type === "Literal" && VALID_TYPES.indexOf(sibling.value) === -1) { + context.report(sibling, "Invalid typeof comparison value"); + } + } + } + } + + }; + +}; diff --git a/tools/eslint/lib/rules/vars-on-top.js b/tools/eslint/lib/rules/vars-on-top.js new file mode 100644 index 0000000000..4af70a8803 --- /dev/null +++ b/tools/eslint/lib/rules/vars-on-top.js @@ -0,0 +1,113 @@ +/** + * @fileoverview Rule to enforce var declarations are only at the top of a function. + * @author Danny Fritz + * @author Gyandeep Singh + * @copyright 2014 Danny Fritz. All rights reserved. + * @copyright 2014 Gyandeep Singh. All rights reserved. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function (context) { + var errorMessage = "All \"var\" declarations must be at the top of the function scope."; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * @param {ASTNode} node - any node + * @returns {Boolean} whether the given node structurally represents a directive + */ + function looksLikeDirective(node) { + return node.type === "ExpressionStatement" && + node.expression.type === "Literal" && typeof node.expression.value === "string"; + } + + /** + * Check to see if its a ES6 import declaration + * @param {ASTNode} node - any node + * @returns {Boolean} whether the given node represents a import declaration + */ + function looksLikeImport(node) { + return node.type === "ImportDeclaration" || node.type === "ImportSpecifier" || + node.type === "ImportDefaultSpecifier" || node.type === "ImportNamespaceSpecifier"; + } + + /** + * Checks whether this variable is on top of the block body + * @param {ASTNode} node - The node to check + * @param {ASTNode[]} statements - collection of ASTNodes for the parent node block + * @returns {Boolean} True if var is on top otherwise false + */ + function isVarOnTop(node, statements) { + var i = 0, l = statements.length; + + // skip over directives + for (; i < l; ++i) { + if (!looksLikeDirective(statements[i]) && !looksLikeImport(statements[i])) { + break; + } + } + + for (; i < l; ++i) { + if (statements[i].type !== "VariableDeclaration") { + return false; + } + if (statements[i] === node) { + return true; + } + } + } + + /** + * Checks whether variable is on top at the global level + * @param {ASTNode} node - The node to check + * @param {ASTNode} parent - Parent of the node + * @returns {void} + */ + function globalVarCheck(node, parent) { + if (!isVarOnTop(node, parent.body)) { + context.report(node, errorMessage); + } + } + + /** + * Checks whether variable is on top at functional block scope level + * @param {ASTNode} node - The node to check + * @param {ASTNode} parent - Parent of the node + * @param {ASTNode} grandParent - Parent of the node's parent + * @returns {void} + */ + function blockScopeVarCheck(node, parent, grandParent) { + if (!(/Function/.test(grandParent.type) && + parent.type === "BlockStatement" && + isVarOnTop(node, parent.body))) { + context.report(node, errorMessage); + } + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + "VariableDeclaration": function (node) { + var ancestors = context.getAncestors(); + var parent = ancestors.pop(); + var grandParent = ancestors.pop(); + + if (node.kind === "var") {// check variable is `var` type and not `let` or `const` + if (parent.type === "Program") {// That means its a global variable + globalVarCheck(node, parent); + } else { + blockScopeVarCheck(node, parent, grandParent); + } + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/wrap-iife.js b/tools/eslint/lib/rules/wrap-iife.js new file mode 100644 index 0000000000..eb3c5d1dc3 --- /dev/null +++ b/tools/eslint/lib/rules/wrap-iife.js @@ -0,0 +1,42 @@ +/** + * @fileoverview Rule to flag when IIFE is not wrapped in parens + * @author Ilya Volodin + * @copyright 2013 Ilya Volodin. All rights reserved. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var style = context.options[0] || "outside"; + + function wrapped(node) { + var previousToken = context.getTokenBefore(node), + nextToken = context.getTokenAfter(node); + return previousToken && previousToken.value === "(" && + nextToken && nextToken.value === ")"; + } + + return { + + "CallExpression": function(node) { + if (node.callee.type === "FunctionExpression") { + var callExpressionWrapped = wrapped(node), + functionExpressionWrapped = wrapped(node.callee); + + if (!callExpressionWrapped && !functionExpressionWrapped) { + context.report(node, "Wrap an immediate function invocation in parentheses."); + } else if (style === "inside" && !functionExpressionWrapped) { + context.report(node, "Wrap only the function expression in parens."); + } else if (style === "outside" && !callExpressionWrapped) { + context.report(node, "Move the invocation into the parens that contain the function."); + } + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/wrap-regex.js b/tools/eslint/lib/rules/wrap-regex.js new file mode 100644 index 0000000000..e2cc7d2253 --- /dev/null +++ b/tools/eslint/lib/rules/wrap-regex.js @@ -0,0 +1,36 @@ +/** + * @fileoverview Rule to flag when regex literals are not wrapped in parens + * @author Matt DuVall <http://www.mattduvall.com> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + return { + + "Literal": function(node) { + var token = context.getFirstToken(node), + nodeType = token.type, + source, + grandparent, + ancestors; + + if (nodeType === "RegularExpression") { + source = context.getTokenBefore(node); + ancestors = context.getAncestors(); + grandparent = ancestors[ancestors.length - 1]; + + if (grandparent.type === "MemberExpression" && grandparent.object === node && + (!source || source.value !== "(")) { + context.report(node, "Wrap the regexp literal in parens to disambiguate the slash."); + } + } + } + }; + +}; diff --git a/tools/eslint/lib/rules/yoda.js b/tools/eslint/lib/rules/yoda.js new file mode 100644 index 0000000000..cdb5bdad08 --- /dev/null +++ b/tools/eslint/lib/rules/yoda.js @@ -0,0 +1,210 @@ +/** + * @fileoverview Rule to require or disallow yoda comparisons + * @author Nicholas C. Zakas + * @copyright 2014 Nicholas C. Zakas. All rights reserved. + * @copyright 2014 Brandon Mills. All rights reserved. + */ +"use strict"; + +//-------------------------------------------------------------------------- +// Helpers +//-------------------------------------------------------------------------- + +/** + * Determines whether an operator is a comparison operator. + * @param {String} operator The operator to check. + * @returns {boolean} Whether or not it is a comparison operator. + */ +function isComparisonOperator(operator) { + return (/^(==|===|!=|!==|<|>|<=|>=)$/).test(operator); +} + +/** + * Determines whether an operator is one used in a range test. + * Allowed operators are `<` and `<=`. + * @param {String} operator The operator to check. + * @returns {boolean} Whether the operator is used in range tests. + */ +function isRangeTestOperator(operator) { + return ["<", "<="].indexOf(operator) >= 0; +} + +/** + * Determines whether a non-Literal node is a negative number that should be + * treated as if it were a single Literal node. + * @param {ASTNode} node Node to test. + * @returns {boolean} True if the node is a negative number that looks like a + * real literal and should be treated as such. + */ +function looksLikeLiteral(node) { + return (node.type === "UnaryExpression" && + node.operator === "-" && + node.prefix && + node.argument.type === "Literal" && + typeof node.argument.value === "number"); +} + +/** + * Attempts to derive a Literal node from nodes that are treated like literals. + * @param {ASTNode} node Node to normalize. + * @returns {ASTNode} The original node if the node is already a Literal, or a + * normalized Literal node with the negative number as the + * value if the node represents a negative number literal, + * otherwise null if the node cannot be converted to a + * normalized literal. + */ +function getNormalizedLiteral(node) { + if (node.type === "Literal") { + return node; + } + + if (looksLikeLiteral(node)) { + return { + type: "Literal", + value: -node.argument.value, + raw: "-" + node.argument.value + }; + } + + return null; +} + +/** + * Checks whether two expressions reference the same value. For example: + * a = a + * a.b = a.b + * a[0] = a[0] + * a['b'] = a['b'] + * @param {ASTNode} a Left side of the comparison. + * @param {ASTNode} b Right side of the comparison. + * @returns {boolean} True if both sides match and reference the same value. + */ +function same(a, b) { + if (a.type !== b.type) { + return false; + } + + switch (a.type) { + case "Identifier": + return a.name === b.name; + case "Literal": + return a.value === b.value; + case "MemberExpression": + // x[0] = x[0] + // x[y] = x[y] + // x.y = x.y + return same(a.object, b.object) && same(a.property, b.property); + case "ThisExpression": + return true; + default: + return false; + } +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function (context) { + + // Default to "never" (!always) if no option + var always = (context.options[0] === "always"); + var exceptRange = (context.options[1] && context.options[1].exceptRange); + + /** + * Determines whether node represents a range test. + * A range test is a "between" test like `(0 <= x && x < 1)` or an "outside" + * test like `(x < 0 || 1 <= x)`. It must be wrapped in parentheses, and + * both operators must be `<` or `<=`. Finally, the literal on the left side + * must be less than or equal to the literal on the right side so that the + * test makes any sense. + * @param {ASTNode} node LogicalExpression node to test. + * @returns {Boolean} Whether node is a range test. + */ + function isRangeTest(node) { + var left = node.left, + right = node.right; + + /** + * Determines whether node is of the form `0 <= x && x < 1`. + * @returns {Boolean} Whether node is a "between" range test. + */ + function isBetweenTest() { + var leftLiteral, rightLiteral; + + return (node.operator === "&&" && + (leftLiteral = getNormalizedLiteral(left.left)) && + (rightLiteral = getNormalizedLiteral(right.right)) && + leftLiteral.value <= rightLiteral.value && + same(left.right, right.left)); + } + + /** + * Determines whether node is of the form `x < 0 || 1 <= x`. + * @returns {Boolean} Whether node is an "outside" range test. + */ + function isOutsideTest() { + var leftLiteral, rightLiteral; + + return (node.operator === "||" && + (leftLiteral = getNormalizedLiteral(left.right)) && + (rightLiteral = getNormalizedLiteral(right.left)) && + leftLiteral.value <= rightLiteral.value && + same(left.left, right.right)); + } + + /** + * Determines whether node is wrapped in parentheses. + * @returns {Boolean} Whether node is preceded immediately by an open + * paren token and followed immediately by a close + * paren token. + */ + function isParenWrapped() { + var tokenBefore, tokenAfter; + + return ((tokenBefore = context.getTokenBefore(node)) && + tokenBefore.value === "(" && + (tokenAfter = context.getTokenAfter(node)) && + tokenAfter.value === ")"); + } + + return (node.type === "LogicalExpression" && + left.type === "BinaryExpression" && + right.type === "BinaryExpression" && + isRangeTestOperator(left.operator) && + isRangeTestOperator(right.operator) && + (isBetweenTest() || isOutsideTest()) && + isParenWrapped()); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + "BinaryExpression": always ? function(node) { + + // Comparisons must always be yoda-style: if ("blue" === color) + if ( + (node.right.type === "Literal" || looksLikeLiteral(node.right)) && + isComparisonOperator(node.operator) && + !(exceptRange && isRangeTest(context.getAncestors().pop())) + ) { + context.report(node, "Expected literal to be on the left side of " + node.operator + "."); + } + + } : function(node) { + + // Comparisons must never be yoda-style (default) + if ( + (node.left.type === "Literal" || looksLikeLiteral(node.left)) && + isComparisonOperator(node.operator) && + !(exceptRange && isRangeTest(context.getAncestors().pop())) + ) { + context.report(node, "Expected literal to be on the right side of " + node.operator + "."); + } + + } + }; + +}; |