From 42d1ecc9077328153f46dc5b6f972806777ec168 Mon Sep 17 00:00:00 2001 From: Vitor Balocco Date: Tue, 26 Apr 2016 21:38:46 +0200 Subject: [PATCH] Chore: Add metadata to existing rules - Batch 7 (refs #5417) (#5969) Chore: Add metadata to existing rules - Batch 7 of 7 (refs #5417) --- lib/rules/prefer-spread.js | 40 +- lib/rules/prefer-template.js | 80 +-- lib/rules/quote-props.js | 352 ++++++------ lib/rules/quotes.js | 306 ++++++----- lib/rules/radix.js | 154 +++--- lib/rules/require-jsdoc.js | 160 +++--- lib/rules/require-yield.js | 94 ++-- lib/rules/semi-spacing.js | 326 ++++++------ lib/rules/semi.js | 364 +++++++------ lib/rules/sort-imports.js | 278 +++++----- lib/rules/sort-vars.js | 88 +-- lib/rules/space-before-blocks.js | 236 +++++---- lib/rules/space-before-function-paren.js | 220 ++++---- lib/rules/space-in-parens.js | 452 ++++++++-------- lib/rules/space-infix-ops.js | 254 ++++----- lib/rules/space-unary-ops.js | 436 +++++++-------- lib/rules/spaced-comment.js | 290 +++++----- lib/rules/strict.js | 268 +++++----- lib/rules/template-curly-spacing.js | 154 +++--- lib/rules/use-isnan.js | 28 +- lib/rules/valid-jsdoc.js | 648 ++++++++++++----------- lib/rules/valid-typeof.js | 48 +- lib/rules/vars-on-top.js | 206 +++---- lib/rules/wrap-iife.js | 82 +-- lib/rules/wrap-regex.js | 56 +- lib/rules/yield-star-spacing.js | 168 +++--- lib/rules/yoda.js | 232 ++++---- 27 files changed, 3156 insertions(+), 2864 deletions(-) diff --git a/lib/rules/prefer-spread.js b/lib/rules/prefer-spread.js index c0181c6ef0d..79c7eb0243b 100644 --- a/lib/rules/prefer-spread.js +++ b/lib/rules/prefer-spread.js @@ -70,22 +70,32 @@ function isValidThisArg(expectedThis, thisArg, context) { // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - return { - CallExpression: function(node) { - if (!isVariadicApplyCalling(node)) { - return; - } +module.exports = { + meta: { + docs: { + description: "require spread operators instead of `.apply()`", + category: "ECMAScript 6", + recommended: false + }, + + schema: [] + }, + + create: function(context) { + return { + CallExpression: function(node) { + if (!isVariadicApplyCalling(node)) { + return; + } - var applied = node.callee.object; - var expectedThis = (applied.type === "MemberExpression") ? applied.object : null; - var thisArg = node.arguments[0]; + var applied = node.callee.object; + var expectedThis = (applied.type === "MemberExpression") ? applied.object : null; + var thisArg = node.arguments[0]; - if (isValidThisArg(expectedThis, thisArg, context)) { - context.report(node, "use the spread operator instead of the '.apply()'."); + if (isValidThisArg(expectedThis, thisArg, context)) { + context.report(node, "use the spread operator instead of the '.apply()'."); + } } - } - }; + }; + } }; - -module.exports.schema = []; diff --git a/lib/rules/prefer-template.js b/lib/rules/prefer-template.js index c7b7cc84786..0165aaecd65 100644 --- a/lib/rules/prefer-template.js +++ b/lib/rules/prefer-template.js @@ -54,43 +54,53 @@ function hasNonStringLiteral(node) { // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - var done = Object.create(null); - - /** - * Reports if a given node is string concatenation with non string literals. - * - * @param {ASTNode} node - A node to check. - * @returns {void} - */ - function checkForStringConcat(node) { - if (!astUtils.isStringLiteral(node) || !isConcatenation(node.parent)) { - return; - } - - var topBinaryExpr = getTopConcatBinaryExpression(node.parent); +module.exports = { + meta: { + docs: { + description: "require template literals instead of string concatenation", + category: "ECMAScript 6", + recommended: false + }, - // Checks whether or not this node had been checked already. - if (done[topBinaryExpr.range[0]]) { - return; + schema: [] + }, + + create: function(context) { + var done = Object.create(null); + + /** + * Reports if a given node is string concatenation with non string literals. + * + * @param {ASTNode} node - A node to check. + * @returns {void} + */ + function checkForStringConcat(node) { + if (!astUtils.isStringLiteral(node) || !isConcatenation(node.parent)) { + return; + } + + var topBinaryExpr = getTopConcatBinaryExpression(node.parent); + + // Checks whether or not this node had been checked already. + if (done[topBinaryExpr.range[0]]) { + return; + } + done[topBinaryExpr.range[0]] = true; + + if (hasNonStringLiteral(topBinaryExpr)) { + context.report( + topBinaryExpr, + "Unexpected string concatenation."); + } } - done[topBinaryExpr.range[0]] = true; - if (hasNonStringLiteral(topBinaryExpr)) { - context.report( - topBinaryExpr, - "Unexpected string concatenation."); - } - } + return { + Program: function() { + done = Object.create(null); + }, - return { - Program: function() { - done = Object.create(null); - }, - - Literal: checkForStringConcat, - TemplateLiteral: checkForStringConcat - }; + Literal: checkForStringConcat, + TemplateLiteral: checkForStringConcat + }; + } }; - -module.exports.schema = []; diff --git a/lib/rules/quote-props.js b/lib/rules/quote-props.js index 117eca21fdc..1fd2e74a5a3 100644 --- a/lib/rules/quote-props.js +++ b/lib/rules/quote-props.js @@ -15,207 +15,217 @@ var espree = require("espree"), // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - - var MODE = context.options[0], - KEYWORDS = context.options[1] && context.options[1].keywords, - CHECK_UNNECESSARY = !context.options[1] || context.options[1].unnecessary !== false, - NUMBERS = context.options[1] && context.options[1].numbers, - - MESSAGE_UNNECESSARY = "Unnecessarily quoted property '{{property}}' found.", - MESSAGE_UNQUOTED = "Unquoted property '{{property}}' found.", - MESSAGE_NUMERIC = "Unquoted number literal '{{property}}' used as key.", - MESSAGE_RESERVED = "Unquoted reserved word '{{property}}' used as key."; - - - /** - * Checks whether a certain string constitutes an ES3 token - * @param {string} tokenStr - The string to be checked. - * @returns {boolean} `true` if it is an ES3 token. - */ - function isKeyword(tokenStr) { - return keywords.indexOf(tokenStr) >= 0; - } - - /** - * Checks if an espree-tokenized key has redundant quotes (i.e. whether quotes are unnecessary) - * @param {string} rawKey The raw key value from the source - * @param {espreeTokens} tokens The espree-tokenized node key - * @param {boolean} [skipNumberLiterals=false] Indicates whether number literals should be checked - * @returns {boolean} Whether or not a key has redundant quotes. - * @private - */ - function areQuotesRedundant(rawKey, tokens, skipNumberLiterals) { - return tokens.length === 1 && tokens[0].start === 0 && tokens[0].end === rawKey.length && - (["Identifier", "Keyword", "Null", "Boolean"].indexOf(tokens[0].type) >= 0 || - (tokens[0].type === "Numeric" && !skipNumberLiterals && "" + +tokens[0].value === tokens[0].value)); - } +module.exports = { + meta: { + docs: { + description: "require quotes around object literal property names", + category: "Stylistic Issues", + recommended: false + }, - /** - * Ensures that a property's key is quoted only when necessary - * @param {ASTNode} node Property AST node - * @returns {void} - */ - function checkUnnecessaryQuotes(node) { - var key = node.key, - isKeywordToken, - tokens; - - if (node.method || node.computed || node.shorthand) { - return; + schema: { + anyOf: [ + { + type: "array", + items: [ + { + enum: ["always", "as-needed", "consistent", "consistent-as-needed"] + } + ], + minItems: 0, + maxItems: 1 + }, + { + type: "array", + items: [ + { + enum: ["always", "as-needed", "consistent", "consistent-as-needed"] + }, + { + type: "object", + properties: { + keywords: { + type: "boolean" + }, + unnecessary: { + type: "boolean" + }, + numbers: { + type: "boolean" + } + }, + additionalProperties: false + } + ], + minItems: 0, + maxItems: 2 + } + ] } + }, - if (key.type === "Literal" && typeof key.value === "string") { - try { - tokens = espree.tokenize(key.value); - } catch (e) { - return; - } + create: function(context) { - if (tokens.length !== 1) { - return; - } + var MODE = context.options[0], + KEYWORDS = context.options[1] && context.options[1].keywords, + CHECK_UNNECESSARY = !context.options[1] || context.options[1].unnecessary !== false, + NUMBERS = context.options[1] && context.options[1].numbers, - isKeywordToken = isKeyword(tokens[0].value); + MESSAGE_UNNECESSARY = "Unnecessarily quoted property '{{property}}' found.", + MESSAGE_UNQUOTED = "Unquoted property '{{property}}' found.", + MESSAGE_NUMERIC = "Unquoted number literal '{{property}}' used as key.", + MESSAGE_RESERVED = "Unquoted reserved word '{{property}}' used as key."; - if (isKeywordToken && KEYWORDS) { - return; - } - if (CHECK_UNNECESSARY && areQuotesRedundant(key.value, tokens, NUMBERS)) { - context.report(node, MESSAGE_UNNECESSARY, {property: key.value}); - } - } else if (KEYWORDS && key.type === "Identifier" && isKeyword(key.name)) { - context.report(node, MESSAGE_RESERVED, {property: key.name}); - } else if (NUMBERS && key.type === "Literal" && typeof key.value === "number") { - context.report(node, MESSAGE_NUMERIC, {property: key.value}); + /** + * Checks whether a certain string constitutes an ES3 token + * @param {string} tokenStr - The string to be checked. + * @returns {boolean} `true` if it is an ES3 token. + */ + function isKeyword(tokenStr) { + return keywords.indexOf(tokenStr) >= 0; } - } - /** - * Ensures that a property's key is quoted - * @param {ASTNode} node Property AST node - * @returns {void} - */ - function checkOmittedQuotes(node) { - var key = node.key; - - if (!node.method && !node.computed && !node.shorthand && !(key.type === "Literal" && typeof key.value === "string")) { - context.report(node, MESSAGE_UNQUOTED, { - property: key.name || key.value - }); + /** + * Checks if an espree-tokenized key has redundant quotes (i.e. whether quotes are unnecessary) + * @param {string} rawKey The raw key value from the source + * @param {espreeTokens} tokens The espree-tokenized node key + * @param {boolean} [skipNumberLiterals=false] Indicates whether number literals should be checked + * @returns {boolean} Whether or not a key has redundant quotes. + * @private + */ + function areQuotesRedundant(rawKey, tokens, skipNumberLiterals) { + return tokens.length === 1 && tokens[0].start === 0 && tokens[0].end === rawKey.length && + (["Identifier", "Keyword", "Null", "Boolean"].indexOf(tokens[0].type) >= 0 || + (tokens[0].type === "Numeric" && !skipNumberLiterals && "" + +tokens[0].value === tokens[0].value)); } - } - /** - * Ensures that an object's keys are consistently quoted, optionally checks for redundancy of quotes - * @param {ASTNode} node Property AST node - * @param {boolean} checkQuotesRedundancy Whether to check quotes' redundancy - * @returns {void} - */ - function checkConsistency(node, checkQuotesRedundancy) { - var quotes = false, - lackOfQuotes = false, - necessaryQuotes = false; - - node.properties.forEach(function(property) { - var key = property.key, + /** + * Ensures that a property's key is quoted only when necessary + * @param {ASTNode} node Property AST node + * @returns {void} + */ + function checkUnnecessaryQuotes(node) { + var key = node.key, + isKeywordToken, tokens; - if (!key || property.method || property.computed || property.shorthand) { + if (node.method || node.computed || node.shorthand) { return; } if (key.type === "Literal" && typeof key.value === "string") { + try { + tokens = espree.tokenize(key.value); + } catch (e) { + return; + } - quotes = true; + if (tokens.length !== 1) { + return; + } - if (checkQuotesRedundancy) { - try { - tokens = espree.tokenize(key.value); - } catch (e) { - necessaryQuotes = true; - return; - } + isKeywordToken = isKeyword(tokens[0].value); - necessaryQuotes = necessaryQuotes || !areQuotesRedundant(key.value, tokens) || KEYWORDS && isKeyword(tokens[0].value); + if (isKeywordToken && KEYWORDS) { + return; } - } else if (KEYWORDS && checkQuotesRedundancy && key.type === "Identifier" && isKeyword(key.name)) { - necessaryQuotes = true; - context.report(node, "Properties should be quoted as '{{property}}' is a reserved word.", {property: key.name}); - } else { - lackOfQuotes = true; - } - if (quotes && lackOfQuotes) { - context.report(node, "Inconsistently quoted property '{{key}}' found.", { - key: key.name || key.value - }); + if (CHECK_UNNECESSARY && areQuotesRedundant(key.value, tokens, NUMBERS)) { + context.report(node, MESSAGE_UNNECESSARY, {property: key.value}); + } + } else if (KEYWORDS && key.type === "Identifier" && isKeyword(key.name)) { + context.report(node, MESSAGE_RESERVED, {property: key.name}); + } else if (NUMBERS && key.type === "Literal" && typeof key.value === "number") { + context.report(node, MESSAGE_NUMERIC, {property: key.value}); } - }); - - if (checkQuotesRedundancy && quotes && !necessaryQuotes) { - context.report(node, "Properties shouldn't be quoted as all quotes are redundant."); } - } - return { - Property: function(node) { - if (MODE === "always" || !MODE) { - checkOmittedQuotes(node); - } - if (MODE === "as-needed") { - checkUnnecessaryQuotes(node); - } - }, - ObjectExpression: function(node) { - if (MODE === "consistent") { - checkConsistency(node, false); - } - if (MODE === "consistent-as-needed") { - checkConsistency(node, true); + /** + * Ensures that a property's key is quoted + * @param {ASTNode} node Property AST node + * @returns {void} + */ + function checkOmittedQuotes(node) { + var key = node.key; + + if (!node.method && !node.computed && !node.shorthand && !(key.type === "Literal" && typeof key.value === "string")) { + context.report(node, MESSAGE_UNQUOTED, { + property: key.name || key.value + }); } } - }; - -}; -module.exports.schema = { - anyOf: [ - { - type: "array", - items: [ - { - enum: ["always", "as-needed", "consistent", "consistent-as-needed"] + /** + * Ensures that an object's keys are consistently quoted, optionally checks for redundancy of quotes + * @param {ASTNode} node Property AST node + * @param {boolean} checkQuotesRedundancy Whether to check quotes' redundancy + * @returns {void} + */ + function checkConsistency(node, checkQuotesRedundancy) { + var quotes = false, + lackOfQuotes = false, + necessaryQuotes = false; + + node.properties.forEach(function(property) { + var key = property.key, + tokens; + + if (!key || property.method || property.computed || property.shorthand) { + return; } - ], - minItems: 0, - maxItems: 1 - }, - { - type: "array", - items: [ - { - enum: ["always", "as-needed", "consistent", "consistent-as-needed"] - }, - { - type: "object", - properties: { - keywords: { - type: "boolean" - }, - unnecessary: { - type: "boolean" - }, - numbers: { - type: "boolean" + + if (key.type === "Literal" && typeof key.value === "string") { + + quotes = true; + + if (checkQuotesRedundancy) { + try { + tokens = espree.tokenize(key.value); + } catch (e) { + necessaryQuotes = true; + return; } - }, - additionalProperties: false + + necessaryQuotes = necessaryQuotes || !areQuotesRedundant(key.value, tokens) || KEYWORDS && isKeyword(tokens[0].value); + } + } else if (KEYWORDS && checkQuotesRedundancy && key.type === "Identifier" && isKeyword(key.name)) { + necessaryQuotes = true; + context.report(node, "Properties should be quoted as '{{property}}' is a reserved word.", {property: key.name}); + } else { + lackOfQuotes = true; + } + + if (quotes && lackOfQuotes) { + context.report(node, "Inconsistently quoted property '{{key}}' found.", { + key: key.name || key.value + }); } - ], - minItems: 0, - maxItems: 2 + }); + + if (checkQuotesRedundancy && quotes && !necessaryQuotes) { + context.report(node, "Properties shouldn't be quoted as all quotes are redundant."); + } } - ] + + return { + Property: function(node) { + if (MODE === "always" || !MODE) { + checkOmittedQuotes(node); + } + if (MODE === "as-needed") { + checkUnnecessaryQuotes(node); + } + }, + ObjectExpression: function(node) { + if (MODE === "consistent") { + checkConsistency(node, false); + } + if (MODE === "consistent-as-needed") { + checkConsistency(node, true); + } + } + }; + + } }; diff --git a/lib/rules/quotes.js b/lib/rules/quotes.js index 558aeba16f9..d4e9203811c 100644 --- a/lib/rules/quotes.js +++ b/lib/rules/quotes.js @@ -72,177 +72,189 @@ var AVOID_ESCAPE = "avoid-escape", // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - - var quoteOption = context.options[0], - settings = QUOTE_SETTINGS[quoteOption || "double"], - options = context.options[1], - avoidEscape = options && options.avoidEscape === true, - allowTemplateLiterals = options && options.allowTemplateLiterals === true, - sourceCode = context.getSourceCode(); - - // deprecated - if (options === AVOID_ESCAPE) { - avoidEscape = true; - } +module.exports = { + meta: { + docs: { + description: "enforce the consistent use of either backticks, double, or single quotes", + category: "Stylistic Issues", + recommended: false + }, - /** - * 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; - } + fixable: "code", - /** - * Checks whether or not a given node is a directive. - * The directive is a `ExpressionStatement` which has only a string literal. - * @param {ASTNode} node - A node to check. - * @returns {boolean} Whether or not the node is a directive. - * @private - */ - function isDirective(node) { - return ( - node.type === "ExpressionStatement" && - node.expression.type === "Literal" && - typeof node.expression.value === "string" - ); - } + schema: [ + { + enum: ["single", "double", "backtick"] + }, + { + anyOf: [ + { + enum: ["avoid-escape"] + }, + { + type: "object", + properties: { + avoidEscape: { + type: "boolean" + }, + allowTemplateLiterals: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + } + ] + }, - /** - * Checks whether or not a given node is a part of directive prologues. - * See also: http://www.ecma-international.org/ecma-262/6.0/#sec-directive-prologues-and-the-use-strict-directive - * @param {ASTNode} node - A node to check. - * @returns {boolean} Whether or not the node is a part of directive prologues. - * @private - */ - function isPartOfDirectivePrologue(node) { - var block = node.parent.parent; - - if (block.type !== "Program" && (block.type !== "BlockStatement" || !FUNCTION_TYPE.test(block.parent.type))) { - return false; + create: function(context) { + + var quoteOption = context.options[0], + settings = QUOTE_SETTINGS[quoteOption || "double"], + options = context.options[1], + avoidEscape = options && options.avoidEscape === true, + allowTemplateLiterals = options && options.allowTemplateLiterals === true, + sourceCode = context.getSourceCode(); + + // deprecated + if (options === AVOID_ESCAPE) { + avoidEscape = true; } - // Check the node is at a prologue. - for (var i = 0; i < block.body.length; ++i) { - var statement = block.body[i]; + /** + * 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; + } + + /** + * Checks whether or not a given node is a directive. + * The directive is a `ExpressionStatement` which has only a string literal. + * @param {ASTNode} node - A node to check. + * @returns {boolean} Whether or not the node is a directive. + * @private + */ + function isDirective(node) { + return ( + node.type === "ExpressionStatement" && + node.expression.type === "Literal" && + typeof node.expression.value === "string" + ); + } - if (statement === node.parent) { - return true; + /** + * Checks whether or not a given node is a part of directive prologues. + * See also: http://www.ecma-international.org/ecma-262/6.0/#sec-directive-prologues-and-the-use-strict-directive + * @param {ASTNode} node - A node to check. + * @returns {boolean} Whether or not the node is a part of directive prologues. + * @private + */ + function isPartOfDirectivePrologue(node) { + var block = node.parent.parent; + + if (block.type !== "Program" && (block.type !== "BlockStatement" || !FUNCTION_TYPE.test(block.parent.type))) { + return false; } - if (!isDirective(statement)) { - break; + + // Check the node is at a prologue. + for (var i = 0; i < block.body.length; ++i) { + var statement = block.body[i]; + + if (statement === node.parent) { + return true; + } + if (!isDirective(statement)) { + break; + } } - } - return false; - } + return false; + } - /** - * Checks whether or not a given node is allowed as non backtick. - * @param {ASTNode} node - A node to check. - * @returns {boolean} Whether or not the node is allowed as non backtick. - * @private - */ - function isAllowedAsNonBacktick(node) { - var parent = node.parent; - - switch (parent.type) { - - // Directive Prologues. - case "ExpressionStatement": - return isPartOfDirectivePrologue(node); - - // LiteralPropertyName. - case "Property": - return parent.key === node && !parent.computed; - - // ModuleSpecifier. - case "ImportDeclaration": - case "ExportNamedDeclaration": - case "ExportAllDeclaration": - return parent.source === node; - - // Others don't allow. - default: - return false; + /** + * Checks whether or not a given node is allowed as non backtick. + * @param {ASTNode} node - A node to check. + * @returns {boolean} Whether or not the node is allowed as non backtick. + * @private + */ + function isAllowedAsNonBacktick(node) { + var parent = node.parent; + + switch (parent.type) { + + // Directive Prologues. + case "ExpressionStatement": + return isPartOfDirectivePrologue(node); + + // LiteralPropertyName. + case "Property": + return parent.key === node && !parent.computed; + + // ModuleSpecifier. + case "ImportDeclaration": + case "ExportNamedDeclaration": + case "ExportAllDeclaration": + return parent.source === node; + + // Others don't allow. + default: + return false; + } } - } - return { + return { + + Literal: function(node) { + var val = node.value, + rawVal = node.raw, + isValid; + + if (settings && typeof val === "string") { + isValid = (quoteOption === "backtick" && isAllowedAsNonBacktick(node)) || + isJSXElement(node.parent) || + astUtils.isSurroundedBy(rawVal, settings.quote); + + if (!isValid && avoidEscape) { + isValid = astUtils.isSurroundedBy(rawVal, settings.alternateQuote) && rawVal.indexOf(settings.quote) >= 0; + } - Literal: function(node) { - var val = node.value, - rawVal = node.raw, - isValid; + if (!isValid) { + context.report({ + node: node, + message: "Strings must use " + settings.description + ".", + fix: function(fixer) { + return fixer.replaceText(node, settings.convert(node.raw)); + } + }); + } + } + }, - if (settings && typeof val === "string") { - isValid = (quoteOption === "backtick" && isAllowedAsNonBacktick(node)) || - isJSXElement(node.parent) || - astUtils.isSurroundedBy(rawVal, settings.quote); + TemplateLiteral: function(node) { - if (!isValid && avoidEscape) { - isValid = astUtils.isSurroundedBy(rawVal, settings.alternateQuote) && rawVal.indexOf(settings.quote) >= 0; + // If backticks are expected or it's a tagged template, then this shouldn't throw an errors + if (allowTemplateLiterals || quoteOption === "backtick" || node.parent.type === "TaggedTemplateExpression") { + return; } - if (!isValid) { + var shouldWarn = node.quasis.length === 1 && (node.quasis[0].value.cooked.indexOf("\n") === -1); + + if (shouldWarn) { context.report({ node: node, message: "Strings must use " + settings.description + ".", fix: function(fixer) { - return fixer.replaceText(node, settings.convert(node.raw)); + return fixer.replaceText(node, settings.convert(sourceCode.getText(node))); } }); } } - }, + }; - TemplateLiteral: function(node) { - - // If backticks are expected or it's a tagged template, then this shouldn't throw an errors - if (allowTemplateLiterals || quoteOption === "backtick" || node.parent.type === "TaggedTemplateExpression") { - return; - } - - var shouldWarn = node.quasis.length === 1 && (node.quasis[0].value.cooked.indexOf("\n") === -1); - - if (shouldWarn) { - context.report({ - node: node, - message: "Strings must use " + settings.description + ".", - fix: function(fixer) { - return fixer.replaceText(node, settings.convert(sourceCode.getText(node))); - } - }); - } - } - }; - -}; - -module.exports.schema = [ - { - enum: ["single", "double", "backtick"] - }, - { - anyOf: [ - { - enum: ["avoid-escape"] - }, - { - type: "object", - properties: { - avoidEscape: { - type: "boolean" - }, - allowTemplateLiterals: { - type: "boolean" - } - }, - additionalProperties: false - } - ] } -]; +}; diff --git a/lib/rules/radix.js b/lib/rules/radix.js index 0560c58fb0e..05a1c130a9f 100644 --- a/lib/rules/radix.js +++ b/lib/rules/radix.js @@ -76,86 +76,96 @@ function isDefaultRadix(radix) { // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - var mode = context.options[0] || MODE_ALWAYS; - - /** - * Checks the arguments of a given CallExpression node and reports it if it - * offends this rule. - * - * @param {ASTNode} node - A CallExpression node to check. - * @returns {void} - */ - function checkArguments(node) { - var args = node.arguments; - - switch (args.length) { - case 0: - context.report({ - node: node, - message: "Missing parameters." - }); - break; - - case 1: - if (mode === MODE_ALWAYS) { - context.report({ - node: node, - message: "Missing radix parameter." - }); - } - break; - - default: - if (mode === MODE_AS_NEEDED && isDefaultRadix(args[1])) { - context.report({ - node: node, - message: "Redundant radix parameter." - }); - } else if (!isValidRadix(args[1])) { +module.exports = { + meta: { + docs: { + description: "enforce the consistent use of the radix argument when using `parseInt()`", + category: "Best Practices", + recommended: false + }, + + schema: [ + { + enum: ["always", "as-needed"] + } + ] + }, + + create: function(context) { + var mode = context.options[0] || MODE_ALWAYS; + + /** + * Checks the arguments of a given CallExpression node and reports it if it + * offends this rule. + * + * @param {ASTNode} node - A CallExpression node to check. + * @returns {void} + */ + function checkArguments(node) { + var args = node.arguments; + + switch (args.length) { + case 0: context.report({ node: node, - message: "Invalid radix parameter." + message: "Missing parameters." }); - } - break; + break; + + case 1: + if (mode === MODE_ALWAYS) { + context.report({ + node: node, + message: "Missing radix parameter." + }); + } + break; + + default: + if (mode === MODE_AS_NEEDED && isDefaultRadix(args[1])) { + context.report({ + node: node, + message: "Redundant radix parameter." + }); + } else if (!isValidRadix(args[1])) { + context.report({ + node: node, + message: "Invalid radix parameter." + }); + } + break; + } } - } - return { - "Program:exit": function() { - var scope = context.getScope(); - var variable; + return { + "Program:exit": function() { + var scope = context.getScope(); + var variable; - // Check `parseInt()` - variable = astUtils.getVariableByName(scope, "parseInt"); - if (!isShadowed(variable)) { - variable.references.forEach(function(reference) { - var node = reference.identifier; + // Check `parseInt()` + variable = astUtils.getVariableByName(scope, "parseInt"); + if (!isShadowed(variable)) { + variable.references.forEach(function(reference) { + var node = reference.identifier; - if (astUtils.isCallee(node)) { - checkArguments(node.parent); - } - }); - } + if (astUtils.isCallee(node)) { + checkArguments(node.parent); + } + }); + } - // Check `Number.parseInt()` - variable = astUtils.getVariableByName(scope, "Number"); - if (!isShadowed(variable)) { - variable.references.forEach(function(reference) { - var node = reference.identifier.parent; + // Check `Number.parseInt()` + variable = astUtils.getVariableByName(scope, "Number"); + if (!isShadowed(variable)) { + variable.references.forEach(function(reference) { + var node = reference.identifier.parent; - if (isParseIntMethod(node) && astUtils.isCallee(node)) { - checkArguments(node.parent); - } - }); + if (isParseIntMethod(node) && astUtils.isCallee(node)) { + checkArguments(node.parent); + } + }); + } } - } - }; -}; - -module.exports.schema = [ - { - enum: ["always", "as-needed"] + }; } -]; +}; diff --git a/lib/rules/require-jsdoc.js b/lib/rules/require-jsdoc.js index acf75cbcfcf..083e79b1f55 100644 --- a/lib/rules/require-jsdoc.js +++ b/lib/rules/require-jsdoc.js @@ -6,91 +6,101 @@ var lodash = require("lodash"); -module.exports = function(context) { - var source = context.getSourceCode(); - var DEFAULT_OPTIONS = { - FunctionDeclaration: true, - MethodDefinition: false, - ClassDeclaration: false - }; - var options = lodash.assign(DEFAULT_OPTIONS, context.options[0] && context.options[0].require || {}); +module.exports = { + meta: { + docs: { + description: "require JSDoc comments", + category: "Stylistic Issues", + recommended: false + }, - /** - * Report the error message - * @param {ASTNode} node node to report - * @returns {void} - */ - function report(node) { - context.report(node, "Missing JSDoc comment."); - } + schema: [ + { + type: "object", + properties: { + require: { + type: "object", + properties: { + ClassDeclaration: { + type: "boolean" + }, + MethodDefinition: { + type: "boolean" + }, + FunctionDeclaration: { + type: "boolean" + } + }, + additionalProperties: false + } + }, + additionalProperties: false + } + ] + }, - /** - * Check if the jsdoc comment is present for class methods - * @param {ASTNode} node node to examine - * @returns {void} - */ - function checkClassMethodJsDoc(node) { - if (node.parent.type === "MethodDefinition") { - var jsdocComment = source.getJSDocComment(node); + create: function(context) { + var source = context.getSourceCode(); + var DEFAULT_OPTIONS = { + FunctionDeclaration: true, + MethodDefinition: false, + ClassDeclaration: false + }; + var options = lodash.assign(DEFAULT_OPTIONS, context.options[0] && context.options[0].require || {}); - if (!jsdocComment) { - report(node); - } + /** + * Report the error message + * @param {ASTNode} node node to report + * @returns {void} + */ + function report(node) { + context.report(node, "Missing JSDoc comment."); } - } - /** - * Check if the jsdoc comment is present or not. - * @param {ASTNode} node node to examine - * @returns {void} - */ - function checkJsDoc(node) { - var jsdocComment = source.getJSDocComment(node); + /** + * Check if the jsdoc comment is present for class methods + * @param {ASTNode} node node to examine + * @returns {void} + */ + function checkClassMethodJsDoc(node) { + if (node.parent.type === "MethodDefinition") { + var jsdocComment = source.getJSDocComment(node); - if (!jsdocComment) { - report(node); + if (!jsdocComment) { + report(node); + } + } } - } - return { - FunctionDeclaration: function(node) { - if (options.FunctionDeclaration) { - checkJsDoc(node); - } - }, - FunctionExpression: function(node) { - if (options.MethodDefinition) { - checkClassMethodJsDoc(node); - } - }, - ClassDeclaration: function(node) { - if (options.ClassDeclaration) { - checkJsDoc(node); + /** + * Check if the jsdoc comment is present or not. + * @param {ASTNode} node node to examine + * @returns {void} + */ + function checkJsDoc(node) { + var jsdocComment = source.getJSDocComment(node); + + if (!jsdocComment) { + report(node); } } - }; -}; -module.exports.schema = [ - { - type: "object", - properties: { - require: { - type: "object", - properties: { - ClassDeclaration: { - type: "boolean" - }, - MethodDefinition: { - type: "boolean" - }, - FunctionDeclaration: { - type: "boolean" - } - }, - additionalProperties: false + return { + FunctionDeclaration: function(node) { + if (options.FunctionDeclaration) { + checkJsDoc(node); + } + }, + FunctionExpression: function(node) { + if (options.MethodDefinition) { + checkClassMethodJsDoc(node); + } + }, + ClassDeclaration: function(node) { + if (options.ClassDeclaration) { + checkJsDoc(node); + } } - }, - additionalProperties: false + }; } -]; +}; diff --git a/lib/rules/require-yield.js b/lib/rules/require-yield.js index e27ee63aa09..441d354ed80 100644 --- a/lib/rules/require-yield.js +++ b/lib/rules/require-yield.js @@ -9,55 +9,65 @@ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - var stack = []; - - /** - * If the node is a generator function, start counting `yield` keywords. - * @param {Node} node - A function node to check. - * @returns {void} - */ - function beginChecking(node) { - if (node.generator) { - stack.push(0); - } - } +module.exports = { + meta: { + docs: { + description: "require generator functions to contain `yield`", + category: "ECMAScript 6", + recommended: false + }, - /** - * If the node is a generator function, end counting `yield` keywords, then - * reports result. - * @param {Node} node - A function node to check. - * @returns {void} - */ - function endChecking(node) { - if (!node.generator) { - return; - } + schema: [] + }, - var countYield = stack.pop(); + create: function(context) { + var stack = []; - if (countYield === 0 && node.body.body.length > 0) { - context.report( - node, - "This generator function does not have 'yield'."); + /** + * If the node is a generator function, start counting `yield` keywords. + * @param {Node} node - A function node to check. + * @returns {void} + */ + function beginChecking(node) { + if (node.generator) { + stack.push(0); + } } - } - return { - FunctionDeclaration: beginChecking, - "FunctionDeclaration:exit": endChecking, - FunctionExpression: beginChecking, - "FunctionExpression:exit": endChecking, + /** + * If the node is a generator function, end counting `yield` keywords, then + * reports result. + * @param {Node} node - A function node to check. + * @returns {void} + */ + function endChecking(node) { + if (!node.generator) { + return; + } - // Increases the count of `yield` keyword. - YieldExpression: function() { + var countYield = stack.pop(); - /* istanbul ignore else */ - if (stack.length > 0) { - stack[stack.length - 1] += 1; + if (countYield === 0 && node.body.body.length > 0) { + context.report( + node, + "This generator function does not have 'yield'."); } } - }; -}; -module.exports.schema = []; + return { + FunctionDeclaration: beginChecking, + "FunctionDeclaration:exit": endChecking, + FunctionExpression: beginChecking, + "FunctionExpression:exit": endChecking, + + // Increases the count of `yield` keyword. + YieldExpression: function() { + + /* istanbul ignore else */ + if (stack.length > 0) { + stack[stack.length - 1] += 1; + } + } + }; + } +}; diff --git a/lib/rules/semi-spacing.js b/lib/rules/semi-spacing.js index 436ad41f191..ea43e9243df 100644 --- a/lib/rules/semi-spacing.js +++ b/lib/rules/semi-spacing.js @@ -11,200 +11,212 @@ var astUtils = require("../ast-utils"); // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: { + description: "enforce consistent spacing before and after semicolons", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + type: "object", + properties: { + before: { + type: "boolean" + }, + after: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, - var config = context.options[0], - requireSpaceBefore = false, - requireSpaceAfter = true, - sourceCode = context.getSourceCode(); + create: function(context) { - if (typeof config === "object") { - if (config.hasOwnProperty("before")) { - requireSpaceBefore = config.before; - } - if (config.hasOwnProperty("after")) { - requireSpaceAfter = config.after; + var config = context.options[0], + requireSpaceBefore = false, + requireSpaceAfter = true, + sourceCode = context.getSourceCode(); + + if (typeof config === "object") { + if (config.hasOwnProperty("before")) { + requireSpaceBefore = config.before; + } + if (config.hasOwnProperty("after")) { + requireSpaceAfter = config.after; + } } - } - /** - * 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); + /** + * 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 && astUtils.isTokenOnSameLine(tokenBefore, token) && sourceCode.isSpaceBetweenTokens(tokenBefore, token); - } + return tokenBefore && astUtils.isTokenOnSameLine(tokenBefore, token) && sourceCode.isSpaceBetweenTokens(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); + /** + * 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 && astUtils.isTokenOnSameLine(token, tokenAfter) && sourceCode.isSpaceBetweenTokens(token, tokenAfter); - } + return tokenAfter && astUtils.isTokenOnSameLine(token, tokenAfter) && sourceCode.isSpaceBetweenTokens(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); + /** + * 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 && astUtils.isTokenOnSameLine(token, tokenAfter)); - } + return !(tokenAfter && astUtils.isTokenOnSameLine(token, tokenAfter)); + } - /** - * Checks if the given token is the first token in its line - * @param {Token} token The token to check. - * @returns {boolean} Whether or not the token is the first in its line. - */ - function isFirstTokenInCurrentLine(token) { - var tokenBefore = context.getTokenBefore(token); + /** + * Checks if the given token is the first token in its line + * @param {Token} token The token to check. + * @returns {boolean} Whether or not the token is the first in its line. + */ + function isFirstTokenInCurrentLine(token) { + var tokenBefore = context.getTokenBefore(token); - return !(tokenBefore && astUtils.isTokenOnSameLine(token, tokenBefore)); - } + return !(tokenBefore && astUtils.isTokenOnSameLine(token, tokenBefore)); + } - /** - * Checks if the next token of a given token is a closing parenthesis. - * @param {Token} token The token to check. - * @returns {boolean} Whether or not the next token of a given token is a closing parenthesis. - */ - function isBeforeClosingParen(token) { - var nextToken = context.getTokenAfter(token); - - return ( - nextToken && - nextToken.type === "Punctuator" && - (nextToken.value === "}" || nextToken.value === ")") - ); - } + /** + * Checks if the next token of a given token is a closing parenthesis. + * @param {Token} token The token to check. + * @returns {boolean} Whether or not the next token of a given token is a closing parenthesis. + */ + function isBeforeClosingParen(token) { + var nextToken = context.getTokenAfter(token); + + return ( + nextToken && + nextToken.type === "Punctuator" && + (nextToken.value === "}" || nextToken.value === ")") + ); + } - /** - * 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 === ";"; - } + /** + * 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: node, - loc: location, - message: "Unexpected whitespace before semicolon.", - fix: function(fixer) { - var tokenBefore = context.getTokenBefore(token); - - return fixer.removeRange([tokenBefore.range[1], token.range[0]]); - } - }); - } - } else { - if (requireSpaceBefore) { - context.report({ - node: node, - loc: location, - message: "Missing whitespace before semicolon.", - fix: function(fixer) { - return fixer.insertTextBefore(token, " "); - } - }); - } - } + /** + * 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 (!isFirstTokenInCurrentLine(token) && !isLastTokenInCurrentLine(token) && !isBeforeClosingParen(token)) { - if (hasTrailingSpace(token)) { - if (!requireSpaceAfter) { + if (hasLeadingSpace(token)) { + if (!requireSpaceBefore) { context.report({ node: node, loc: location, - message: "Unexpected whitespace after semicolon.", + message: "Unexpected whitespace before semicolon.", fix: function(fixer) { - var tokenAfter = context.getTokenAfter(token); + var tokenBefore = context.getTokenBefore(token); - return fixer.removeRange([token.range[1], tokenAfter.range[0]]); + return fixer.removeRange([tokenBefore.range[1], token.range[0]]); } }); } } else { - if (requireSpaceAfter) { + if (requireSpaceBefore) { context.report({ node: node, loc: location, - message: "Missing whitespace after semicolon.", + message: "Missing whitespace before semicolon.", fix: function(fixer) { - return fixer.insertTextAfter(token, " "); + return fixer.insertTextBefore(token, " "); } }); } } + + if (!isFirstTokenInCurrentLine(token) && !isLastTokenInCurrentLine(token) && !isBeforeClosingParen(token)) { + if (hasTrailingSpace(token)) { + if (!requireSpaceAfter) { + context.report({ + node: node, + loc: location, + message: "Unexpected whitespace after semicolon.", + fix: function(fixer) { + var tokenAfter = context.getTokenAfter(token); + + return fixer.removeRange([token.range[1], tokenAfter.range[0]]); + } + }); + } + } else { + if (requireSpaceAfter) { + context.report({ + node: node, + loc: location, + message: "Missing whitespace after semicolon.", + fix: function(fixer) { + return fixer.insertTextAfter(token, " "); + } + }); + } + } + } } } - } - /** - * 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); + /** + * 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); - } + checkSemicolonSpacing(token, node); } - }; -}; -module.exports.schema = [ - { - type: "object", - properties: { - before: { - type: "boolean" - }, - after: { - type: "boolean" + 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); + } } - }, - additionalProperties: false + }; } -]; +}; diff --git a/lib/rules/semi.js b/lib/rules/semi.js index cb9854b74b1..e386084faf2 100644 --- a/lib/rules/semi.js +++ b/lib/rules/semi.js @@ -8,207 +8,219 @@ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - - var OPT_OUT_PATTERN = /[\[\(\/\+\-]/; // One of [(/+- - var options = context.options[1]; - var never = context.options[0] === "never", - exceptOneLine = options && options.omitLastInOneLineBlock === true, - sourceCode = context.getSourceCode(); - - //-------------------------------------------------------------------------- - // Helpers - //-------------------------------------------------------------------------- - - /** - * Reports a semicolon error with appropriate location and message. - * @param {ASTNode} node The node with an extra or missing semicolon. - * @param {boolean} missing True if the semicolon is missing. - * @returns {void} - */ - function report(node, missing) { - var message, - fix, - lastToken = sourceCode.getLastToken(node), - loc = lastToken.loc; - - if (!missing) { - message = "Missing semicolon."; - loc = loc.end; - fix = function(fixer) { - return fixer.insertTextAfter(lastToken, ";"); - }; - } else { - message = "Extra semicolon."; - loc = loc.start; - fix = function(fixer) { - return fixer.remove(lastToken); - }; - } +module.exports = { + meta: { + docs: { + description: "require or disallow semicolons instead of ASI", + category: "Stylistic Issues", + recommended: false + }, - context.report({ - node: node, - loc: loc, - message: message, - fix: fix - }); + fixable: "code", - } + schema: { + anyOf: [ + { + type: "array", + items: [ + { + enum: ["never"] + } + ], + minItems: 0, + maxItems: 1 + }, + { + type: "array", + items: [ + { + enum: ["always"] + }, + { + type: "object", + properties: { + omitLastInOneLineBlock: {type: "boolean"} + }, + additionalProperties: false + } + ], + minItems: 0, + maxItems: 2 + } + ] + } + }, + + create: function(context) { + + var OPT_OUT_PATTERN = /[\[\(\/\+\-]/; // One of [(/+- + var options = context.options[1]; + var never = context.options[0] === "never", + exceptOneLine = options && options.omitLastInOneLineBlock === true, + sourceCode = context.getSourceCode(); + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Reports a semicolon error with appropriate location and message. + * @param {ASTNode} node The node with an extra or missing semicolon. + * @param {boolean} missing True if the semicolon is missing. + * @returns {void} + */ + function report(node, missing) { + var message, + fix, + lastToken = sourceCode.getLastToken(node), + loc = lastToken.loc; + + if (!missing) { + message = "Missing semicolon."; + loc = loc.end; + fix = function(fixer) { + return fixer.insertTextAfter(lastToken, ";"); + }; + } else { + message = "Extra semicolon."; + loc = loc.start; + fix = function(fixer) { + return fixer.remove(lastToken); + }; + } - /** - * 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 === ";"); - } + context.report({ + node: node, + loc: loc, + message: message, + fix: fix + }); - /** - * 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; + /** + * 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 === ";"); } - lastTokenLine = lastToken.loc.end.line; - nextTokenLine = nextToken.loc.start.line; - isOptOutToken = OPT_OUT_PATTERN.test(nextToken.value); - isDivider = (nextToken.value === "}" || nextToken.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; - return (lastTokenLine !== nextTokenLine && !isOptOutToken) || isDivider; - } + if (!isSemicolon(lastToken)) { + return false; + } - /** - * Checks a node to see if it's in a one-liner block statement. - * @param {ASTNode} node The node to check. - * @returns {boolean} whether the node is in a one-liner block statement. - */ - function isOneLinerBlock(node) { - var nextToken = context.getTokenAfter(node); + nextToken = context.getTokenAfter(lastToken); - if (!nextToken || nextToken.value !== "}") { - return false; - } + if (!nextToken) { + return true; + } - var parent = node.parent; + lastTokenLine = lastToken.loc.end.line; + nextTokenLine = nextToken.loc.start.line; + isOptOutToken = OPT_OUT_PATTERN.test(nextToken.value); + isDivider = (nextToken.value === "}" || nextToken.value === ";"); - return parent && parent.type === "BlockStatement" && - parent.loc.start.line === parent.loc.end.line; - } + return (lastTokenLine !== nextTokenLine && !isOptOutToken) || isDivider; + } + + /** + * Checks a node to see if it's in a one-liner block statement. + * @param {ASTNode} node The node to check. + * @returns {boolean} whether the node is in a one-liner block statement. + */ + function isOneLinerBlock(node) { + var nextToken = context.getTokenAfter(node); - /** - * 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 (never) { - if (isUnnecessarySemicolon(lastToken)) { - report(node, true); + if (!nextToken || nextToken.value !== "}") { + return false; } - } else { - if (!isSemicolon(lastToken)) { - if (!exceptOneLine || !isOneLinerBlock(node)) { - report(node); + + var parent = node.parent; + + return parent && parent.type === "BlockStatement" && + parent.loc.start.line === parent.loc.end.line; + } + + /** + * 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 (never) { + if (isUnnecessarySemicolon(lastToken)) { + report(node, true); } } else { - if (exceptOneLine && isOneLinerBlock(node)) { - report(node, true); + if (!isSemicolon(lastToken)) { + if (!exceptOneLine || !isOneLinerBlock(node)) { + report(node); + } + } else { + if (exceptOneLine && isOneLinerBlock(node)) { + report(node, true); + } } } } - } - - /** - * 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, - DoWhileStatement: 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)) { + /** + * 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); } } - }; -}; - -module.exports.schema = { - anyOf: [ - { - type: "array", - items: [ - { - enum: ["never"] + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + VariableDeclaration: checkForSemicolonForVariableDeclaration, + ExpressionStatement: checkForSemicolon, + ReturnStatement: checkForSemicolon, + ThrowStatement: checkForSemicolon, + DoWhileStatement: checkForSemicolon, + DebuggerStatement: checkForSemicolon, + BreakStatement: checkForSemicolon, + ContinueStatement: checkForSemicolon, + ImportDeclaration: checkForSemicolon, + ExportAllDeclaration: checkForSemicolon, + ExportNamedDeclaration: function(node) { + if (!node.declaration) { + checkForSemicolon(node); } - ], - minItems: 0, - maxItems: 1 - }, - { - type: "array", - items: [ - { - enum: ["always"] - }, - { - type: "object", - properties: { - omitLastInOneLineBlock: {type: "boolean"} - }, - additionalProperties: false + }, + ExportDefaultDeclaration: function(node) { + if (!/(?:Class|Function)Declaration/.test(node.declaration.type)) { + checkForSemicolon(node); } - ], - minItems: 0, - maxItems: 2 - } - ] + } + }; + + } }; diff --git a/lib/rules/sort-imports.js b/lib/rules/sort-imports.js index 29a99b0081b..18b34f86a09 100644 --- a/lib/rules/sort-imports.js +++ b/lib/rules/sort-imports.js @@ -9,157 +9,167 @@ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - - var configuration = context.options[0] || {}, - ignoreCase = configuration.ignoreCase || false, - ignoreMemberSort = configuration.ignoreMemberSort || false, - memberSyntaxSortOrder = configuration.memberSyntaxSortOrder || ["none", "all", "multiple", "single"], - previousDeclaration = null; - - /** - * Gets the used member syntax style. - * - * import "my-module.js" --> none - * import * as myModule from "my-module.js" --> all - * import {myMember} from "my-module.js" --> single - * import {foo, bar} from "my-module.js" --> multiple - * - * @param {ASTNode} node - the ImportDeclaration node. - * @returns {string} used member parameter style, ["all", "multiple", "single"] - */ - function usedMemberSyntax(node) { - if (node.specifiers.length === 0) { - return "none"; - } else if (node.specifiers[0].type === "ImportNamespaceSpecifier") { - return "all"; - } else if (node.specifiers.length === 1) { - return "single"; - } else { - return "multiple"; +module.exports = { + meta: { + docs: { + description: "enforce sorted import declarations within modules", + category: "ECMAScript 6", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + ignoreCase: { + type: "boolean" + }, + memberSyntaxSortOrder: { + type: "array", + items: { + enum: ["none", "all", "multiple", "single"] + }, + uniqueItems: true, + minItems: 4, + maxItems: 4 + }, + ignoreMemberSort: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create: function(context) { + + var configuration = context.options[0] || {}, + ignoreCase = configuration.ignoreCase || false, + ignoreMemberSort = configuration.ignoreMemberSort || false, + memberSyntaxSortOrder = configuration.memberSyntaxSortOrder || ["none", "all", "multiple", "single"], + previousDeclaration = null; + + /** + * Gets the used member syntax style. + * + * import "my-module.js" --> none + * import * as myModule from "my-module.js" --> all + * import {myMember} from "my-module.js" --> single + * import {foo, bar} from "my-module.js" --> multiple + * + * @param {ASTNode} node - the ImportDeclaration node. + * @returns {string} used member parameter style, ["all", "multiple", "single"] + */ + function usedMemberSyntax(node) { + if (node.specifiers.length === 0) { + return "none"; + } else if (node.specifiers[0].type === "ImportNamespaceSpecifier") { + return "all"; + } else if (node.specifiers.length === 1) { + return "single"; + } else { + return "multiple"; + } } - } - /** - * Gets the group by member parameter index for given declaration. - * @param {ASTNode} node - the ImportDeclaration node. - * @returns {number} the declaration group by member index. - */ - function getMemberParameterGroupIndex(node) { - return memberSyntaxSortOrder.indexOf(usedMemberSyntax(node)); - } + /** + * Gets the group by member parameter index for given declaration. + * @param {ASTNode} node - the ImportDeclaration node. + * @returns {number} the declaration group by member index. + */ + function getMemberParameterGroupIndex(node) { + return memberSyntaxSortOrder.indexOf(usedMemberSyntax(node)); + } - /** - * Gets the local name of the first imported module. - * @param {ASTNode} node - the ImportDeclaration node. - * @returns {?string} the local name of the first imported module. - */ - function getFirstLocalMemberName(node) { - if (node.specifiers[0]) { - return node.specifiers[0].local.name; - } else { - return null; + /** + * Gets the local name of the first imported module. + * @param {ASTNode} node - the ImportDeclaration node. + * @returns {?string} the local name of the first imported module. + */ + function getFirstLocalMemberName(node) { + if (node.specifiers[0]) { + return node.specifiers[0].local.name; + } else { + return null; + } } - } - return { - ImportDeclaration: function(node) { - if (previousDeclaration) { - var currentLocalMemberName = getFirstLocalMemberName(node), - currentMemberSyntaxGroupIndex = getMemberParameterGroupIndex(node), - previousLocalMemberName = getFirstLocalMemberName(previousDeclaration), - previousMemberSyntaxGroupIndex = getMemberParameterGroupIndex(previousDeclaration); - - if (ignoreCase) { - previousLocalMemberName = previousLocalMemberName && previousLocalMemberName.toLowerCase(); - currentLocalMemberName = currentLocalMemberName && currentLocalMemberName.toLowerCase(); - } + return { + ImportDeclaration: function(node) { + if (previousDeclaration) { + var currentLocalMemberName = getFirstLocalMemberName(node), + currentMemberSyntaxGroupIndex = getMemberParameterGroupIndex(node), + previousLocalMemberName = getFirstLocalMemberName(previousDeclaration), + previousMemberSyntaxGroupIndex = getMemberParameterGroupIndex(previousDeclaration); - // When the current declaration uses a different member syntax, - // then check if the ordering is correct. - // Otherwise, make a default string compare (like rule sort-vars to be consistent) of the first used local member name. - if (currentMemberSyntaxGroupIndex !== previousMemberSyntaxGroupIndex) { - if (currentMemberSyntaxGroupIndex < previousMemberSyntaxGroupIndex) { - context.report({ - node: node, - message: "Expected '{{syntaxA}}' syntax before '{{syntaxB}}' syntax.", - data: { - syntaxA: memberSyntaxSortOrder[currentMemberSyntaxGroupIndex], - syntaxB: memberSyntaxSortOrder[previousMemberSyntaxGroupIndex] - } - }); + if (ignoreCase) { + previousLocalMemberName = previousLocalMemberName && previousLocalMemberName.toLowerCase(); + currentLocalMemberName = currentLocalMemberName && currentLocalMemberName.toLowerCase(); } - } else { - if (previousLocalMemberName && - currentLocalMemberName && - currentLocalMemberName < previousLocalMemberName - ) { - context.report({ - node: node, - message: "Imports should be sorted alphabetically." - }); + + // When the current declaration uses a different member syntax, + // then check if the ordering is correct. + // Otherwise, make a default string compare (like rule sort-vars to be consistent) of the first used local member name. + if (currentMemberSyntaxGroupIndex !== previousMemberSyntaxGroupIndex) { + if (currentMemberSyntaxGroupIndex < previousMemberSyntaxGroupIndex) { + context.report({ + node: node, + message: "Expected '{{syntaxA}}' syntax before '{{syntaxB}}' syntax.", + data: { + syntaxA: memberSyntaxSortOrder[currentMemberSyntaxGroupIndex], + syntaxB: memberSyntaxSortOrder[previousMemberSyntaxGroupIndex] + } + }); + } + } else { + if (previousLocalMemberName && + currentLocalMemberName && + currentLocalMemberName < previousLocalMemberName + ) { + context.report({ + node: node, + message: "Imports should be sorted alphabetically." + }); + } } } - } - // Multiple members of an import declaration should also be sorted alphabetically. - if (!ignoreMemberSort && node.specifiers.length > 1) { - var previousSpecifier = null; - var previousSpecifierName = null; + // Multiple members of an import declaration should also be sorted alphabetically. + if (!ignoreMemberSort && node.specifiers.length > 1) { + var previousSpecifier = null; + var previousSpecifierName = null; - for (var i = 0; i < node.specifiers.length; ++i) { - var currentSpecifier = node.specifiers[i]; + for (var i = 0; i < node.specifiers.length; ++i) { + var currentSpecifier = node.specifiers[i]; - if (currentSpecifier.type !== "ImportSpecifier") { - continue; - } + if (currentSpecifier.type !== "ImportSpecifier") { + continue; + } - var currentSpecifierName = currentSpecifier.local.name; + var currentSpecifierName = currentSpecifier.local.name; - if (ignoreCase) { - currentSpecifierName = currentSpecifierName.toLowerCase(); - } + if (ignoreCase) { + currentSpecifierName = currentSpecifierName.toLowerCase(); + } - if (previousSpecifier && currentSpecifierName < previousSpecifierName) { - context.report({ - node: currentSpecifier, - message: "Member '{{memberName}}' of the import declaration should be sorted alphabetically.", - data: { - memberName: currentSpecifier.local.name - } - }); - } + if (previousSpecifier && currentSpecifierName < previousSpecifierName) { + context.report({ + node: currentSpecifier, + message: "Member '{{memberName}}' of the import declaration should be sorted alphabetically.", + data: { + memberName: currentSpecifier.local.name + } + }); + } - previousSpecifier = currentSpecifier; - previousSpecifierName = currentSpecifierName; + previousSpecifier = currentSpecifier; + previousSpecifierName = currentSpecifierName; + } } - } - previousDeclaration = node; - } - }; -}; - -module.exports.schema = [ - { - type: "object", - properties: { - ignoreCase: { - type: "boolean" - }, - memberSyntaxSortOrder: { - type: "array", - items: { - enum: ["none", "all", "multiple", "single"] - }, - uniqueItems: true, - minItems: 4, - maxItems: 4 - }, - ignoreMemberSort: { - type: "boolean" + previousDeclaration = node; } - }, - additionalProperties: false + }; } -]; +}; diff --git a/lib/rules/sort-vars.js b/lib/rules/sort-vars.js index 76466a149ff..8a7b0e710a7 100644 --- a/lib/rules/sort-vars.js +++ b/lib/rules/sort-vars.js @@ -9,45 +9,55 @@ // 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) { - if (decl.id.type === "ObjectPattern" || decl.id.type === "ArrayPattern") { - return memo; - } - - 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]); - } - }; -}; +module.exports = { + meta: { + docs: { + description: "require variables within the same declaration block to be sorted", + category: "Stylistic Issues", + recommended: false + }, -module.exports.schema = [ - { - type: "object", - properties: { - ignoreCase: { - type: "boolean" + schema: [ + { + type: "object", + properties: { + ignoreCase: { + type: "boolean" + } + }, + additionalProperties: false } - }, - additionalProperties: false + ] + }, + + create: function(context) { + + var configuration = context.options[0] || {}, + ignoreCase = configuration.ignoreCase || false; + + return { + VariableDeclaration: function(node) { + node.declarations.reduce(function(memo, decl) { + if (decl.id.type === "ObjectPattern" || decl.id.type === "ArrayPattern") { + return memo; + } + + 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/lib/rules/space-before-blocks.js b/lib/rules/space-before-blocks.js index e9bdc0207b6..7fb9d5cddcc 100644 --- a/lib/rules/space-before-blocks.js +++ b/lib/rules/space-before-blocks.js @@ -11,129 +11,141 @@ var astUtils = require("../ast-utils"); // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - var config = context.options[0], - sourceCode = context.getSourceCode(), - checkFunctions = true, - checkKeywords = true, - checkClasses = true; - - if (typeof config === "object") { - checkFunctions = config.functions !== "never"; - checkKeywords = config.keywords !== "never"; - checkClasses = config.classes !== "never"; - } else if (config === "never") { - checkFunctions = false; - checkKeywords = false; - checkClasses = false; - } +module.exports = { + meta: { + docs: { + description: "enforce consistent spacing before blocks", + category: "Stylistic Issues", + recommended: false + }, - /** - * Checks whether or not a given token is an arrow operator (=>) or a keyword - * in order to avoid to conflict with `arrow-spacing` and `keyword-spacing`. - * - * @param {Token} token - A token to check. - * @returns {boolean} `true` if the token is an arrow operator. - */ - function isConflicted(token) { - return (token.type === "Punctuator" && token.value === "=>") || token.type === "Keyword"; - } + fixable: "whitespace", - /** - * 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, - parent, - requireSpace; - - if (precedingToken && !isConflicted(precedingToken) && astUtils.isTokenOnSameLine(precedingToken, node)) { - hasSpace = sourceCode.isSpaceBetweenTokens(precedingToken, node); - parent = context.getAncestors().pop(); - if (parent.type === "FunctionExpression" || parent.type === "FunctionDeclaration") { - requireSpace = checkFunctions; - } else if (node.type === "ClassBody") { - requireSpace = checkClasses; - } else { - requireSpace = checkKeywords; + schema: [ + { + oneOf: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + keywords: { + enum: ["always", "never"] + }, + functions: { + enum: ["always", "never"] + }, + classes: { + enum: ["always", "never"] + } + }, + additionalProperties: false + } + ] } + ] + }, + + create: function(context) { + var config = context.options[0], + sourceCode = context.getSourceCode(), + checkFunctions = true, + checkKeywords = true, + checkClasses = true; - if (requireSpace) { - if (!hasSpace) { - context.report({ - node: node, - message: "Missing space before opening brace.", - fix: function(fixer) { - return fixer.insertTextBefore(node, " "); - } - }); + if (typeof config === "object") { + checkFunctions = config.functions !== "never"; + checkKeywords = config.keywords !== "never"; + checkClasses = config.classes !== "never"; + } else if (config === "never") { + checkFunctions = false; + checkKeywords = false; + checkClasses = false; + } + + /** + * Checks whether or not a given token is an arrow operator (=>) or a keyword + * in order to avoid to conflict with `arrow-spacing` and `keyword-spacing`. + * + * @param {Token} token - A token to check. + * @returns {boolean} `true` if the token is an arrow operator. + */ + function isConflicted(token) { + return (token.type === "Punctuator" && token.value === "=>") || token.type === "Keyword"; + } + + /** + * 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, + parent, + requireSpace; + + if (precedingToken && !isConflicted(precedingToken) && astUtils.isTokenOnSameLine(precedingToken, node)) { + hasSpace = sourceCode.isSpaceBetweenTokens(precedingToken, node); + parent = context.getAncestors().pop(); + if (parent.type === "FunctionExpression" || parent.type === "FunctionDeclaration") { + requireSpace = checkFunctions; + } else if (node.type === "ClassBody") { + requireSpace = checkClasses; + } else { + requireSpace = checkKeywords; } - } else { - if (hasSpace) { - context.report({ - node: node, - message: "Unexpected space before opening brace.", - fix: function(fixer) { - return fixer.removeRange([precedingToken.range[1], node.range[0]]); - } - }); + + if (requireSpace) { + if (!hasSpace) { + context.report({ + node: node, + message: "Missing space before opening brace.", + fix: function(fixer) { + return fixer.insertTextBefore(node, " "); + } + }); + } + } else { + if (hasSpace) { + context.report({ + node: node, + message: "Unexpected space before opening brace.", + fix: function(fixer) { + return fixer.removeRange([precedingToken.range[1], node.range[0]]); + } + }); + } } } } - } - /** - * 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); - } + /** + * 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; - checkPrecedingSpace(openingBrace); - } + if (cases.length > 0) { + firstCase = cases[0]; + openingBrace = context.getTokenBefore(firstCase); + } else { + openingBrace = context.getLastToken(node, 1); + } - return { - BlockStatement: checkPrecedingSpace, - ClassBody: checkPrecedingSpace, - SwitchStatement: checkSpaceBeforeCaseBlock - }; + checkPrecedingSpace(openingBrace); + } -}; + return { + BlockStatement: checkPrecedingSpace, + ClassBody: checkPrecedingSpace, + SwitchStatement: checkSpaceBeforeCaseBlock + }; -module.exports.schema = [ - { - oneOf: [ - { - enum: ["always", "never"] - }, - { - type: "object", - properties: { - keywords: { - enum: ["always", "never"] - }, - functions: { - enum: ["always", "never"] - }, - classes: { - enum: ["always", "never"] - } - }, - additionalProperties: false - } - ] } -]; +}; diff --git a/lib/rules/space-before-function-paren.js b/lib/rules/space-before-function-paren.js index d9092061d8d..d96cb4a6085 100644 --- a/lib/rules/space-before-function-paren.js +++ b/lib/rules/space-before-function-paren.js @@ -8,123 +8,135 @@ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: { + description: "enforce consistent spacing before `function` definition opening parenthesis", + category: "Stylistic Issues", + recommended: false + }, - var configuration = context.options[0], - sourceCode = context.getSourceCode(), - requireAnonymousFunctionSpacing = true, - forbidAnonymousFunctionSpacing = false, - requireNamedFunctionSpacing = true, - forbidNamedFunctionSpacing = false; + fixable: "whitespace", - if (typeof configuration === "object") { - requireAnonymousFunctionSpacing = ( - !configuration.anonymous || configuration.anonymous === "always"); - forbidAnonymousFunctionSpacing = configuration.anonymous === "never"; - requireNamedFunctionSpacing = ( - !configuration.named || configuration.named === "always"); - forbidNamedFunctionSpacing = configuration.named === "never"; - } else if (configuration === "never") { - requireAnonymousFunctionSpacing = false; - forbidAnonymousFunctionSpacing = true; - requireNamedFunctionSpacing = false; - forbidNamedFunctionSpacing = true; - } + schema: [ + { + oneOf: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + anonymous: { + enum: ["always", "never", "ignore"] + }, + named: { + enum: ["always", "never", "ignore"] + } + }, + additionalProperties: false + } + ] + } + ] + }, + + create: function(context) { - /** - * 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; + var configuration = context.options[0], + sourceCode = context.getSourceCode(), + requireAnonymousFunctionSpacing = true, + forbidAnonymousFunctionSpacing = false, + requireNamedFunctionSpacing = true, + forbidNamedFunctionSpacing = false; - if (node.id) { - return true; + if (typeof configuration === "object") { + requireAnonymousFunctionSpacing = ( + !configuration.anonymous || configuration.anonymous === "always"); + forbidAnonymousFunctionSpacing = configuration.anonymous === "never"; + requireNamedFunctionSpacing = ( + !configuration.named || configuration.named === "always"); + forbidNamedFunctionSpacing = configuration.named === "never"; + } else if (configuration === "never") { + requireAnonymousFunctionSpacing = false; + forbidAnonymousFunctionSpacing = true; + requireNamedFunctionSpacing = false; + forbidNamedFunctionSpacing = true; } - parent = node.parent; - return parent.type === "MethodDefinition" || - (parent.type === "Property" && - ( - parent.kind === "get" || - parent.kind === "set" || - parent.method - ) - ); - } + /** + * 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; - /** - * Validates the spacing before function parentheses. - * @param {ASTNode} node The node to be validated. - * @returns {void} - */ - function validateSpacingBeforeParentheses(node) { - var isNamed = isNamedFunction(node), - leftToken, - rightToken, - location; + if (node.id) { + return true; + } - if (node.generator && !isNamed) { - return; + parent = node.parent; + return parent.type === "MethodDefinition" || + (parent.type === "Property" && + ( + parent.kind === "get" || + parent.kind === "set" || + parent.method + ) + ); } - rightToken = sourceCode.getFirstToken(node); - while (rightToken.value !== "(") { - rightToken = sourceCode.getTokenAfter(rightToken); - } - leftToken = context.getTokenBefore(rightToken); - location = leftToken.loc.end; + /** + * Validates the spacing before function parentheses. + * @param {ASTNode} node The node to be validated. + * @returns {void} + */ + function validateSpacingBeforeParentheses(node) { + var isNamed = isNamedFunction(node), + leftToken, + rightToken, + location; - if (sourceCode.isSpaceBetweenTokens(leftToken, rightToken)) { - if ((isNamed && forbidNamedFunctionSpacing) || (!isNamed && forbidAnonymousFunctionSpacing)) { - context.report({ - node: node, - loc: location, - message: "Unexpected space before function parentheses.", - fix: function(fixer) { - return fixer.removeRange([leftToken.range[1], rightToken.range[0]]); - } - }); - } - } else { - if ((isNamed && requireNamedFunctionSpacing) || (!isNamed && requireAnonymousFunctionSpacing)) { - context.report({ - node: node, - loc: location, - message: "Missing space before function parentheses.", - fix: function(fixer) { - return fixer.insertTextAfter(leftToken, " "); - } - }); + if (node.generator && !isNamed) { + return; } - } - } - return { - FunctionDeclaration: validateSpacingBeforeParentheses, - FunctionExpression: validateSpacingBeforeParentheses - }; -}; + rightToken = sourceCode.getFirstToken(node); + while (rightToken.value !== "(") { + rightToken = sourceCode.getTokenAfter(rightToken); + } + leftToken = context.getTokenBefore(rightToken); + location = leftToken.loc.end; -module.exports.schema = [ - { - oneOf: [ - { - enum: ["always", "never"] - }, - { - type: "object", - properties: { - anonymous: { - enum: ["always", "never", "ignore"] - }, - named: { - enum: ["always", "never", "ignore"] - } - }, - additionalProperties: false + if (sourceCode.isSpaceBetweenTokens(leftToken, rightToken)) { + if ((isNamed && forbidNamedFunctionSpacing) || (!isNamed && forbidAnonymousFunctionSpacing)) { + context.report({ + node: node, + loc: location, + message: "Unexpected space before function parentheses.", + fix: function(fixer) { + return fixer.removeRange([leftToken.range[1], rightToken.range[0]]); + } + }); + } + } else { + if ((isNamed && requireNamedFunctionSpacing) || (!isNamed && requireAnonymousFunctionSpacing)) { + context.report({ + node: node, + loc: location, + message: "Missing space before function parentheses.", + fix: function(fixer) { + return fixer.insertTextAfter(leftToken, " "); + } + }); + } } - ] + } + + return { + FunctionDeclaration: validateSpacingBeforeParentheses, + FunctionExpression: validateSpacingBeforeParentheses + }; } -]; +}; diff --git a/lib/rules/space-in-parens.js b/lib/rules/space-in-parens.js index af1f3353717..11361aec120 100644 --- a/lib/rules/space-in-parens.js +++ b/lib/rules/space-in-parens.js @@ -10,260 +10,272 @@ var astUtils = require("../ast-utils"); // 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.", - ALWAYS = context.options[0] === "always", - - exceptionsArrayOptions = (context.options.length === 2) ? context.options[1].exceptions : [], - options = {}, - exceptions; +module.exports = { + meta: { + docs: { + description: "enforce consistent spacing inside parentheses", + category: "Stylistic Issues", + recommended: false + }, - if (exceptionsArrayOptions.length) { - options.braceException = exceptionsArrayOptions.indexOf("{}") !== -1; - options.bracketException = exceptionsArrayOptions.indexOf("[]") !== -1; - options.parenException = exceptionsArrayOptions.indexOf("()") !== -1; - options.empty = exceptionsArrayOptions.indexOf("empty") !== -1; - } + fixable: "whitespace", + + schema: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + exceptions: { + type: "array", + items: { + enum: ["{}", "[]", "()", "empty"] + }, + uniqueItems: true + } + }, + additionalProperties: false + } + ] + }, - /** - * Produces an object with the opener and closer exception values - * @param {Object} opts The exception options - * @returns {Object} `openers` and `closers` exception values - * @private - */ - function getExceptions() { - var openers = [], - closers = []; - - if (options.braceException) { - openers.push("{"); - closers.push("}"); - } + create: function(context) { - if (options.bracketException) { - openers.push("["); - closers.push("]"); - } + var MISSING_SPACE_MESSAGE = "There must be a space inside this paren.", + REJECTED_SPACE_MESSAGE = "There should be no spaces inside this paren.", + ALWAYS = context.options[0] === "always", - if (options.parenException) { - openers.push("("); - closers.push(")"); - } + exceptionsArrayOptions = (context.options.length === 2) ? context.options[1].exceptions : [], + options = {}, + exceptions; - if (options.empty) { - openers.push(")"); - closers.push("("); + if (exceptionsArrayOptions.length) { + options.braceException = exceptionsArrayOptions.indexOf("{}") !== -1; + options.bracketException = exceptionsArrayOptions.indexOf("[]") !== -1; + options.parenException = exceptionsArrayOptions.indexOf("()") !== -1; + options.empty = exceptionsArrayOptions.indexOf("empty") !== -1; } - return { - openers: openers, - closers: closers - }; - } - - //-------------------------------------------------------------------------- - // Helpers - //-------------------------------------------------------------------------- - var sourceCode = context.getSourceCode(); - - /** - * Determines if a token is one of the exceptions for the opener paren - * @param {Object} token The token to check - * @returns {boolean} True if the token is one of the exceptions for the opener paren - */ - function isOpenerException(token) { - return token.type === "Punctuator" && exceptions.openers.indexOf(token.value) >= 0; - } - - /** - * Determines if a token is one of the exceptions for the closer paren - * @param {Object} token The token to check - * @returns {boolean} True if the token is one of the exceptions for the closer paren - */ - function isCloserException(token) { - return token.type === "Punctuator" && exceptions.closers.indexOf(token.value) >= 0; - } - - /** - * Determines if an opener paren should have a missing space after it - * @param {Object} left The paren token - * @param {Object} right The token after it - * @returns {boolean} True if the paren should have a space - */ - function shouldOpenerHaveSpace(left, right) { - if (sourceCode.isSpaceBetweenTokens(left, right)) { - return false; - } + /** + * Produces an object with the opener and closer exception values + * @param {Object} opts The exception options + * @returns {Object} `openers` and `closers` exception values + * @private + */ + function getExceptions() { + var openers = [], + closers = []; + + if (options.braceException) { + openers.push("{"); + closers.push("}"); + } - if (ALWAYS) { - if (right.type === "Punctuator" && right.value === ")") { - return false; + if (options.bracketException) { + openers.push("["); + closers.push("]"); } - return !isOpenerException(right); - } else { - return isOpenerException(right); - } - } - /** - * Determines if an closer paren should have a missing space after it - * @param {Object} left The token before the paren - * @param {Object} right The paren token - * @returns {boolean} True if the paren should have a space - */ - function shouldCloserHaveSpace(left, right) { - if (left.type === "Punctuator" && left.value === "(") { - return false; - } + if (options.parenException) { + openers.push("("); + closers.push(")"); + } - if (sourceCode.isSpaceBetweenTokens(left, right)) { - return false; - } + if (options.empty) { + openers.push(")"); + closers.push("("); + } - if (ALWAYS) { - return !isCloserException(left); - } else { - return isCloserException(left); + return { + openers: openers, + closers: closers + }; } - } - /** - * Determines if an opener paren should not have an existing space after it - * @param {Object} left The paren token - * @param {Object} right The token after it - * @returns {boolean} True if the paren should reject the space - */ - function shouldOpenerRejectSpace(left, right) { - if (right.type === "Line") { - return false; + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + var sourceCode = context.getSourceCode(); + + /** + * Determines if a token is one of the exceptions for the opener paren + * @param {Object} token The token to check + * @returns {boolean} True if the token is one of the exceptions for the opener paren + */ + function isOpenerException(token) { + return token.type === "Punctuator" && exceptions.openers.indexOf(token.value) >= 0; } - if (!astUtils.isTokenOnSameLine(left, right)) { - return false; + /** + * Determines if a token is one of the exceptions for the closer paren + * @param {Object} token The token to check + * @returns {boolean} True if the token is one of the exceptions for the closer paren + */ + function isCloserException(token) { + return token.type === "Punctuator" && exceptions.closers.indexOf(token.value) >= 0; } - if (!sourceCode.isSpaceBetweenTokens(left, right)) { - return false; - } + /** + * Determines if an opener paren should have a missing space after it + * @param {Object} left The paren token + * @param {Object} right The token after it + * @returns {boolean} True if the paren should have a space + */ + function shouldOpenerHaveSpace(left, right) { + if (sourceCode.isSpaceBetweenTokens(left, right)) { + return false; + } - if (ALWAYS) { - return isOpenerException(right); - } else { - return !isOpenerException(right); + if (ALWAYS) { + if (right.type === "Punctuator" && right.value === ")") { + return false; + } + return !isOpenerException(right); + } else { + return isOpenerException(right); + } } - } - /** - * Determines if an closer paren should not have an existing space after it - * @param {Object} left The token before the paren - * @param {Object} right The paren token - * @returns {boolean} True if the paren should reject the space - */ - function shouldCloserRejectSpace(left, right) { - if (left.type === "Punctuator" && left.value === "(") { - return false; - } + /** + * Determines if an closer paren should have a missing space after it + * @param {Object} left The token before the paren + * @param {Object} right The paren token + * @returns {boolean} True if the paren should have a space + */ + function shouldCloserHaveSpace(left, right) { + if (left.type === "Punctuator" && left.value === "(") { + return false; + } - if (!astUtils.isTokenOnSameLine(left, right)) { - return false; - } + if (sourceCode.isSpaceBetweenTokens(left, right)) { + return false; + } - if (!sourceCode.isSpaceBetweenTokens(left, right)) { - return false; + if (ALWAYS) { + return !isCloserException(left); + } else { + return isCloserException(left); + } } - if (ALWAYS) { - return isCloserException(left); - } else { - return !isCloserException(left); - } - } + /** + * Determines if an opener paren should not have an existing space after it + * @param {Object} left The paren token + * @param {Object} right The token after it + * @returns {boolean} True if the paren should reject the space + */ + function shouldOpenerRejectSpace(left, right) { + if (right.type === "Line") { + return false; + } - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- + if (!astUtils.isTokenOnSameLine(left, right)) { + return false; + } - return { - Program: function checkParenSpaces(node) { - var tokens, prevToken, nextToken; + if (!sourceCode.isSpaceBetweenTokens(left, right)) { + return false; + } - exceptions = getExceptions(); - tokens = sourceCode.tokensAndComments; + if (ALWAYS) { + return isOpenerException(right); + } else { + return !isOpenerException(right); + } + } - tokens.forEach(function(token, i) { - prevToken = tokens[i - 1]; - nextToken = tokens[i + 1]; + /** + * Determines if an closer paren should not have an existing space after it + * @param {Object} left The token before the paren + * @param {Object} right The paren token + * @returns {boolean} True if the paren should reject the space + */ + function shouldCloserRejectSpace(left, right) { + if (left.type === "Punctuator" && left.value === "(") { + return false; + } - if (token.type !== "Punctuator") { - return; - } + if (!astUtils.isTokenOnSameLine(left, right)) { + return false; + } - if (token.value !== "(" && token.value !== ")") { - return; - } + if (!sourceCode.isSpaceBetweenTokens(left, right)) { + return false; + } - if (token.value === "(" && shouldOpenerHaveSpace(token, nextToken)) { - context.report({ - node: node, - loc: token.loc.start, - message: MISSING_SPACE_MESSAGE, - fix: function(fixer) { - return fixer.insertTextAfter(token, " "); - } - }); - } else if (token.value === "(" && shouldOpenerRejectSpace(token, nextToken)) { - context.report({ - node: node, - loc: token.loc.start, - message: REJECTED_SPACE_MESSAGE, - fix: function(fixer) { - return fixer.removeRange([token.range[1], nextToken.range[0]]); - } - }); - } else if (token.value === ")" && shouldCloserHaveSpace(prevToken, token)) { - - // context.report(node, token.loc.start, MISSING_SPACE_MESSAGE); - context.report({ - node: node, - loc: token.loc.start, - message: MISSING_SPACE_MESSAGE, - fix: function(fixer) { - return fixer.insertTextBefore(token, " "); - } - }); - } else if (token.value === ")" && shouldCloserRejectSpace(prevToken, token)) { - context.report({ - node: node, - loc: token.loc.start, - message: REJECTED_SPACE_MESSAGE, - fix: function(fixer) { - return fixer.removeRange([prevToken.range[1], token.range[0]]); - } - }); - } - }); + if (ALWAYS) { + return isCloserException(left); + } else { + return !isCloserException(left); + } } - }; -}; + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- -module.exports.schema = [ - { - enum: ["always", "never"] - }, - { - type: "object", - properties: { - exceptions: { - type: "array", - items: { - enum: ["{}", "[]", "()", "empty"] - }, - uniqueItems: true + return { + Program: function checkParenSpaces(node) { + var tokens, prevToken, nextToken; + + exceptions = getExceptions(); + tokens = sourceCode.tokensAndComments; + + tokens.forEach(function(token, i) { + prevToken = tokens[i - 1]; + nextToken = tokens[i + 1]; + + if (token.type !== "Punctuator") { + return; + } + + if (token.value !== "(" && token.value !== ")") { + return; + } + + if (token.value === "(" && shouldOpenerHaveSpace(token, nextToken)) { + context.report({ + node: node, + loc: token.loc.start, + message: MISSING_SPACE_MESSAGE, + fix: function(fixer) { + return fixer.insertTextAfter(token, " "); + } + }); + } else if (token.value === "(" && shouldOpenerRejectSpace(token, nextToken)) { + context.report({ + node: node, + loc: token.loc.start, + message: REJECTED_SPACE_MESSAGE, + fix: function(fixer) { + return fixer.removeRange([token.range[1], nextToken.range[0]]); + } + }); + } else if (token.value === ")" && shouldCloserHaveSpace(prevToken, token)) { + + // context.report(node, token.loc.start, MISSING_SPACE_MESSAGE); + context.report({ + node: node, + loc: token.loc.start, + message: MISSING_SPACE_MESSAGE, + fix: function(fixer) { + return fixer.insertTextBefore(token, " "); + } + }); + } else if (token.value === ")" && shouldCloserRejectSpace(prevToken, token)) { + context.report({ + node: node, + loc: token.loc.start, + message: REJECTED_SPACE_MESSAGE, + fix: function(fixer) { + return fixer.removeRange([prevToken.range[1], token.range[0]]); + } + }); + } + }); } - }, - additionalProperties: false + }; + } -]; +}; diff --git a/lib/rules/space-infix-ops.js b/lib/rules/space-infix-ops.js index 8893be7b1f3..862ff66fb5e 100644 --- a/lib/rules/space-infix-ops.js +++ b/lib/rules/space-infix-ops.js @@ -8,141 +8,153 @@ // 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; +module.exports = { + meta: { + docs: { + description: "require spacing around operators", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + type: "object", + properties: { + int32Hint: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create: 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; } - 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({ - node: mainNode, - loc: culpritToken.loc.start, - message: "Infix operators must be spaced.", - fix: function(fixer) { - var previousToken = context.getTokenBefore(culpritToken); - var afterToken = context.getTokenAfter(culpritToken); - var fixString = ""; - - if (culpritToken.range[0] - previousToken.range[1] === 0) { - fixString = " "; + /** + * 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({ + node: mainNode, + loc: culpritToken.loc.start, + message: "Infix operators must be spaced.", + fix: function(fixer) { + var previousToken = context.getTokenBefore(culpritToken); + var afterToken = context.getTokenAfter(culpritToken); + var fixString = ""; + + if (culpritToken.range[0] - previousToken.range[1] === 0) { + fixString = " "; + } + + fixString += culpritToken.value; + + if (afterToken.range[0] - culpritToken.range[1] === 0) { + fixString += " "; + } + + return fixer.replaceText(culpritToken, fixString); } + }); + } - fixString += culpritToken.value; + /** + * Check if the node is binary then report + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function checkBinary(node) { + var nonSpacedNode = getFirstNonSpacedToken(node.left, node.right); - if (afterToken.range[0] - culpritToken.range[1] === 0) { - fixString += " "; + if (nonSpacedNode) { + if (!(int32Hint && context.getSource(node).substr(-2) === "|0")) { + report(node, nonSpacedNode); } - - return fixer.replaceText(culpritToken, fixString); - } - }); - } - - /** - * Check if the node is binary then report - * @param {ASTNode} node node to evaluate - * @returns {void} - * @private - */ - function checkBinary(node) { - var nonSpacedNode = getFirstNonSpacedToken(node.left, node.right); - - if (nonSpacedNode) { - if (!(int32Hint && context.getSource(node).substr(-2) === "|0")) { - report(node, nonSpacedNode); } } - } - /** - * Check if the node is conditional - * @param {ASTNode} node node to evaluate - * @returns {void} - * @private - */ - 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); + /** + * Check if the node is conditional + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + 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); + } } - } - /** - * Check if the node is a variable - * @param {ASTNode} node node to evaluate - * @returns {void} - * @private - */ - function checkVar(node) { - var nonSpacedNode; - - if (node.init) { - nonSpacedNode = getFirstNonSpacedToken(node.id, node.init); - if (nonSpacedNode) { - report(node, nonSpacedNode); + /** + * Check if the node is a variable + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function checkVar(node) { + var nonSpacedNode; + + if (node.init) { + nonSpacedNode = getFirstNonSpacedToken(node.id, node.init); + if (nonSpacedNode) { + report(node, nonSpacedNode); + } } } - } - return { - AssignmentExpression: checkBinary, - AssignmentPattern: checkBinary, - BinaryExpression: checkBinary, - LogicalExpression: checkBinary, - ConditionalExpression: checkConditional, - VariableDeclarator: checkVar - }; + return { + AssignmentExpression: checkBinary, + AssignmentPattern: checkBinary, + BinaryExpression: checkBinary, + LogicalExpression: checkBinary, + ConditionalExpression: checkConditional, + VariableDeclarator: checkVar + }; -}; - -module.exports.schema = [ - { - type: "object", - properties: { - int32Hint: { - type: "boolean" - } - }, - additionalProperties: false } -]; +}; diff --git a/lib/rules/space-unary-ops.js b/lib/rules/space-unary-ops.js index 5a43fd89695..0bb92af1e68 100644 --- a/lib/rules/space-unary-ops.js +++ b/lib/rules/space-unary-ops.js @@ -8,259 +8,271 @@ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - var options = context.options && Array.isArray(context.options) && context.options[0] || { words: true, nonwords: false }; +module.exports = { + meta: { + docs: { + description: "enforce consistent spacing before or after unary operators", + category: "Stylistic Issues", + recommended: false + }, - //-------------------------------------------------------------------------- - // Helpers - //-------------------------------------------------------------------------- + fixable: "whitespace", - /** - * Check if the node is the first "!" in a "!!" convert to Boolean expression - * @param {ASTnode} node AST node - * @returns {boolean} Whether or not the node is first "!" in "!!" - */ - function isFirstBangInBangBangExpression(node) { - return node && node.type === "UnaryExpression" && node.argument.operator === "!" && - node.argument && node.argument.type === "UnaryExpression" && node.argument.operator === "!"; - } + schema: [ + { + type: "object", + properties: { + words: { + type: "boolean" + }, + nonwords: { + type: "boolean" + }, + overrides: { + type: "object", + additionalProperties: { + type: "boolean" + } + } + }, + additionalProperties: false + } + ] + }, - /** - * 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"; - } + create: function(context) { + var options = context.options && Array.isArray(context.options) && context.options[0] || { words: true, nonwords: false }; - /** - * Checks if an override exists for a given operator. - * @param {ASTnode} node AST node - * @param {string} operator Operator - * @returns {boolean} Whether or not an override has been provided for the operator - */ - function overrideExistsForOperator(node, operator) { - return options.overrides && options.overrides.hasOwnProperty(operator); - } + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- - /** - * Gets the value that the override was set to for this operator - * @param {ASTnode} node AST node - * @param {string} operator Operator - * @returns {boolean} Whether or not an override enforces a space with this operator - */ - function overrideEnforcesSpaces(node, operator) { - return options.overrides[operator]; - } + /** + * Check if the node is the first "!" in a "!!" convert to Boolean expression + * @param {ASTnode} node AST node + * @returns {boolean} Whether or not the node is first "!" in "!!" + */ + function isFirstBangInBangBangExpression(node) { + return node && node.type === "UnaryExpression" && node.argument.operator === "!" && + node.argument && node.argument.type === "UnaryExpression" && node.argument.operator === "!"; + } - /** - * Verify Unary Word Operator has 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 - * @param {string} word The word to be used for reporting - * @returns {void} - */ - function verifyWordHasSpaces(node, firstToken, secondToken, word) { - if (secondToken.range[0] === firstToken.range[1]) { - context.report({ - node: node, - message: "Unary word operator '" + word + "' must be followed by whitespace.", - fix: function(fixer) { - return fixer.insertTextAfter(firstToken, " "); - } - }); + /** + * 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"; + } + + /** + * Checks if an override exists for a given operator. + * @param {ASTnode} node AST node + * @param {string} operator Operator + * @returns {boolean} Whether or not an override has been provided for the operator + */ + function overrideExistsForOperator(node, operator) { + return options.overrides && options.overrides.hasOwnProperty(operator); + } + + /** + * Gets the value that the override was set to for this operator + * @param {ASTnode} node AST node + * @param {string} operator Operator + * @returns {boolean} Whether or not an override enforces a space with this operator + */ + function overrideEnforcesSpaces(node, operator) { + return options.overrides[operator]; } - } - /** - * Verify Unary Word Operator doesn't have 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 - * @param {string} word The word to be used for reporting - * @returns {void} - */ - function verifyWordDoesntHaveSpaces(node, firstToken, secondToken, word) { - if (isArgumentObjectExpression(node)) { - if (secondToken.range[0] > firstToken.range[1]) { + /** + * Verify Unary Word Operator has 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 + * @param {string} word The word to be used for reporting + * @returns {void} + */ + function verifyWordHasSpaces(node, firstToken, secondToken, word) { + if (secondToken.range[0] === firstToken.range[1]) { context.report({ node: node, - message: "Unexpected space after unary word operator '" + word + "'.", + message: "Unary word operator '" + word + "' must be followed by whitespace.", fix: function(fixer) { - return fixer.removeRange([firstToken.range[1], secondToken.range[0]]); + return fixer.insertTextAfter(firstToken, " "); } }); } } - } - /** - * 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 - * @param {string} word The word to be used for reporting - * @returns {void} - */ - function checkUnaryWordOperatorForSpaces(node, firstToken, secondToken, word) { - word = word || firstToken.value; + /** + * Verify Unary Word Operator doesn't have 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 + * @param {string} word The word to be used for reporting + * @returns {void} + */ + function verifyWordDoesntHaveSpaces(node, firstToken, secondToken, word) { + if (isArgumentObjectExpression(node)) { + if (secondToken.range[0] > firstToken.range[1]) { + context.report({ + node: node, + message: "Unexpected space after unary word operator '" + word + "'.", + fix: function(fixer) { + return fixer.removeRange([firstToken.range[1], secondToken.range[0]]); + } + }); + } + } + } + + /** + * 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 + * @param {string} word The word to be used for reporting + * @returns {void} + */ + function checkUnaryWordOperatorForSpaces(node, firstToken, secondToken, word) { + word = word || firstToken.value; - if (overrideExistsForOperator(node, word)) { - if (overrideEnforcesSpaces(node, word)) { + if (overrideExistsForOperator(node, word)) { + if (overrideEnforcesSpaces(node, word)) { + verifyWordHasSpaces(node, firstToken, secondToken, word); + } else { + verifyWordDoesntHaveSpaces(node, firstToken, secondToken, word); + } + } else if (options.words) { verifyWordHasSpaces(node, firstToken, secondToken, word); } else { verifyWordDoesntHaveSpaces(node, firstToken, secondToken, word); } - } else if (options.words) { - verifyWordHasSpaces(node, firstToken, secondToken, word); - } else { - verifyWordDoesntHaveSpaces(node, firstToken, secondToken, word); } - } - /** - * Verifies YieldExpressions satisfy spacing requirements - * @param {ASTnode} node AST node - * @returns {void} - */ - function checkForSpacesAfterYield(node) { - var tokens = context.getFirstTokens(node, 3), - word = "yield"; + /** + * Verifies YieldExpressions satisfy spacing requirements + * @param {ASTnode} node AST node + * @returns {void} + */ + function checkForSpacesAfterYield(node) { + var tokens = context.getFirstTokens(node, 3), + word = "yield"; - if (!node.argument || node.delegate) { - return; - } - - checkUnaryWordOperatorForSpaces(node, tokens[0], tokens[1], word); - } - - /** - * Verifies UnaryExpression, UpdateExpression and NewExpression have spaces before or after the operator - * @param {ASTnode} node AST node - * @param {object} firstToken First token in the expression - * @param {object} secondToken Second token in the expression - * @returns {void} - */ - function verifyNonWordsHaveSpaces(node, firstToken, secondToken) { - if (node.prefix) { - if (isFirstBangInBangBangExpression(node)) { + if (!node.argument || node.delegate) { return; } - if (firstToken.range[1] === secondToken.range[0]) { - context.report({ - node: node, - message: "Unary operator '" + firstToken.value + "' must be followed by whitespace.", - fix: function(fixer) { - return fixer.insertTextAfter(firstToken, " "); - } - }); - } - } else { - if (firstToken.range[1] === secondToken.range[0]) { - context.report({ - node: node, - message: "Space is required before unary expressions '" + secondToken.value + "'.", - fix: function(fixer) { - return fixer.insertTextBefore(secondToken, " "); - } - }); - } + + checkUnaryWordOperatorForSpaces(node, tokens[0], tokens[1], word); } - } - /** - * Verifies UnaryExpression, UpdateExpression and NewExpression don't have spaces before or after the operator - * @param {ASTnode} node AST node - * @param {object} firstToken First token in the expression - * @param {object} secondToken Second token in the expression - * @returns {void} - */ - function verifyNonWordsDontHaveSpaces(node, firstToken, secondToken) { - if (node.prefix) { - if (secondToken.range[0] > firstToken.range[1]) { - context.report({ - node: node, - message: "Unexpected space after unary operator '" + firstToken.value + "'.", - fix: function(fixer) { - return fixer.removeRange([firstToken.range[1], secondToken.range[0]]); - } - }); + /** + * Verifies UnaryExpression, UpdateExpression and NewExpression have spaces before or after the operator + * @param {ASTnode} node AST node + * @param {object} firstToken First token in the expression + * @param {object} secondToken Second token in the expression + * @returns {void} + */ + function verifyNonWordsHaveSpaces(node, firstToken, secondToken) { + if (node.prefix) { + if (isFirstBangInBangBangExpression(node)) { + return; + } + if (firstToken.range[1] === secondToken.range[0]) { + context.report({ + node: node, + message: "Unary operator '" + firstToken.value + "' must be followed by whitespace.", + fix: function(fixer) { + return fixer.insertTextAfter(firstToken, " "); + } + }); + } + } else { + if (firstToken.range[1] === secondToken.range[0]) { + context.report({ + node: node, + message: "Space is required before unary expressions '" + secondToken.value + "'.", + fix: function(fixer) { + return fixer.insertTextBefore(secondToken, " "); + } + }); + } } - } else { - if (secondToken.range[0] > firstToken.range[1]) { - context.report({ - node: node, - message: "Unexpected space before unary operator '" + secondToken.value + "'.", - fix: function(fixer) { - return fixer.removeRange([firstToken.range[1], secondToken.range[0]]); - } - }); + } + + /** + * Verifies UnaryExpression, UpdateExpression and NewExpression don't have spaces before or after the operator + * @param {ASTnode} node AST node + * @param {object} firstToken First token in the expression + * @param {object} secondToken Second token in the expression + * @returns {void} + */ + function verifyNonWordsDontHaveSpaces(node, firstToken, secondToken) { + if (node.prefix) { + if (secondToken.range[0] > firstToken.range[1]) { + context.report({ + node: node, + message: "Unexpected space after unary operator '" + firstToken.value + "'.", + fix: function(fixer) { + return fixer.removeRange([firstToken.range[1], secondToken.range[0]]); + } + }); + } + } else { + if (secondToken.range[0] > firstToken.range[1]) { + context.report({ + node: node, + message: "Unexpected space before unary operator '" + secondToken.value + "'.", + fix: function(fixer) { + return fixer.removeRange([firstToken.range[1], secondToken.range[0]]); + } + }); + } } } - } - /** - * Verifies UnaryExpression, UpdateExpression and NewExpression satisfy spacing requirements - * @param {ASTnode} node AST node - * @returns {void} - */ - function checkForSpaces(node) { - var tokens = context.getFirstTokens(node, 2), - firstToken = tokens[0], - secondToken = tokens[1]; + /** + * Verifies UnaryExpression, UpdateExpression and NewExpression satisfy spacing requirements + * @param {ASTnode} node AST node + * @returns {void} + */ + function checkForSpaces(node) { + var tokens = context.getFirstTokens(node, 2), + firstToken = tokens[0], + secondToken = tokens[1]; - if ((node.type === "NewExpression" || node.prefix) && firstToken.type === "Keyword") { - checkUnaryWordOperatorForSpaces(node, firstToken, secondToken); - return; - } + if ((node.type === "NewExpression" || node.prefix) && firstToken.type === "Keyword") { + checkUnaryWordOperatorForSpaces(node, firstToken, secondToken); + return; + } - var operator = node.prefix ? tokens[0].value : tokens[1].value; + var operator = node.prefix ? tokens[0].value : tokens[1].value; - if (overrideExistsForOperator(node, operator)) { - if (overrideEnforcesSpaces(node, operator)) { + if (overrideExistsForOperator(node, operator)) { + if (overrideEnforcesSpaces(node, operator)) { + verifyNonWordsHaveSpaces(node, firstToken, secondToken); + } else { + verifyNonWordsDontHaveSpaces(node, firstToken, secondToken); + } + } else if (options.nonwords) { verifyNonWordsHaveSpaces(node, firstToken, secondToken); } else { verifyNonWordsDontHaveSpaces(node, firstToken, secondToken); } - } else if (options.nonwords) { - verifyNonWordsHaveSpaces(node, firstToken, secondToken); - } else { - verifyNonWordsDontHaveSpaces(node, firstToken, secondToken); } - } - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- - return { - UnaryExpression: checkForSpaces, - UpdateExpression: checkForSpaces, - NewExpression: checkForSpaces, - YieldExpression: checkForSpacesAfterYield - }; + return { + UnaryExpression: checkForSpaces, + UpdateExpression: checkForSpaces, + NewExpression: checkForSpaces, + YieldExpression: checkForSpacesAfterYield + }; -}; - -module.exports.schema = [ - { - type: "object", - properties: { - words: { - type: "boolean" - }, - nonwords: { - type: "boolean" - }, - overrides: { - type: "object", - additionalProperties: { - type: "boolean" - } - } - }, - additionalProperties: false } -]; +}; diff --git a/lib/rules/spaced-comment.js b/lib/rules/spaced-comment.js index 44ad99786f0..149862024c2 100644 --- a/lib/rules/spaced-comment.js +++ b/lib/rules/spaced-comment.js @@ -137,130 +137,21 @@ function createNeverStylePattern(markers) { // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - - // Unless the first option is never, require a space - var requireSpace = context.options[0] !== "never"; - - /* - * Parse the second options. - * If markers don't include `"*"`, it's added automatically for JSDoc - * comments. - */ - var config = context.options[1] || {}; - var styleRules = ["block", "line"].reduce(function(rule, type) { - var markers = parseMarkersOption(config[type] && config[type].markers || config.markers); - var exceptions = config[type] && config[type].exceptions || config.exceptions || []; - - // Create RegExp object for valid patterns. - rule[type] = { - regex: requireSpace ? createAlwaysStylePattern(markers, exceptions) : createNeverStylePattern(markers), - hasExceptions: exceptions.length > 0, - markers: new RegExp("^(" + markers.map(escape).join("|") + ")") - }; - - return rule; - }, {}); - - /** - * Reports a spacing error with an appropriate message. - * @param {ASTNode} node - A comment node to check. - * @param {string} message - An error message to report - * @param {Array} match - An array of match results for markers. - * @returns {void} - */ - function report(node, message, match) { - var type = node.type.toLowerCase(), - commentIdentifier = type === "block" ? "/*" : "//"; - - context.report({ - node: node, - fix: function(fixer) { - var start = node.range[0], - end = start + 2; - - if (requireSpace) { - if (match) { - end += match[0].length; - } - return fixer.insertTextAfterRange([start, end], " "); - } else { - end += match[0].length; - return fixer.replaceTextRange([start, end], commentIdentifier + (match[1] ? match[1] : "")); - } - }, - message: message - }); - } - - /** - * Reports a given comment if it's invalid. - * @param {ASTNode} node - a comment node to check. - * @returns {void} - */ - function checkCommentForSpace(node) { - var type = node.type.toLowerCase(), - rule = styleRules[type], - commentIdentifier = type === "block" ? "/*" : "//"; - - // Ignores empty comments. - if (node.value.length === 0) { - return; - } - - // Checks. - if (requireSpace) { - if (!rule.regex.test(node.value)) { - var hasMarker = rule.markers.exec(node.value); - var marker = hasMarker ? commentIdentifier + hasMarker[0] : commentIdentifier; - - if (rule.hasExceptions) { - report(node, "Expected exception block, space or tab after '" + marker + "' in comment.", hasMarker); - } else { - report(node, "Expected space or tab after '" + marker + "' in comment.", hasMarker); - } - } - } else { - var matched = rule.regex.exec(node.value); - - if (matched) { - if (!matched[1]) { - report(node, "Unexpected space or tab after '" + commentIdentifier + "' in comment.", matched); - } else { - report(node, "Unexpected space or tab after marker (" + matched[1] + ") in comment.", matched); - } - } - } - } - - return { - - LineComment: checkCommentForSpace, - BlockComment: checkCommentForSpace +module.exports = { + meta: { + docs: { + description: "enforce consistent spacing after the `//` or `/*` in a comment", + category: "Stylistic Issues", + recommended: false + }, - }; -}; + fixable: "whitespace", -module.exports.schema = [ - { - enum: ["always", "never"] - }, - { - type: "object", - properties: { - exceptions: { - type: "array", - items: { - type: "string" - } - }, - markers: { - type: "array", - items: { - type: "string" - } + schema: [ + { + enum: ["always", "never"] }, - line: { + { type: "object", properties: { exceptions: { @@ -274,29 +165,150 @@ module.exports.schema = [ items: { type: "string" } + }, + line: { + type: "object", + properties: { + exceptions: { + type: "array", + items: { + type: "string" + } + }, + markers: { + type: "array", + items: { + type: "string" + } + } + }, + additionalProperties: false + }, + block: { + type: "object", + properties: { + exceptions: { + type: "array", + items: { + type: "string" + } + }, + markers: { + type: "array", + items: { + type: "string" + } + } + }, + additionalProperties: false } }, additionalProperties: false - }, - block: { - type: "object", - properties: { - exceptions: { - type: "array", - items: { - type: "string" - } - }, - markers: { - type: "array", - items: { - type: "string" + } + ] + }, + + create: function(context) { + + // Unless the first option is never, require a space + var requireSpace = context.options[0] !== "never"; + + /* + * Parse the second options. + * If markers don't include `"*"`, it's added automatically for JSDoc + * comments. + */ + var config = context.options[1] || {}; + var styleRules = ["block", "line"].reduce(function(rule, type) { + var markers = parseMarkersOption(config[type] && config[type].markers || config.markers); + var exceptions = config[type] && config[type].exceptions || config.exceptions || []; + + // Create RegExp object for valid patterns. + rule[type] = { + regex: requireSpace ? createAlwaysStylePattern(markers, exceptions) : createNeverStylePattern(markers), + hasExceptions: exceptions.length > 0, + markers: new RegExp("^(" + markers.map(escape).join("|") + ")") + }; + + return rule; + }, {}); + + /** + * Reports a spacing error with an appropriate message. + * @param {ASTNode} node - A comment node to check. + * @param {string} message - An error message to report + * @param {Array} match - An array of match results for markers. + * @returns {void} + */ + function report(node, message, match) { + var type = node.type.toLowerCase(), + commentIdentifier = type === "block" ? "/*" : "//"; + + context.report({ + node: node, + fix: function(fixer) { + var start = node.range[0], + end = start + 2; + + if (requireSpace) { + if (match) { + end += match[0].length; } + return fixer.insertTextAfterRange([start, end], " "); + } else { + end += match[0].length; + return fixer.replaceTextRange([start, end], commentIdentifier + (match[1] ? match[1] : "")); } }, - additionalProperties: false + message: message + }); + } + + /** + * Reports a given comment if it's invalid. + * @param {ASTNode} node - a comment node to check. + * @returns {void} + */ + function checkCommentForSpace(node) { + var type = node.type.toLowerCase(), + rule = styleRules[type], + commentIdentifier = type === "block" ? "/*" : "//"; + + // Ignores empty comments. + if (node.value.length === 0) { + return; } - }, - additionalProperties: false + + // Checks. + if (requireSpace) { + if (!rule.regex.test(node.value)) { + var hasMarker = rule.markers.exec(node.value); + var marker = hasMarker ? commentIdentifier + hasMarker[0] : commentIdentifier; + + if (rule.hasExceptions) { + report(node, "Expected exception block, space or tab after '" + marker + "' in comment.", hasMarker); + } else { + report(node, "Expected space or tab after '" + marker + "' in comment.", hasMarker); + } + } + } else { + var matched = rule.regex.exec(node.value); + + if (matched) { + if (!matched[1]) { + report(node, "Unexpected space or tab after '" + commentIdentifier + "' in comment.", matched); + } else { + report(node, "Unexpected space or tab after marker (" + matched[1] + ") in comment.", matched); + } + } + } + } + + return { + + LineComment: checkCommentForSpace, + BlockComment: checkCommentForSpace + + }; } -]; +}; diff --git a/lib/rules/strict.js b/lib/rules/strict.js index 97b840e4e82..4097a327931 100644 --- a/lib/rules/strict.js +++ b/lib/rules/strict.js @@ -57,155 +57,165 @@ function getUseStrictDirectives(statements) { // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - - var mode = context.options[0] || "safe", - ecmaFeatures = context.parserOptions.ecmaFeatures || {}, - scopes = [], - classScopes = [], - rule; - - if (ecmaFeatures.impliedStrict) { - mode = "implied"; - } else if (mode === "safe") { - mode = ecmaFeatures.globalReturn ? "global" : "function"; - } +module.exports = { + meta: { + docs: { + description: "require or disallow strict mode directives", + category: "Strict Mode", + recommended: false + }, - /** - * Report a slice of an array of nodes with a given message. - * @param {ASTNode[]} nodes Nodes. - * @param {string} start Index to start from. - * @param {string} end Index to end before. - * @param {string} message Message to display. - * @returns {void} - */ - function reportSlice(nodes, start, end, message) { - var i; - - for (i = start; i < end; i++) { - context.report(nodes[i], message); - } - } + schema: [ + { + enum: ["never", "global", "function", "safe"] + } + ] + }, - /** - * Report all nodes in an array with a given message. - * @param {ASTNode[]} nodes Nodes. - * @param {string} message Message to display. - * @returns {void} - */ - function reportAll(nodes, message) { - reportSlice(nodes, 0, nodes.length, message); - } + create: function(context) { - /** - * Report all nodes in an array, except the first, with a given message. - * @param {ASTNode[]} nodes Nodes. - * @param {string} message Message to display. - * @returns {void} - */ - function reportAllExceptFirst(nodes, message) { - reportSlice(nodes, 1, nodes.length, message); - } + var mode = context.options[0] || "safe", + ecmaFeatures = context.parserOptions.ecmaFeatures || {}, + scopes = [], + classScopes = [], + rule; + + if (ecmaFeatures.impliedStrict) { + mode = "implied"; + } else if (mode === "safe") { + mode = ecmaFeatures.globalReturn ? "global" : "function"; + } - /** - * Entering a function in 'function' mode 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. - * @param {ASTNode[]} useStrictDirectives The Use Strict Directives of the node. - * @returns {void} - */ - function enterFunctionInFunctionMode(node, useStrictDirectives) { - var isInClass = classScopes.length > 0, - isParentGlobal = scopes.length === 0 && classScopes.length === 0, - isParentStrict = scopes.length > 0 && scopes[scopes.length - 1], - isStrict = useStrictDirectives.length > 0; - - if (isStrict) { - if (isParentStrict) { - context.report(useStrictDirectives[0], messages.unnecessary); - } else if (isInClass) { - context.report(useStrictDirectives[0], messages.unnecessaryInClasses); + /** + * Report a slice of an array of nodes with a given message. + * @param {ASTNode[]} nodes Nodes. + * @param {string} start Index to start from. + * @param {string} end Index to end before. + * @param {string} message Message to display. + * @returns {void} + */ + function reportSlice(nodes, start, end, message) { + var i; + + for (i = start; i < end; i++) { + context.report(nodes[i], message); } + } - reportAllExceptFirst(useStrictDirectives, messages.multiple); - } else if (isParentGlobal) { - context.report(node, messages.function); + /** + * Report all nodes in an array with a given message. + * @param {ASTNode[]} nodes Nodes. + * @param {string} message Message to display. + * @returns {void} + */ + function reportAll(nodes, message) { + reportSlice(nodes, 0, nodes.length, message); } - scopes.push(isParentStrict || isStrict); - } + /** + * Report all nodes in an array, except the first, with a given message. + * @param {ASTNode[]} nodes Nodes. + * @param {string} message Message to display. + * @returns {void} + */ + function reportAllExceptFirst(nodes, message) { + reportSlice(nodes, 1, nodes.length, message); + } - /** - * Exiting a function in 'function' mode pops its scope off the stack. - * @returns {void} - */ - function exitFunctionInFunctionMode() { - scopes.pop(); - } + /** + * Entering a function in 'function' mode 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. + * @param {ASTNode[]} useStrictDirectives The Use Strict Directives of the node. + * @returns {void} + */ + function enterFunctionInFunctionMode(node, useStrictDirectives) { + var isInClass = classScopes.length > 0, + isParentGlobal = scopes.length === 0 && classScopes.length === 0, + isParentStrict = scopes.length > 0 && scopes[scopes.length - 1], + isStrict = useStrictDirectives.length > 0; + + if (isStrict) { + if (isParentStrict) { + context.report(useStrictDirectives[0], messages.unnecessary); + } else if (isInClass) { + context.report(useStrictDirectives[0], messages.unnecessaryInClasses); + } - /** - * Enter a function and either: - * - Push a new nested scope onto the stack (in 'function' mode). - * - Report all the Use Strict Directives (in the other modes). - * @param {ASTNode} node The function declaration or expression. - * @returns {void} - */ - function enterFunction(node) { - var isBlock = node.body.type === "BlockStatement", - useStrictDirectives = isBlock ? - getUseStrictDirectives(node.body.body) : []; + reportAllExceptFirst(useStrictDirectives, messages.multiple); + } else if (isParentGlobal) { + context.report(node, messages.function); + } - if (mode === "function") { - enterFunctionInFunctionMode(node, useStrictDirectives); - } else { - reportAll(useStrictDirectives, messages[mode]); + scopes.push(isParentStrict || isStrict); } - } - - rule = { - Program: function(node) { - var useStrictDirectives = getUseStrictDirectives(node.body); - if (node.sourceType === "module") { - mode = "module"; - } + /** + * Exiting a function in 'function' mode pops its scope off the stack. + * @returns {void} + */ + function exitFunctionInFunctionMode() { + scopes.pop(); + } - if (mode === "global") { - if (node.body.length > 0 && useStrictDirectives.length === 0) { - context.report(node, messages.global); - } - reportAllExceptFirst(useStrictDirectives, messages.multiple); + /** + * Enter a function and either: + * - Push a new nested scope onto the stack (in 'function' mode). + * - Report all the Use Strict Directives (in the other modes). + * @param {ASTNode} node The function declaration or expression. + * @returns {void} + */ + function enterFunction(node) { + var isBlock = node.body.type === "BlockStatement", + useStrictDirectives = isBlock ? + getUseStrictDirectives(node.body.body) : []; + + if (mode === "function") { + enterFunctionInFunctionMode(node, useStrictDirectives); } else { reportAll(useStrictDirectives, messages[mode]); } - }, - FunctionDeclaration: enterFunction, - FunctionExpression: enterFunction, - ArrowFunctionExpression: enterFunction - }; + } - if (mode === "function") { - lodash.assign(rule, { + rule = { + Program: function(node) { + var useStrictDirectives = getUseStrictDirectives(node.body); - // Inside of class bodies are always strict mode. - ClassBody: function() { - classScopes.push(true); - }, - "ClassBody:exit": function() { - classScopes.pop(); - }, + if (node.sourceType === "module") { + mode = "module"; + } - "FunctionDeclaration:exit": exitFunctionInFunctionMode, - "FunctionExpression:exit": exitFunctionInFunctionMode, - "ArrowFunctionExpression:exit": exitFunctionInFunctionMode - }); - } + if (mode === "global") { + if (node.body.length > 0 && useStrictDirectives.length === 0) { + context.report(node, messages.global); + } + reportAllExceptFirst(useStrictDirectives, messages.multiple); + } else { + reportAll(useStrictDirectives, messages[mode]); + } + }, + FunctionDeclaration: enterFunction, + FunctionExpression: enterFunction, + ArrowFunctionExpression: enterFunction + }; - return rule; -}; + if (mode === "function") { + lodash.assign(rule, { + + // Inside of class bodies are always strict mode. + ClassBody: function() { + classScopes.push(true); + }, + "ClassBody:exit": function() { + classScopes.pop(); + }, + + "FunctionDeclaration:exit": exitFunctionInFunctionMode, + "FunctionExpression:exit": exitFunctionInFunctionMode, + "ArrowFunctionExpression:exit": exitFunctionInFunctionMode + }); + } -module.exports.schema = [ - { - enum: ["never", "global", "function", "safe"] + return rule; } -]; +}; diff --git a/lib/rules/template-curly-spacing.js b/lib/rules/template-curly-spacing.js index a591da7f863..144a0353690 100644 --- a/lib/rules/template-curly-spacing.js +++ b/lib/rules/template-curly-spacing.js @@ -22,82 +22,94 @@ var CLOSE_PAREN = /^\}/; // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - var sourceCode = context.getSourceCode(); - var always = context.options[0] === "always"; - var prefix = always ? "Expected" : "Unexpected"; - - /** - * Checks spacing before `}` of a given token. - * @param {Token} token - A token to check. This is a Template token. - * @returns {void} - */ - function checkSpacingBefore(token) { - var prevToken = sourceCode.getTokenBefore(token); - - if (prevToken && - CLOSE_PAREN.test(token.value) && - astUtils.isTokenOnSameLine(prevToken, token) && - sourceCode.isSpaceBetweenTokens(prevToken, token) !== always - ) { - context.report({ - loc: token.loc.start, - message: prefix + " space(s) before '}'.", - fix: function(fixer) { - if (always) { - return fixer.insertTextBefore(token, " "); +module.exports = { + meta: { + docs: { + description: "require or disallow spacing around embedded expressions of template strings", + category: "ECMAScript 6", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + {enum: ["always", "never"]} + ] + }, + + create: function(context) { + var sourceCode = context.getSourceCode(); + var always = context.options[0] === "always"; + var prefix = always ? "Expected" : "Unexpected"; + + /** + * Checks spacing before `}` of a given token. + * @param {Token} token - A token to check. This is a Template token. + * @returns {void} + */ + function checkSpacingBefore(token) { + var prevToken = sourceCode.getTokenBefore(token); + + if (prevToken && + CLOSE_PAREN.test(token.value) && + astUtils.isTokenOnSameLine(prevToken, token) && + sourceCode.isSpaceBetweenTokens(prevToken, token) !== always + ) { + context.report({ + loc: token.loc.start, + message: prefix + " space(s) before '}'.", + fix: function(fixer) { + if (always) { + return fixer.insertTextBefore(token, " "); + } + return fixer.removeRange([ + prevToken.range[1], + token.range[0] + ]); } - return fixer.removeRange([ - prevToken.range[1], - token.range[0] - ]); - } - }); + }); + } } - } - /** - * Checks spacing after `${` of a given token. - * @param {Token} token - A token to check. This is a Template token. - * @returns {void} - */ - function checkSpacingAfter(token) { - var nextToken = sourceCode.getTokenAfter(token); - - if (nextToken && - OPEN_PAREN.test(token.value) && - astUtils.isTokenOnSameLine(token, nextToken) && - sourceCode.isSpaceBetweenTokens(token, nextToken) !== always - ) { - context.report({ - loc: { - line: token.loc.end.line, - column: token.loc.end.column - 2 - }, - message: prefix + " space(s) after '${'.", - fix: function(fixer) { - if (always) { - return fixer.insertTextAfter(token, " "); + /** + * Checks spacing after `${` of a given token. + * @param {Token} token - A token to check. This is a Template token. + * @returns {void} + */ + function checkSpacingAfter(token) { + var nextToken = sourceCode.getTokenAfter(token); + + if (nextToken && + OPEN_PAREN.test(token.value) && + astUtils.isTokenOnSameLine(token, nextToken) && + sourceCode.isSpaceBetweenTokens(token, nextToken) !== always + ) { + context.report({ + loc: { + line: token.loc.end.line, + column: token.loc.end.column - 2 + }, + message: prefix + " space(s) after '${'.", + fix: function(fixer) { + if (always) { + return fixer.insertTextAfter(token, " "); + } + return fixer.removeRange([ + token.range[1], + nextToken.range[0] + ]); } - return fixer.removeRange([ - token.range[1], - nextToken.range[0] - ]); - } - }); + }); + } } - } - return { - TemplateElement: function(node) { - var token = sourceCode.getFirstToken(node); + return { + TemplateElement: function(node) { + var token = sourceCode.getFirstToken(node); - checkSpacingBefore(token); - checkSpacingAfter(token); - } - }; + checkSpacingBefore(token); + checkSpacingAfter(token); + } + }; + } }; - -module.exports.schema = [ - {enum: ["always", "never"]} -]; diff --git a/lib/rules/use-isnan.js b/lib/rules/use-isnan.js index dda676b7afb..10f7b71c8bd 100644 --- a/lib/rules/use-isnan.js +++ b/lib/rules/use-isnan.js @@ -9,16 +9,26 @@ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: { + description: "require calls to `isNaN()` when checking for `NaN`", + category: "Possible Errors", + recommended: true + }, - 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."); + schema: [] + }, + + create: 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."); + } } - } - }; + }; + } }; - -module.exports.schema = []; diff --git a/lib/rules/valid-jsdoc.js b/lib/rules/valid-jsdoc.js index bc3f3b3783a..e7d6fdeadf8 100644 --- a/lib/rules/valid-jsdoc.js +++ b/lib/rules/valid-jsdoc.js @@ -14,373 +14,383 @@ var doctrine = require("doctrine"); // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - - var options = context.options[0] || {}, - prefer = options.prefer || {}, - sourceCode = context.getSourceCode(), - - // these both default to true, so you have to explicitly make them false - requireReturn = options.requireReturn !== false, - requireParamDescription = options.requireParamDescription !== false, - requireReturnDescription = options.requireReturnDescription !== false, - requireReturnType = options.requireReturnType !== false, - preferType = options.preferType || {}, - checkPreferType = Object.keys(preferType).length !== 0; - - //-------------------------------------------------------------------------- - // Helpers - //-------------------------------------------------------------------------- - - // Using a stack to store if a function returns or not (handling nested functions) - var fns = []; - - /** - * Check if node type is a Class - * @param {ASTNode} node node to check. - * @returns {boolean} True is its a class - * @private - */ - function isTypeClass(node) { - return node.type === "ClassExpression" || node.type === "ClassDeclaration"; - } - - /** - * When parsing a new function, store it in our function stack. - * @param {ASTNode} node A function node to check. - * @returns {void} - * @private - */ - function startFunction(node) { - fns.push({ - returnPresent: (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") || - isTypeClass(node) - }); - } +module.exports = { + meta: { + docs: { + description: "enforce valid JSDoc comments", + category: "Possible Errors", + recommended: 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; + schema: [ + { + type: "object", + properties: { + prefer: { + type: "object", + additionalProperties: { + type: "string" + } + }, + preferType: { + type: "object", + additionalProperties: { + type: "string" + } + }, + requireReturn: { + type: "boolean" + }, + requireParamDescription: { + type: "boolean" + }, + requireReturnDescription: { + type: "boolean" + }, + matchDescription: { + type: "string" + }, + requireReturnType: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create: function(context) { + + var options = context.options[0] || {}, + prefer = options.prefer || {}, + sourceCode = context.getSourceCode(), + + // these both default to true, so you have to explicitly make them false + requireReturn = options.requireReturn !== false, + requireParamDescription = options.requireParamDescription !== false, + requireReturnDescription = options.requireReturnDescription !== false, + requireReturnType = options.requireReturnType !== false, + preferType = options.preferType || {}, + checkPreferType = Object.keys(preferType).length !== 0; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + // Using a stack to store if a function returns or not (handling nested functions) + var fns = []; + + /** + * Check if node type is a Class + * @param {ASTNode} node node to check. + * @returns {boolean} True is its a class + * @private + */ + function isTypeClass(node) { + return node.type === "ClassExpression" || node.type === "ClassDeclaration"; } - } - - /** - * Check if return tag type is void or undefined - * @param {Object} tag JSDoc tag - * @returns {boolean} True if its of type void or undefined - * @private - */ - function isValidReturnType(tag) { - return tag.type === null || tag.type.name === "void" || tag.type.type === "UndefinedLiteral"; - } - - /** - * Check if type should be validated based on some exceptions - * @param {Object} type JSDoc tag - * @returns {boolean} True if it can be validated - * @private - */ - function canTypeBeValidated(type) { - return type !== "UndefinedLiteral" && // {undefined} as there is no name property available. - type !== "NullLiteral" && // {null} - type !== "NullableLiteral" && // {?} - type !== "FunctionType" && // {function(a)} - type !== "AllLiteral"; // {*} - } - /** - * Extract the current and expected type based on the input type object - * @param {Object} type JSDoc tag - * @returns {Object} current and expected type object - * @private - */ - function getCurrentExpectedTypes(type) { - var currentType; - var expectedType; - - if (type.name) { - currentType = type.name; - } else if (type.expression) { - currentType = type.expression.name; + /** + * When parsing a new function, store it in our function stack. + * @param {ASTNode} node A function node to check. + * @returns {void} + * @private + */ + function startFunction(node) { + fns.push({ + returnPresent: (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") || + isTypeClass(node) + }); } - expectedType = currentType && preferType[currentType]; - - return { - currentType: currentType, - expectedType: expectedType - }; - } + /** + * 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; + } + } - /** - * Check if return tag type is void or undefined - * @param {Object} jsdocNode JSDoc node - * @param {Object} type JSDoc tag - * @returns {void} - * @private - */ - function validateType(jsdocNode, type) { - if (!type || !canTypeBeValidated(type.type)) { - return; + /** + * Check if return tag type is void or undefined + * @param {Object} tag JSDoc tag + * @returns {boolean} True if its of type void or undefined + * @private + */ + function isValidReturnType(tag) { + return tag.type === null || tag.type.name === "void" || tag.type.type === "UndefinedLiteral"; } - var typesToCheck = []; - var elements = []; - - switch (type.type) { - case "TypeApplication": // {Array.} - elements = type.applications[0].type === "UnionType" ? type.applications[0].elements : type.applications; - typesToCheck.push(getCurrentExpectedTypes(type)); - break; - case "RecordType": // {{20:String}} - elements = type.fields; - break; - case "UnionType": // {String|number|Test} - case "ArrayType": // {[String, number, Test]} - elements = type.elements; - break; - case "FieldType": // Array.<{count: number, votes: number}> - typesToCheck.push(getCurrentExpectedTypes(type.value)); - break; - default: - typesToCheck.push(getCurrentExpectedTypes(type)); + /** + * Check if type should be validated based on some exceptions + * @param {Object} type JSDoc tag + * @returns {boolean} True if it can be validated + * @private + */ + function canTypeBeValidated(type) { + return type !== "UndefinedLiteral" && // {undefined} as there is no name property available. + type !== "NullLiteral" && // {null} + type !== "NullableLiteral" && // {?} + type !== "FunctionType" && // {function(a)} + type !== "AllLiteral"; // {*} } - elements.forEach(validateType.bind(null, jsdocNode)); - - typesToCheck.forEach(function(typeToCheck) { - if (typeToCheck.expectedType && - typeToCheck.expectedType !== typeToCheck.currentType) { - context.report({ - node: jsdocNode, - message: "Use '{{expectedType}}' instead of '{{currentType}}'.", - data: { - currentType: typeToCheck.currentType, - expectedType: typeToCheck.expectedType - } - }); + /** + * Extract the current and expected type based on the input type object + * @param {Object} type JSDoc tag + * @returns {Object} current and expected type object + * @private + */ + function getCurrentExpectedTypes(type) { + var currentType; + var expectedType; + + if (type.name) { + currentType = type.name; + } else if (type.expression) { + currentType = type.expression.name; } - }); - } - /** - * 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 = sourceCode.getJSDocComment(node), - functionData = fns.pop(), - hasReturns = false, - hasConstructor = false, - isInterface = false, - isOverride = false, - isAbstract = 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) { + expectedType = currentType && preferType[currentType]; - if (/braces/i.test(ex.message)) { - context.report(jsdocNode, "JSDoc type missing brace."); - } else { - context.report(jsdocNode, "JSDoc syntax error."); - } + return { + currentType: currentType, + expectedType: expectedType + }; + } + /** + * Check if return tag type is void or undefined + * @param {Object} jsdocNode JSDoc node + * @param {Object} type JSDoc tag + * @returns {void} + * @private + */ + function validateType(jsdocNode, type) { + if (!type || !canTypeBeValidated(type.type)) { return; } - jsdoc.tags.forEach(function(tag) { - - switch (tag.title.toLowerCase()) { + var typesToCheck = []; + var elements = []; + + switch (type.type) { + case "TypeApplication": // {Array.} + elements = type.applications[0].type === "UnionType" ? type.applications[0].elements : type.applications; + typesToCheck.push(getCurrentExpectedTypes(type)); + break; + case "RecordType": // {{20:String}} + elements = type.fields; + break; + case "UnionType": // {String|number|Test} + case "ArrayType": // {[String, number, Test]} + elements = type.elements; + break; + case "FieldType": // Array.<{count: number, votes: number}> + typesToCheck.push(getCurrentExpectedTypes(type.value)); + break; + default: + typesToCheck.push(getCurrentExpectedTypes(type)); + } - case "param": - case "arg": - case "argument": - if (!tag.type) { - context.report(jsdocNode, "Missing JSDoc parameter type for '{{name}}'.", { name: tag.name }); + elements.forEach(validateType.bind(null, jsdocNode)); + + typesToCheck.forEach(function(typeToCheck) { + if (typeToCheck.expectedType && + typeToCheck.expectedType !== typeToCheck.currentType) { + context.report({ + node: jsdocNode, + message: "Use '{{expectedType}}' instead of '{{currentType}}'.", + data: { + currentType: typeToCheck.currentType, + expectedType: typeToCheck.expectedType } + }); + } + }); + } - if (!tag.description && requireParamDescription) { - context.report(jsdocNode, "Missing JSDoc parameter description for '{{name}}'.", { name: tag.name }); - } + /** + * 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 = sourceCode.getJSDocComment(node), + functionData = fns.pop(), + hasReturns = false, + hasConstructor = false, + isInterface = false, + isOverride = false, + isAbstract = 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."); + } - 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; + return; + } - case "return": - case "returns": - hasReturns = true; + jsdoc.tags.forEach(function(tag) { - if (!requireReturn && !functionData.returnPresent && (tag.type === null || !isValidReturnType(tag)) && !isAbstract) { - context.report(jsdocNode, "Unexpected @" + tag.title + " tag; function has no return statement."); - } else { - if (requireReturnType && !tag.type) { - context.report(jsdocNode, "Missing JSDoc return type."); + switch (tag.title.toLowerCase()) { + + case "param": + case "arg": + case "argument": + if (!tag.type) { + context.report(jsdocNode, "Missing JSDoc parameter type for '{{name}}'.", { name: tag.name }); } - if (!isValidReturnType(tag) && !tag.description && requireReturnDescription) { - context.report(jsdocNode, "Missing JSDoc return description."); + if (!tag.description && requireParamDescription) { + context.report(jsdocNode, "Missing JSDoc parameter description for '{{name}}'.", { name: tag.name }); } - } - break; + 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 === null || !isValidReturnType(tag)) && !isAbstract) { + context.report(jsdocNode, "Unexpected @" + tag.title + " tag; function has no return statement."); + } else { + if (requireReturnType && !tag.type) { + context.report(jsdocNode, "Missing JSDoc return type."); + } + + if (!isValidReturnType(tag) && !tag.description && requireReturnDescription) { + context.report(jsdocNode, "Missing JSDoc return description."); + } + } - case "constructor": - case "class": - hasConstructor = true; - break; + break; - case "override": - case "inheritdoc": - isOverride = true; - break; + case "constructor": + case "class": + hasConstructor = true; + break; - case "abstract": - case "virtual": - isAbstract = true; - break; + case "override": + case "inheritdoc": + isOverride = true; + break; - case "interface": - isInterface = true; - break; + case "abstract": + case "virtual": + isAbstract = true; + break; - // no default - } + case "interface": + isInterface = true; + break; - // check tag preferences - if (prefer.hasOwnProperty(tag.title) && tag.title !== prefer[tag.title]) { - context.report(jsdocNode, "Use @{{name}} instead.", { name: prefer[tag.title] }); - } + // no default + } - // validate the types - if (checkPreferType && tag.type) { - validateType(jsdocNode, tag.type); - } - }); + // check tag preferences + if (prefer.hasOwnProperty(tag.title) && tag.title !== prefer[tag.title]) { + context.report(jsdocNode, "Use @{{name}} instead.", { name: prefer[tag.title] }); + } - // check for functions missing @returns - if (!isOverride && !hasReturns && !hasConstructor && !isInterface && - node.parent.kind !== "get" && node.parent.kind !== "constructor" && - node.parent.kind !== "set" && !isTypeClass(node)) { - if (requireReturn || functionData.returnPresent) { - context.report(jsdocNode, "Missing JSDoc @" + (prefer.returns || "returns") + " for function."); + // validate the types + if (checkPreferType && tag.type) { + validateType(jsdocNode, tag.type); + } + }); + + // check for functions missing @returns + if (!isOverride && !hasReturns && !hasConstructor && !isInterface && + node.parent.kind !== "get" && node.parent.kind !== "constructor" && + node.parent.kind !== "set" && !isTypeClass(node)) { + if (requireReturn || functionData.returnPresent) { + context.report(jsdocNode, "Missing JSDoc @" + (prefer.returns || "returns") + " for function."); + } } - } - // check the parameters - var jsdocParams = Object.keys(params); + // check the parameters + var jsdocParams = Object.keys(params); - if (node.params) { - node.params.forEach(function(param, i) { - var name = param.name; + if (node.params) { + node.params.forEach(function(param, i) { + var name = param.name; - if (param.type === "AssignmentPattern") { - name = param.left.name; - } + if (param.type === "AssignmentPattern") { + name = param.left.name; + } - // TODO(nzakas): Figure out logical things to do with destructured, default, rest params - if (param.type === "Identifier" || param.type === "AssignmentPattern") { - 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] && !isOverride) { - context.report(jsdocNode, "Missing JSDoc for parameter '{{name}}'.", { - name: name - }); + // TODO(nzakas): Figure out logical things to do with destructured, default, rest params + if (param.type === "Identifier" || param.type === "AssignmentPattern") { + 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] && !isOverride) { + context.report(jsdocNode, "Missing JSDoc for parameter '{{name}}'.", { + name: name + }); + } } - } - }); - } + }); + } - if (options.matchDescription) { - var regex = new RegExp(options.matchDescription); + if (options.matchDescription) { + var regex = new RegExp(options.matchDescription); - if (!regex.test(jsdoc.description)) { - context.report(jsdocNode, "JSDoc description does not satisfy the regex pattern."); + if (!regex.test(jsdoc.description)) { + context.report(jsdocNode, "JSDoc description does not satisfy the regex pattern."); + } } + } } - } - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - return { - ArrowFunctionExpression: startFunction, - FunctionExpression: startFunction, - FunctionDeclaration: startFunction, - ClassExpression: startFunction, - ClassDeclaration: startFunction, - "ArrowFunctionExpression:exit": checkJSDoc, - "FunctionExpression:exit": checkJSDoc, - "FunctionDeclaration:exit": checkJSDoc, - "ClassExpression:exit": checkJSDoc, - "ClassDeclaration:exit": checkJSDoc, - ReturnStatement: addReturn - }; + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- -}; + return { + ArrowFunctionExpression: startFunction, + FunctionExpression: startFunction, + FunctionDeclaration: startFunction, + ClassExpression: startFunction, + ClassDeclaration: startFunction, + "ArrowFunctionExpression:exit": checkJSDoc, + "FunctionExpression:exit": checkJSDoc, + "FunctionDeclaration:exit": checkJSDoc, + "ClassExpression:exit": checkJSDoc, + "ClassDeclaration:exit": checkJSDoc, + ReturnStatement: addReturn + }; -module.exports.schema = [ - { - type: "object", - properties: { - prefer: { - type: "object", - additionalProperties: { - type: "string" - } - }, - preferType: { - type: "object", - additionalProperties: { - type: "string" - } - }, - requireReturn: { - type: "boolean" - }, - requireParamDescription: { - type: "boolean" - }, - requireReturnDescription: { - type: "boolean" - }, - matchDescription: { - type: "string" - }, - requireReturnType: { - type: "boolean" - } - }, - additionalProperties: false } -]; +}; diff --git a/lib/rules/valid-typeof.js b/lib/rules/valid-typeof.js index 369c6064670..289375a88ba 100644 --- a/lib/rules/valid-typeof.js +++ b/lib/rules/valid-typeof.js @@ -8,35 +8,45 @@ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: { + description: "enforce comparing `typeof` expressions against valid strings", + category: "Possible Errors", + recommended: true + }, - var VALID_TYPES = ["symbol", "undefined", "object", "boolean", "number", "string", "function"], - OPERATORS = ["==", "===", "!=", "!=="]; + schema: [] + }, - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- + create: function(context) { - return { + var VALID_TYPES = ["symbol", "undefined", "object", "boolean", "number", "string", "function"], + OPERATORS = ["==", "===", "!=", "!=="]; - UnaryExpression: function(node) { - var parent, sibling; + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- - if (node.operator === "typeof") { - parent = context.getAncestors().pop(); + return { - if (parent.type === "BinaryExpression" && OPERATORS.indexOf(parent.operator) !== -1) { - sibling = parent.left === node ? parent.right : parent.left; + UnaryExpression: function(node) { + var parent, sibling; - if (sibling.type === "Literal" && VALID_TYPES.indexOf(sibling.value) === -1) { - context.report(sibling, "Invalid typeof comparison value"); + 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"); + } } } } - } - }; + }; + } }; - -module.exports.schema = []; diff --git a/lib/rules/vars-on-top.js b/lib/rules/vars-on-top.js index 5a7f1ba9139..b44f77eb3fa 100644 --- a/lib/rules/vars-on-top.js +++ b/lib/rules/vars-on-top.js @@ -9,116 +9,126 @@ // 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"; - } +module.exports = { + meta: { + docs: { + description: "require `var` declarations be placed at the top of their containing scope", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create: 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"; + } - /** - * 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; - } + /** + * 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"; } - for (; i < l; ++i) { - if (statements[i].type !== "VariableDeclaration" && - (statements[i].type !== "ExportNamedDeclaration" || - statements[i].declaration.type !== "VariableDeclaration")) { - return false; - } - if (statements[i] === node) { - return true; + /** + * 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; + } } - } - return false; - } + for (; i < l; ++i) { + if (statements[i].type !== "VariableDeclaration" && + (statements[i].type !== "ExportNamedDeclaration" || + statements[i].declaration.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); + return false; } - } - /** - * 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); + /** + * 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); + } } - } - //-------------------------------------------------------------------------- - // 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 === "ExportNamedDeclaration") { - node = parent; - parent = grandParent; - grandParent = ancestors.pop(); - } + /** + * 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); + } + } - if (parent.type === "Program") { // That means its a global variable - globalVarCheck(node, parent); - } else { - blockScopeVarCheck(node, parent, grandParent); + //-------------------------------------------------------------------------- + // 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 === "ExportNamedDeclaration") { + node = parent; + parent = grandParent; + grandParent = ancestors.pop(); + } + + if (parent.type === "Program") { // That means its a global variable + globalVarCheck(node, parent); + } else { + blockScopeVarCheck(node, parent, grandParent); + } } } - } - }; + }; + } }; - -module.exports.schema = []; diff --git a/lib/rules/wrap-iife.js b/lib/rules/wrap-iife.js index 287bd5eca48..1dd1a0c5af8 100644 --- a/lib/rules/wrap-iife.js +++ b/lib/rules/wrap-iife.js @@ -9,46 +9,56 @@ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - - var style = context.options[0] || "outside"; - - /** - * Check if the node is wrapped in () - * @param {ASTNode} node node to evaluate - * @returns {boolean} True if it is wrapped - * @private - */ - function wrapped(node) { - var previousToken = context.getTokenBefore(node), - nextToken = context.getTokenAfter(node); - - return previousToken && previousToken.value === "(" && - nextToken && nextToken.value === ")"; - } +module.exports = { + meta: { + docs: { + description: "require parentheses around immediate `function` invocations", + category: "Best Practices", + recommended: false + }, - return { + schema: [ + { + enum: ["outside", "inside", "any"] + } + ] + }, - CallExpression: function(node) { - if (node.callee.type === "FunctionExpression") { - var callExpressionWrapped = wrapped(node), - functionExpressionWrapped = wrapped(node.callee); + create: function(context) { - 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."); - } - } + var style = context.options[0] || "outside"; + + /** + * Check if the node is wrapped in () + * @param {ASTNode} node node to evaluate + * @returns {boolean} True if it is wrapped + * @private + */ + 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."); + } + } + } + }; -module.exports.schema = [ - { - enum: ["outside", "inside", "any"] } -]; +}; diff --git a/lib/rules/wrap-regex.js b/lib/rules/wrap-regex.js index 34cb9950027..96df3304c8b 100644 --- a/lib/rules/wrap-regex.js +++ b/lib/rules/wrap-regex.js @@ -9,30 +9,40 @@ // 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."); +module.exports = { + meta: { + docs: { + description: "require parenthesis around regex literals", + category: "Stylistic Issues", + recommended: false + }, + + schema: [] + }, + + create: 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."); + } } } - } - }; + }; + } }; - -module.exports.schema = []; diff --git a/lib/rules/yield-star-spacing.js b/lib/rules/yield-star-spacing.js index a34ec1d6cdc..e2911b72005 100644 --- a/lib/rules/yield-star-spacing.js +++ b/lib/rules/yield-star-spacing.js @@ -9,93 +9,105 @@ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - var sourceCode = context.getSourceCode(); +module.exports = { + meta: { + docs: { + description: "require or disallow spacing around the `*` in `yield*` expressions", + category: "ECMAScript 6", + recommended: false + }, - var mode = (function(option) { - if (!option || typeof option === "string") { - return { - before: { before: true, after: false }, - after: { before: false, after: true }, - both: { before: true, after: true }, - neither: { before: false, after: false } - }[option || "after"]; - } - return option; - }(context.options[0])); + fixable: "whitespace", + + schema: [ + { + oneOf: [ + { + enum: ["before", "after", "both", "neither"] + }, + { + type: "object", + properties: { + before: {type: "boolean"}, + after: {type: "boolean"} + }, + additionalProperties: false + } + ] + } + ] + }, - /** - * 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 (sourceCode.isSpaceBetweenTokens(leftToken, rightToken) !== mode[side]) { - var after = leftToken.value === "*"; - var spaceRequired = mode[side]; - var node = after ? leftToken : rightToken; - var type = spaceRequired ? "Missing" : "Unexpected"; - var message = type + " space " + side + " *."; + create: function(context) { + var sourceCode = context.getSourceCode(); - context.report({ - node: node, - message: message, - fix: function(fixer) { - if (spaceRequired) { - if (after) { - return fixer.insertTextAfter(node, " "); + var mode = (function(option) { + if (!option || typeof option === "string") { + return { + before: { before: true, after: false }, + after: { before: false, after: true }, + both: { before: true, after: true }, + neither: { before: false, after: false } + }[option || "after"]; + } + return option; + }(context.options[0])); + + /** + * 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 (sourceCode.isSpaceBetweenTokens(leftToken, rightToken) !== mode[side]) { + var after = leftToken.value === "*"; + var spaceRequired = mode[side]; + var node = after ? leftToken : rightToken; + var type = spaceRequired ? "Missing" : "Unexpected"; + var message = type + " space " + side + " *."; + + context.report({ + node: node, + message: message, + fix: function(fixer) { + if (spaceRequired) { + if (after) { + return fixer.insertTextAfter(node, " "); + } + return fixer.insertTextBefore(node, " "); } - return fixer.insertTextBefore(node, " "); + return fixer.removeRange([leftToken.range[1], rightToken.range[0]]); } - return fixer.removeRange([leftToken.range[1], rightToken.range[0]]); - } - }); - } - } - - /** - * Enforces the spacing around the star if node is a yield* expression. - * @param {ASTNode} node A yield expression node. - * @returns {void} - */ - function checkExpression(node) { - if (!node.delegate) { - return; + }); + } } - var tokens = sourceCode.getFirstTokens(node, 3); - var yieldToken = tokens[0]; - var starToken = tokens[1]; - var nextToken = tokens[2]; + /** + * Enforces the spacing around the star if node is a yield* expression. + * @param {ASTNode} node A yield expression node. + * @returns {void} + */ + function checkExpression(node) { + if (!node.delegate) { + return; + } - checkSpacing("before", yieldToken, starToken); - checkSpacing("after", starToken, nextToken); - } + var tokens = sourceCode.getFirstTokens(node, 3); + var yieldToken = tokens[0]; + var starToken = tokens[1]; + var nextToken = tokens[2]; - return { - YieldExpression: checkExpression - }; + checkSpacing("before", yieldToken, starToken); + checkSpacing("after", starToken, nextToken); + } -}; + return { + YieldExpression: checkExpression + }; -module.exports.schema = [ - { - oneOf: [ - { - enum: ["before", "after", "both", "neither"] - }, - { - type: "object", - properties: { - before: {type: "boolean"}, - after: {type: "boolean"} - }, - additionalProperties: false - } - ] } -]; +}; diff --git a/lib/rules/yoda.js b/lib/rules/yoda.js index 949a83d73e5..ce2709ec9a4 100644 --- a/lib/rules/yoda.js +++ b/lib/rules/yoda.js @@ -117,129 +117,139 @@ function same(a, b) { // 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); - var onlyEquality = (context.options[1] && context.options[1].onlyEquality); - - /** - * 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; +module.exports = { + meta: { + docs: { + description: "require or disallow \"Yoda\" conditions", + category: "Best Practices", + recommended: false + }, - /** - * 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)); - } + schema: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + exceptRange: { + type: "boolean" + }, + onlyEquality: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, - /** - * 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)); - } + create: function(context) { + + // Default to "never" (!always) if no option + var always = (context.options[0] === "always"); + var exceptRange = (context.options[1] && context.options[1].exceptRange); + var onlyEquality = (context.options[1] && context.options[1].onlyEquality); /** - * 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. + * 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 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()); - } + 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)); + } - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - return { - BinaryExpression: always ? function(node) { - - // Comparisons must always be yoda-style: if ("blue" === color) - if ( - (node.right.type === "Literal" || looksLikeLiteral(node.right)) && - !(node.left.type === "Literal" || looksLikeLiteral(node.left)) && - !(!isEqualityOperator(node.operator) && onlyEquality) && - isComparisonOperator(node.operator) && - !(exceptRange && isRangeTest(context.getAncestors().pop())) - ) { - context.report(node, "Expected literal to be on the left side of " + node.operator + "."); + /** + * 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)); } - } : function(node) { - - // Comparisons must never be yoda-style (default) - if ( - (node.left.type === "Literal" || looksLikeLiteral(node.left)) && - !(node.right.type === "Literal" || looksLikeLiteral(node.right)) && - !(!isEqualityOperator(node.operator) && onlyEquality) && - isComparisonOperator(node.operator) && - !(exceptRange && isRangeTest(context.getAncestors().pop())) - ) { - context.report(node, "Expected literal to be on the right side of " + node.operator + "."); + /** + * 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)) && + !(node.left.type === "Literal" || looksLikeLiteral(node.left)) && + !(!isEqualityOperator(node.operator) && onlyEquality) && + 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)) && + !(node.right.type === "Literal" || looksLikeLiteral(node.right)) && + !(!isEqualityOperator(node.operator) && onlyEquality) && + isComparisonOperator(node.operator) && + !(exceptRange && isRangeTest(context.getAncestors().pop())) + ) { + context.report(node, "Expected literal to be on the right side of " + node.operator + "."); + } -module.exports.schema = [ - { - enum: ["always", "never"] - }, - { - type: "object", - properties: { - exceptRange: { - type: "boolean" - }, - onlyEquality: { - type: "boolean" } - }, - additionalProperties: false + }; + } -]; +};