diff options
Diffstat (limited to 'tools/node_modules/eslint/lib/rules')
258 files changed, 41165 insertions, 0 deletions
diff --git a/tools/node_modules/eslint/lib/rules/.eslintrc.yml b/tools/node_modules/eslint/lib/rules/.eslintrc.yml new file mode 100644 index 0000000000..2a8d907935 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/.eslintrc.yml @@ -0,0 +1,3 @@ +rules: + rulesdir/no-invalid-meta: "error" + rulesdir/consistent-docs-description: "error" diff --git a/tools/node_modules/eslint/lib/rules/accessor-pairs.js b/tools/node_modules/eslint/lib/rules/accessor-pairs.js new file mode 100644 index 0000000000..4afdc7136c --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/accessor-pairs.js @@ -0,0 +1,156 @@ +/** + * @fileoverview Rule to flag wrapping non-iife in parens + * @author Gyandeep Singh + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether or not a given node is an `Identifier` node which was named a given name. + * @param {ASTNode} node - A node to check. + * @param {string} name - An expected name of the node. + * @returns {boolean} `true` if the node is an `Identifier` node which was named as expected. + */ +function isIdentifier(node, name) { + return node.type === "Identifier" && node.name === name; +} + +/** + * Checks whether or not a given node is an argument of a specified method call. + * @param {ASTNode} node - A node to check. + * @param {number} index - An expected index of the node in arguments. + * @param {string} object - An expected name of the object of the method. + * @param {string} property - An expected name of the method. + * @returns {boolean} `true` if the node is an argument of the specified method call. + */ +function isArgumentOfMethodCall(node, index, object, property) { + const parent = node.parent; + + return ( + parent.type === "CallExpression" && + parent.callee.type === "MemberExpression" && + parent.callee.computed === false && + isIdentifier(parent.callee.object, object) && + isIdentifier(parent.callee.property, property) && + parent.arguments[index] === node + ); +} + +/** + * Checks whether or not a given node is a property descriptor. + * @param {ASTNode} node - A node to check. + * @returns {boolean} `true` if the node is a property descriptor. + */ +function isPropertyDescriptor(node) { + + // Object.defineProperty(obj, "foo", {set: ...}) + if (isArgumentOfMethodCall(node, 2, "Object", "defineProperty") || + isArgumentOfMethodCall(node, 2, "Reflect", "defineProperty") + ) { + return true; + } + + /* + * Object.defineProperties(obj, {foo: {set: ...}}) + * Object.create(proto, {foo: {set: ...}}) + */ + node = node.parent.parent; + + return node.type === "ObjectExpression" && ( + isArgumentOfMethodCall(node, 1, "Object", "create") || + isArgumentOfMethodCall(node, 1, "Object", "defineProperties") + ); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce getter and setter pairs in objects", + category: "Best Practices", + recommended: false + }, + schema: [{ + type: "object", + properties: { + getWithoutSet: { + type: "boolean" + }, + setWithoutGet: { + type: "boolean" + } + }, + additionalProperties: false + }] + }, + create(context) { + const config = context.options[0] || {}; + const checkGetWithoutSet = config.getWithoutSet === true; + const checkSetWithoutGet = config.setWithoutGet !== false; + + /** + * Checks a object expression to see if it has setter and getter both present or none. + * @param {ASTNode} node The node to check. + * @returns {void} + * @private + */ + function checkLonelySetGet(node) { + let isSetPresent = false; + let isGetPresent = false; + const isDescriptor = isPropertyDescriptor(node); + + for (let i = 0, end = node.properties.length; i < end; i++) { + const property = node.properties[i]; + + let propToCheck = ""; + + if (property.kind === "init") { + if (isDescriptor && !property.computed) { + propToCheck = property.key.name; + } + } else { + propToCheck = property.kind; + } + + switch (propToCheck) { + case "set": + isSetPresent = true; + break; + + case "get": + isGetPresent = true; + break; + + default: + + // Do nothing + } + + if (isSetPresent && isGetPresent) { + break; + } + } + + if (checkSetWithoutGet && isSetPresent && !isGetPresent) { + context.report({ node, message: "Getter is not present." }); + } else if (checkGetWithoutSet && isGetPresent && !isSetPresent) { + context.report({ node, message: "Setter is not present." }); + } + } + + return { + ObjectExpression(node) { + if (checkSetWithoutGet || checkGetWithoutSet) { + checkLonelySetGet(node); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/array-bracket-newline.js b/tools/node_modules/eslint/lib/rules/array-bracket-newline.js new file mode 100644 index 0000000000..cb7350a825 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/array-bracket-newline.js @@ -0,0 +1,249 @@ +/** + * @fileoverview Rule to enforce linebreaks after open and before close array brackets + * @author Jan Peer Stöcklmair <https://github.com/JPeer264> + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce linebreaks after opening and before closing array brackets", + category: "Stylistic Issues", + recommended: false + }, + fixable: "whitespace", + schema: [ + { + oneOf: [ + { + enum: ["always", "never", "consistent"] + }, + { + type: "object", + properties: { + multiline: { + type: "boolean" + }, + minItems: { + type: ["integer", "null"], + minimum: 0 + } + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + + //---------------------------------------------------------------------- + // Helpers + //---------------------------------------------------------------------- + + /** + * Normalizes a given option value. + * + * @param {string|Object|undefined} option - An option value to parse. + * @returns {{multiline: boolean, minItems: number}} Normalized option object. + */ + function normalizeOptionValue(option) { + let consistent = false; + let multiline = false; + let minItems = 0; + + if (option) { + if (option === "consistent") { + consistent = true; + minItems = Number.POSITIVE_INFINITY; + } else if (option === "always" || option.minItems === 0) { + minItems = 0; + } else if (option === "never") { + minItems = Number.POSITIVE_INFINITY; + } else { + multiline = Boolean(option.multiline); + minItems = option.minItems || Number.POSITIVE_INFINITY; + } + } else { + consistent = false; + multiline = true; + minItems = Number.POSITIVE_INFINITY; + } + + return { consistent, multiline, minItems }; + } + + /** + * Normalizes a given option value. + * + * @param {string|Object|undefined} options - An option value to parse. + * @returns {{ArrayExpression: {multiline: boolean, minItems: number}, ArrayPattern: {multiline: boolean, minItems: number}}} Normalized option object. + */ + function normalizeOptions(options) { + const value = normalizeOptionValue(options); + + return { ArrayExpression: value, ArrayPattern: value }; + } + + /** + * Reports that there shouldn't be a linebreak after the first token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportNoBeginningLinebreak(node, token) { + context.report({ + node, + loc: token.loc, + message: "There should be no linebreak after '['.", + fix(fixer) { + const nextToken = sourceCode.getTokenAfter(token, { includeComments: true }); + + if (astUtils.isCommentToken(nextToken)) { + return null; + } + + return fixer.removeRange([token.range[1], nextToken.range[0]]); + } + }); + } + + /** + * Reports that there shouldn't be a linebreak before the last token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportNoEndingLinebreak(node, token) { + context.report({ + node, + loc: token.loc, + message: "There should be no linebreak before ']'.", + fix(fixer) { + const previousToken = sourceCode.getTokenBefore(token, { includeComments: true }); + + if (astUtils.isCommentToken(previousToken)) { + return null; + } + + return fixer.removeRange([previousToken.range[1], token.range[0]]); + } + }); + } + + /** + * Reports that there should be a linebreak after the first token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportRequiredBeginningLinebreak(node, token) { + context.report({ + node, + loc: token.loc, + message: "A linebreak is required after '['.", + fix(fixer) { + return fixer.insertTextAfter(token, "\n"); + } + }); + } + + /** + * Reports that there should be a linebreak before the last token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportRequiredEndingLinebreak(node, token) { + context.report({ + node, + loc: token.loc, + message: "A linebreak is required before ']'.", + fix(fixer) { + return fixer.insertTextBefore(token, "\n"); + } + }); + } + + /** + * Reports a given node if it violated this rule. + * + * @param {ASTNode} node - A node to check. This is an ArrayExpression node or an ArrayPattern node. + * @returns {void} + */ + function check(node) { + const elements = node.elements; + const normalizedOptions = normalizeOptions(context.options[0]); + const options = normalizedOptions[node.type]; + const openBracket = sourceCode.getFirstToken(node); + const closeBracket = sourceCode.getLastToken(node); + const firstIncComment = sourceCode.getTokenAfter(openBracket, { includeComments: true }); + const lastIncComment = sourceCode.getTokenBefore(closeBracket, { includeComments: true }); + const first = sourceCode.getTokenAfter(openBracket); + const last = sourceCode.getTokenBefore(closeBracket); + + const needsLinebreaks = ( + elements.length >= options.minItems || + ( + options.multiline && + elements.length > 0 && + firstIncComment.loc.start.line !== lastIncComment.loc.end.line + ) || + ( + elements.length === 0 && + firstIncComment.type === "Block" && + firstIncComment.loc.start.line !== lastIncComment.loc.end.line && + firstIncComment === lastIncComment + ) || + ( + options.consistent && + firstIncComment.loc.start.line !== openBracket.loc.end.line + ) + ); + + /* + * Use tokens or comments to check multiline or not. + * But use only tokens to check whether linebreaks are needed. + * This allows: + * var arr = [ // eslint-disable-line foo + * 'a' + * ] + */ + + if (needsLinebreaks) { + if (astUtils.isTokenOnSameLine(openBracket, first)) { + reportRequiredBeginningLinebreak(node, openBracket); + } + if (astUtils.isTokenOnSameLine(last, closeBracket)) { + reportRequiredEndingLinebreak(node, closeBracket); + } + } else { + if (!astUtils.isTokenOnSameLine(openBracket, first)) { + reportNoBeginningLinebreak(node, openBracket); + } + if (!astUtils.isTokenOnSameLine(last, closeBracket)) { + reportNoEndingLinebreak(node, closeBracket); + } + } + } + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + return { + ArrayPattern: check, + ArrayExpression: check + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/array-bracket-spacing.js b/tools/node_modules/eslint/lib/rules/array-bracket-spacing.js new file mode 100644 index 0000000000..aecef2c4f2 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/array-bracket-spacing.js @@ -0,0 +1,229 @@ +/** + * @fileoverview Disallows or enforces spaces inside of array brackets. + * @author Jamund Ferguson + */ +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent spacing inside array brackets", + category: "Stylistic Issues", + recommended: false + }, + fixable: "whitespace", + schema: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + singleValue: { + type: "boolean" + }, + objectsInArrays: { + type: "boolean" + }, + arraysInArrays: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + create(context) { + const spaced = context.options[0] === "always", + sourceCode = context.getSourceCode(); + + /** + * Determines whether an option is set, relative to the spacing option. + * If spaced is "always", then check whether option is set to false. + * If spaced is "never", then check whether option is set to true. + * @param {Object} option - The option to exclude. + * @returns {boolean} Whether or not the property is excluded. + */ + function isOptionSet(option) { + return context.options[1] ? context.options[1][option] === !spaced : false; + } + + const options = { + spaced, + singleElementException: isOptionSet("singleValue"), + objectsInArraysException: isOptionSet("objectsInArrays"), + arraysInArraysException: isOptionSet("arraysInArrays") + }; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Reports that there shouldn't be a space after the first token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportNoBeginningSpace(node, token) { + context.report({ + node, + loc: token.loc.start, + message: "There should be no space after '{{tokenValue}}'.", + data: { + tokenValue: token.value + }, + fix(fixer) { + const nextToken = sourceCode.getTokenAfter(token); + + return fixer.removeRange([token.range[1], nextToken.range[0]]); + } + }); + } + + /** + * Reports that there shouldn't be a space before the last token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportNoEndingSpace(node, token) { + context.report({ + node, + loc: token.loc.start, + message: "There should be no space before '{{tokenValue}}'.", + data: { + tokenValue: token.value + }, + fix(fixer) { + const previousToken = sourceCode.getTokenBefore(token); + + return fixer.removeRange([previousToken.range[1], token.range[0]]); + } + }); + } + + /** + * Reports that there should be a space after the first token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportRequiredBeginningSpace(node, token) { + context.report({ + node, + loc: token.loc.start, + message: "A space is required after '{{tokenValue}}'.", + data: { + tokenValue: token.value + }, + fix(fixer) { + return fixer.insertTextAfter(token, " "); + } + }); + } + + /** + * Reports that there should be a space before the last token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportRequiredEndingSpace(node, token) { + context.report({ + node, + loc: token.loc.start, + message: "A space is required before '{{tokenValue}}'.", + data: { + tokenValue: token.value + }, + fix(fixer) { + return fixer.insertTextBefore(token, " "); + } + }); + } + + /** + * Determines if a node is an object type + * @param {ASTNode} node - The node to check. + * @returns {boolean} Whether or not the node is an object type. + */ + function isObjectType(node) { + return node && (node.type === "ObjectExpression" || node.type === "ObjectPattern"); + } + + /** + * Determines if a node is an array type + * @param {ASTNode} node - The node to check. + * @returns {boolean} Whether or not the node is an array type. + */ + function isArrayType(node) { + return node && (node.type === "ArrayExpression" || node.type === "ArrayPattern"); + } + + /** + * Validates the spacing around array brackets + * @param {ASTNode} node - The node we're checking for spacing + * @returns {void} + */ + function validateArraySpacing(node) { + if (options.spaced && node.elements.length === 0) { + return; + } + + const first = sourceCode.getFirstToken(node), + second = sourceCode.getFirstToken(node, 1), + last = node.typeAnnotation + ? sourceCode.getTokenBefore(node.typeAnnotation) + : sourceCode.getLastToken(node), + penultimate = sourceCode.getTokenBefore(last), + firstElement = node.elements[0], + lastElement = node.elements[node.elements.length - 1]; + + const openingBracketMustBeSpaced = + options.objectsInArraysException && isObjectType(firstElement) || + options.arraysInArraysException && isArrayType(firstElement) || + options.singleElementException && node.elements.length === 1 + ? !options.spaced : options.spaced; + + const closingBracketMustBeSpaced = + options.objectsInArraysException && isObjectType(lastElement) || + options.arraysInArraysException && isArrayType(lastElement) || + options.singleElementException && node.elements.length === 1 + ? !options.spaced : options.spaced; + + if (astUtils.isTokenOnSameLine(first, second)) { + if (openingBracketMustBeSpaced && !sourceCode.isSpaceBetweenTokens(first, second)) { + reportRequiredBeginningSpace(node, first); + } + if (!openingBracketMustBeSpaced && sourceCode.isSpaceBetweenTokens(first, second)) { + reportNoBeginningSpace(node, first); + } + } + + if (first !== penultimate && astUtils.isTokenOnSameLine(penultimate, last)) { + if (closingBracketMustBeSpaced && !sourceCode.isSpaceBetweenTokens(penultimate, last)) { + reportRequiredEndingSpace(node, last); + } + if (!closingBracketMustBeSpaced && sourceCode.isSpaceBetweenTokens(penultimate, last)) { + reportNoEndingSpace(node, last); + } + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + ArrayPattern: validateArraySpacing, + ArrayExpression: validateArraySpacing + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/array-callback-return.js b/tools/node_modules/eslint/lib/rules/array-callback-return.js new file mode 100644 index 0000000000..37d6ebe3a7 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/array-callback-return.js @@ -0,0 +1,232 @@ +/** + * @fileoverview Rule to enforce return statements in callbacks of array's methods + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const lodash = require("lodash"); + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/; +const TARGET_METHODS = /^(?:every|filter|find(?:Index)?|map|reduce(?:Right)?|some|sort)$/; + +/** + * Checks a given code path segment is reachable. + * + * @param {CodePathSegment} segment - A segment to check. + * @returns {boolean} `true` if the segment is reachable. + */ +function isReachable(segment) { + return segment.reachable; +} + +/** + * Gets a readable location. + * + * - FunctionExpression -> the function name or `function` keyword. + * - ArrowFunctionExpression -> `=>` token. + * + * @param {ASTNode} node - A function node to get. + * @param {SourceCode} sourceCode - A source code to get tokens. + * @returns {ASTNode|Token} The node or the token of a location. + */ +function getLocation(node, sourceCode) { + if (node.type === "ArrowFunctionExpression") { + return sourceCode.getTokenBefore(node.body); + } + return node.id || node; +} + +/** + * Checks a given node is a MemberExpression node which has the specified name's + * property. + * + * @param {ASTNode} node - A node to check. + * @returns {boolean} `true` if the node is a MemberExpression node which has + * the specified name's property + */ +function isTargetMethod(node) { + return ( + node.type === "MemberExpression" && + TARGET_METHODS.test(astUtils.getStaticPropertyName(node) || "") + ); +} + +/** + * Checks whether or not a given node is a function expression which is the + * callback of an array method. + * + * @param {ASTNode} node - A node to check. This is one of + * FunctionExpression or ArrowFunctionExpression. + * @returns {boolean} `true` if the node is the callback of an array method. + */ +function isCallbackOfArrayMethod(node) { + while (node) { + const parent = node.parent; + + switch (parent.type) { + + /* + * Looks up the destination. e.g., + * foo.every(nativeFoo || function foo() { ... }); + */ + case "LogicalExpression": + case "ConditionalExpression": + node = parent; + break; + + /* + * If the upper function is IIFE, checks the destination of the return value. + * e.g. + * foo.every((function() { + * // setup... + * return function callback() { ... }; + * })()); + */ + case "ReturnStatement": { + const func = astUtils.getUpperFunction(parent); + + if (func === null || !astUtils.isCallee(func)) { + return false; + } + node = func.parent; + break; + } + + /* + * e.g. + * Array.from([], function() {}); + * list.every(function() {}); + */ + case "CallExpression": + if (astUtils.isArrayFromMethod(parent.callee)) { + return ( + parent.arguments.length >= 2 && + parent.arguments[1] === node + ); + } + if (isTargetMethod(parent.callee)) { + return ( + parent.arguments.length >= 1 && + parent.arguments[0] === node + ); + } + return false; + + // Otherwise this node is not target. + default: + return false; + } + } + + /* istanbul ignore next: unreachable */ + return false; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce `return` statements in callbacks of array methods", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + let funcInfo = { + upper: null, + codePath: null, + hasReturn: false, + shouldCheck: false, + node: null + }; + + /** + * Checks whether or not the last code path segment is reachable. + * Then reports this function if the segment is reachable. + * + * If the last code path segment is reachable, there are paths which are not + * returned or thrown. + * + * @param {ASTNode} node - A node to check. + * @returns {void} + */ + function checkLastSegment(node) { + if (funcInfo.shouldCheck && + funcInfo.codePath.currentSegments.some(isReachable) + ) { + context.report({ + node, + loc: getLocation(node, context.getSourceCode()).loc.start, + message: funcInfo.hasReturn + ? "Expected to return a value at the end of {{name}}." + : "Expected to return a value in {{name}}.", + data: { + name: astUtils.getFunctionNameWithKind(funcInfo.node) + } + }); + } + } + + return { + + // Stacks this function's information. + onCodePathStart(codePath, node) { + funcInfo = { + upper: funcInfo, + codePath, + hasReturn: false, + shouldCheck: + TARGET_NODE_TYPE.test(node.type) && + node.body.type === "BlockStatement" && + isCallbackOfArrayMethod(node) && + !node.async && + !node.generator, + node + }; + }, + + // Pops this function's information. + onCodePathEnd() { + funcInfo = funcInfo.upper; + }, + + // Checks the return statement is valid. + ReturnStatement(node) { + if (funcInfo.shouldCheck) { + funcInfo.hasReturn = true; + + if (!node.argument) { + context.report({ + node, + message: "{{name}} expected a return value.", + data: { + name: lodash.upperFirst(astUtils.getFunctionNameWithKind(funcInfo.node)) + } + }); + } + } + }, + + // Reports a given function if the last path is reachable. + "FunctionExpression:exit": checkLastSegment, + "ArrowFunctionExpression:exit": checkLastSegment + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/array-element-newline.js b/tools/node_modules/eslint/lib/rules/array-element-newline.js new file mode 100644 index 0000000000..26dc9bfa0b --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/array-element-newline.js @@ -0,0 +1,230 @@ +/** + * @fileoverview Rule to enforce line breaks after each array element + * @author Jan Peer Stöcklmair <https://github.com/JPeer264> + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce line breaks after each array element", + category: "Stylistic Issues", + recommended: false + }, + fixable: "whitespace", + schema: [ + { + oneOf: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + multiline: { + type: "boolean" + }, + minItems: { + type: ["integer", "null"], + minimum: 0 + } + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + //---------------------------------------------------------------------- + // Helpers + //---------------------------------------------------------------------- + + /** + * Normalizes a given option value. + * + * @param {string|Object|undefined} option - An option value to parse. + * @returns {{multiline: boolean, minItems: number}} Normalized option object. + */ + function normalizeOptionValue(option) { + let multiline = false; + let minItems; + + option = option || "always"; + + if (option === "always" || option.minItems === 0) { + minItems = 0; + } else if (option === "never") { + minItems = Number.POSITIVE_INFINITY; + } else { + multiline = Boolean(option.multiline); + minItems = option.minItems || Number.POSITIVE_INFINITY; + } + + return { multiline, minItems }; + } + + /** + * Normalizes a given option value. + * + * @param {string|Object|undefined} options - An option value to parse. + * @returns {{ArrayExpression: {multiline: boolean, minItems: number}, ArrayPattern: {multiline: boolean, minItems: number}}} Normalized option object. + */ + function normalizeOptions(options) { + const value = normalizeOptionValue(options); + + return { ArrayExpression: value, ArrayPattern: value }; + } + + /** + * Reports that there shouldn't be a line break after the first token + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportNoLineBreak(token) { + const tokenBefore = sourceCode.getTokenBefore(token, { includeComments: true }); + + context.report({ + loc: { + start: tokenBefore.loc.end, + end: token.loc.start + }, + message: "There should be no linebreak here.", + fix(fixer) { + if (astUtils.isCommentToken(tokenBefore)) { + return null; + } + + if (!astUtils.isTokenOnSameLine(tokenBefore, token)) { + return fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], " "); + } + + /* + * This will check if the comma is on the same line as the next element + * Following array: + * [ + * 1 + * , 2 + * , 3 + * ] + * + * will be fixed to: + * [ + * 1, 2, 3 + * ] + */ + const twoTokensBefore = sourceCode.getTokenBefore(tokenBefore, { includeComments: true }); + + if (astUtils.isCommentToken(twoTokensBefore)) { + return null; + } + + return fixer.replaceTextRange([twoTokensBefore.range[1], tokenBefore.range[0]], ""); + + } + }); + } + + /** + * Reports that there should be a line break after the first token + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportRequiredLineBreak(token) { + const tokenBefore = sourceCode.getTokenBefore(token, { includeComments: true }); + + context.report({ + loc: { + start: tokenBefore.loc.end, + end: token.loc.start + }, + message: "There should be a linebreak after this element.", + fix(fixer) { + return fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], "\n"); + } + }); + } + + /** + * Reports a given node if it violated this rule. + * + * @param {ASTNode} node - A node to check. This is an ObjectExpression node or an ObjectPattern node. + * @param {{multiline: boolean, minItems: number}} options - An option object. + * @returns {void} + */ + function check(node) { + const elements = node.elements; + const normalizedOptions = normalizeOptions(context.options[0]); + const options = normalizedOptions[node.type]; + + let elementBreak = false; + + /* + * MULTILINE: true + * loop through every element and check + * if at least one element has linebreaks inside + * this ensures that following is not valid (due to elements are on the same line): + * + * [ + * 1, + * 2, + * 3 + * ] + */ + if (options.multiline) { + elementBreak = elements + .filter(element => element !== null) + .some(element => element.loc.start.line !== element.loc.end.line); + } + + const needsLinebreaks = ( + elements.length >= options.minItems || + ( + options.multiline && + elementBreak + ) + ); + + elements.forEach((element, i) => { + const previousElement = elements[i - 1]; + + if (i === 0 || element === null || previousElement === null) { + return; + } + + const commaToken = sourceCode.getFirstTokenBetween(previousElement, element, astUtils.isCommaToken); + const lastTokenOfPreviousElement = sourceCode.getTokenBefore(commaToken); + const firstTokenOfCurrentElement = sourceCode.getTokenAfter(commaToken); + + if (needsLinebreaks) { + if (astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) { + reportRequiredLineBreak(firstTokenOfCurrentElement); + } + } else { + if (!astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) { + reportNoLineBreak(firstTokenOfCurrentElement); + } + } + }); + } + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + return { + ArrayPattern: check, + ArrayExpression: check + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/arrow-body-style.js b/tools/node_modules/eslint/lib/rules/arrow-body-style.js new file mode 100644 index 0000000000..78a391334d --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/arrow-body-style.js @@ -0,0 +1,215 @@ +/** + * @fileoverview Rule to require braces in arrow function body. + * @author Alberto RodrÃguez + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require braces around arrow function bodies", + category: "ECMAScript 6", + recommended: false + }, + + schema: { + anyOf: [ + { + type: "array", + items: [ + { + enum: ["always", "never"] + } + ], + minItems: 0, + maxItems: 1 + }, + { + type: "array", + items: [ + { + enum: ["as-needed"] + }, + { + type: "object", + properties: { + requireReturnForObjectLiteral: { type: "boolean" } + }, + additionalProperties: false + } + ], + minItems: 0, + maxItems: 2 + } + ] + }, + + fixable: "code" + }, + + create(context) { + const options = context.options; + const always = options[0] === "always"; + const asNeeded = !options[0] || options[0] === "as-needed"; + const never = options[0] === "never"; + const requireReturnForObjectLiteral = options[1] && options[1].requireReturnForObjectLiteral; + const sourceCode = context.getSourceCode(); + + /** + * Checks whether the given node has ASI problem or not. + * @param {Token} token The token to check. + * @returns {boolean} `true` if it changes semantics if `;` or `}` followed by the token are removed. + */ + function hasASIProblem(token) { + return token && token.type === "Punctuator" && /^[([/`+-]/.test(token.value); + } + + /** + * Gets the closing parenthesis which is the pair of the given opening parenthesis. + * @param {Token} token The opening parenthesis token to get. + * @returns {Token} The found closing parenthesis token. + */ + function findClosingParen(token) { + let node = sourceCode.getNodeByRangeIndex(token.range[1]); + + while (!astUtils.isParenthesised(sourceCode, node)) { + node = node.parent; + } + return sourceCode.getTokenAfter(node); + } + + /** + * Determines whether a arrow function body needs braces + * @param {ASTNode} node The arrow function node. + * @returns {void} + */ + function validate(node) { + const arrowBody = node.body; + + if (arrowBody.type === "BlockStatement") { + const blockBody = arrowBody.body; + + if (blockBody.length !== 1 && !never) { + return; + } + + if (asNeeded && requireReturnForObjectLiteral && blockBody[0].type === "ReturnStatement" && + blockBody[0].argument && blockBody[0].argument.type === "ObjectExpression") { + return; + } + + if (never || asNeeded && blockBody[0].type === "ReturnStatement") { + context.report({ + node, + loc: arrowBody.loc.start, + message: "Unexpected block statement surrounding arrow body.", + fix(fixer) { + const fixes = []; + + if (blockBody.length !== 1 || + blockBody[0].type !== "ReturnStatement" || + !blockBody[0].argument || + hasASIProblem(sourceCode.getTokenAfter(arrowBody)) + ) { + return fixes; + } + + const openingBrace = sourceCode.getFirstToken(arrowBody); + const closingBrace = sourceCode.getLastToken(arrowBody); + const firstValueToken = sourceCode.getFirstToken(blockBody[0], 1); + const lastValueToken = sourceCode.getLastToken(blockBody[0]); + const commentsExist = + sourceCode.commentsExistBetween(openingBrace, firstValueToken) || + sourceCode.commentsExistBetween(lastValueToken, closingBrace); + + /* + * Remove tokens around the return value. + * If comments don't exist, remove extra spaces as well. + */ + if (commentsExist) { + fixes.push( + fixer.remove(openingBrace), + fixer.remove(closingBrace), + fixer.remove(sourceCode.getTokenAfter(openingBrace)) // return keyword + ); + } else { + fixes.push( + fixer.removeRange([openingBrace.range[0], firstValueToken.range[0]]), + fixer.removeRange([lastValueToken.range[1], closingBrace.range[1]]) + ); + } + + /* + * If the first token of the reutrn value is `{`, + * enclose the return value by parentheses to avoid syntax error. + */ + if (astUtils.isOpeningBraceToken(firstValueToken)) { + fixes.push( + fixer.insertTextBefore(firstValueToken, "("), + fixer.insertTextAfter(lastValueToken, ")") + ); + } + + /* + * If the last token of the return statement is semicolon, remove it. + * Non-block arrow body is an expression, not a statement. + */ + if (astUtils.isSemicolonToken(lastValueToken)) { + fixes.push(fixer.remove(lastValueToken)); + } + + return fixes; + } + }); + } + } else { + if (always || (asNeeded && requireReturnForObjectLiteral && arrowBody.type === "ObjectExpression")) { + context.report({ + node, + loc: arrowBody.loc.start, + message: "Expected block statement surrounding arrow body.", + fix(fixer) { + const fixes = []; + const arrowToken = sourceCode.getTokenBefore(arrowBody, astUtils.isArrowToken); + const firstBodyToken = sourceCode.getTokenAfter(arrowToken); + const lastBodyToken = sourceCode.getLastToken(node); + const isParenthesisedObjectLiteral = + astUtils.isOpeningParenToken(firstBodyToken) && + astUtils.isOpeningBraceToken(sourceCode.getTokenAfter(firstBodyToken)); + + // Wrap the value by a block and a return statement. + fixes.push( + fixer.insertTextBefore(firstBodyToken, "{return "), + fixer.insertTextAfter(lastBodyToken, "}") + ); + + // If the value is object literal, remove parentheses which were forced by syntax. + if (isParenthesisedObjectLiteral) { + fixes.push( + fixer.remove(firstBodyToken), + fixer.remove(findClosingParen(firstBodyToken)) + ); + } + + return fixes; + } + }); + } + } + } + + return { + "ArrowFunctionExpression:exit": validate + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/arrow-parens.js b/tools/node_modules/eslint/lib/rules/arrow-parens.js new file mode 100644 index 0000000000..e8f8ddd8e7 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/arrow-parens.js @@ -0,0 +1,156 @@ +/** + * @fileoverview Rule to require parens in arrow function arguments. + * @author Jxck + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require parentheses around arrow function arguments", + category: "ECMAScript 6", + recommended: false + }, + + fixable: "code", + + schema: [ + { + enum: ["always", "as-needed"] + }, + { + type: "object", + properties: { + requireForBlockBody: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const message = "Expected parentheses around arrow function argument."; + const asNeededMessage = "Unexpected parentheses around single function argument."; + const asNeeded = context.options[0] === "as-needed"; + const requireForBlockBodyMessage = "Unexpected parentheses around single function argument having a body with no curly braces"; + const requireForBlockBodyNoParensMessage = "Expected parentheses around arrow function argument having a body with curly braces."; + const requireForBlockBody = asNeeded && context.options[1] && context.options[1].requireForBlockBody === true; + + const sourceCode = context.getSourceCode(); + + /** + * Determines whether a arrow function argument end with `)` + * @param {ASTNode} node The arrow function node. + * @returns {void} + */ + function parens(node) { + const isAsync = node.async; + const firstTokenOfParam = sourceCode.getFirstToken(node, isAsync ? 1 : 0); + + /** + * Remove the parenthesis around a parameter + * @param {Fixer} fixer Fixer + * @returns {string} fixed parameter + */ + function fixParamsWithParenthesis(fixer) { + const paramToken = sourceCode.getTokenAfter(firstTokenOfParam); + + /* + * ES8 allows Trailing commas in function parameter lists and calls + * https://github.com/eslint/eslint/issues/8834 + */ + const closingParenToken = sourceCode.getTokenAfter(paramToken, astUtils.isClosingParenToken); + const asyncToken = isAsync ? sourceCode.getTokenBefore(firstTokenOfParam) : null; + const shouldAddSpaceForAsync = asyncToken && (asyncToken.range[1] === firstTokenOfParam.range[0]); + + return fixer.replaceTextRange([ + firstTokenOfParam.range[0], + closingParenToken.range[1] + ], `${shouldAddSpaceForAsync ? " " : ""}${paramToken.value}`); + } + + // "as-needed", { "requireForBlockBody": true }: x => x + if ( + requireForBlockBody && + node.params.length === 1 && + node.params[0].type === "Identifier" && + !node.params[0].typeAnnotation && + node.body.type !== "BlockStatement" && + !node.returnType + ) { + if (astUtils.isOpeningParenToken(firstTokenOfParam)) { + context.report({ + node, + message: requireForBlockBodyMessage, + fix: fixParamsWithParenthesis + }); + } + return; + } + + if ( + requireForBlockBody && + node.body.type === "BlockStatement" + ) { + if (!astUtils.isOpeningParenToken(firstTokenOfParam)) { + context.report({ + node, + message: requireForBlockBodyNoParensMessage, + fix(fixer) { + return fixer.replaceText(firstTokenOfParam, `(${firstTokenOfParam.value})`); + } + }); + } + return; + } + + // "as-needed": x => x + if (asNeeded && + node.params.length === 1 && + node.params[0].type === "Identifier" && + !node.params[0].typeAnnotation && + !node.returnType + ) { + if (astUtils.isOpeningParenToken(firstTokenOfParam)) { + context.report({ + node, + message: asNeededMessage, + fix: fixParamsWithParenthesis + }); + } + return; + } + + if (firstTokenOfParam.type === "Identifier") { + const after = sourceCode.getTokenAfter(firstTokenOfParam); + + // (x) => x + if (after.value !== ")") { + context.report({ + node, + message, + fix(fixer) { + return fixer.replaceText(firstTokenOfParam, `(${firstTokenOfParam.value})`); + } + }); + } + } + } + + return { + ArrowFunctionExpression: parens + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/arrow-spacing.js b/tools/node_modules/eslint/lib/rules/arrow-spacing.js new file mode 100644 index 0000000000..37e03907cb --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/arrow-spacing.js @@ -0,0 +1,149 @@ +/** + * @fileoverview Rule to define spacing before/after arrow function's arrow. + * @author Jxck + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent spacing before and after the arrow in arrow functions", + category: "ECMAScript 6", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + type: "object", + properties: { + before: { + type: "boolean" + }, + after: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + // merge rules with default + const rule = { before: true, after: true }, + option = context.options[0] || {}; + + rule.before = option.before !== false; + rule.after = option.after !== false; + + const sourceCode = context.getSourceCode(); + + /** + * Get tokens of arrow(`=>`) and before/after arrow. + * @param {ASTNode} node The arrow function node. + * @returns {Object} Tokens of arrow and before/after arrow. + */ + function getTokens(node) { + const arrow = sourceCode.getTokenBefore(node.body, astUtils.isArrowToken); + + return { + before: sourceCode.getTokenBefore(arrow), + arrow, + after: sourceCode.getTokenAfter(arrow) + }; + } + + /** + * Count spaces before/after arrow(`=>`) token. + * @param {Object} tokens Tokens before/after arrow. + * @returns {Object} count of space before/after arrow. + */ + function countSpaces(tokens) { + const before = tokens.arrow.range[0] - tokens.before.range[1]; + const after = tokens.after.range[0] - tokens.arrow.range[1]; + + return { before, after }; + } + + /** + * Determines whether space(s) before after arrow(`=>`) is satisfy rule. + * if before/after value is `true`, there should be space(s). + * if before/after value is `false`, there should be no space. + * @param {ASTNode} node The arrow function node. + * @returns {void} + */ + function spaces(node) { + const tokens = getTokens(node); + const countSpace = countSpaces(tokens); + + if (rule.before) { + + // should be space(s) before arrow + if (countSpace.before === 0) { + context.report({ + node: tokens.before, + message: "Missing space before =>.", + fix(fixer) { + return fixer.insertTextBefore(tokens.arrow, " "); + } + }); + } + } else { + + // should be no space before arrow + if (countSpace.before > 0) { + context.report({ + node: tokens.before, + message: "Unexpected space before =>.", + fix(fixer) { + return fixer.removeRange([tokens.before.range[1], tokens.arrow.range[0]]); + } + }); + } + } + + if (rule.after) { + + // should be space(s) after arrow + if (countSpace.after === 0) { + context.report({ + node: tokens.after, + message: "Missing space after =>.", + fix(fixer) { + return fixer.insertTextAfter(tokens.arrow, " "); + } + }); + } + } else { + + // should be no space after arrow + if (countSpace.after > 0) { + context.report({ + node: tokens.after, + message: "Unexpected space after =>.", + fix(fixer) { + return fixer.removeRange([tokens.arrow.range[1], tokens.after.range[0]]); + } + }); + } + } + } + + return { + ArrowFunctionExpression: spaces + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/block-scoped-var.js b/tools/node_modules/eslint/lib/rules/block-scoped-var.js new file mode 100644 index 0000000000..0b4d855fae --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/block-scoped-var.js @@ -0,0 +1,115 @@ +/** + * @fileoverview Rule to check for "block scoped" variables by binding context + * @author Matt DuVall <http://www.mattduvall.com> + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce the use of variables within the scope they are defined", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + let stack = []; + + /** + * Makes a block scope. + * @param {ASTNode} node - A node of a scope. + * @returns {void} + */ + function enterScope(node) { + stack.push(node.range); + } + + /** + * Pops the last block scope. + * @returns {void} + */ + function exitScope() { + stack.pop(); + } + + /** + * Reports a given reference. + * @param {eslint-scope.Reference} reference - A reference to report. + * @returns {void} + */ + function report(reference) { + const identifier = reference.identifier; + + context.report({ node: identifier, message: "'{{name}}' used outside of binding context.", data: { name: identifier.name } }); + } + + /** + * Finds and reports references which are outside of valid scopes. + * @param {ASTNode} node - A node to get variables. + * @returns {void} + */ + function checkForVariables(node) { + if (node.kind !== "var") { + return; + } + + // Defines a predicate to check whether or not a given reference is outside of valid scope. + const scopeRange = stack[stack.length - 1]; + + /** + * Check if a reference is out of scope + * @param {ASTNode} reference node to examine + * @returns {boolean} True is its outside the scope + * @private + */ + function isOutsideOfScope(reference) { + const idRange = reference.identifier.range; + + return idRange[0] < scopeRange[0] || idRange[1] > scopeRange[1]; + } + + // Gets declared variables, and checks its references. + const variables = context.getDeclaredVariables(node); + + for (let i = 0; i < variables.length; ++i) { + + // Reports. + variables[i] + .references + .filter(isOutsideOfScope) + .forEach(report); + } + } + + return { + Program(node) { + stack = [node.range]; + }, + + // Manages scopes. + BlockStatement: enterScope, + "BlockStatement:exit": exitScope, + ForStatement: enterScope, + "ForStatement:exit": exitScope, + ForInStatement: enterScope, + "ForInStatement:exit": exitScope, + ForOfStatement: enterScope, + "ForOfStatement:exit": exitScope, + SwitchStatement: enterScope, + "SwitchStatement:exit": exitScope, + CatchClause: enterScope, + "CatchClause:exit": exitScope, + + // Finds and reports references which are outside of valid scope. + VariableDeclaration: checkForVariables + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/block-spacing.js b/tools/node_modules/eslint/lib/rules/block-spacing.js new file mode 100644 index 0000000000..b3ea8405e2 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/block-spacing.js @@ -0,0 +1,137 @@ +/** + * @fileoverview A rule to disallow or enforce spaces inside of single line blocks. + * @author Toru Nagashima + */ + +"use strict"; + +const util = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow or enforce spaces inside of blocks after opening block and before closing block", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { enum: ["always", "never"] } + ] + }, + + create(context) { + const always = (context.options[0] !== "never"), + message = always ? "Requires a space" : "Unexpected space(s)", + sourceCode = context.getSourceCode(); + + /** + * Gets the open brace token from a given node. + * @param {ASTNode} node - A BlockStatement/SwitchStatement node to get. + * @returns {Token} The token of the open brace. + */ + function getOpenBrace(node) { + if (node.type === "SwitchStatement") { + if (node.cases.length > 0) { + return sourceCode.getTokenBefore(node.cases[0]); + } + return sourceCode.getLastToken(node, 1); + } + return sourceCode.getFirstToken(node); + } + + /** + * Checks whether or not: + * - given tokens are on same line. + * - there is/isn't a space between given tokens. + * @param {Token} left - A token to check. + * @param {Token} right - The token which is next to `left`. + * @returns {boolean} + * When the option is `"always"`, `true` if there are one or more spaces between given tokens. + * When the option is `"never"`, `true` if there are not any spaces between given tokens. + * If given tokens are not on same line, it's always `true`. + */ + function isValid(left, right) { + return ( + !util.isTokenOnSameLine(left, right) || + sourceCode.isSpaceBetweenTokens(left, right) === always + ); + } + + /** + * Reports invalid spacing style inside braces. + * @param {ASTNode} node - A BlockStatement/SwitchStatement node to get. + * @returns {void} + */ + function checkSpacingInsideBraces(node) { + + // Gets braces and the first/last token of content. + const openBrace = getOpenBrace(node); + const closeBrace = sourceCode.getLastToken(node); + const firstToken = sourceCode.getTokenAfter(openBrace, { includeComments: true }); + const lastToken = sourceCode.getTokenBefore(closeBrace, { includeComments: true }); + + // Skip if the node is invalid or empty. + if (openBrace.type !== "Punctuator" || + openBrace.value !== "{" || + closeBrace.type !== "Punctuator" || + closeBrace.value !== "}" || + firstToken === closeBrace + ) { + return; + } + + // Skip line comments for option never + if (!always && firstToken.type === "Line") { + return; + } + + // Check. + if (!isValid(openBrace, firstToken)) { + context.report({ + node, + loc: openBrace.loc.start, + message: "{{message}} after '{'.", + data: { + message + }, + fix(fixer) { + if (always) { + return fixer.insertTextBefore(firstToken, " "); + } + + return fixer.removeRange([openBrace.range[1], firstToken.range[0]]); + } + }); + } + if (!isValid(lastToken, closeBrace)) { + context.report({ + node, + loc: closeBrace.loc.start, + message: "{{message}} before '}'.", + data: { + message + }, + fix(fixer) { + if (always) { + return fixer.insertTextAfter(lastToken, " "); + } + + return fixer.removeRange([lastToken.range[1], closeBrace.range[0]]); + } + }); + } + } + + return { + BlockStatement: checkSpacingInsideBraces, + SwitchStatement: checkSpacingInsideBraces + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/brace-style.js b/tools/node_modules/eslint/lib/rules/brace-style.js new file mode 100644 index 0000000000..320da9dac9 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/brace-style.js @@ -0,0 +1,182 @@ +/** + * @fileoverview Rule to flag block statements that do not use the one true brace style + * @author Ian Christian Myers + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent brace style for blocks", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + enum: ["1tbs", "stroustrup", "allman"] + }, + { + type: "object", + properties: { + allowSingleLine: { + type: "boolean" + } + }, + additionalProperties: false + } + ], + + fixable: "whitespace" + }, + + create(context) { + const style = context.options[0] || "1tbs", + params = context.options[1] || {}, + sourceCode = context.getSourceCode(); + + const OPEN_MESSAGE = "Opening curly brace does not appear on the same line as controlling statement.", + OPEN_MESSAGE_ALLMAN = "Opening curly brace appears on the same line as controlling statement.", + BODY_MESSAGE = "Statement inside of curly braces should be on next line.", + CLOSE_MESSAGE = "Closing curly brace does not appear on the same line as the subsequent block.", + CLOSE_MESSAGE_SINGLE = "Closing curly brace should be on the same line as opening curly brace or on the line after the previous block.", + CLOSE_MESSAGE_STROUSTRUP_ALLMAN = "Closing curly brace appears on the same line as the subsequent block."; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Fixes a place where a newline unexpectedly appears + * @param {Token} firstToken The token before the unexpected newline + * @param {Token} secondToken The token after the unexpected newline + * @returns {Function} A fixer function to remove the newlines between the tokens + */ + function removeNewlineBetween(firstToken, secondToken) { + const textRange = [firstToken.range[1], secondToken.range[0]]; + const textBetween = sourceCode.text.slice(textRange[0], textRange[1]); + + // Don't do a fix if there is a comment between the tokens + if (textBetween.trim()) { + return null; + } + return fixer => fixer.replaceTextRange(textRange, " "); + } + + /** + * Validates a pair of curly brackets based on the user's config + * @param {Token} openingCurly The opening curly bracket + * @param {Token} closingCurly The closing curly bracket + * @returns {void} + */ + function validateCurlyPair(openingCurly, closingCurly) { + const tokenBeforeOpeningCurly = sourceCode.getTokenBefore(openingCurly); + const tokenAfterOpeningCurly = sourceCode.getTokenAfter(openingCurly); + const tokenBeforeClosingCurly = sourceCode.getTokenBefore(closingCurly); + const singleLineException = params.allowSingleLine && astUtils.isTokenOnSameLine(openingCurly, closingCurly); + + if (style !== "allman" && !astUtils.isTokenOnSameLine(tokenBeforeOpeningCurly, openingCurly)) { + context.report({ + node: openingCurly, + message: OPEN_MESSAGE, + fix: removeNewlineBetween(tokenBeforeOpeningCurly, openingCurly) + }); + } + + if (style === "allman" && astUtils.isTokenOnSameLine(tokenBeforeOpeningCurly, openingCurly) && !singleLineException) { + context.report({ + node: openingCurly, + message: OPEN_MESSAGE_ALLMAN, + fix: fixer => fixer.insertTextBefore(openingCurly, "\n") + }); + } + + if (astUtils.isTokenOnSameLine(openingCurly, tokenAfterOpeningCurly) && tokenAfterOpeningCurly !== closingCurly && !singleLineException) { + context.report({ + node: openingCurly, + message: BODY_MESSAGE, + fix: fixer => fixer.insertTextAfter(openingCurly, "\n") + }); + } + + if (tokenBeforeClosingCurly !== openingCurly && !singleLineException && astUtils.isTokenOnSameLine(tokenBeforeClosingCurly, closingCurly)) { + context.report({ + node: closingCurly, + message: CLOSE_MESSAGE_SINGLE, + fix: fixer => fixer.insertTextBefore(closingCurly, "\n") + }); + } + } + + /** + * Validates the location of a token that appears before a keyword (e.g. a newline before `else`) + * @param {Token} curlyToken The closing curly token. This is assumed to precede a keyword token (such as `else` or `finally`). + * @returns {void} + */ + function validateCurlyBeforeKeyword(curlyToken) { + const keywordToken = sourceCode.getTokenAfter(curlyToken); + + if (style === "1tbs" && !astUtils.isTokenOnSameLine(curlyToken, keywordToken)) { + context.report({ + node: curlyToken, + message: CLOSE_MESSAGE, + fix: removeNewlineBetween(curlyToken, keywordToken) + }); + } + + if (style !== "1tbs" && astUtils.isTokenOnSameLine(curlyToken, keywordToken)) { + context.report({ + node: curlyToken, + message: CLOSE_MESSAGE_STROUSTRUP_ALLMAN, + fix: fixer => fixer.insertTextAfter(curlyToken, "\n") + }); + } + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + BlockStatement(node) { + if (!astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type)) { + validateCurlyPair(sourceCode.getFirstToken(node), sourceCode.getLastToken(node)); + } + }, + ClassBody(node) { + validateCurlyPair(sourceCode.getFirstToken(node), sourceCode.getLastToken(node)); + }, + SwitchStatement(node) { + const closingCurly = sourceCode.getLastToken(node); + const openingCurly = sourceCode.getTokenBefore(node.cases.length ? node.cases[0] : closingCurly); + + validateCurlyPair(openingCurly, closingCurly); + }, + IfStatement(node) { + if (node.consequent.type === "BlockStatement" && node.alternate) { + + // Handle the keyword after the `if` block (before `else`) + validateCurlyBeforeKeyword(sourceCode.getLastToken(node.consequent)); + } + }, + TryStatement(node) { + + // Handle the keyword after the `try` block (before `catch` or `finally`) + validateCurlyBeforeKeyword(sourceCode.getLastToken(node.block)); + + if (node.handler && node.finalizer) { + + // Handle the keyword after the `catch` block (before `finally`) + validateCurlyBeforeKeyword(sourceCode.getLastToken(node.handler.body)); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/callback-return.js b/tools/node_modules/eslint/lib/rules/callback-return.js new file mode 100644 index 0000000000..0fa7f278a1 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/callback-return.js @@ -0,0 +1,175 @@ +/** + * @fileoverview Enforce return after a callback. + * @author Jamund Ferguson + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require `return` statements after callbacks", + category: "Node.js and CommonJS", + recommended: false + }, + + schema: [{ + type: "array", + items: { type: "string" } + }] + }, + + create(context) { + + const callbacks = context.options[0] || ["callback", "cb", "next"], + sourceCode = context.getSourceCode(); + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Find the closest parent matching a list of types. + * @param {ASTNode} node The node whose parents we are searching + * @param {Array} types The node types to match + * @returns {ASTNode} The matched node or undefined. + */ + function findClosestParentOfType(node, types) { + if (!node.parent) { + return null; + } + if (types.indexOf(node.parent.type) === -1) { + return findClosestParentOfType(node.parent, types); + } + return node.parent; + } + + /** + * Check to see if a node contains only identifers + * @param {ASTNode} node The node to check + * @returns {boolean} Whether or not the node contains only identifers + */ + function containsOnlyIdentifiers(node) { + if (node.type === "Identifier") { + return true; + } + + if (node.type === "MemberExpression") { + if (node.object.type === "Identifier") { + return true; + } + if (node.object.type === "MemberExpression") { + return containsOnlyIdentifiers(node.object); + } + } + + return false; + } + + /** + * Check to see if a CallExpression is in our callback list. + * @param {ASTNode} node The node to check against our callback names list. + * @returns {boolean} Whether or not this function matches our callback name. + */ + function isCallback(node) { + return containsOnlyIdentifiers(node.callee) && callbacks.indexOf(sourceCode.getText(node.callee)) > -1; + } + + /** + * Determines whether or not the callback is part of a callback expression. + * @param {ASTNode} node The callback node + * @param {ASTNode} parentNode The expression node + * @returns {boolean} Whether or not this is part of a callback expression + */ + function isCallbackExpression(node, parentNode) { + + // ensure the parent node exists and is an expression + if (!parentNode || parentNode.type !== "ExpressionStatement") { + return false; + } + + // cb() + if (parentNode.expression === node) { + return true; + } + + // special case for cb && cb() and similar + if (parentNode.expression.type === "BinaryExpression" || parentNode.expression.type === "LogicalExpression") { + if (parentNode.expression.right === node) { + return true; + } + } + + return false; + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + CallExpression(node) { + + // if we're not a callback we can return + if (!isCallback(node)) { + return; + } + + // find the closest block, return or loop + const closestBlock = findClosestParentOfType(node, ["BlockStatement", "ReturnStatement", "ArrowFunctionExpression"]) || {}; + + // if our parent is a return we know we're ok + if (closestBlock.type === "ReturnStatement") { + return; + } + + // arrow functions don't always have blocks and implicitly return + if (closestBlock.type === "ArrowFunctionExpression") { + return; + } + + // block statements are part of functions and most if statements + if (closestBlock.type === "BlockStatement") { + + // find the last item in the block + const lastItem = closestBlock.body[closestBlock.body.length - 1]; + + // if the callback is the last thing in a block that might be ok + if (isCallbackExpression(node, lastItem)) { + + const parentType = closestBlock.parent.type; + + // but only if the block is part of a function + if (parentType === "FunctionExpression" || + parentType === "FunctionDeclaration" || + parentType === "ArrowFunctionExpression" + ) { + return; + } + + } + + // ending a block with a return is also ok + if (lastItem.type === "ReturnStatement") { + + // but only if the callback is immediately before + if (isCallbackExpression(node, closestBlock.body[closestBlock.body.length - 2])) { + return; + } + } + + } + + // as long as you're the child of a function at this point you should be asked to return + if (findClosestParentOfType(node, ["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"])) { + context.report({ node, message: "Expected return with your callback function." }); + } + + } + + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/camelcase.js b/tools/node_modules/eslint/lib/rules/camelcase.js new file mode 100644 index 0000000000..6fb1475b21 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/camelcase.js @@ -0,0 +1,143 @@ +/** + * @fileoverview Rule to flag non-camelcased identifiers + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce camelcase naming convention", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + properties: { + enum: ["always", "never"] + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + // contains reported nodes to avoid reporting twice on destructuring with shorthand notation + const reported = []; + const ALLOWED_PARENT_TYPES = new Set(["CallExpression", "NewExpression"]); + + /** + * Checks if a string contains an underscore and isn't all upper-case + * @param {string} name The string to check. + * @returns {boolean} if the string is underscored + * @private + */ + function isUnderscored(name) { + + // if there's an underscore, it might be A_CONSTANT, which is okay + return name.indexOf("_") > -1 && name !== name.toUpperCase(); + } + + /** + * Reports an AST node as a rule violation. + * @param {ASTNode} node The node to report. + * @returns {void} + * @private + */ + function report(node) { + if (reported.indexOf(node) < 0) { + reported.push(node); + context.report({ node, message: "Identifier '{{name}}' is not in camel case.", data: { name: node.name } }); + } + } + + const options = context.options[0] || {}; + let properties = options.properties || ""; + + if (properties !== "always" && properties !== "never") { + properties = "always"; + } + + return { + + Identifier(node) { + + /* + * Leading and trailing underscores are commonly used to flag + * private/protected identifiers, strip them + */ + const name = node.name.replace(/^_+|_+$/g, ""), + effectiveParent = (node.parent.type === "MemberExpression") ? node.parent.parent : node.parent; + + // MemberExpressions get special rules + if (node.parent.type === "MemberExpression") { + + // "never" check properties + if (properties === "never") { + return; + } + + // Always report underscored object names + if (node.parent.object.type === "Identifier" && + node.parent.object.name === node.name && + isUnderscored(name)) { + report(node); + + // Report AssignmentExpressions only if they are the left side of the assignment + } else if (effectiveParent.type === "AssignmentExpression" && + isUnderscored(name) && + (effectiveParent.right.type !== "MemberExpression" || + effectiveParent.left.type === "MemberExpression" && + effectiveParent.left.property.name === node.name)) { + report(node); + } + + // Properties have their own rules + } else if (node.parent.type === "Property") { + + // "never" check properties + if (properties === "never") { + return; + } + + if (node.parent.parent && node.parent.parent.type === "ObjectPattern" && + node.parent.key === node && node.parent.value !== node) { + return; + } + + if (isUnderscored(name) && !ALLOWED_PARENT_TYPES.has(effectiveParent.type)) { + report(node); + } + + // Check if it's an import specifier + } else if (["ImportSpecifier", "ImportNamespaceSpecifier", "ImportDefaultSpecifier"].indexOf(node.parent.type) >= 0) { + + // Report only if the local imported identifier is underscored + if (node.parent.local && node.parent.local.name === node.name && isUnderscored(name)) { + report(node); + } + + // Report anything that is underscored that isn't a CallExpression + } else if (isUnderscored(name) && !ALLOWED_PARENT_TYPES.has(effectiveParent.type)) { + report(node); + } + } + + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/capitalized-comments.js b/tools/node_modules/eslint/lib/rules/capitalized-comments.js new file mode 100644 index 0000000000..1a27608067 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/capitalized-comments.js @@ -0,0 +1,303 @@ +/** + * @fileoverview enforce or disallow capitalization of the first letter of a comment + * @author Kevin Partington + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const LETTER_PATTERN = require("../util/patterns/letters"); +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const ALWAYS_MESSAGE = "Comments should not begin with a lowercase character", + NEVER_MESSAGE = "Comments should not begin with an uppercase character", + DEFAULT_IGNORE_PATTERN = astUtils.COMMENTS_IGNORE_PATTERN, + WHITESPACE = /\s/g, + MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/, // TODO: Combine w/ max-len pattern? + DEFAULTS = { + ignorePattern: null, + ignoreInlineComments: false, + ignoreConsecutiveComments: false + }; + +/* + * Base schema body for defining the basic capitalization rule, ignorePattern, + * and ignoreInlineComments values. + * This can be used in a few different ways in the actual schema. + */ +const SCHEMA_BODY = { + type: "object", + properties: { + ignorePattern: { + type: "string" + }, + ignoreInlineComments: { + type: "boolean" + }, + ignoreConsecutiveComments: { + type: "boolean" + } + }, + additionalProperties: false +}; + +/** + * Get normalized options for either block or line comments from the given + * user-provided options. + * - If the user-provided options is just a string, returns a normalized + * set of options using default values for all other options. + * - If the user-provided options is an object, then a normalized option + * set is returned. Options specified in overrides will take priority + * over options specified in the main options object, which will in + * turn take priority over the rule's defaults. + * + * @param {Object|string} rawOptions The user-provided options. + * @param {string} which Either "line" or "block". + * @returns {Object} The normalized options. + */ +function getNormalizedOptions(rawOptions, which) { + if (!rawOptions) { + return Object.assign({}, DEFAULTS); + } + + return Object.assign({}, DEFAULTS, rawOptions[which] || rawOptions); +} + +/** + * Get normalized options for block and line comments. + * + * @param {Object|string} rawOptions The user-provided options. + * @returns {Object} An object with "Line" and "Block" keys and corresponding + * normalized options objects. + */ +function getAllNormalizedOptions(rawOptions) { + return { + Line: getNormalizedOptions(rawOptions, "line"), + Block: getNormalizedOptions(rawOptions, "block") + }; +} + +/** + * Creates a regular expression for each ignorePattern defined in the rule + * options. + * + * This is done in order to avoid invoking the RegExp constructor repeatedly. + * + * @param {Object} normalizedOptions The normalized rule options. + * @returns {void} + */ +function createRegExpForIgnorePatterns(normalizedOptions) { + Object.keys(normalizedOptions).forEach(key => { + const ignorePatternStr = normalizedOptions[key].ignorePattern; + + if (ignorePatternStr) { + const regExp = RegExp(`^\\s*(?:${ignorePatternStr})`); + + normalizedOptions[key].ignorePatternRegExp = regExp; + } + }); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce or disallow capitalization of the first letter of a comment", + category: "Stylistic Issues", + recommended: false + }, + fixable: "code", + schema: [ + { enum: ["always", "never"] }, + { + oneOf: [ + SCHEMA_BODY, + { + type: "object", + properties: { + line: SCHEMA_BODY, + block: SCHEMA_BODY + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + + const capitalize = context.options[0] || "always", + normalizedOptions = getAllNormalizedOptions(context.options[1]), + sourceCode = context.getSourceCode(); + + createRegExpForIgnorePatterns(normalizedOptions); + + //---------------------------------------------------------------------- + // Helpers + //---------------------------------------------------------------------- + + /** + * Checks whether a comment is an inline comment. + * + * For the purpose of this rule, a comment is inline if: + * 1. The comment is preceded by a token on the same line; and + * 2. The command is followed by a token on the same line. + * + * Note that the comment itself need not be single-line! + * + * Also, it follows from this definition that only block comments can + * be considered as possibly inline. This is because line comments + * would consume any following tokens on the same line as the comment. + * + * @param {ASTNode} comment The comment node to check. + * @returns {boolean} True if the comment is an inline comment, false + * otherwise. + */ + function isInlineComment(comment) { + const previousToken = sourceCode.getTokenBefore(comment, { includeComments: true }), + nextToken = sourceCode.getTokenAfter(comment, { includeComments: true }); + + return Boolean( + previousToken && + nextToken && + comment.loc.start.line === previousToken.loc.end.line && + comment.loc.end.line === nextToken.loc.start.line + ); + } + + /** + * Determine if a comment follows another comment. + * + * @param {ASTNode} comment The comment to check. + * @returns {boolean} True if the comment follows a valid comment. + */ + function isConsecutiveComment(comment) { + const previousTokenOrComment = sourceCode.getTokenBefore(comment, { includeComments: true }); + + return Boolean( + previousTokenOrComment && + ["Block", "Line"].indexOf(previousTokenOrComment.type) !== -1 + ); + } + + /** + * Check a comment to determine if it is valid for this rule. + * + * @param {ASTNode} comment The comment node to process. + * @param {Object} options The options for checking this comment. + * @returns {boolean} True if the comment is valid, false otherwise. + */ + function isCommentValid(comment, options) { + + // 1. Check for default ignore pattern. + if (DEFAULT_IGNORE_PATTERN.test(comment.value)) { + return true; + } + + // 2. Check for custom ignore pattern. + const commentWithoutAsterisks = comment.value + .replace(/\*/g, ""); + + if (options.ignorePatternRegExp && options.ignorePatternRegExp.test(commentWithoutAsterisks)) { + return true; + } + + // 3. Check for inline comments. + if (options.ignoreInlineComments && isInlineComment(comment)) { + return true; + } + + // 4. Is this a consecutive comment (and are we tolerating those)? + if (options.ignoreConsecutiveComments && isConsecutiveComment(comment)) { + return true; + } + + // 5. Does the comment start with a possible URL? + if (MAYBE_URL.test(commentWithoutAsterisks)) { + return true; + } + + // 6. Is the initial word character a letter? + const commentWordCharsOnly = commentWithoutAsterisks + .replace(WHITESPACE, ""); + + if (commentWordCharsOnly.length === 0) { + return true; + } + + const firstWordChar = commentWordCharsOnly[0]; + + if (!LETTER_PATTERN.test(firstWordChar)) { + return true; + } + + // 7. Check the case of the initial word character. + const isUppercase = firstWordChar !== firstWordChar.toLocaleLowerCase(), + isLowercase = firstWordChar !== firstWordChar.toLocaleUpperCase(); + + if (capitalize === "always" && isLowercase) { + return false; + } + if (capitalize === "never" && isUppercase) { + return false; + } + + return true; + } + + /** + * Process a comment to determine if it needs to be reported. + * + * @param {ASTNode} comment The comment node to process. + * @returns {void} + */ + function processComment(comment) { + const options = normalizedOptions[comment.type], + commentValid = isCommentValid(comment, options); + + if (!commentValid) { + const message = capitalize === "always" + ? ALWAYS_MESSAGE + : NEVER_MESSAGE; + + context.report({ + node: null, // Intentionally using loc instead + loc: comment.loc, + message, + fix(fixer) { + const match = comment.value.match(LETTER_PATTERN); + + return fixer.replaceTextRange( + + // Offset match.index by 2 to account for the first 2 characters that start the comment (// or /*) + [comment.range[0] + match.index + 2, comment.range[0] + match.index + 3], + capitalize === "always" ? match[0].toLocaleUpperCase() : match[0].toLocaleLowerCase() + ); + } + }); + } + } + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + return { + Program() { + const comments = sourceCode.getAllComments(); + + comments.filter(token => token.type !== "Shebang").forEach(processComment); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/class-methods-use-this.js b/tools/node_modules/eslint/lib/rules/class-methods-use-this.js new file mode 100644 index 0000000000..d429c579b9 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/class-methods-use-this.js @@ -0,0 +1,110 @@ +/** + * @fileoverview Rule to enforce that all class methods use 'this'. + * @author Patrick Williams + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce that class methods utilize `this`", + category: "Best Practices", + recommended: false + }, + schema: [{ + type: "object", + properties: { + exceptMethods: { + type: "array", + items: { + type: "string" + } + } + }, + additionalProperties: false + }] + }, + create(context) { + const config = context.options[0] ? Object.assign({}, context.options[0]) : {}; + const exceptMethods = new Set(config.exceptMethods || []); + + const stack = []; + + /** + * Initializes the current context to false and pushes it onto the stack. + * These booleans represent whether 'this' has been used in the context. + * @returns {void} + * @private + */ + function enterFunction() { + stack.push(false); + } + + /** + * Check if the node is an instance method + * @param {ASTNode} node - node to check + * @returns {boolean} True if its an instance method + * @private + */ + function isInstanceMethod(node) { + return !node.static && node.kind !== "constructor" && node.type === "MethodDefinition"; + } + + /** + * Check if the node is an instance method not excluded by config + * @param {ASTNode} node - node to check + * @returns {boolean} True if it is an instance method, and not excluded by config + * @private + */ + function isIncludedInstanceMethod(node) { + return isInstanceMethod(node) && !exceptMethods.has(node.key.name); + } + + /** + * Checks if we are leaving a function that is a method, and reports if 'this' has not been used. + * Static methods and the constructor are exempt. + * Then pops the context off the stack. + * @param {ASTNode} node - A function node that was entered. + * @returns {void} + * @private + */ + function exitFunction(node) { + const methodUsesThis = stack.pop(); + + if (isIncludedInstanceMethod(node.parent) && !methodUsesThis) { + context.report({ + node, + message: "Expected 'this' to be used by class method '{{classMethod}}'.", + data: { + classMethod: node.parent.key.name + } + }); + } + } + + /** + * Mark the current context as having used 'this'. + * @returns {void} + * @private + */ + function markThisUsed() { + if (stack.length) { + stack[stack.length - 1] = true; + } + } + + return { + FunctionDeclaration: enterFunction, + "FunctionDeclaration:exit": exitFunction, + FunctionExpression: enterFunction, + "FunctionExpression:exit": exitFunction, + ThisExpression: markThisUsed, + Super: markThisUsed + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/comma-dangle.js b/tools/node_modules/eslint/lib/rules/comma-dangle.js new file mode 100644 index 0000000000..ddcc0cd229 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/comma-dangle.js @@ -0,0 +1,337 @@ +/** + * @fileoverview Rule to forbid or enforce dangling commas. + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const lodash = require("lodash"); +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const DEFAULT_OPTIONS = Object.freeze({ + arrays: "never", + objects: "never", + imports: "never", + exports: "never", + functions: "ignore" +}); + +/** + * Checks whether or not a trailing comma is allowed in a given node. + * If the `lastItem` is `RestElement` or `RestProperty`, it disallows trailing commas. + * + * @param {ASTNode} lastItem - The node of the last element in the given node. + * @returns {boolean} `true` if a trailing comma is allowed. + */ +function isTrailingCommaAllowed(lastItem) { + return !( + lastItem.type === "RestElement" || + lastItem.type === "RestProperty" || + lastItem.type === "ExperimentalRestProperty" + ); +} + +/** + * Normalize option value. + * + * @param {string|Object|undefined} optionValue - The 1st option value to normalize. + * @returns {Object} The normalized option value. + */ +function normalizeOptions(optionValue) { + if (typeof optionValue === "string") { + return { + arrays: optionValue, + objects: optionValue, + imports: optionValue, + exports: optionValue, + + // For backward compatibility, always ignore functions. + functions: "ignore" + }; + } + if (typeof optionValue === "object" && optionValue !== null) { + return { + arrays: optionValue.arrays || DEFAULT_OPTIONS.arrays, + objects: optionValue.objects || DEFAULT_OPTIONS.objects, + imports: optionValue.imports || DEFAULT_OPTIONS.imports, + exports: optionValue.exports || DEFAULT_OPTIONS.exports, + functions: optionValue.functions || DEFAULT_OPTIONS.functions + }; + } + + return DEFAULT_OPTIONS; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require or disallow trailing commas", + category: "Stylistic Issues", + recommended: false + }, + fixable: "code", + schema: { + definitions: { + value: { + enum: [ + "always-multiline", + "always", + "never", + "only-multiline" + ] + }, + valueWithIgnore: { + enum: [ + "always-multiline", + "always", + "ignore", + "never", + "only-multiline" + ] + } + }, + type: "array", + items: [ + { + oneOf: [ + { + $ref: "#/definitions/value" + }, + { + type: "object", + properties: { + arrays: { $ref: "#/definitions/valueWithIgnore" }, + objects: { $ref: "#/definitions/valueWithIgnore" }, + imports: { $ref: "#/definitions/valueWithIgnore" }, + exports: { $ref: "#/definitions/valueWithIgnore" }, + functions: { $ref: "#/definitions/valueWithIgnore" } + }, + additionalProperties: false + } + ] + } + ] + } + }, + + create(context) { + const options = normalizeOptions(context.options[0]); + const sourceCode = context.getSourceCode(); + const UNEXPECTED_MESSAGE = "Unexpected trailing comma."; + const MISSING_MESSAGE = "Missing trailing comma."; + + /** + * Gets the last item of the given node. + * @param {ASTNode} node - The node to get. + * @returns {ASTNode|null} The last node or null. + */ + function getLastItem(node) { + switch (node.type) { + case "ObjectExpression": + case "ObjectPattern": + return lodash.last(node.properties); + case "ArrayExpression": + case "ArrayPattern": + return lodash.last(node.elements); + case "ImportDeclaration": + case "ExportNamedDeclaration": + return lodash.last(node.specifiers); + case "FunctionDeclaration": + case "FunctionExpression": + case "ArrowFunctionExpression": + return lodash.last(node.params); + case "CallExpression": + case "NewExpression": + return lodash.last(node.arguments); + default: + return null; + } + } + + /** + * Gets the trailing comma token of the given node. + * If the trailing comma does not exist, this returns the token which is + * the insertion point of the trailing comma token. + * + * @param {ASTNode} node - The node to get. + * @param {ASTNode} lastItem - The last item of the node. + * @returns {Token} The trailing comma token or the insertion point. + */ + function getTrailingToken(node, lastItem) { + switch (node.type) { + case "ObjectExpression": + case "ArrayExpression": + case "CallExpression": + case "NewExpression": + return sourceCode.getLastToken(node, 1); + default: { + const nextToken = sourceCode.getTokenAfter(lastItem); + + if (astUtils.isCommaToken(nextToken)) { + return nextToken; + } + return sourceCode.getLastToken(lastItem); + } + } + } + + /** + * Checks whether or not a given node is multiline. + * This rule handles a given node as multiline when the closing parenthesis + * and the last element are not on the same line. + * + * @param {ASTNode} node - A node to check. + * @returns {boolean} `true` if the node is multiline. + */ + function isMultiline(node) { + const lastItem = getLastItem(node); + + if (!lastItem) { + return false; + } + + const penultimateToken = getTrailingToken(node, lastItem); + const lastToken = sourceCode.getTokenAfter(penultimateToken); + + return lastToken.loc.end.line !== penultimateToken.loc.end.line; + } + + /** + * Reports a trailing comma if it exists. + * + * @param {ASTNode} node - A node to check. Its type is one of + * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern, + * ImportDeclaration, and ExportNamedDeclaration. + * @returns {void} + */ + function forbidTrailingComma(node) { + const lastItem = getLastItem(node); + + if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) { + return; + } + + const trailingToken = getTrailingToken(node, lastItem); + + if (astUtils.isCommaToken(trailingToken)) { + context.report({ + node: lastItem, + loc: trailingToken.loc.start, + message: UNEXPECTED_MESSAGE, + fix(fixer) { + return fixer.remove(trailingToken); + } + }); + } + } + + /** + * Reports the last element of a given node if it does not have a trailing + * comma. + * + * If a given node is `ArrayPattern` which has `RestElement`, the trailing + * comma is disallowed, so report if it exists. + * + * @param {ASTNode} node - A node to check. Its type is one of + * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern, + * ImportDeclaration, and ExportNamedDeclaration. + * @returns {void} + */ + function forceTrailingComma(node) { + const lastItem = getLastItem(node); + + if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) { + return; + } + if (!isTrailingCommaAllowed(lastItem)) { + forbidTrailingComma(node); + return; + } + + const trailingToken = getTrailingToken(node, lastItem); + + if (trailingToken.value !== ",") { + context.report({ + node: lastItem, + loc: trailingToken.loc.end, + message: MISSING_MESSAGE, + fix(fixer) { + return fixer.insertTextAfter(trailingToken, ","); + } + }); + } + } + + /** + * If a given node is multiline, reports the last element of a given node + * when it does not have a trailing comma. + * Otherwise, reports a trailing comma if it exists. + * + * @param {ASTNode} node - A node to check. Its type is one of + * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern, + * ImportDeclaration, and ExportNamedDeclaration. + * @returns {void} + */ + function forceTrailingCommaIfMultiline(node) { + if (isMultiline(node)) { + forceTrailingComma(node); + } else { + forbidTrailingComma(node); + } + } + + /** + * Only if a given node is not multiline, reports the last element of a given node + * when it does not have a trailing comma. + * Otherwise, reports a trailing comma if it exists. + * + * @param {ASTNode} node - A node to check. Its type is one of + * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern, + * ImportDeclaration, and ExportNamedDeclaration. + * @returns {void} + */ + function allowTrailingCommaIfMultiline(node) { + if (!isMultiline(node)) { + forbidTrailingComma(node); + } + } + + const predicate = { + always: forceTrailingComma, + "always-multiline": forceTrailingCommaIfMultiline, + "only-multiline": allowTrailingCommaIfMultiline, + never: forbidTrailingComma, + ignore: lodash.noop + }; + + return { + ObjectExpression: predicate[options.objects], + ObjectPattern: predicate[options.objects], + + ArrayExpression: predicate[options.arrays], + ArrayPattern: predicate[options.arrays], + + ImportDeclaration: predicate[options.imports], + + ExportNamedDeclaration: predicate[options.exports], + + FunctionDeclaration: predicate[options.functions], + FunctionExpression: predicate[options.functions], + ArrowFunctionExpression: predicate[options.functions], + CallExpression: predicate[options.functions], + NewExpression: predicate[options.functions] + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/comma-spacing.js b/tools/node_modules/eslint/lib/rules/comma-spacing.js new file mode 100644 index 0000000000..25a0e7d82c --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/comma-spacing.js @@ -0,0 +1,183 @@ +/** + * @fileoverview Comma spacing - validates spacing before and after comma + * @author Vignesh Anand aka vegetableman. + */ +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent spacing before and after commas", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + type: "object", + properties: { + before: { + type: "boolean" + }, + after: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + const sourceCode = context.getSourceCode(); + const tokensAndComments = sourceCode.tokensAndComments; + + const options = { + before: context.options[0] ? !!context.options[0].before : false, + after: context.options[0] ? !!context.options[0].after : true + }; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + // list of comma tokens to ignore for the check of leading whitespace + const commaTokensToIgnore = []; + + /** + * Reports a spacing error with an appropriate message. + * @param {ASTNode} node The binary expression node to report. + * @param {string} dir Is the error "before" or "after" the comma? + * @param {ASTNode} otherNode The node at the left or right of `node` + * @returns {void} + * @private + */ + function report(node, dir, otherNode) { + context.report({ + node, + fix(fixer) { + if (options[dir]) { + if (dir === "before") { + return fixer.insertTextBefore(node, " "); + } + return fixer.insertTextAfter(node, " "); + + } + let start, end; + const newText = ""; + + if (dir === "before") { + start = otherNode.range[1]; + end = node.range[0]; + } else { + start = node.range[1]; + end = otherNode.range[0]; + } + + return fixer.replaceTextRange([start, end], newText); + + }, + message: options[dir] + ? "A space is required {{dir}} ','." + : "There should be no space {{dir}} ','.", + data: { + dir + } + }); + } + + /** + * Validates the spacing around a comma token. + * @param {Object} tokens - The tokens to be validated. + * @param {Token} tokens.comma The token representing the comma. + * @param {Token} [tokens.left] The last token before the comma. + * @param {Token} [tokens.right] The first token after the comma. + * @param {Token|ASTNode} reportItem The item to use when reporting an error. + * @returns {void} + * @private + */ + function validateCommaItemSpacing(tokens, reportItem) { + if (tokens.left && astUtils.isTokenOnSameLine(tokens.left, tokens.comma) && + (options.before !== sourceCode.isSpaceBetweenTokens(tokens.left, tokens.comma)) + ) { + report(reportItem, "before", tokens.left); + } + + if (tokens.right && !options.after && tokens.right.type === "Line") { + return; + } + + if (tokens.right && astUtils.isTokenOnSameLine(tokens.comma, tokens.right) && + (options.after !== sourceCode.isSpaceBetweenTokens(tokens.comma, tokens.right)) + ) { + report(reportItem, "after", tokens.right); + } + } + + /** + * Adds null elements of the given ArrayExpression or ArrayPattern node to the ignore list. + * @param {ASTNode} node An ArrayExpression or ArrayPattern node. + * @returns {void} + */ + function addNullElementsToIgnoreList(node) { + let previousToken = sourceCode.getFirstToken(node); + + node.elements.forEach(element => { + let token; + + if (element === null) { + token = sourceCode.getTokenAfter(previousToken); + + if (astUtils.isCommaToken(token)) { + commaTokensToIgnore.push(token); + } + } else { + token = sourceCode.getTokenAfter(element); + } + + previousToken = token; + }); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + "Program:exit"() { + tokensAndComments.forEach((token, i) => { + + if (!astUtils.isCommaToken(token)) { + return; + } + + if (token && token.type === "JSXText") { + return; + } + + const previousToken = tokensAndComments[i - 1]; + const nextToken = tokensAndComments[i + 1]; + + validateCommaItemSpacing({ + comma: token, + left: astUtils.isCommaToken(previousToken) || commaTokensToIgnore.indexOf(token) > -1 ? null : previousToken, + right: astUtils.isCommaToken(nextToken) ? null : nextToken + }, token); + }); + }, + ArrayExpression: addNullElementsToIgnoreList, + ArrayPattern: addNullElementsToIgnoreList + + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/comma-style.js b/tools/node_modules/eslint/lib/rules/comma-style.js new file mode 100644 index 0000000000..4c65338a44 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/comma-style.js @@ -0,0 +1,299 @@ +/** + * @fileoverview Comma style - enforces comma styles of two types: last and first + * @author Vignesh Anand aka vegetableman + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent comma style", + category: "Stylistic Issues", + recommended: false + }, + fixable: "code", + schema: [ + { + enum: ["first", "last"] + }, + { + type: "object", + properties: { + exceptions: { + type: "object", + additionalProperties: { + type: "boolean" + } + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const style = context.options[0] || "last", + sourceCode = context.getSourceCode(); + const exceptions = { + ArrayPattern: true, + ArrowFunctionExpression: true, + CallExpression: true, + FunctionDeclaration: true, + FunctionExpression: true, + ImportDeclaration: true, + ObjectPattern: true + }; + + if (context.options.length === 2 && context.options[1].hasOwnProperty("exceptions")) { + const keys = Object.keys(context.options[1].exceptions); + + for (let i = 0; i < keys.length; i++) { + exceptions[keys[i]] = context.options[1].exceptions[keys[i]]; + } + } + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Modified text based on the style + * @param {string} styleType Style type + * @param {string} text Source code text + * @returns {string} modified text + * @private + */ + function getReplacedText(styleType, text) { + switch (styleType) { + case "between": + return `,${text.replace("\n", "")}`; + + case "first": + return `${text},`; + + case "last": + return `,${text}`; + + default: + return ""; + } + } + + /** + * Determines the fixer function for a given style. + * @param {string} styleType comma style + * @param {ASTNode} previousItemToken The token to check. + * @param {ASTNode} commaToken The token to check. + * @param {ASTNode} currentItemToken The token to check. + * @returns {Function} Fixer function + * @private + */ + function getFixerFunction(styleType, previousItemToken, commaToken, currentItemToken) { + const text = + sourceCode.text.slice(previousItemToken.range[1], commaToken.range[0]) + + sourceCode.text.slice(commaToken.range[1], currentItemToken.range[0]); + const range = [previousItemToken.range[1], currentItemToken.range[0]]; + + return function(fixer) { + return fixer.replaceTextRange(range, getReplacedText(styleType, text)); + }; + } + + /** + * Validates the spacing around single items in lists. + * @param {Token} previousItemToken The last token from the previous item. + * @param {Token} commaToken The token representing the comma. + * @param {Token} currentItemToken The first token of the current item. + * @param {Token} reportItem The item to use when reporting an error. + * @returns {void} + * @private + */ + function validateCommaItemSpacing(previousItemToken, commaToken, currentItemToken, reportItem) { + + // if single line + if (astUtils.isTokenOnSameLine(commaToken, currentItemToken) && + astUtils.isTokenOnSameLine(previousItemToken, commaToken)) { + + // do nothing. + + } else if (!astUtils.isTokenOnSameLine(commaToken, currentItemToken) && + !astUtils.isTokenOnSameLine(previousItemToken, commaToken)) { + + // lone comma + context.report({ + node: reportItem, + loc: { + line: commaToken.loc.end.line, + column: commaToken.loc.start.column + }, + message: "Bad line breaking before and after ','.", + fix: getFixerFunction("between", previousItemToken, commaToken, currentItemToken) + }); + + } else if (style === "first" && !astUtils.isTokenOnSameLine(commaToken, currentItemToken)) { + + context.report({ + node: reportItem, + message: "',' should be placed first.", + fix: getFixerFunction(style, previousItemToken, commaToken, currentItemToken) + }); + + } else if (style === "last" && astUtils.isTokenOnSameLine(commaToken, currentItemToken)) { + + context.report({ + node: reportItem, + loc: { + line: commaToken.loc.end.line, + column: commaToken.loc.end.column + }, + message: "',' should be placed last.", + fix: getFixerFunction(style, previousItemToken, commaToken, currentItemToken) + }); + } + } + + /** + * Checks the comma placement with regards to a declaration/property/element + * @param {ASTNode} node The binary expression node to check + * @param {string} property The property of the node containing child nodes. + * @private + * @returns {void} + */ + function validateComma(node, property) { + const items = node[property], + arrayLiteral = (node.type === "ArrayExpression" || node.type === "ArrayPattern"); + + if (items.length > 1 || arrayLiteral) { + + // seed as opening [ + let previousItemToken = sourceCode.getFirstToken(node); + + items.forEach(item => { + const commaToken = item ? sourceCode.getTokenBefore(item) : previousItemToken, + currentItemToken = item ? sourceCode.getFirstToken(item) : sourceCode.getTokenAfter(commaToken), + reportItem = item || currentItemToken, + tokenBeforeComma = sourceCode.getTokenBefore(commaToken); + + // Check if previous token is wrapped in parentheses + if (tokenBeforeComma && astUtils.isClosingParenToken(tokenBeforeComma)) { + previousItemToken = tokenBeforeComma; + } + + /* + * This works by comparing three token locations: + * - previousItemToken is the last token of the previous item + * - commaToken is the location of the comma before the current item + * - currentItemToken is the first token of the current item + * + * These values get switched around if item is undefined. + * previousItemToken will refer to the last token not belonging + * to the current item, which could be a comma or an opening + * square bracket. currentItemToken could be a comma. + * + * All comparisons are done based on these tokens directly, so + * they are always valid regardless of an undefined item. + */ + if (astUtils.isCommaToken(commaToken)) { + validateCommaItemSpacing(previousItemToken, commaToken, + currentItemToken, reportItem); + } + + if (item) { + const tokenAfterItem = sourceCode.getTokenAfter(item, astUtils.isNotClosingParenToken); + + previousItemToken = tokenAfterItem + ? sourceCode.getTokenBefore(tokenAfterItem) + : sourceCode.ast.tokens[sourceCode.ast.tokens.length - 1]; + } + }); + + /* + * Special case for array literals that have empty last items, such + * as [ 1, 2, ]. These arrays only have two items show up in the + * AST, so we need to look at the token to verify that there's no + * dangling comma. + */ + if (arrayLiteral) { + + const lastToken = sourceCode.getLastToken(node), + nextToLastToken = sourceCode.getTokenBefore(lastToken); + + if (astUtils.isCommaToken(nextToLastToken)) { + validateCommaItemSpacing( + sourceCode.getTokenBefore(nextToLastToken), + nextToLastToken, + lastToken, + lastToken + ); + } + } + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + const nodes = {}; + + if (!exceptions.VariableDeclaration) { + nodes.VariableDeclaration = function(node) { + validateComma(node, "declarations"); + }; + } + if (!exceptions.ObjectExpression) { + nodes.ObjectExpression = function(node) { + validateComma(node, "properties"); + }; + } + if (!exceptions.ObjectPattern) { + nodes.ObjectPattern = function(node) { + validateComma(node, "properties"); + }; + } + if (!exceptions.ArrayExpression) { + nodes.ArrayExpression = function(node) { + validateComma(node, "elements"); + }; + } + if (!exceptions.ArrayPattern) { + nodes.ArrayPattern = function(node) { + validateComma(node, "elements"); + }; + } + if (!exceptions.FunctionDeclaration) { + nodes.FunctionDeclaration = function(node) { + validateComma(node, "params"); + }; + } + if (!exceptions.FunctionExpression) { + nodes.FunctionExpression = function(node) { + validateComma(node, "params"); + }; + } + if (!exceptions.ArrowFunctionExpression) { + nodes.ArrowFunctionExpression = function(node) { + validateComma(node, "params"); + }; + } + if (!exceptions.CallExpression) { + nodes.CallExpression = function(node) { + validateComma(node, "arguments"); + }; + } + if (!exceptions.ImportDeclaration) { + nodes.ImportDeclaration = function(node) { + validateComma(node, "specifiers"); + }; + } + + return nodes; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/complexity.js b/tools/node_modules/eslint/lib/rules/complexity.js new file mode 100644 index 0000000000..e0313fa78f --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/complexity.js @@ -0,0 +1,168 @@ +/** + * @fileoverview Counts the cyclomatic complexity of each function of the script. See http://en.wikipedia.org/wiki/Cyclomatic_complexity. + * Counts the number of if, conditional, for, whilte, try, switch/case, + * @author Patrick Brosset + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const lodash = require("lodash"); + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce a maximum cyclomatic complexity allowed in a program", + category: "Best Practices", + recommended: false + }, + + schema: [ + { + oneOf: [ + { + type: "integer", + minimum: 0 + }, + { + type: "object", + properties: { + maximum: { + type: "integer", + minimum: 0 + }, + max: { + type: "integer", + minimum: 0 + } + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + const option = context.options[0]; + let THRESHOLD = 20; + + if (typeof option === "object" && option.hasOwnProperty("maximum") && typeof option.maximum === "number") { + THRESHOLD = option.maximum; + } + if (typeof option === "object" && option.hasOwnProperty("max") && typeof option.max === "number") { + THRESHOLD = option.max; + } + if (typeof option === "number") { + THRESHOLD = option; + } + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + // Using a stack to store complexity (handling nested functions) + const fns = []; + + /** + * When parsing a new function, store it in our function stack + * @returns {void} + * @private + */ + function startFunction() { + fns.push(1); + } + + /** + * Evaluate the node at the end of function + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function endFunction(node) { + const name = lodash.upperFirst(astUtils.getFunctionNameWithKind(node)); + const complexity = fns.pop(); + + if (complexity > THRESHOLD) { + context.report({ + node, + message: "{{name}} has a complexity of {{complexity}}.", + data: { name, complexity } + }); + } + } + + /** + * Increase the complexity of the function in context + * @returns {void} + * @private + */ + function increaseComplexity() { + if (fns.length) { + fns[fns.length - 1]++; + } + } + + /** + * Increase the switch complexity in context + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function increaseSwitchComplexity(node) { + + // Avoiding `default` + if (node.test) { + increaseComplexity(); + } + } + + /** + * Increase the logical path complexity in context + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function increaseLogicalComplexity(node) { + + // Avoiding && + if (node.operator === "||") { + increaseComplexity(); + } + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + FunctionDeclaration: startFunction, + FunctionExpression: startFunction, + ArrowFunctionExpression: startFunction, + "FunctionDeclaration:exit": endFunction, + "FunctionExpression:exit": endFunction, + "ArrowFunctionExpression:exit": endFunction, + + CatchClause: increaseComplexity, + ConditionalExpression: increaseComplexity, + LogicalExpression: increaseLogicalComplexity, + ForStatement: increaseComplexity, + ForInStatement: increaseComplexity, + ForOfStatement: increaseComplexity, + IfStatement: increaseComplexity, + SwitchCase: increaseSwitchComplexity, + WhileStatement: increaseComplexity, + DoWhileStatement: increaseComplexity + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/computed-property-spacing.js b/tools/node_modules/eslint/lib/rules/computed-property-spacing.js new file mode 100644 index 0000000000..19c28fd22e --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/computed-property-spacing.js @@ -0,0 +1,176 @@ +/** + * @fileoverview Disallows or enforces spaces inside computed properties. + * @author Jamund Ferguson + */ +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent spacing inside computed property brackets", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + enum: ["always", "never"] + } + ] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + const propertyNameMustBeSpaced = context.options[0] === "always"; // default is "never" + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Reports that there shouldn't be a space after the first token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @param {Token} tokenAfter - The token after `token`. + * @returns {void} + */ + function reportNoBeginningSpace(node, token, tokenAfter) { + context.report({ + node, + loc: token.loc.start, + message: "There should be no space after '{{tokenValue}}'.", + data: { + tokenValue: token.value + }, + fix(fixer) { + return fixer.removeRange([token.range[1], tokenAfter.range[0]]); + } + }); + } + + /** + * Reports that there shouldn't be a space before the last token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @param {Token} tokenBefore - The token before `token`. + * @returns {void} + */ + function reportNoEndingSpace(node, token, tokenBefore) { + context.report({ + node, + loc: token.loc.start, + message: "There should be no space before '{{tokenValue}}'.", + data: { + tokenValue: token.value + }, + fix(fixer) { + return fixer.removeRange([tokenBefore.range[1], token.range[0]]); + } + }); + } + + /** + * Reports that there should be a space after the first token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportRequiredBeginningSpace(node, token) { + context.report({ + node, + loc: token.loc.start, + message: "A space is required after '{{tokenValue}}'.", + data: { + tokenValue: token.value + }, + fix(fixer) { + return fixer.insertTextAfter(token, " "); + } + }); + } + + /** + * Reports that there should be a space before the last token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportRequiredEndingSpace(node, token) { + context.report({ + node, + loc: token.loc.start, + message: "A space is required before '{{tokenValue}}'.", + data: { + tokenValue: token.value + }, + fix(fixer) { + return fixer.insertTextBefore(token, " "); + } + }); + } + + /** + * Returns a function that checks the spacing of a node on the property name + * that was passed in. + * @param {string} propertyName The property on the node to check for spacing + * @returns {Function} A function that will check spacing on a node + */ + function checkSpacing(propertyName) { + return function(node) { + if (!node.computed) { + return; + } + + const property = node[propertyName]; + + const before = sourceCode.getTokenBefore(property), + first = sourceCode.getFirstToken(property), + last = sourceCode.getLastToken(property), + after = sourceCode.getTokenAfter(property); + + if (astUtils.isTokenOnSameLine(before, first)) { + if (propertyNameMustBeSpaced) { + if (!sourceCode.isSpaceBetweenTokens(before, first) && astUtils.isTokenOnSameLine(before, first)) { + reportRequiredBeginningSpace(node, before); + } + } else { + if (sourceCode.isSpaceBetweenTokens(before, first)) { + reportNoBeginningSpace(node, before, first); + } + } + } + + if (astUtils.isTokenOnSameLine(last, after)) { + if (propertyNameMustBeSpaced) { + if (!sourceCode.isSpaceBetweenTokens(last, after) && astUtils.isTokenOnSameLine(last, after)) { + reportRequiredEndingSpace(node, after); + } + } else { + if (sourceCode.isSpaceBetweenTokens(last, after)) { + reportNoEndingSpace(node, after, last); + } + } + } + }; + } + + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + Property: checkSpacing("key"), + MemberExpression: checkSpacing("property") + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/consistent-return.js b/tools/node_modules/eslint/lib/rules/consistent-return.js new file mode 100644 index 0000000000..a42faaa1ed --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/consistent-return.js @@ -0,0 +1,188 @@ +/** + * @fileoverview Rule to flag consistent return values + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const lodash = require("lodash"); + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether or not a given node is an `Identifier` node which was named a given name. + * @param {ASTNode} node - A node to check. + * @param {string} name - An expected name of the node. + * @returns {boolean} `true` if the node is an `Identifier` node which was named as expected. + */ +function isIdentifier(node, name) { + return node.type === "Identifier" && node.name === name; +} + +/** + * Checks whether or not a given code path segment is unreachable. + * @param {CodePathSegment} segment - A CodePathSegment to check. + * @returns {boolean} `true` if the segment is unreachable. + */ +function isUnreachable(segment) { + return !segment.reachable; +} + +/** + * Checks whether a given node is a `constructor` method in an ES6 class + * @param {ASTNode} node A node to check + * @returns {boolean} `true` if the node is a `constructor` method + */ +function isClassConstructor(node) { + return node.type === "FunctionExpression" && + node.parent && + node.parent.type === "MethodDefinition" && + node.parent.kind === "constructor"; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require `return` statements to either always or never specify values", + category: "Best Practices", + recommended: false + }, + + schema: [{ + type: "object", + properties: { + treatUndefinedAsUnspecified: { + type: "boolean" + } + }, + additionalProperties: false + }] + }, + + create(context) { + const options = context.options[0] || {}; + const treatUndefinedAsUnspecified = options.treatUndefinedAsUnspecified === true; + let funcInfo = null; + + /** + * Checks whether of not the implicit returning is consistent if the last + * code path segment is reachable. + * + * @param {ASTNode} node - A program/function node to check. + * @returns {void} + */ + function checkLastSegment(node) { + let loc, name; + + /* + * Skip if it expected no return value or unreachable. + * When unreachable, all paths are returned or thrown. + */ + if (!funcInfo.hasReturnValue || + funcInfo.codePath.currentSegments.every(isUnreachable) || + astUtils.isES5Constructor(node) || + isClassConstructor(node) + ) { + return; + } + + // Adjust a location and a message. + if (node.type === "Program") { + + // The head of program. + loc = { line: 1, column: 0 }; + name = "program"; + } else if (node.type === "ArrowFunctionExpression") { + + // `=>` token + loc = context.getSourceCode().getTokenBefore(node.body, astUtils.isArrowToken).loc.start; + } else if ( + node.parent.type === "MethodDefinition" || + (node.parent.type === "Property" && node.parent.method) + ) { + + // Method name. + loc = node.parent.key.loc.start; + } else { + + // Function name or `function` keyword. + loc = (node.id || node).loc.start; + } + + if (!name) { + name = astUtils.getFunctionNameWithKind(node); + } + + // Reports. + context.report({ + node, + loc, + message: "Expected to return a value at the end of {{name}}.", + data: { name } + }); + } + + return { + + // Initializes/Disposes state of each code path. + onCodePathStart(codePath, node) { + funcInfo = { + upper: funcInfo, + codePath, + hasReturn: false, + hasReturnValue: false, + message: "", + node + }; + }, + onCodePathEnd() { + funcInfo = funcInfo.upper; + }, + + // Reports a given return statement if it's inconsistent. + ReturnStatement(node) { + const argument = node.argument; + let hasReturnValue = Boolean(argument); + + if (treatUndefinedAsUnspecified && hasReturnValue) { + hasReturnValue = !isIdentifier(argument, "undefined") && argument.operator !== "void"; + } + + if (!funcInfo.hasReturn) { + funcInfo.hasReturn = true; + funcInfo.hasReturnValue = hasReturnValue; + funcInfo.message = "{{name}} expected {{which}} return value."; + funcInfo.data = { + name: funcInfo.node.type === "Program" + ? "Program" + : lodash.upperFirst(astUtils.getFunctionNameWithKind(funcInfo.node)), + which: hasReturnValue ? "a" : "no" + }; + } else if (funcInfo.hasReturnValue !== hasReturnValue) { + context.report({ + node, + message: funcInfo.message, + data: funcInfo.data + }); + } + }, + + // Reports a given program/function if the implicit returning is not consistent. + "Program:exit": checkLastSegment, + "FunctionDeclaration:exit": checkLastSegment, + "FunctionExpression:exit": checkLastSegment, + "ArrowFunctionExpression:exit": checkLastSegment + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/consistent-this.js b/tools/node_modules/eslint/lib/rules/consistent-this.js new file mode 100644 index 0000000000..151cdcf9c9 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/consistent-this.js @@ -0,0 +1,143 @@ +/** + * @fileoverview Rule to enforce consistent naming of "this" context variables + * @author Raphael Pigulla + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent naming when capturing the current execution context", + category: "Stylistic Issues", + recommended: false + }, + + schema: { + type: "array", + items: { + type: "string", + minLength: 1 + }, + uniqueItems: true + } + }, + + create(context) { + let aliases = []; + + if (context.options.length === 0) { + aliases.push("that"); + } else { + aliases = context.options; + } + + /** + * Reports that a variable declarator or assignment expression is assigning + * a non-'this' value to the specified alias. + * @param {ASTNode} node - The assigning node. + * @param {string} alias - the name of the alias that was incorrectly used. + * @returns {void} + */ + function reportBadAssignment(node, alias) { + context.report({ node, message: "Designated alias '{{alias}}' is not assigned to 'this'.", data: { alias } }); + } + + /** + * Checks that an assignment to an identifier only assigns 'this' to the + * appropriate alias, and the alias is only assigned to 'this'. + * @param {ASTNode} node - The assigning node. + * @param {Identifier} name - The name of the variable assigned to. + * @param {Expression} value - The value of the assignment. + * @returns {void} + */ + function checkAssignment(node, name, value) { + const isThis = value.type === "ThisExpression"; + + if (aliases.indexOf(name) !== -1) { + if (!isThis || node.operator && node.operator !== "=") { + reportBadAssignment(node, name); + } + } else if (isThis) { + context.report({ node, message: "Unexpected alias '{{name}}' for 'this'.", data: { name } }); + } + } + + /** + * Ensures that a variable declaration of the alias in a program or function + * is assigned to the correct value. + * @param {string} alias alias the check the assignment of. + * @param {Object} scope scope of the current code we are checking. + * @private + * @returns {void} + */ + function checkWasAssigned(alias, scope) { + const variable = scope.set.get(alias); + + if (!variable) { + return; + } + + if (variable.defs.some(def => def.node.type === "VariableDeclarator" && + def.node.init !== null)) { + return; + } + + /* + * The alias has been declared and not assigned: check it was + * assigned later in the same scope. + */ + if (!variable.references.some(reference => { + const write = reference.writeExpr; + + return ( + reference.from === scope && + write && write.type === "ThisExpression" && + write.parent.operator === "=" + ); + })) { + variable.defs.map(def => def.node).forEach(node => { + reportBadAssignment(node, alias); + }); + } + } + + /** + * Check each alias to ensure that is was assinged to the correct value. + * @returns {void} + */ + function ensureWasAssigned() { + const scope = context.getScope(); + + aliases.forEach(alias => { + checkWasAssigned(alias, scope); + }); + } + + return { + "Program:exit": ensureWasAssigned, + "FunctionExpression:exit": ensureWasAssigned, + "FunctionDeclaration:exit": ensureWasAssigned, + + VariableDeclarator(node) { + const id = node.id; + const isDestructuring = + id.type === "ArrayPattern" || id.type === "ObjectPattern"; + + if (node.init !== null && !isDestructuring) { + checkAssignment(node, id.name, node.init); + } + }, + + AssignmentExpression(node) { + if (node.left.type === "Identifier") { + checkAssignment(node, node.left.name, node.right); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/constructor-super.js b/tools/node_modules/eslint/lib/rules/constructor-super.js new file mode 100644 index 0000000000..d0a238df8e --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/constructor-super.js @@ -0,0 +1,385 @@ +/** + * @fileoverview A rule to verify `super()` callings in constructor. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether a given code path segment is reachable or not. + * + * @param {CodePathSegment} segment - A code path segment to check. + * @returns {boolean} `true` if the segment is reachable. + */ +function isReachable(segment) { + return segment.reachable; +} + +/** + * Checks whether or not a given node is a constructor. + * @param {ASTNode} node - A node to check. This node type is one of + * `Program`, `FunctionDeclaration`, `FunctionExpression`, and + * `ArrowFunctionExpression`. + * @returns {boolean} `true` if the node is a constructor. + */ +function isConstructorFunction(node) { + return ( + node.type === "FunctionExpression" && + node.parent.type === "MethodDefinition" && + node.parent.kind === "constructor" + ); +} + +/** + * Checks whether a given node can be a constructor or not. + * + * @param {ASTNode} node - A node to check. + * @returns {boolean} `true` if the node can be a constructor. + */ +function isPossibleConstructor(node) { + if (!node) { + return false; + } + + switch (node.type) { + case "ClassExpression": + case "FunctionExpression": + case "ThisExpression": + case "MemberExpression": + case "CallExpression": + case "NewExpression": + case "YieldExpression": + case "TaggedTemplateExpression": + case "MetaProperty": + return true; + + case "Identifier": + return node.name !== "undefined"; + + case "AssignmentExpression": + return isPossibleConstructor(node.right); + + case "LogicalExpression": + return ( + isPossibleConstructor(node.left) || + isPossibleConstructor(node.right) + ); + + case "ConditionalExpression": + return ( + isPossibleConstructor(node.alternate) || + isPossibleConstructor(node.consequent) + ); + + case "SequenceExpression": { + const lastExpression = node.expressions[node.expressions.length - 1]; + + return isPossibleConstructor(lastExpression); + } + + default: + return false; + } +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require `super()` calls in constructors", + category: "ECMAScript 6", + recommended: true + }, + + schema: [] + }, + + create(context) { + + /* + * {{hasExtends: boolean, scope: Scope, codePath: CodePath}[]} + * Information for each constructor. + * - upper: Information of the upper constructor. + * - hasExtends: A flag which shows whether own class has a valid `extends` + * part. + * - scope: The scope of own class. + * - codePath: The code path object of the constructor. + */ + let funcInfo = null; + + /* + * {Map<string, {calledInSomePaths: boolean, calledInEveryPaths: boolean}>} + * Information for each code path segment. + * - calledInSomePaths: A flag of be called `super()` in some code paths. + * - calledInEveryPaths: A flag of be called `super()` in all code paths. + * - validNodes: + */ + let segInfoMap = Object.create(null); + + /** + * Gets the flag which shows `super()` is called in some paths. + * @param {CodePathSegment} segment - A code path segment to get. + * @returns {boolean} The flag which shows `super()` is called in some paths + */ + function isCalledInSomePath(segment) { + return segment.reachable && segInfoMap[segment.id].calledInSomePaths; + } + + /** + * Gets the flag which shows `super()` is called in all paths. + * @param {CodePathSegment} segment - A code path segment to get. + * @returns {boolean} The flag which shows `super()` is called in all paths. + */ + function isCalledInEveryPath(segment) { + + /* + * If specific segment is the looped segment of the current segment, + * skip the segment. + * If not skipped, this never becomes true after a loop. + */ + if (segment.nextSegments.length === 1 && + segment.nextSegments[0].isLoopedPrevSegment(segment) + ) { + return true; + } + return segment.reachable && segInfoMap[segment.id].calledInEveryPaths; + } + + return { + + /** + * Stacks a constructor information. + * @param {CodePath} codePath - A code path which was started. + * @param {ASTNode} node - The current node. + * @returns {void} + */ + onCodePathStart(codePath, node) { + if (isConstructorFunction(node)) { + + // Class > ClassBody > MethodDefinition > FunctionExpression + const classNode = node.parent.parent.parent; + const superClass = classNode.superClass; + + funcInfo = { + upper: funcInfo, + isConstructor: true, + hasExtends: Boolean(superClass), + superIsConstructor: isPossibleConstructor(superClass), + codePath + }; + } else { + funcInfo = { + upper: funcInfo, + isConstructor: false, + hasExtends: false, + superIsConstructor: false, + codePath + }; + } + }, + + /** + * Pops a constructor information. + * And reports if `super()` lacked. + * @param {CodePath} codePath - A code path which was ended. + * @param {ASTNode} node - The current node. + * @returns {void} + */ + onCodePathEnd(codePath, node) { + const hasExtends = funcInfo.hasExtends; + + // Pop. + funcInfo = funcInfo.upper; + + if (!hasExtends) { + return; + } + + // Reports if `super()` lacked. + const segments = codePath.returnedSegments; + const calledInEveryPaths = segments.every(isCalledInEveryPath); + const calledInSomePaths = segments.some(isCalledInSomePath); + + if (!calledInEveryPaths) { + context.report({ + message: calledInSomePaths + ? "Lacked a call of 'super()' in some code paths." + : "Expected to call 'super()'.", + node: node.parent + }); + } + }, + + /** + * Initialize information of a given code path segment. + * @param {CodePathSegment} segment - A code path segment to initialize. + * @returns {void} + */ + onCodePathSegmentStart(segment) { + if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) { + return; + } + + // Initialize info. + const info = segInfoMap[segment.id] = { + calledInSomePaths: false, + calledInEveryPaths: false, + validNodes: [] + }; + + // When there are previous segments, aggregates these. + const prevSegments = segment.prevSegments; + + if (prevSegments.length > 0) { + info.calledInSomePaths = prevSegments.some(isCalledInSomePath); + info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath); + } + }, + + /** + * Update information of the code path segment when a code path was + * looped. + * @param {CodePathSegment} fromSegment - The code path segment of the + * end of a loop. + * @param {CodePathSegment} toSegment - A code path segment of the head + * of a loop. + * @returns {void} + */ + onCodePathSegmentLoop(fromSegment, toSegment) { + if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) { + return; + } + + // Update information inside of the loop. + const isRealLoop = toSegment.prevSegments.length >= 2; + + funcInfo.codePath.traverseSegments( + { first: toSegment, last: fromSegment }, + segment => { + const info = segInfoMap[segment.id]; + const prevSegments = segment.prevSegments; + + // Updates flags. + info.calledInSomePaths = prevSegments.some(isCalledInSomePath); + info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath); + + // If flags become true anew, reports the valid nodes. + if (info.calledInSomePaths || isRealLoop) { + const nodes = info.validNodes; + + info.validNodes = []; + + for (let i = 0; i < nodes.length; ++i) { + const node = nodes[i]; + + context.report({ + message: "Unexpected duplicate 'super()'.", + node + }); + } + } + } + ); + }, + + /** + * Checks for a call of `super()`. + * @param {ASTNode} node - A CallExpression node to check. + * @returns {void} + */ + "CallExpression:exit"(node) { + if (!(funcInfo && funcInfo.isConstructor)) { + return; + } + + // Skips except `super()`. + if (node.callee.type !== "Super") { + return; + } + + // Reports if needed. + if (funcInfo.hasExtends) { + const segments = funcInfo.codePath.currentSegments; + let duplicate = false; + let info = null; + + for (let i = 0; i < segments.length; ++i) { + const segment = segments[i]; + + if (segment.reachable) { + info = segInfoMap[segment.id]; + + duplicate = duplicate || info.calledInSomePaths; + info.calledInSomePaths = info.calledInEveryPaths = true; + } + } + + if (info) { + if (duplicate) { + context.report({ + message: "Unexpected duplicate 'super()'.", + node + }); + } else if (!funcInfo.superIsConstructor) { + context.report({ + message: "Unexpected 'super()' because 'super' is not a constructor.", + node + }); + } else { + info.validNodes.push(node); + } + } + } else if (funcInfo.codePath.currentSegments.some(isReachable)) { + context.report({ + message: "Unexpected 'super()'.", + node + }); + } + }, + + /** + * Set the mark to the returned path as `super()` was called. + * @param {ASTNode} node - A ReturnStatement node to check. + * @returns {void} + */ + ReturnStatement(node) { + if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) { + return; + } + + // Skips if no argument. + if (!node.argument) { + return; + } + + // Returning argument is a substitute of 'super()'. + const segments = funcInfo.codePath.currentSegments; + + for (let i = 0; i < segments.length; ++i) { + const segment = segments[i]; + + if (segment.reachable) { + const info = segInfoMap[segment.id]; + + info.calledInSomePaths = info.calledInEveryPaths = true; + } + } + }, + + /** + * Resets state. + * @returns {void} + */ + "Program:exit"() { + segInfoMap = Object.create(null); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/curly.js b/tools/node_modules/eslint/lib/rules/curly.js new file mode 100644 index 0000000000..37f07d95ee --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/curly.js @@ -0,0 +1,399 @@ +/** + * @fileoverview Rule to flag statements without curly braces + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent brace style for all control statements", + category: "Best Practices", + recommended: false + }, + + schema: { + anyOf: [ + { + type: "array", + items: [ + { + enum: ["all"] + } + ], + minItems: 0, + maxItems: 1 + }, + { + type: "array", + items: [ + { + enum: ["multi", "multi-line", "multi-or-nest"] + }, + { + enum: ["consistent"] + } + ], + minItems: 0, + maxItems: 2 + } + ] + }, + + fixable: "code" + }, + + create(context) { + + const multiOnly = (context.options[0] === "multi"); + const multiLine = (context.options[0] === "multi-line"); + const multiOrNest = (context.options[0] === "multi-or-nest"); + const consistent = (context.options[1] === "consistent"); + + const sourceCode = context.getSourceCode(); + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Determines if a given node is a one-liner that's on the same line as it's preceding code. + * @param {ASTNode} node The node to check. + * @returns {boolean} True if the node is a one-liner that's on the same line as it's preceding code. + * @private + */ + function isCollapsedOneLiner(node) { + const before = sourceCode.getTokenBefore(node); + const last = sourceCode.getLastToken(node); + const lastExcludingSemicolon = astUtils.isSemicolonToken(last) ? sourceCode.getTokenBefore(last) : last; + + return before.loc.start.line === lastExcludingSemicolon.loc.end.line; + } + + /** + * Determines if a given node is a one-liner. + * @param {ASTNode} node The node to check. + * @returns {boolean} True if the node is a one-liner. + * @private + */ + function isOneLiner(node) { + const first = sourceCode.getFirstToken(node), + last = sourceCode.getLastToken(node); + + return first.loc.start.line === last.loc.end.line; + } + + /** + * Checks if the given token is an `else` token or not. + * + * @param {Token} token - The token to check. + * @returns {boolean} `true` if the token is an `else` token. + */ + function isElseKeywordToken(token) { + return token.value === "else" && token.type === "Keyword"; + } + + /** + * Gets the `else` keyword token of a given `IfStatement` node. + * @param {ASTNode} node - A `IfStatement` node to get. + * @returns {Token} The `else` keyword token. + */ + function getElseKeyword(node) { + return node.alternate && sourceCode.getFirstTokenBetween(node.consequent, node.alternate, isElseKeywordToken); + } + + /** + * Checks a given IfStatement node requires braces of the consequent chunk. + * This returns `true` when below: + * + * 1. The given node has the `alternate` node. + * 2. There is a `IfStatement` which doesn't have `alternate` node in the + * trailing statement chain of the `consequent` node. + * + * @param {ASTNode} node - A IfStatement node to check. + * @returns {boolean} `true` if the node requires braces of the consequent chunk. + */ + function requiresBraceOfConsequent(node) { + if (node.alternate && node.consequent.type === "BlockStatement") { + if (node.consequent.body.length >= 2) { + return true; + } + + node = node.consequent.body[0]; + while (node) { + if (node.type === "IfStatement" && !node.alternate) { + return true; + } + node = astUtils.getTrailingStatement(node); + } + } + + return false; + } + + /** + * Reports "Expected { after ..." error + * @param {ASTNode} node The node to report. + * @param {ASTNode} bodyNode The body node that is incorrectly missing curly brackets + * @param {string} name The name to report. + * @param {string} suffix Additional string to add to the end of a report. + * @returns {void} + * @private + */ + function reportExpectedBraceError(node, bodyNode, name, suffix) { + context.report({ + node, + loc: (name !== "else" ? node : getElseKeyword(node)).loc.start, + message: "Expected { after '{{name}}'{{suffix}}.", + data: { + name, + suffix: (suffix ? ` ${suffix}` : "") + }, + fix: fixer => fixer.replaceText(bodyNode, `{${sourceCode.getText(bodyNode)}}`) + }); + } + + /** + * Determines if a semicolon needs to be inserted after removing a set of curly brackets, in order to avoid a SyntaxError. + * @param {Token} closingBracket The } token + * @returns {boolean} `true` if a semicolon needs to be inserted after the last statement in the block. + */ + function needsSemicolon(closingBracket) { + const tokenBefore = sourceCode.getTokenBefore(closingBracket); + const tokenAfter = sourceCode.getTokenAfter(closingBracket); + const lastBlockNode = sourceCode.getNodeByRangeIndex(tokenBefore.range[0]); + + if (astUtils.isSemicolonToken(tokenBefore)) { + + // If the last statement already has a semicolon, don't add another one. + return false; + } + + if (!tokenAfter) { + + // If there are no statements after this block, there is no need to add a semicolon. + return false; + } + + if (lastBlockNode.type === "BlockStatement" && lastBlockNode.parent.type !== "FunctionExpression" && lastBlockNode.parent.type !== "ArrowFunctionExpression") { + + /* + * If the last node surrounded by curly brackets is a BlockStatement (other than a FunctionExpression or an ArrowFunctionExpression), + * don't insert a semicolon. Otherwise, the semicolon would be parsed as a separate statement, which would cause + * a SyntaxError if it was followed by `else`. + */ + return false; + } + + if (tokenBefore.loc.end.line === tokenAfter.loc.start.line) { + + // If the next token is on the same line, insert a semicolon. + return true; + } + + if (/^[([/`+-]/.test(tokenAfter.value)) { + + // If the next token starts with a character that would disrupt ASI, insert a semicolon. + return true; + } + + if (tokenBefore.type === "Punctuator" && (tokenBefore.value === "++" || tokenBefore.value === "--")) { + + // If the last token is ++ or --, insert a semicolon to avoid disrupting ASI. + return true; + } + + // Otherwise, do not insert a semicolon. + return false; + } + + /** + * Reports "Unnecessary { after ..." error + * @param {ASTNode} node The node to report. + * @param {ASTNode} bodyNode The block statement that is incorrectly surrounded by parens + * @param {string} name The name to report. + * @param {string} suffix Additional string to add to the end of a report. + * @returns {void} + * @private + */ + function reportUnnecessaryBraceError(node, bodyNode, name, suffix) { + context.report({ + node, + loc: (name !== "else" ? node : getElseKeyword(node)).loc.start, + message: "Unnecessary { after '{{name}}'{{suffix}}.", + data: { + name, + suffix: (suffix ? ` ${suffix}` : "") + }, + fix(fixer) { + + /* + * `do while` expressions sometimes need a space to be inserted after `do`. + * e.g. `do{foo()} while (bar)` should be corrected to `do foo() while (bar)` + */ + const needsPrecedingSpace = node.type === "DoWhileStatement" && + sourceCode.getTokenBefore(bodyNode).range[1] === bodyNode.range[0] && + !astUtils.canTokensBeAdjacent("do", sourceCode.getFirstToken(bodyNode, { skip: 1 })); + + const openingBracket = sourceCode.getFirstToken(bodyNode); + const closingBracket = sourceCode.getLastToken(bodyNode); + const lastTokenInBlock = sourceCode.getTokenBefore(closingBracket); + + if (needsSemicolon(closingBracket)) { + + /* + * If removing braces would cause a SyntaxError due to multiple statements on the same line (or + * change the semantics of the code due to ASI), don't perform a fix. + */ + return null; + } + + const resultingBodyText = sourceCode.getText().slice(openingBracket.range[1], lastTokenInBlock.range[0]) + + sourceCode.getText(lastTokenInBlock) + + sourceCode.getText().slice(lastTokenInBlock.range[1], closingBracket.range[0]); + + return fixer.replaceText(bodyNode, (needsPrecedingSpace ? " " : "") + resultingBodyText); + } + }); + } + + /** + * Prepares to check the body of a node to see if it's a block statement. + * @param {ASTNode} node The node to report if there's a problem. + * @param {ASTNode} body The body node to check for blocks. + * @param {string} name The name to report if there's a problem. + * @param {string} suffix Additional string to add to the end of a report. + * @returns {Object} a prepared check object, with "actual", "expected", "check" properties. + * "actual" will be `true` or `false` whether the body is already a block statement. + * "expected" will be `true` or `false` if the body should be a block statement or not, or + * `null` if it doesn't matter, depending on the rule options. It can be modified to change + * the final behavior of "check". + * "check" will be a function reporting appropriate problems depending on the other + * properties. + */ + function prepareCheck(node, body, name, suffix) { + const hasBlock = (body.type === "BlockStatement"); + let expected = null; + + if (node.type === "IfStatement" && node.consequent === body && requiresBraceOfConsequent(node)) { + expected = true; + } else if (multiOnly) { + if (hasBlock && body.body.length === 1) { + expected = false; + } + } else if (multiLine) { + if (!isCollapsedOneLiner(body)) { + expected = true; + } + } else if (multiOrNest) { + if (hasBlock && body.body.length === 1 && isOneLiner(body.body[0])) { + const leadingComments = sourceCode.getCommentsBefore(body.body[0]); + + expected = leadingComments.length > 0; + } else if (!isOneLiner(body)) { + expected = true; + } + } else { + expected = true; + } + + return { + actual: hasBlock, + expected, + check() { + if (this.expected !== null && this.expected !== this.actual) { + if (this.expected) { + reportExpectedBraceError(node, body, name, suffix); + } else { + reportUnnecessaryBraceError(node, body, name, suffix); + } + } + } + }; + } + + /** + * Prepares to check the bodies of a "if", "else if" and "else" chain. + * @param {ASTNode} node The first IfStatement node of the chain. + * @returns {Object[]} prepared checks for each body of the chain. See `prepareCheck` for more + * information. + */ + function prepareIfChecks(node) { + const preparedChecks = []; + + do { + preparedChecks.push(prepareCheck(node, node.consequent, "if", "condition")); + if (node.alternate && node.alternate.type !== "IfStatement") { + preparedChecks.push(prepareCheck(node, node.alternate, "else")); + break; + } + node = node.alternate; + } while (node); + + if (consistent) { + + /* + * If any node should have or already have braces, make sure they + * all have braces. + * If all nodes shouldn't have braces, make sure they don't. + */ + const expected = preparedChecks.some(preparedCheck => { + if (preparedCheck.expected !== null) { + return preparedCheck.expected; + } + return preparedCheck.actual; + }); + + preparedChecks.forEach(preparedCheck => { + preparedCheck.expected = expected; + }); + } + + return preparedChecks; + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + IfStatement(node) { + if (node.parent.type !== "IfStatement") { + prepareIfChecks(node).forEach(preparedCheck => { + preparedCheck.check(); + }); + } + }, + + WhileStatement(node) { + prepareCheck(node, node.body, "while", "condition").check(); + }, + + DoWhileStatement(node) { + prepareCheck(node, node.body, "do").check(); + }, + + ForStatement(node) { + prepareCheck(node, node.body, "for", "condition").check(); + }, + + ForInStatement(node) { + prepareCheck(node, node.body, "for-in").check(); + }, + + ForOfStatement(node) { + prepareCheck(node, node.body, "for-of").check(); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/default-case.js b/tools/node_modules/eslint/lib/rules/default-case.js new file mode 100644 index 0000000000..32cd8dfe49 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/default-case.js @@ -0,0 +1,90 @@ +/** + * @fileoverview require default case in switch statements + * @author Aliaksei Shytkin + */ +"use strict"; + +const DEFAULT_COMMENT_PATTERN = /^no default$/i; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require `default` cases in `switch` statements", + category: "Best Practices", + recommended: false + }, + + schema: [{ + type: "object", + properties: { + commentPattern: { + type: "string" + } + }, + additionalProperties: false + }] + }, + + create(context) { + const options = context.options[0] || {}; + const commentPattern = options.commentPattern + ? new RegExp(options.commentPattern) + : DEFAULT_COMMENT_PATTERN; + + const sourceCode = context.getSourceCode(); + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Shortcut to get last element of array + * @param {*[]} collection Array + * @returns {*} Last element + */ + function last(collection) { + return collection[collection.length - 1]; + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + SwitchStatement(node) { + + if (!node.cases.length) { + + /* + * skip check of empty switch because there is no easy way + * to extract comments inside it now + */ + return; + } + + const hasDefault = node.cases.some(v => v.test === null); + + if (!hasDefault) { + + let comment; + + const lastCase = last(node.cases); + const comments = sourceCode.getCommentsAfter(lastCase); + + if (comments.length) { + comment = last(comments); + } + + if (!comment || !commentPattern.test(comment.value.trim())) { + context.report({ node, message: "Expected a default case." }); + } + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/dot-location.js b/tools/node_modules/eslint/lib/rules/dot-location.js new file mode 100644 index 0000000000..60f4af7013 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/dot-location.js @@ -0,0 +1,88 @@ +/** + * @fileoverview Validates newlines before and after dots + * @author Greg Cochard + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent newlines before and after dots", + category: "Best Practices", + recommended: false + }, + + schema: [ + { + enum: ["object", "property"] + } + ], + + fixable: "code" + }, + + create(context) { + + const config = context.options[0]; + + // default to onObject if no preference is passed + const onObject = config === "object" || !config; + + const sourceCode = context.getSourceCode(); + + /** + * Reports if the dot between object and property is on the correct loccation. + * @param {ASTNode} obj The object owning the property. + * @param {ASTNode} prop The property of the object. + * @param {ASTNode} node The corresponding node of the token. + * @returns {void} + */ + function checkDotLocation(obj, prop, node) { + const dot = sourceCode.getTokenBefore(prop); + const textBeforeDot = sourceCode.getText().slice(obj.range[1], dot.range[0]); + const textAfterDot = sourceCode.getText().slice(dot.range[1], prop.range[0]); + + if (dot.type === "Punctuator" && dot.value === ".") { + if (onObject) { + if (!astUtils.isTokenOnSameLine(obj, dot)) { + const neededTextAfterObj = astUtils.isDecimalInteger(obj) ? " " : ""; + + context.report({ + node, + loc: dot.loc.start, + message: "Expected dot to be on same line as object.", + fix: fixer => fixer.replaceTextRange([obj.range[1], prop.range[0]], `${neededTextAfterObj}.${textBeforeDot}${textAfterDot}`) + }); + } + } else if (!astUtils.isTokenOnSameLine(dot, prop)) { + context.report({ + node, + loc: dot.loc.start, + message: "Expected dot to be on same line as property.", + fix: fixer => fixer.replaceTextRange([obj.range[1], prop.range[0]], `${textBeforeDot}${textAfterDot}.`) + }); + } + } + } + + /** + * Checks the spacing of the dot within a member expression. + * @param {ASTNode} node The node to check. + * @returns {void} + */ + function checkNode(node) { + checkDotLocation(node.object, node.property, node); + } + + return { + MemberExpression: checkNode + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/dot-notation.js b/tools/node_modules/eslint/lib/rules/dot-notation.js new file mode 100644 index 0000000000..21e3df1741 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/dot-notation.js @@ -0,0 +1,159 @@ +/** + * @fileoverview Rule to warn about using dot notation instead of square bracket notation when possible. + * @author Josh Perez + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; +const keywords = require("../util/keywords"); + +module.exports = { + meta: { + docs: { + description: "enforce dot notation whenever possible", + category: "Best Practices", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + allowKeywords: { + type: "boolean" + }, + allowPattern: { + type: "string" + } + }, + additionalProperties: false + } + ], + + fixable: "code" + }, + + create(context) { + const options = context.options[0] || {}; + const allowKeywords = options.allowKeywords === void 0 || !!options.allowKeywords; + const sourceCode = context.getSourceCode(); + + let allowPattern; + + if (options.allowPattern) { + allowPattern = new RegExp(options.allowPattern); + } + + /** + * Check if the property is valid dot notation + * @param {ASTNode} node The dot notation node + * @param {string} value Value which is to be checked + * @returns {void} + */ + function checkComputedProperty(node, value) { + if ( + validIdentifier.test(value) && + (allowKeywords || keywords.indexOf(String(value)) === -1) && + !(allowPattern && allowPattern.test(value)) + ) { + const formattedValue = node.property.type === "Literal" ? JSON.stringify(value) : `\`${value}\``; + + context.report({ + node: node.property, + message: "[{{propertyValue}}] is better written in dot notation.", + data: { + propertyValue: formattedValue + }, + fix(fixer) { + const leftBracket = sourceCode.getTokenAfter(node.object, astUtils.isOpeningBracketToken); + const rightBracket = sourceCode.getLastToken(node); + + if (sourceCode.getFirstTokenBetween(leftBracket, rightBracket, { includeComments: true, filter: astUtils.isCommentToken })) { + + // Don't perform any fixes if there are comments inside the brackets. + return null; + } + + const tokenAfterProperty = sourceCode.getTokenAfter(rightBracket); + const needsSpaceAfterProperty = tokenAfterProperty && + rightBracket.range[1] === tokenAfterProperty.range[0] && + !astUtils.canTokensBeAdjacent(String(value), tokenAfterProperty); + + const textBeforeDot = astUtils.isDecimalInteger(node.object) ? " " : ""; + const textAfterProperty = needsSpaceAfterProperty ? " " : ""; + + return fixer.replaceTextRange( + [leftBracket.range[0], rightBracket.range[1]], + `${textBeforeDot}.${value}${textAfterProperty}` + ); + } + }); + } + } + + return { + MemberExpression(node) { + if ( + node.computed && + node.property.type === "Literal" + ) { + checkComputedProperty(node, node.property.value); + } + if ( + node.computed && + node.property.type === "TemplateLiteral" && + node.property.expressions.length === 0 + ) { + checkComputedProperty(node, node.property.quasis[0].value.cooked); + } + if ( + !allowKeywords && + !node.computed && + keywords.indexOf(String(node.property.name)) !== -1 + ) { + context.report({ + node: node.property, + message: ".{{propertyName}} is a syntax error.", + data: { + propertyName: node.property.name + }, + fix(fixer) { + const dot = sourceCode.getTokenBefore(node.property); + const textAfterDot = sourceCode.text.slice(dot.range[1], node.property.range[0]); + + if (textAfterDot.trim()) { + + // Don't perform any fixes if there are comments between the dot and the property name. + return null; + } + + if (node.object.type === "Identifier" && node.object.name === "let") { + + /* + * A statement that starts with `let[` is parsed as a destructuring variable declaration, not + * a MemberExpression. + */ + return null; + } + + return fixer.replaceTextRange( + [dot.range[0], node.property.range[1]], + `[${textAfterDot}"${node.property.name}"]` + ); + } + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/eol-last.js b/tools/node_modules/eslint/lib/rules/eol-last.js new file mode 100644 index 0000000000..1f3676b0e9 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/eol-last.js @@ -0,0 +1,94 @@ +/** + * @fileoverview Require or disallow newline at the end of files + * @author Nodeca Team <https://github.com/nodeca> + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const lodash = require("lodash"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require or disallow newline at the end of files", + category: "Stylistic Issues", + recommended: false + }, + fixable: "whitespace", + schema: [ + { + enum: ["always", "never", "unix", "windows"] + } + ] + }, + create(context) { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + Program: function checkBadEOF(node) { + const sourceCode = context.getSourceCode(), + src = sourceCode.getText(), + location = { + column: lodash.last(sourceCode.lines).length, + line: sourceCode.lines.length + }, + LF = "\n", + CRLF = `\r${LF}`, + endsWithNewline = lodash.endsWith(src, LF); + + let mode = context.options[0] || "always", + appendCRLF = false; + + if (mode === "unix") { + + // `"unix"` should behave exactly as `"always"` + mode = "always"; + } + if (mode === "windows") { + + // `"windows"` should behave exactly as `"always"`, but append CRLF in the fixer for backwards compatibility + mode = "always"; + appendCRLF = true; + } + if (mode === "always" && !endsWithNewline) { + + // File is not newline-terminated, but should be + context.report({ + node, + loc: location, + message: "Newline required at end of file but not found.", + fix(fixer) { + return fixer.insertTextAfterRange([0, src.length], appendCRLF ? CRLF : LF); + } + }); + } else if (mode === "never" && endsWithNewline) { + + // File is newline-terminated, but shouldn't be + context.report({ + node, + loc: location, + message: "Newline not allowed at end of file.", + fix(fixer) { + const finalEOLs = /(?:\r?\n)+$/, + match = finalEOLs.exec(sourceCode.text), + start = match.index, + end = sourceCode.text.length; + + return fixer.replaceTextRange([start, end], ""); + } + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/eqeqeq.js b/tools/node_modules/eslint/lib/rules/eqeqeq.js new file mode 100644 index 0000000000..8801102e64 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/eqeqeq.js @@ -0,0 +1,180 @@ +/** + * @fileoverview Rule to flag statements that use != and == instead of !== and === + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require the use of `===` and `!==`", + category: "Best Practices", + recommended: false + }, + + schema: { + anyOf: [ + { + type: "array", + items: [ + { + enum: ["always"] + }, + { + type: "object", + properties: { + null: { + enum: ["always", "never", "ignore"] + } + }, + additionalProperties: false + } + ], + additionalItems: false + }, + { + type: "array", + items: [ + { + enum: ["smart", "allow-null"] + } + ], + additionalItems: false + } + ] + }, + + fixable: "code" + }, + + create(context) { + const config = context.options[0] || "always"; + const options = context.options[1] || {}; + const sourceCode = context.getSourceCode(); + + const nullOption = (config === "always") + ? options.null || "always" + : "ignore"; + const enforceRuleForNull = (nullOption === "always"); + const enforceInverseRuleForNull = (nullOption === "never"); + + /** + * Checks if an expression is a typeof expression + * @param {ASTNode} node The node to check + * @returns {boolean} if the node is a typeof expression + */ + function isTypeOf(node) { + return node.type === "UnaryExpression" && node.operator === "typeof"; + } + + /** + * Checks if either operand of a binary expression is a typeof operation + * @param {ASTNode} node The node to check + * @returns {boolean} if one of the operands is typeof + * @private + */ + function isTypeOfBinary(node) { + return isTypeOf(node.left) || isTypeOf(node.right); + } + + /** + * Checks if operands are literals of the same type (via typeof) + * @param {ASTNode} node The node to check + * @returns {boolean} if operands are of same type + * @private + */ + function areLiteralsAndSameType(node) { + return node.left.type === "Literal" && node.right.type === "Literal" && + typeof node.left.value === typeof node.right.value; + } + + /** + * Checks if one of the operands is a literal null + * @param {ASTNode} node The node to check + * @returns {boolean} if operands are null + * @private + */ + function isNullCheck(node) { + return astUtils.isNullLiteral(node.right) || astUtils.isNullLiteral(node.left); + } + + /** + * Gets the location (line and column) of the binary expression's operator + * @param {ASTNode} node The binary expression node to check + * @param {string} operator The operator to find + * @returns {Object} { line, column } location of operator + * @private + */ + function getOperatorLocation(node) { + const opToken = sourceCode.getTokenAfter(node.left); + + return { line: opToken.loc.start.line, column: opToken.loc.start.column }; + } + + /** + * Reports a message for this rule. + * @param {ASTNode} node The binary expression node that was checked + * @param {string} expectedOperator The operator that was expected (either '==', '!=', '===', or '!==') + * @returns {void} + * @private + */ + function report(node, expectedOperator) { + context.report({ + node, + loc: getOperatorLocation(node), + message: "Expected '{{expectedOperator}}' and instead saw '{{actualOperator}}'.", + data: { expectedOperator, actualOperator: node.operator }, + fix(fixer) { + + // If the comparison is a `typeof` comparison or both sides are literals with the same type, then it's safe to fix. + if (isTypeOfBinary(node) || areLiteralsAndSameType(node)) { + const operatorToken = sourceCode.getFirstTokenBetween( + node.left, + node.right, + token => token.value === node.operator + ); + + return fixer.replaceText(operatorToken, expectedOperator); + } + return null; + } + }); + } + + return { + BinaryExpression(node) { + const isNull = isNullCheck(node); + + if (node.operator !== "==" && node.operator !== "!=") { + if (enforceInverseRuleForNull && isNull) { + report(node, node.operator.slice(0, -1)); + } + return; + } + + if (config === "smart" && (isTypeOfBinary(node) || + areLiteralsAndSameType(node) || isNull)) { + return; + } + + if (!enforceRuleForNull && isNull) { + return; + } + + report(node, `${node.operator}=`); + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/for-direction.js b/tools/node_modules/eslint/lib/rules/for-direction.js new file mode 100644 index 0000000000..7178ced9db --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/for-direction.js @@ -0,0 +1,105 @@ +/** + * @fileoverview enforce "for" loop update clause moving the counter in the right direction.(for-direction) + * @author Aladdin-ADD<hh_2013@foxmail.com> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce \"for\" loop update clause moving the counter in the right direction.", + category: "Possible Errors", + recommended: false + }, + fixable: null, + schema: [] + }, + + create(context) { + + /** + * report an error. + * @param {ASTNode} node the node to report. + * @returns {void} + */ + function report(node) { + context.report({ + node, + message: "The update clause in this loop moves the variable in the wrong direction." + }); + } + + /** + * check UpdateExpression add/sub the counter + * @param {ASTNode} update UpdateExpression to check + * @param {string} counter variable name to check + * @returns {int} if add return 1, if sub return -1, if nochange, return 0 + */ + function getUpdateDirection(update, counter) { + if (update.argument.type === "Identifier" && update.argument.name === counter) { + if (update.operator === "++") { + return 1; + } + if (update.operator === "--") { + return -1; + } + } + return 0; + } + + /** + * check AssignmentExpression add/sub the counter + * @param {ASTNode} update AssignmentExpression to check + * @param {string} counter variable name to check + * @returns {int} if add return 1, if sub return -1, if nochange, return 0 + */ + function getAssignmentDirection(update, counter) { + if (update.left.name === counter) { + if (update.operator === "+=") { + return 1; + } + if (update.operator === "-=") { + return -1; + } + } + return 0; + } + return { + ForStatement(node) { + + if (node.test && node.test.type === "BinaryExpression" && node.test.left.type === "Identifier" && node.update) { + const counter = node.test.left.name; + const operator = node.test.operator; + const update = node.update; + + if (operator === "<" || operator === "<=") { + + // report error if update sub the counter (--, -=) + if (update.type === "UpdateExpression" && getUpdateDirection(update, counter) < 0) { + report(node); + } + + if (update.type === "AssignmentExpression" && getAssignmentDirection(update, counter) < 0) { + report(node); + } + } else if (operator === ">" || operator === ">=") { + + // report error if update add the counter (++, +=) + if (update.type === "UpdateExpression" && getUpdateDirection(update, counter) > 0) { + report(node); + } + + if (update.type === "AssignmentExpression" && getAssignmentDirection(update, counter) > 0) { + report(node); + } + } + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/func-call-spacing.js b/tools/node_modules/eslint/lib/rules/func-call-spacing.js new file mode 100644 index 0000000000..00e677d33b --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/func-call-spacing.js @@ -0,0 +1,159 @@ +/** + * @fileoverview Rule to control spacing within function calls + * @author Matt DuVall <http://www.mattduvall.com> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require or disallow spacing between function identifiers and their invocations", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + schema: { + anyOf: [ + { + type: "array", + items: [ + { + enum: ["never"] + } + ], + minItems: 0, + maxItems: 1 + }, + { + type: "array", + items: [ + { + enum: ["always"] + }, + { + type: "object", + properties: { + allowNewlines: { + type: "boolean" + } + }, + additionalProperties: false + } + ], + minItems: 0, + maxItems: 2 + } + ] + } + }, + + create(context) { + + const never = context.options[0] !== "always"; + const allowNewlines = !never && context.options[1] && context.options[1].allowNewlines; + const sourceCode = context.getSourceCode(); + const text = sourceCode.getText(); + + /** + * Check if open space is present in a function name + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function checkSpacing(node) { + const lastToken = sourceCode.getLastToken(node); + const lastCalleeToken = sourceCode.getLastToken(node.callee); + const parenToken = sourceCode.getFirstTokenBetween(lastCalleeToken, lastToken, astUtils.isOpeningParenToken); + const prevToken = parenToken && sourceCode.getTokenBefore(parenToken); + + // Parens in NewExpression are optional + if (!(parenToken && parenToken.range[1] < node.range[1])) { + return; + } + + const textBetweenTokens = text.slice(prevToken.range[1], parenToken.range[0]).replace(/\/\*.*?\*\//g, ""); + const hasWhitespace = /\s/.test(textBetweenTokens); + const hasNewline = hasWhitespace && astUtils.LINEBREAK_MATCHER.test(textBetweenTokens); + + /* + * never allowNewlines hasWhitespace hasNewline message + * F F F F Missing space between function name and paren. + * F F F T (Invalid `!hasWhitespace && hasNewline`) + * F F T T Unexpected newline between function name and paren. + * F F T F (OK) + * F T T F (OK) + * F T T T (OK) + * F T F T (Invalid `!hasWhitespace && hasNewline`) + * F T F F Missing space between function name and paren. + * T T F F (Invalid `never && allowNewlines`) + * T T F T (Invalid `!hasWhitespace && hasNewline`) + * T T T T (Invalid `never && allowNewlines`) + * T T T F (Invalid `never && allowNewlines`) + * T F T F Unexpected space between function name and paren. + * T F T T Unexpected space between function name and paren. + * T F F T (Invalid `!hasWhitespace && hasNewline`) + * T F F F (OK) + * + * T T Unexpected space between function name and paren. + * F F Missing space between function name and paren. + * F F T Unexpected newline between function name and paren. + */ + + if (never && hasWhitespace) { + context.report({ + node, + loc: lastCalleeToken.loc.start, + message: "Unexpected space between function name and paren.", + fix(fixer) { + + /* + * Only autofix if there is no newline + * https://github.com/eslint/eslint/issues/7787 + */ + if (!hasNewline) { + return fixer.removeRange([prevToken.range[1], parenToken.range[0]]); + } + + return null; + } + }); + } else if (!never && !hasWhitespace) { + context.report({ + node, + loc: lastCalleeToken.loc.start, + message: "Missing space between function name and paren.", + fix(fixer) { + return fixer.insertTextBefore(parenToken, " "); + } + }); + } else if (!never && !allowNewlines && hasNewline) { + context.report({ + node, + loc: lastCalleeToken.loc.start, + message: "Unexpected newline between function name and paren.", + fix(fixer) { + return fixer.replaceTextRange([prevToken.range[1], parenToken.range[0]], " "); + } + }); + } + } + + return { + CallExpression: checkSpacing, + NewExpression: checkSpacing + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/func-name-matching.js b/tools/node_modules/eslint/lib/rules/func-name-matching.js new file mode 100644 index 0000000000..db06d5d468 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/func-name-matching.js @@ -0,0 +1,193 @@ +/** + * @fileoverview Rule to require function names to match the name of the variable or property to which they are assigned. + * @author Annie Zhang, Pavel Strashkin + */ + +"use strict"; + +//-------------------------------------------------------------------------- +// Requirements +//-------------------------------------------------------------------------- + +const astUtils = require("../ast-utils"); +const esutils = require("esutils"); + +//-------------------------------------------------------------------------- +// Helpers +//-------------------------------------------------------------------------- + +/** + * Determines if a pattern is `module.exports` or `module["exports"]` + * @param {ASTNode} pattern The left side of the AssignmentExpression + * @returns {boolean} True if the pattern is `module.exports` or `module["exports"]` + */ +function isModuleExports(pattern) { + if (pattern.type === "MemberExpression" && pattern.object.type === "Identifier" && pattern.object.name === "module") { + + // module.exports + if (pattern.property.type === "Identifier" && pattern.property.name === "exports") { + return true; + } + + // module["exports"] + if (pattern.property.type === "Literal" && pattern.property.value === "exports") { + return true; + } + } + return false; +} + +/** + * Determines if a string name is a valid identifier + * @param {string} name The string to be checked + * @param {int} ecmaVersion The ECMAScript version if specified in the parserOptions config + * @returns {boolean} True if the string is a valid identifier + */ +function isIdentifier(name, ecmaVersion) { + if (ecmaVersion >= 6) { + return esutils.keyword.isIdentifierES6(name); + } + return esutils.keyword.isIdentifierES5(name); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +const alwaysOrNever = { enum: ["always", "never"] }; +const optionsObject = { + type: "object", + properties: { + includeCommonJSModuleExports: { + type: "boolean" + } + }, + additionalProperties: false +}; + +module.exports = { + meta: { + docs: { + description: "require function names to match the name of the variable or property to which they are assigned", + category: "Stylistic Issues", + recommended: false + }, + + schema: { + anyOf: [{ + type: "array", + additionalItems: false, + items: [alwaysOrNever, optionsObject] + }, { + type: "array", + additionalItems: false, + items: [optionsObject] + }] + } + }, + + create(context) { + const options = (typeof context.options[0] === "object" ? context.options[0] : context.options[1]) || {}; + const nameMatches = typeof context.options[0] === "string" ? context.options[0] : "always"; + const includeModuleExports = options.includeCommonJSModuleExports; + const ecmaVersion = context.parserOptions && context.parserOptions.ecmaVersion ? context.parserOptions.ecmaVersion : 5; + + /** + * Compares identifiers based on the nameMatches option + * @param {string} x the first identifier + * @param {string} y the second identifier + * @returns {boolean} whether the two identifiers should warn. + */ + function shouldWarn(x, y) { + return (nameMatches === "always" && x !== y) || (nameMatches === "never" && x === y); + } + + /** + * Reports + * @param {ASTNode} node The node to report + * @param {string} name The variable or property name + * @param {string} funcName The function name + * @param {boolean} isProp True if the reported node is a property assignment + * @returns {void} + */ + function report(node, name, funcName, isProp) { + let message; + + if (nameMatches === "always" && isProp) { + message = "Function name `{{funcName}}` should match property name `{{name}}`"; + } else if (nameMatches === "always") { + message = "Function name `{{funcName}}` should match variable name `{{name}}`"; + } else if (isProp) { + message = "Function name `{{funcName}}` should not match property name `{{name}}`"; + } else { + message = "Function name `{{funcName}}` should not match variable name `{{name}}`"; + } + context.report({ + node, + message, + data: { + name, + funcName + } + }); + } + + /** + * Determines whether a given node is a string literal + * @param {ASTNode} node The node to check + * @returns {boolean} `true` if the node is a string literal + */ + function isStringLiteral(node) { + return node.type === "Literal" && typeof node.value === "string"; + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + VariableDeclarator(node) { + if (!node.init || node.init.type !== "FunctionExpression" || node.id.type !== "Identifier") { + return; + } + if (node.init.id && shouldWarn(node.id.name, node.init.id.name)) { + report(node, node.id.name, node.init.id.name, false); + } + }, + + AssignmentExpression(node) { + if ( + node.right.type !== "FunctionExpression" || + (node.left.computed && node.left.property.type !== "Literal") || + (!includeModuleExports && isModuleExports(node.left)) || + (node.left.type !== "Identifier" && node.left.type !== "MemberExpression") + ) { + return; + } + + const isProp = node.left.type === "MemberExpression"; + const name = isProp ? astUtils.getStaticPropertyName(node.left) : node.left.name; + + if (node.right.id && isIdentifier(name) && shouldWarn(name, node.right.id.name)) { + report(node, name, node.right.id.name, isProp); + } + }, + + Property(node) { + if (node.value.type !== "FunctionExpression" || !node.value.id || node.computed && !isStringLiteral(node.key)) { + return; + } + if (node.key.type === "Identifier" && shouldWarn(node.key.name, node.value.id.name)) { + report(node, node.key.name, node.value.id.name, true); + } else if ( + isStringLiteral(node.key) && + isIdentifier(node.key.value, ecmaVersion) && + shouldWarn(node.key.value, node.value.id.name) + ) { + report(node, node.key.value, node.value.id.name, true); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/func-names.js b/tools/node_modules/eslint/lib/rules/func-names.js new file mode 100644 index 0000000000..848ce97574 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/func-names.js @@ -0,0 +1,114 @@ +/** + * @fileoverview Rule to warn when a function expression does not have a name. + * @author Kyle T. Nunery + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +/** + * Checks whether or not a given variable is a function name. + * @param {eslint-scope.Variable} variable - A variable to check. + * @returns {boolean} `true` if the variable is a function name. + */ +function isFunctionName(variable) { + return variable && variable.defs[0].type === "FunctionName"; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require or disallow named `function` expressions", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + enum: ["always", "as-needed", "never"] + } + ] + }, + + create(context) { + const never = context.options[0] === "never"; + const asNeeded = context.options[0] === "as-needed"; + + /** + * Determines whether the current FunctionExpression node is a get, set, or + * shorthand method in an object literal or a class. + * @param {ASTNode} node - A node to check. + * @returns {boolean} True if the node is a get, set, or shorthand method. + */ + function isObjectOrClassMethod(node) { + const parent = node.parent; + + return (parent.type === "MethodDefinition" || ( + parent.type === "Property" && ( + parent.method || + parent.kind === "get" || + parent.kind === "set" + ) + )); + } + + /** + * Determines whether the current FunctionExpression node has a name that would be + * inferred from context in a conforming ES6 environment. + * @param {ASTNode} node - A node to check. + * @returns {boolean} True if the node would have a name assigned automatically. + */ + function hasInferredName(node) { + const parent = node.parent; + + return isObjectOrClassMethod(node) || + (parent.type === "VariableDeclarator" && parent.id.type === "Identifier" && parent.init === node) || + (parent.type === "Property" && parent.value === node) || + (parent.type === "AssignmentExpression" && parent.left.type === "Identifier" && parent.right === node) || + (parent.type === "ExportDefaultDeclaration" && parent.declaration === node) || + (parent.type === "AssignmentPattern" && parent.right === node); + } + + return { + "FunctionExpression:exit"(node) { + + // Skip recursive functions. + const nameVar = context.getDeclaredVariables(node)[0]; + + if (isFunctionName(nameVar) && nameVar.references.length > 0) { + return; + } + + const hasName = Boolean(node.id && node.id.name); + const name = astUtils.getFunctionNameWithKind(node); + + if (never) { + if (hasName) { + context.report({ + node, + message: "Unexpected named {{name}}.", + data: { name } + }); + } + } else { + if (!hasName && (asNeeded ? !hasInferredName(node) : !isObjectOrClassMethod(node))) { + context.report({ + node, + message: "Unexpected unnamed {{name}}.", + data: { name } + }); + } + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/func-style.js b/tools/node_modules/eslint/lib/rules/func-style.js new file mode 100644 index 0000000000..123eae3d8a --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/func-style.js @@ -0,0 +1,89 @@ +/** + * @fileoverview Rule to enforce a particular function style + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce the consistent use of either `function` declarations or expressions", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + enum: ["declaration", "expression"] + }, + { + type: "object", + properties: { + allowArrowFunctions: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + const style = context.options[0], + allowArrowFunctions = context.options[1] && context.options[1].allowArrowFunctions === true, + enforceDeclarations = (style === "declaration"), + stack = []; + + const nodesToCheck = { + FunctionDeclaration(node) { + stack.push(false); + + if (!enforceDeclarations && node.parent.type !== "ExportDefaultDeclaration") { + context.report({ node, message: "Expected a function expression." }); + } + }, + "FunctionDeclaration:exit"() { + stack.pop(); + }, + + FunctionExpression(node) { + stack.push(false); + + if (enforceDeclarations && node.parent.type === "VariableDeclarator") { + context.report({ node: node.parent, message: "Expected a function declaration." }); + } + }, + "FunctionExpression:exit"() { + stack.pop(); + }, + + ThisExpression() { + if (stack.length > 0) { + stack[stack.length - 1] = true; + } + } + }; + + if (!allowArrowFunctions) { + nodesToCheck.ArrowFunctionExpression = function() { + stack.push(false); + }; + + nodesToCheck["ArrowFunctionExpression:exit"] = function(node) { + const hasThisExpr = stack.pop(); + + if (enforceDeclarations && !hasThisExpr && node.parent.type === "VariableDeclarator") { + context.report({ node: node.parent, message: "Expected a function declaration." }); + } + }; + } + + return nodesToCheck; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/function-paren-newline.js b/tools/node_modules/eslint/lib/rules/function-paren-newline.js new file mode 100644 index 0000000000..10ad960a4d --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/function-paren-newline.js @@ -0,0 +1,221 @@ +/** + * @fileoverview enforce consistent line breaks inside function parentheses + * @author Teddy Katz + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent line breaks inside function parentheses", + category: "Stylistic Issues", + recommended: false + }, + fixable: "whitespace", + schema: [ + { + oneOf: [ + { + enum: ["always", "never", "consistent", "multiline"] + }, + { + type: "object", + properties: { + minItems: { + type: "integer", + minimum: 0 + } + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + const rawOption = context.options[0] || "multiline"; + const multilineOption = rawOption === "multiline"; + const consistentOption = rawOption === "consistent"; + let minItems; + + if (typeof rawOption === "object") { + minItems = rawOption.minItems; + } else if (rawOption === "always") { + minItems = 0; + } else if (rawOption === "never") { + minItems = Infinity; + } else { + minItems = null; + } + + //---------------------------------------------------------------------- + // Helpers + //---------------------------------------------------------------------- + + /** + * Determines whether there should be newlines inside function parens + * @param {ASTNode[]} elements The arguments or parameters in the list + * @param {boolean} hasLeftNewline `true` if the left paren has a newline in the current code. + * @returns {boolean} `true` if there should be newlines inside the function parens + */ + function shouldHaveNewlines(elements, hasLeftNewline) { + if (multilineOption) { + return elements.some((element, index) => index !== elements.length - 1 && element.loc.end.line !== elements[index + 1].loc.start.line); + } + if (consistentOption) { + return hasLeftNewline; + } + return elements.length >= minItems; + } + + /** + * Validates a list of arguments or parameters + * @param {Object} parens An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token + * @param {ASTNode[]} elements The arguments or parameters in the list + * @returns {void} + */ + function validateParens(parens, elements) { + const leftParen = parens.leftParen; + const rightParen = parens.rightParen; + const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParen); + const tokenBeforeRightParen = sourceCode.getTokenBefore(rightParen); + const hasLeftNewline = !astUtils.isTokenOnSameLine(leftParen, tokenAfterLeftParen); + const hasRightNewline = !astUtils.isTokenOnSameLine(tokenBeforeRightParen, rightParen); + const needsNewlines = shouldHaveNewlines(elements, hasLeftNewline); + + if (hasLeftNewline && !needsNewlines) { + context.report({ + node: leftParen, + message: "Unexpected newline after '('.", + fix(fixer) { + return sourceCode.getText().slice(leftParen.range[1], tokenAfterLeftParen.range[0]).trim() + + // If there is a comment between the ( and the first element, don't do a fix. + ? null + : fixer.removeRange([leftParen.range[1], tokenAfterLeftParen.range[0]]); + } + }); + } else if (!hasLeftNewline && needsNewlines) { + context.report({ + node: leftParen, + message: "Expected a newline after '('.", + fix: fixer => fixer.insertTextAfter(leftParen, "\n") + }); + } + + if (hasRightNewline && !needsNewlines) { + context.report({ + node: rightParen, + message: "Unexpected newline before ')'.", + fix(fixer) { + return sourceCode.getText().slice(tokenBeforeRightParen.range[1], rightParen.range[0]).trim() + + // If there is a comment between the last element and the ), don't do a fix. + ? null + : fixer.removeRange([tokenBeforeRightParen.range[1], rightParen.range[0]]); + } + }); + } else if (!hasRightNewline && needsNewlines) { + context.report({ + node: rightParen, + message: "Expected a newline before ')'.", + fix: fixer => fixer.insertTextBefore(rightParen, "\n") + }); + } + } + + /** + * Gets the left paren and right paren tokens of a node. + * @param {ASTNode} node The node with parens + * @returns {Object} An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token. + * Can also return `null` if an expression has no parens (e.g. a NewExpression with no arguments, or an ArrowFunctionExpression + * with a single parameter) + */ + function getParenTokens(node) { + switch (node.type) { + case "NewExpression": + if (!node.arguments.length && !( + astUtils.isOpeningParenToken(sourceCode.getLastToken(node, { skip: 1 })) && + astUtils.isClosingParenToken(sourceCode.getLastToken(node)) + )) { + + // If the NewExpression does not have parens (e.g. `new Foo`), return null. + return null; + } + + // falls through + + case "CallExpression": + return { + leftParen: sourceCode.getTokenAfter(node.callee, astUtils.isOpeningParenToken), + rightParen: sourceCode.getLastToken(node) + }; + + case "FunctionDeclaration": + case "FunctionExpression": { + const leftParen = sourceCode.getFirstToken(node, astUtils.isOpeningParenToken); + const rightParen = node.params.length + ? sourceCode.getTokenAfter(node.params[node.params.length - 1], astUtils.isClosingParenToken) + : sourceCode.getTokenAfter(leftParen); + + return { leftParen, rightParen }; + } + + case "ArrowFunctionExpression": { + const firstToken = sourceCode.getFirstToken(node); + + if (!astUtils.isOpeningParenToken(firstToken)) { + + // If the ArrowFunctionExpression has a single param without parens, return null. + return null; + } + + return { + leftParen: firstToken, + rightParen: sourceCode.getTokenBefore(node.body, astUtils.isClosingParenToken) + }; + } + + default: + throw new TypeError(`unexpected node with type ${node.type}`); + } + } + + /** + * Validates the parentheses for a node + * @param {ASTNode} node The node with parens + * @returns {void} + */ + function validateNode(node) { + const parens = getParenTokens(node); + + if (parens) { + validateParens(parens, astUtils.isFunction(node) ? node.params : node.arguments); + } + } + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + return { + ArrowFunctionExpression: validateNode, + CallExpression: validateNode, + FunctionDeclaration: validateNode, + FunctionExpression: validateNode, + NewExpression: validateNode + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/generator-star-spacing.js b/tools/node_modules/eslint/lib/rules/generator-star-spacing.js new file mode 100644 index 0000000000..a718b59afc --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/generator-star-spacing.js @@ -0,0 +1,199 @@ +/** + * @fileoverview Rule to check the spacing around the * in generator functions. + * @author Jamund Ferguson + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +const OVERRIDE_SCHEMA = { + oneOf: [ + { + enum: ["before", "after", "both", "neither"] + }, + { + type: "object", + properties: { + before: { type: "boolean" }, + after: { type: "boolean" } + }, + additionalProperties: false + } + ] +}; + +module.exports = { + meta: { + docs: { + description: "enforce consistent spacing around `*` operators in generator functions", + category: "ECMAScript 6", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + oneOf: [ + { + enum: ["before", "after", "both", "neither"] + }, + { + type: "object", + properties: { + before: { type: "boolean" }, + after: { type: "boolean" }, + named: OVERRIDE_SCHEMA, + anonymous: OVERRIDE_SCHEMA, + method: OVERRIDE_SCHEMA + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + + const optionDefinitions = { + before: { before: true, after: false }, + after: { before: false, after: true }, + both: { before: true, after: true }, + neither: { before: false, after: false } + }; + + /** + * Returns resolved option definitions based on an option and defaults + * + * @param {any} option - The option object or string value + * @param {Object} defaults - The defaults to use if options are not present + * @returns {Object} the resolved object definition + */ + function optionToDefinition(option, defaults) { + if (!option) { + return defaults; + } + + return typeof option === "string" + ? optionDefinitions[option] + : Object.assign({}, defaults, option); + } + + const modes = (function(option) { + option = option || {}; + const defaults = optionToDefinition(option, optionDefinitions.before); + + return { + named: optionToDefinition(option.named, defaults), + anonymous: optionToDefinition(option.anonymous, defaults), + method: optionToDefinition(option.method, defaults) + }; + }(context.options[0])); + + const sourceCode = context.getSourceCode(); + + /** + * Checks if the given token is a star token or not. + * + * @param {Token} token - The token to check. + * @returns {boolean} `true` if the token is a star token. + */ + function isStarToken(token) { + return token.value === "*" && token.type === "Punctuator"; + } + + /** + * Gets the generator star token of the given function node. + * + * @param {ASTNode} node - The function node to get. + * @returns {Token} Found star token. + */ + function getStarToken(node) { + return sourceCode.getFirstToken( + (node.parent.method || node.parent.type === "MethodDefinition") ? node.parent : node, + isStarToken + ); + } + + /** + * Checks the spacing between two tokens before or after the star token. + * + * @param {string} kind Either "named", "anonymous", or "method" + * @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(kind, side, leftToken, rightToken) { + if (!!(rightToken.range[0] - leftToken.range[1]) !== modes[kind][side]) { + const after = leftToken.value === "*"; + const spaceRequired = modes[kind][side]; + const node = after ? leftToken : rightToken; + const type = spaceRequired ? "Missing" : "Unexpected"; + const message = "{{type}} space {{side}} *."; + const data = { + type, + side + }; + + context.report({ + node, + message, + data, + fix(fixer) { + if (spaceRequired) { + if (after) { + return fixer.insertTextAfter(node, " "); + } + return fixer.insertTextBefore(node, " "); + } + return fixer.removeRange([leftToken.range[1], rightToken.range[0]]); + } + }); + } + } + + /** + * Enforces the spacing around the star if node is a generator function. + * + * @param {ASTNode} node A function expression or declaration node. + * @returns {void} + */ + function checkFunction(node) { + if (!node.generator) { + return; + } + + const starToken = getStarToken(node); + const prevToken = sourceCode.getTokenBefore(starToken); + const nextToken = sourceCode.getTokenAfter(starToken); + + let kind = "named"; + + if (node.parent.type === "MethodDefinition" || (node.parent.type === "Property" && node.parent.method)) { + kind = "method"; + } else if (!node.id) { + kind = "anonymous"; + } + + // Only check before when preceded by `function`|`static` keyword + if (!(kind === "method" && starToken === sourceCode.getFirstToken(node.parent))) { + checkSpacing(kind, "before", prevToken, starToken); + } + + checkSpacing(kind, "after", starToken, nextToken); + } + + return { + FunctionDeclaration: checkFunction, + FunctionExpression: checkFunction + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/getter-return.js b/tools/node_modules/eslint/lib/rules/getter-return.js new file mode 100644 index 0000000000..6eb1efc00c --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/getter-return.js @@ -0,0 +1,177 @@ +/** + * @fileoverview Enforces that a return statement is present in property getters. + * @author Aladdin-ADD(hh_2013@foxmail.com) + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ +const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/; + +/** + * Checks a given code path segment is reachable. + * + * @param {CodePathSegment} segment - A segment to check. + * @returns {boolean} `true` if the segment is reachable. + */ +function isReachable(segment) { + return segment.reachable; +} + +/** + * Gets a readable location. + * + * - FunctionExpression -> the function name or `function` keyword. + * + * @param {ASTNode} node - A function node to get. + * @returns {ASTNode|Token} The node or the token of a location. + */ +function getId(node) { + return node.id || node; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce `return` statements in getters", + category: "Possible Errors", + recommended: false + }, + fixable: null, + schema: [ + { + type: "object", + properties: { + allowImplicit: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + const options = context.options[0] || { allowImplicit: false }; + + let funcInfo = { + upper: null, + codePath: null, + hasReturn: false, + shouldCheck: false, + node: null + }; + + /** + * Checks whether or not the last code path segment is reachable. + * Then reports this function if the segment is reachable. + * + * If the last code path segment is reachable, there are paths which are not + * returned or thrown. + * + * @param {ASTNode} node - A node to check. + * @returns {void} + */ + function checkLastSegment(node) { + if (funcInfo.shouldCheck && + funcInfo.codePath.currentSegments.some(isReachable) + ) { + context.report({ + node, + loc: getId(node).loc.start, + message: funcInfo.hasReturn + ? "Expected {{name}} to always return a value." + : "Expected to return a value in {{name}}.", + data: { + name: astUtils.getFunctionNameWithKind(funcInfo.node) + } + }); + } + } + + /** + * Checks whether a node means a getter function. + * @param {ASTNode} node - a node to check. + * @returns {boolean} if node means a getter, return true; else return false. + */ + function isGetter(node) { + const parent = node.parent; + + if (TARGET_NODE_TYPE.test(node.type) && node.body.type === "BlockStatement") { + if (parent.kind === "get") { + return true; + } + if (parent.type === "Property" && astUtils.getStaticPropertyName(parent) === "get" && parent.parent.type === "ObjectExpression") { + + // Object.defineProperty() + if (parent.parent.parent.type === "CallExpression" && + astUtils.getStaticPropertyName(parent.parent.parent.callee) === "defineProperty") { + return true; + } + + // Object.defineProperties() + if (parent.parent.parent.type === "Property" && + parent.parent.parent.parent.type === "ObjectExpression" && + parent.parent.parent.parent.parent.type === "CallExpression" && + astUtils.getStaticPropertyName(parent.parent.parent.parent.parent.callee) === "defineProperties") { + return true; + } + } + } + return false; + } + return { + + // Stacks this function's information. + onCodePathStart(codePath, node) { + funcInfo = { + upper: funcInfo, + codePath, + hasReturn: false, + shouldCheck: isGetter(node), + node + }; + }, + + // Pops this function's information. + onCodePathEnd() { + funcInfo = funcInfo.upper; + }, + + // Checks the return statement is valid. + ReturnStatement(node) { + if (funcInfo.shouldCheck) { + funcInfo.hasReturn = true; + + // if allowImplicit: false, should also check node.argument + if (!options.allowImplicit && !node.argument) { + context.report({ + node, + message: "Expected to return a value in {{name}}.", + data: { + name: astUtils.getFunctionNameWithKind(funcInfo.node) + } + }); + } + } + }, + + // Reports a given function if the last path is reachable. + "FunctionExpression:exit": checkLastSegment, + "ArrowFunctionExpression:exit": checkLastSegment + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/global-require.js b/tools/node_modules/eslint/lib/rules/global-require.js new file mode 100644 index 0000000000..beda8d99f1 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/global-require.js @@ -0,0 +1,75 @@ +/** + * @fileoverview Rule for disallowing require() outside of the top-level module context + * @author Jamund Ferguson + */ + +"use strict"; + +const ACCEPTABLE_PARENTS = [ + "AssignmentExpression", + "VariableDeclarator", + "MemberExpression", + "ExpressionStatement", + "CallExpression", + "ConditionalExpression", + "Program", + "VariableDeclaration" +]; + +/** + * Finds the eslint-scope reference in the given scope. + * @param {Object} scope The scope to search. + * @param {ASTNode} node The identifier node. + * @returns {Reference|null} Returns the found reference or null if none were found. + */ +function findReference(scope, node) { + const references = scope.references.filter(reference => reference.identifier.range[0] === node.range[0] && + reference.identifier.range[1] === node.range[1]); + + /* istanbul ignore else: correctly returns null */ + if (references.length === 1) { + return references[0]; + } + return null; + +} + +/** + * Checks if the given identifier node is shadowed in the given scope. + * @param {Object} scope The current scope. + * @param {ASTNode} node The identifier node to check. + * @returns {boolean} Whether or not the name is shadowed. + */ +function isShadowed(scope, node) { + const reference = findReference(scope, node); + + return reference && reference.resolved && reference.resolved.defs.length > 0; +} + +module.exports = { + meta: { + docs: { + description: "require `require()` calls to be placed at top-level module scope", + category: "Node.js and CommonJS", + recommended: false + }, + + schema: [] + }, + + create(context) { + return { + CallExpression(node) { + const currentScope = context.getScope(); + + if (node.callee.name === "require" && !isShadowed(currentScope, node.callee)) { + const isGoodRequire = context.getAncestors().every(parent => ACCEPTABLE_PARENTS.indexOf(parent.type) > -1); + + if (!isGoodRequire) { + context.report({ node, message: "Unexpected require()." }); + } + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/guard-for-in.js b/tools/node_modules/eslint/lib/rules/guard-for-in.js new file mode 100644 index 0000000000..754830f6a6 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/guard-for-in.js @@ -0,0 +1,42 @@ +/** + * @fileoverview Rule to flag for-in loops without if statements inside + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require `for-in` loops to include an `if` statement", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + + return { + + ForInStatement(node) { + + /* + * If the for-in statement has {}, then the real body is the body + * of the BlockStatement. Otherwise, just use body as provided. + */ + const body = node.body.type === "BlockStatement" ? node.body.body[0] : node.body; + + if (body && body.type !== "IfStatement") { + context.report({ node, message: "The body of a for-in should be wrapped in an if statement to filter unwanted properties from the prototype." }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/handle-callback-err.js b/tools/node_modules/eslint/lib/rules/handle-callback-err.js new file mode 100644 index 0000000000..de36a0c1b0 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/handle-callback-err.js @@ -0,0 +1,89 @@ +/** + * @fileoverview Ensure handling of errors when we know they exist. + * @author Jamund Ferguson + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require error handling in callbacks", + category: "Node.js and CommonJS", + recommended: false + }, + + schema: [ + { + type: "string" + } + ] + }, + + create(context) { + + const errorArgument = context.options[0] || "err"; + + /** + * Checks if the given argument should be interpreted as a regexp pattern. + * @param {string} stringToCheck The string which should be checked. + * @returns {boolean} Whether or not the string should be interpreted as a pattern. + */ + function isPattern(stringToCheck) { + const firstChar = stringToCheck[0]; + + return firstChar === "^"; + } + + /** + * Checks if the given name matches the configured error argument. + * @param {string} name The name which should be compared. + * @returns {boolean} Whether or not the given name matches the configured error variable name. + */ + function matchesConfiguredErrorName(name) { + if (isPattern(errorArgument)) { + const regexp = new RegExp(errorArgument); + + return regexp.test(name); + } + return name === errorArgument; + } + + /** + * Get the parameters of a given function scope. + * @param {Object} scope The function scope. + * @returns {array} All parameters of the given scope. + */ + function getParameters(scope) { + return scope.variables.filter(variable => variable.defs[0] && variable.defs[0].type === "Parameter"); + } + + /** + * Check to see if we're handling the error object properly. + * @param {ASTNode} node The AST node to check. + * @returns {void} + */ + function checkForError(node) { + const scope = context.getScope(), + parameters = getParameters(scope), + firstParameter = parameters[0]; + + if (firstParameter && matchesConfiguredErrorName(firstParameter.name)) { + if (firstParameter.references.length === 0) { + context.report({ node, message: "Expected error to be handled." }); + } + } + } + + return { + FunctionDeclaration: checkForError, + FunctionExpression: checkForError, + ArrowFunctionExpression: checkForError + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/id-blacklist.js b/tools/node_modules/eslint/lib/rules/id-blacklist.js new file mode 100644 index 0000000000..ee28c0b5a8 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/id-blacklist.js @@ -0,0 +1,121 @@ +/** + * @fileoverview Rule that warns when identifier names that are + * blacklisted in the configuration are used. + * @author Keith Cirkel (http://keithcirkel.co.uk) + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow specified identifiers", + category: "Stylistic Issues", + recommended: false + }, + + schema: { + type: "array", + items: { + type: "string" + }, + uniqueItems: true + } + }, + + create(context) { + + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + const blacklist = context.options; + + + /** + * Checks if a string matches the provided pattern + * @param {string} name The string to check. + * @returns {boolean} if the string is a match + * @private + */ + function isInvalid(name) { + return blacklist.indexOf(name) !== -1; + } + + /** + * Verifies if we should report an error or not based on the effective + * parent node and the identifier name. + * @param {ASTNode} effectiveParent The effective parent node of the node to be reported + * @param {string} name The identifier name of the identifier node + * @returns {boolean} whether an error should be reported or not + */ + function shouldReport(effectiveParent, name) { + return effectiveParent.type !== "CallExpression" && + effectiveParent.type !== "NewExpression" && + isInvalid(name); + } + + /** + * Reports an AST node as a rule violation. + * @param {ASTNode} node The node to report. + * @returns {void} + * @private + */ + function report(node) { + context.report({ + node, + message: "Identifier '{{name}}' is blacklisted.", + data: { + name: node.name + } + }); + } + + return { + + Identifier(node) { + const name = node.name, + effectiveParent = (node.parent.type === "MemberExpression") ? node.parent.parent : node.parent; + + // MemberExpressions get special rules + if (node.parent.type === "MemberExpression") { + + // Always check object names + if (node.parent.object.type === "Identifier" && + node.parent.object.name === node.name) { + if (isInvalid(name)) { + report(node); + } + + // Report AssignmentExpressions only if they are the left side of the assignment + } else if (effectiveParent.type === "AssignmentExpression" && + (effectiveParent.right.type !== "MemberExpression" || + effectiveParent.left.type === "MemberExpression" && + effectiveParent.left.property.name === node.name)) { + if (isInvalid(name)) { + report(node); + } + } + + // Properties have their own rules + } else if (node.parent.type === "Property") { + + if (shouldReport(effectiveParent, name)) { + report(node); + } + + // Report anything that is a match and not a CallExpression + } else if (shouldReport(effectiveParent, name)) { + report(node); + } + } + + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/id-length.js b/tools/node_modules/eslint/lib/rules/id-length.js new file mode 100644 index 0000000000..dad9c40649 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/id-length.js @@ -0,0 +1,116 @@ +/** + * @fileoverview Rule that warns when identifier names are shorter or longer + * than the values provided in configuration. + * @author Burak Yigit Kaya aka BYK + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce minimum and maximum identifier lengths", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + min: { + type: "number" + }, + max: { + type: "number" + }, + exceptions: { + type: "array", + uniqueItems: true, + items: { + type: "string" + } + }, + properties: { + enum: ["always", "never"] + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const options = context.options[0] || {}; + const minLength = typeof options.min !== "undefined" ? options.min : 2; + const maxLength = typeof options.max !== "undefined" ? options.max : Infinity; + const properties = options.properties !== "never"; + const exceptions = (options.exceptions ? options.exceptions : []) + .reduce((obj, item) => { + obj[item] = true; + + return obj; + }, {}); + + const SUPPORTED_EXPRESSIONS = { + MemberExpression: properties && function(parent) { + return !parent.computed && ( + + // regular property assignment + (parent.parent.left === parent && parent.parent.type === "AssignmentExpression" || + + // or the last identifier in an ObjectPattern destructuring + parent.parent.type === "Property" && parent.parent.value === parent && + parent.parent.parent.type === "ObjectPattern" && parent.parent.parent.parent.left === parent.parent.parent) + ); + }, + AssignmentPattern(parent, node) { + return parent.left === node; + }, + VariableDeclarator(parent, node) { + return parent.id === node; + }, + Property: properties && function(parent, node) { + return parent.key === node; + }, + ImportDefaultSpecifier: true, + RestElement: true, + FunctionExpression: true, + ArrowFunctionExpression: true, + ClassDeclaration: true, + FunctionDeclaration: true, + MethodDefinition: true, + CatchClause: true + }; + + return { + Identifier(node) { + const name = node.name; + const parent = node.parent; + + const isShort = name.length < minLength; + const isLong = name.length > maxLength; + + if (!(isShort || isLong) || exceptions[name]) { + return; // Nothing to report + } + + const isValidExpression = SUPPORTED_EXPRESSIONS[parent.type]; + + if (isValidExpression && (isValidExpression === true || isValidExpression(parent, node))) { + context.report({ + node, + message: isShort + ? "Identifier name '{{name}}' is too short (< {{min}})." + : "Identifier name '{{name}}' is too long (> {{max}}).", + data: { name, min: minLength, max: maxLength } + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/id-match.js b/tools/node_modules/eslint/lib/rules/id-match.js new file mode 100644 index 0000000000..0420fdc74e --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/id-match.js @@ -0,0 +1,144 @@ +/** + * @fileoverview Rule to flag non-matching identifiers + * @author Matthieu Larcher + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require identifiers to match a specified regular expression", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + type: "string" + }, + { + type: "object", + properties: { + properties: { + type: "boolean" + } + } + } + ] + }, + + create(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + const pattern = context.options[0] || "^.+$", + regexp = new RegExp(pattern); + + const options = context.options[1] || {}, + properties = !!options.properties, + onlyDeclarations = !!options.onlyDeclarations; + + /** + * Checks if a string matches the provided pattern + * @param {string} name The string to check. + * @returns {boolean} if the string is a match + * @private + */ + function isInvalid(name) { + return !regexp.test(name); + } + + /** + * Verifies if we should report an error or not based on the effective + * parent node and the identifier name. + * @param {ASTNode} effectiveParent The effective parent node of the node to be reported + * @param {string} name The identifier name of the identifier node + * @returns {boolean} whether an error should be reported or not + */ + function shouldReport(effectiveParent, name) { + return effectiveParent.type !== "CallExpression" && + effectiveParent.type !== "NewExpression" && + isInvalid(name); + } + + /** + * Reports an AST node as a rule violation. + * @param {ASTNode} node The node to report. + * @returns {void} + * @private + */ + function report(node) { + context.report({ + node, + message: "Identifier '{{name}}' does not match the pattern '{{pattern}}'.", + data: { + name: node.name, + pattern + } + }); + } + + return { + + Identifier(node) { + const name = node.name, + parent = node.parent, + effectiveParent = (parent.type === "MemberExpression") ? parent.parent : parent; + + if (parent.type === "MemberExpression") { + + if (!properties) { + return; + } + + // Always check object names + if (parent.object.type === "Identifier" && + parent.object.name === name) { + if (isInvalid(name)) { + report(node); + } + + // Report AssignmentExpressions only if they are the left side of the assignment + } else if (effectiveParent.type === "AssignmentExpression" && + (effectiveParent.right.type !== "MemberExpression" || + effectiveParent.left.type === "MemberExpression" && + effectiveParent.left.property.name === name)) { + if (isInvalid(name)) { + report(node); + } + } + + } else if (parent.type === "Property") { + + if (!properties || parent.key.name !== name) { + return; + } + + if (shouldReport(effectiveParent, name)) { + report(node); + } + + } else { + const isDeclaration = effectiveParent.type === "FunctionDeclaration" || effectiveParent.type === "VariableDeclarator"; + + if (onlyDeclarations && !isDeclaration) { + return; + } + + if (shouldReport(effectiveParent, name)) { + report(node); + } + } + } + + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/implicit-arrow-linebreak.js b/tools/node_modules/eslint/lib/rules/implicit-arrow-linebreak.js new file mode 100644 index 0000000000..b8802f4de5 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/implicit-arrow-linebreak.js @@ -0,0 +1,86 @@ +/** + * @fileoverview enforce the location of arrow function bodies + * @author Sharmila Jesupaul + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ +module.exports = { + meta: { + docs: { + description: "enforce the location of arrow function bodies", + category: "Stylistic Issues", + recommended: false + }, + fixable: "whitespace", + schema: [ + { + enum: ["beside", "below"] + } + ] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + //---------------------------------------------------------------------- + // Helpers + //---------------------------------------------------------------------- + /** + * Gets the applicable preference for a particular keyword + * @returns {string} The applicable option for the keyword, e.g. 'beside' + */ + function getOption() { + return context.options[0] || "beside"; + } + + /** + * Validates the location of an arrow function body + * @param {ASTNode} node The arrow function body + * @param {string} keywordName The applicable keyword name for the arrow function body + * @returns {void} + */ + function validateExpression(node) { + const option = getOption(); + + let tokenBefore = sourceCode.getTokenBefore(node.body); + const hasParens = tokenBefore.value === "("; + + if (node.type === "BlockStatement") { + return; + } + + let fixerTarget = node.body; + + if (hasParens) { + + // Gets the first token before the function body that is not an open paren + tokenBefore = sourceCode.getTokenBefore(node.body, token => token.value !== "("); + fixerTarget = sourceCode.getTokenAfter(tokenBefore); + } + + if (tokenBefore.loc.end.line === fixerTarget.loc.start.line && option === "below") { + context.report({ + node: fixerTarget, + message: "Expected a linebreak before this expression.", + fix: fixer => fixer.insertTextBefore(fixerTarget, "\n") + }); + } else if (tokenBefore.loc.end.line !== fixerTarget.loc.start.line && option === "beside") { + context.report({ + node: fixerTarget, + message: "Expected no linebreak before this expression.", + fix: fixer => fixer.replaceTextRange([tokenBefore.range[1], fixerTarget.range[0]], " ") + }); + } + } + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + return { + ArrowFunctionExpression: node => validateExpression(node) + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/indent-legacy.js b/tools/node_modules/eslint/lib/rules/indent-legacy.js new file mode 100644 index 0000000000..cf91406806 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/indent-legacy.js @@ -0,0 +1,1137 @@ +/** + * @fileoverview This option sets a specific tab width for your code + * + * This rule has been ported and modified from nodeca. + * @author Vitaly Puzrin + * @author Gyandeep Singh + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +/* istanbul ignore next: this rule has known coverage issues, but it's deprecated and shouldn't be updated in the future anyway. */ +module.exports = { + meta: { + docs: { + description: "enforce consistent indentation", + category: "Stylistic Issues", + recommended: false, + replacedBy: ["indent"] + }, + + deprecated: true, + + fixable: "whitespace", + + schema: [ + { + oneOf: [ + { + enum: ["tab"] + }, + { + type: "integer", + minimum: 0 + } + ] + }, + { + type: "object", + properties: { + SwitchCase: { + type: "integer", + minimum: 0 + }, + VariableDeclarator: { + oneOf: [ + { + type: "integer", + minimum: 0 + }, + { + type: "object", + properties: { + var: { + type: "integer", + minimum: 0 + }, + let: { + type: "integer", + minimum: 0 + }, + const: { + type: "integer", + minimum: 0 + } + } + } + ] + }, + outerIIFEBody: { + type: "integer", + minimum: 0 + }, + MemberExpression: { + type: "integer", + minimum: 0 + }, + FunctionDeclaration: { + type: "object", + properties: { + parameters: { + oneOf: [ + { + type: "integer", + minimum: 0 + }, + { + enum: ["first"] + } + ] + }, + body: { + type: "integer", + minimum: 0 + } + } + }, + FunctionExpression: { + type: "object", + properties: { + parameters: { + oneOf: [ + { + type: "integer", + minimum: 0 + }, + { + enum: ["first"] + } + ] + }, + body: { + type: "integer", + minimum: 0 + } + } + }, + CallExpression: { + type: "object", + properties: { + parameters: { + oneOf: [ + { + type: "integer", + minimum: 0 + }, + { + enum: ["first"] + } + ] + } + } + }, + ArrayExpression: { + oneOf: [ + { + type: "integer", + minimum: 0 + }, + { + enum: ["first"] + } + ] + }, + ObjectExpression: { + oneOf: [ + { + type: "integer", + minimum: 0 + }, + { + enum: ["first"] + } + ] + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const DEFAULT_VARIABLE_INDENT = 1; + const DEFAULT_PARAMETER_INDENT = null; // For backwards compatibility, don't check parameter indentation unless specified in the config + const DEFAULT_FUNCTION_BODY_INDENT = 1; + + let indentType = "space"; + let indentSize = 4; + const options = { + SwitchCase: 0, + VariableDeclarator: { + var: DEFAULT_VARIABLE_INDENT, + let: DEFAULT_VARIABLE_INDENT, + const: DEFAULT_VARIABLE_INDENT + }, + outerIIFEBody: null, + FunctionDeclaration: { + parameters: DEFAULT_PARAMETER_INDENT, + body: DEFAULT_FUNCTION_BODY_INDENT + }, + FunctionExpression: { + parameters: DEFAULT_PARAMETER_INDENT, + body: DEFAULT_FUNCTION_BODY_INDENT + }, + CallExpression: { + arguments: DEFAULT_PARAMETER_INDENT + }, + ArrayExpression: 1, + ObjectExpression: 1 + }; + + const sourceCode = context.getSourceCode(); + + if (context.options.length) { + if (context.options[0] === "tab") { + indentSize = 1; + indentType = "tab"; + } else /* istanbul ignore else : this will be caught by options validation */ if (typeof context.options[0] === "number") { + indentSize = context.options[0]; + indentType = "space"; + } + + if (context.options[1]) { + const opts = context.options[1]; + + options.SwitchCase = opts.SwitchCase || 0; + const variableDeclaratorRules = opts.VariableDeclarator; + + if (typeof variableDeclaratorRules === "number") { + options.VariableDeclarator = { + var: variableDeclaratorRules, + let: variableDeclaratorRules, + const: variableDeclaratorRules + }; + } else if (typeof variableDeclaratorRules === "object") { + Object.assign(options.VariableDeclarator, variableDeclaratorRules); + } + + if (typeof opts.outerIIFEBody === "number") { + options.outerIIFEBody = opts.outerIIFEBody; + } + + if (typeof opts.MemberExpression === "number") { + options.MemberExpression = opts.MemberExpression; + } + + if (typeof opts.FunctionDeclaration === "object") { + Object.assign(options.FunctionDeclaration, opts.FunctionDeclaration); + } + + if (typeof opts.FunctionExpression === "object") { + Object.assign(options.FunctionExpression, opts.FunctionExpression); + } + + if (typeof opts.CallExpression === "object") { + Object.assign(options.CallExpression, opts.CallExpression); + } + + if (typeof opts.ArrayExpression === "number" || typeof opts.ArrayExpression === "string") { + options.ArrayExpression = opts.ArrayExpression; + } + + if (typeof opts.ObjectExpression === "number" || typeof opts.ObjectExpression === "string") { + options.ObjectExpression = opts.ObjectExpression; + } + } + } + + const caseIndentStore = {}; + + /** + * Creates an error message for a line, given the expected/actual indentation. + * @param {int} expectedAmount The expected amount of indentation characters for this line + * @param {int} actualSpaces The actual number of indentation spaces that were found on this line + * @param {int} actualTabs The actual number of indentation tabs that were found on this line + * @returns {string} An error message for this line + */ + function createErrorMessage(expectedAmount, actualSpaces, actualTabs) { + const expectedStatement = `${expectedAmount} ${indentType}${expectedAmount === 1 ? "" : "s"}`; // e.g. "2 tabs" + const foundSpacesWord = `space${actualSpaces === 1 ? "" : "s"}`; // e.g. "space" + const foundTabsWord = `tab${actualTabs === 1 ? "" : "s"}`; // e.g. "tabs" + let foundStatement; + + if (actualSpaces > 0 && actualTabs > 0) { + foundStatement = `${actualSpaces} ${foundSpacesWord} and ${actualTabs} ${foundTabsWord}`; // e.g. "1 space and 2 tabs" + } else if (actualSpaces > 0) { + + /* + * Abbreviate the message if the expected indentation is also spaces. + * e.g. 'Expected 4 spaces but found 2' rather than 'Expected 4 spaces but found 2 spaces' + */ + foundStatement = indentType === "space" ? actualSpaces : `${actualSpaces} ${foundSpacesWord}`; + } else if (actualTabs > 0) { + foundStatement = indentType === "tab" ? actualTabs : `${actualTabs} ${foundTabsWord}`; + } else { + foundStatement = "0"; + } + + return `Expected indentation of ${expectedStatement} but found ${foundStatement}.`; + } + + /** + * Reports a given indent violation + * @param {ASTNode} node Node violating the indent rule + * @param {int} needed Expected indentation character count + * @param {int} gottenSpaces Indentation space count in the actual node/code + * @param {int} gottenTabs Indentation tab count in the actual node/code + * @param {Object=} loc Error line and column location + * @param {boolean} isLastNodeCheck Is the error for last node check + * @param {int} lastNodeCheckEndOffset Number of charecters to skip from the end + * @returns {void} + */ + function report(node, needed, gottenSpaces, gottenTabs, loc, isLastNodeCheck) { + if (gottenSpaces && gottenTabs) { + + // To avoid conflicts with `no-mixed-spaces-and-tabs`, don't report lines that have both spaces and tabs. + return; + } + + const desiredIndent = (indentType === "space" ? " " : "\t").repeat(needed); + + const textRange = isLastNodeCheck + ? [node.range[1] - node.loc.end.column, node.range[1] - node.loc.end.column + gottenSpaces + gottenTabs] + : [node.range[0] - node.loc.start.column, node.range[0] - node.loc.start.column + gottenSpaces + gottenTabs]; + + context.report({ + node, + loc, + message: createErrorMessage(needed, gottenSpaces, gottenTabs), + fix: fixer => fixer.replaceTextRange(textRange, desiredIndent) + }); + } + + /** + * Get the actual indent of node + * @param {ASTNode|Token} node Node to examine + * @param {boolean} [byLastLine=false] get indent of node's last line + * @returns {Object} The node's indent. Contains keys `space` and `tab`, representing the indent of each character. Also + * contains keys `goodChar` and `badChar`, where `goodChar` is the amount of the user's desired indentation character, and + * `badChar` is the amount of the other indentation character. + */ + function getNodeIndent(node, byLastLine) { + const token = byLastLine ? sourceCode.getLastToken(node) : sourceCode.getFirstToken(node); + const srcCharsBeforeNode = sourceCode.getText(token, token.loc.start.column).split(""); + const indentChars = srcCharsBeforeNode.slice(0, srcCharsBeforeNode.findIndex(char => char !== " " && char !== "\t")); + const spaces = indentChars.filter(char => char === " ").length; + const tabs = indentChars.filter(char => char === "\t").length; + + return { + space: spaces, + tab: tabs, + goodChar: indentType === "space" ? spaces : tabs, + badChar: indentType === "space" ? tabs : spaces + }; + } + + /** + * Checks node is the first in its own start line. By default it looks by start line. + * @param {ASTNode} node The node to check + * @param {boolean} [byEndLocation=false] Lookup based on start position or end + * @returns {boolean} true if its the first in the its start line + */ + function isNodeFirstInLine(node, byEndLocation) { + const firstToken = byEndLocation === true ? sourceCode.getLastToken(node, 1) : sourceCode.getTokenBefore(node), + startLine = byEndLocation === true ? node.loc.end.line : node.loc.start.line, + endLine = firstToken ? firstToken.loc.end.line : -1; + + return startLine !== endLine; + } + + /** + * Check indent for node + * @param {ASTNode} node Node to check + * @param {int} neededIndent needed indent + * @param {boolean} [excludeCommas=false] skip comma on start of line + * @returns {void} + */ + function checkNodeIndent(node, neededIndent) { + const actualIndent = getNodeIndent(node, false); + + if ( + node.type !== "ArrayExpression" && + node.type !== "ObjectExpression" && + (actualIndent.goodChar !== neededIndent || actualIndent.badChar !== 0) && + isNodeFirstInLine(node) + ) { + report(node, neededIndent, actualIndent.space, actualIndent.tab); + } + + if (node.type === "IfStatement" && node.alternate) { + const elseToken = sourceCode.getTokenBefore(node.alternate); + + checkNodeIndent(elseToken, neededIndent); + + if (!isNodeFirstInLine(node.alternate)) { + checkNodeIndent(node.alternate, neededIndent); + } + } + + if (node.type === "TryStatement" && node.handler) { + const catchToken = sourceCode.getFirstToken(node.handler); + + checkNodeIndent(catchToken, neededIndent); + } + + if (node.type === "TryStatement" && node.finalizer) { + const finallyToken = sourceCode.getTokenBefore(node.finalizer); + + checkNodeIndent(finallyToken, neededIndent); + } + + if (node.type === "DoWhileStatement") { + const whileToken = sourceCode.getTokenAfter(node.body); + + checkNodeIndent(whileToken, neededIndent); + } + } + + /** + * Check indent for nodes list + * @param {ASTNode[]} nodes list of node objects + * @param {int} indent needed indent + * @param {boolean} [excludeCommas=false] skip comma on start of line + * @returns {void} + */ + function checkNodesIndent(nodes, indent) { + nodes.forEach(node => checkNodeIndent(node, indent)); + } + + /** + * Check last node line indent this detects, that block closed correctly + * @param {ASTNode} node Node to examine + * @param {int} lastLineIndent needed indent + * @returns {void} + */ + function checkLastNodeLineIndent(node, lastLineIndent) { + const lastToken = sourceCode.getLastToken(node); + const endIndent = getNodeIndent(lastToken, true); + + if ((endIndent.goodChar !== lastLineIndent || endIndent.badChar !== 0) && isNodeFirstInLine(node, true)) { + report( + node, + lastLineIndent, + endIndent.space, + endIndent.tab, + { line: lastToken.loc.start.line, column: lastToken.loc.start.column }, + true + ); + } + } + + /** + * Check last node line indent this detects, that block closed correctly + * This function for more complicated return statement case, where closing parenthesis may be followed by ';' + * @param {ASTNode} node Node to examine + * @param {int} firstLineIndent first line needed indent + * @returns {void} + */ + function checkLastReturnStatementLineIndent(node, firstLineIndent) { + + /* + * in case if return statement ends with ');' we have traverse back to ')' + * otherwise we'll measure indent for ';' and replace ')' + */ + const lastToken = sourceCode.getLastToken(node, astUtils.isClosingParenToken); + const textBeforeClosingParenthesis = sourceCode.getText(lastToken, lastToken.loc.start.column).slice(0, -1); + + if (textBeforeClosingParenthesis.trim()) { + + // There are tokens before the closing paren, don't report this case + return; + } + + const endIndent = getNodeIndent(lastToken, true); + + if (endIndent.goodChar !== firstLineIndent) { + report( + node, + firstLineIndent, + endIndent.space, + endIndent.tab, + { line: lastToken.loc.start.line, column: lastToken.loc.start.column }, + true + ); + } + } + + /** + * Check first node line indent is correct + * @param {ASTNode} node Node to examine + * @param {int} firstLineIndent needed indent + * @returns {void} + */ + function checkFirstNodeLineIndent(node, firstLineIndent) { + const startIndent = getNodeIndent(node, false); + + if ((startIndent.goodChar !== firstLineIndent || startIndent.badChar !== 0) && isNodeFirstInLine(node)) { + report( + node, + firstLineIndent, + startIndent.space, + startIndent.tab, + { line: node.loc.start.line, column: node.loc.start.column } + ); + } + } + + /** + * Returns a parent node of given node based on a specified type + * if not present then return null + * @param {ASTNode} node node to examine + * @param {string} type type that is being looked for + * @param {string} stopAtList end points for the evaluating code + * @returns {ASTNode|void} if found then node otherwise null + */ + function getParentNodeByType(node, type, stopAtList) { + let parent = node.parent; + + if (!stopAtList) { + stopAtList = ["Program"]; + } + + while (parent.type !== type && stopAtList.indexOf(parent.type) === -1 && parent.type !== "Program") { + parent = parent.parent; + } + + return parent.type === type ? parent : null; + } + + /** + * Returns the VariableDeclarator based on the current node + * if not present then return null + * @param {ASTNode} node node to examine + * @returns {ASTNode|void} if found then node otherwise null + */ + function getVariableDeclaratorNode(node) { + return getParentNodeByType(node, "VariableDeclarator"); + } + + /** + * Check to see if the node is part of the multi-line variable declaration. + * Also if its on the same line as the varNode + * @param {ASTNode} node node to check + * @param {ASTNode} varNode variable declaration node to check against + * @returns {boolean} True if all the above condition satisfy + */ + function isNodeInVarOnTop(node, varNode) { + return varNode && + varNode.parent.loc.start.line === node.loc.start.line && + varNode.parent.declarations.length > 1; + } + + /** + * Check to see if the argument before the callee node is multi-line and + * there should only be 1 argument before the callee node + * @param {ASTNode} node node to check + * @returns {boolean} True if arguments are multi-line + */ + function isArgBeforeCalleeNodeMultiline(node) { + const parent = node.parent; + + if (parent.arguments.length >= 2 && parent.arguments[1] === node) { + return parent.arguments[0].loc.end.line > parent.arguments[0].loc.start.line; + } + + return false; + } + + /** + * Check to see if the node is a file level IIFE + * @param {ASTNode} node The function node to check. + * @returns {boolean} True if the node is the outer IIFE + */ + function isOuterIIFE(node) { + const parent = node.parent; + let stmt = parent.parent; + + /* + * Verify that the node is an IIEF + */ + if ( + parent.type !== "CallExpression" || + parent.callee !== node) { + + return false; + } + + /* + * Navigate legal ancestors to determine whether this IIEF is outer + */ + while ( + stmt.type === "UnaryExpression" && ( + stmt.operator === "!" || + stmt.operator === "~" || + stmt.operator === "+" || + stmt.operator === "-") || + stmt.type === "AssignmentExpression" || + stmt.type === "LogicalExpression" || + stmt.type === "SequenceExpression" || + stmt.type === "VariableDeclarator") { + + stmt = stmt.parent; + } + + return (( + stmt.type === "ExpressionStatement" || + stmt.type === "VariableDeclaration") && + stmt.parent && stmt.parent.type === "Program" + ); + } + + /** + * Check indent for function block content + * @param {ASTNode} node A BlockStatement node that is inside of a function. + * @returns {void} + */ + function checkIndentInFunctionBlock(node) { + + /* + * Search first caller in chain. + * Ex.: + * + * Models <- Identifier + * .User + * .find() + * .exec(function() { + * // function body + * }); + * + * Looks for 'Models' + */ + const calleeNode = node.parent; // FunctionExpression + let indent; + + if (calleeNode.parent && + (calleeNode.parent.type === "Property" || + calleeNode.parent.type === "ArrayExpression")) { + + // If function is part of array or object, comma can be put at left + indent = getNodeIndent(calleeNode, false).goodChar; + } else { + + // If function is standalone, simple calculate indent + indent = getNodeIndent(calleeNode).goodChar; + } + + if (calleeNode.parent.type === "CallExpression") { + const calleeParent = calleeNode.parent; + + if (calleeNode.type !== "FunctionExpression" && calleeNode.type !== "ArrowFunctionExpression") { + if (calleeParent && calleeParent.loc.start.line < node.loc.start.line) { + indent = getNodeIndent(calleeParent).goodChar; + } + } else { + if (isArgBeforeCalleeNodeMultiline(calleeNode) && + calleeParent.callee.loc.start.line === calleeParent.callee.loc.end.line && + !isNodeFirstInLine(calleeNode)) { + indent = getNodeIndent(calleeParent).goodChar; + } + } + } + + /* + * function body indent should be indent + indent size, unless this + * is a FunctionDeclaration, FunctionExpression, or outer IIFE and the corresponding options are enabled. + */ + let functionOffset = indentSize; + + if (options.outerIIFEBody !== null && isOuterIIFE(calleeNode)) { + functionOffset = options.outerIIFEBody * indentSize; + } else if (calleeNode.type === "FunctionExpression") { + functionOffset = options.FunctionExpression.body * indentSize; + } else if (calleeNode.type === "FunctionDeclaration") { + functionOffset = options.FunctionDeclaration.body * indentSize; + } + indent += functionOffset; + + // check if the node is inside a variable + const parentVarNode = getVariableDeclaratorNode(node); + + if (parentVarNode && isNodeInVarOnTop(node, parentVarNode)) { + indent += indentSize * options.VariableDeclarator[parentVarNode.parent.kind]; + } + + if (node.body.length > 0) { + checkNodesIndent(node.body, indent); + } + + checkLastNodeLineIndent(node, indent - functionOffset); + } + + + /** + * Checks if the given node starts and ends on the same line + * @param {ASTNode} node The node to check + * @returns {boolean} Whether or not the block starts and ends on the same line. + */ + function isSingleLineNode(node) { + const lastToken = sourceCode.getLastToken(node), + startLine = node.loc.start.line, + endLine = lastToken.loc.end.line; + + return startLine === endLine; + } + + /** + * Check to see if the first element inside an array is an object and on the same line as the node + * If the node is not an array then it will return false. + * @param {ASTNode} node node to check + * @returns {boolean} success/failure + */ + function isFirstArrayElementOnSameLine(node) { + if (node.type === "ArrayExpression" && node.elements[0]) { + return node.elements[0].loc.start.line === node.loc.start.line && node.elements[0].type === "ObjectExpression"; + } + return false; + + } + + /** + * Check indent for array block content or object block content + * @param {ASTNode} node node to examine + * @returns {void} + */ + function checkIndentInArrayOrObjectBlock(node) { + + // Skip inline + if (isSingleLineNode(node)) { + return; + } + + let elements = (node.type === "ArrayExpression") ? node.elements : node.properties; + + // filter out empty elements example would be [ , 2] so remove first element as espree considers it as null + elements = elements.filter(elem => elem !== null); + + let nodeIndent; + let elementsIndent; + const parentVarNode = getVariableDeclaratorNode(node); + + // TODO - come up with a better strategy in future + if (isNodeFirstInLine(node)) { + const parent = node.parent; + + nodeIndent = getNodeIndent(parent).goodChar; + if (!parentVarNode || parentVarNode.loc.start.line !== node.loc.start.line) { + if (parent.type !== "VariableDeclarator" || parentVarNode === parentVarNode.parent.declarations[0]) { + if (parent.type === "VariableDeclarator" && parentVarNode.loc.start.line === parent.loc.start.line) { + nodeIndent += (indentSize * options.VariableDeclarator[parentVarNode.parent.kind]); + } else if (parent.type === "ObjectExpression" || parent.type === "ArrayExpression") { + const parentElements = node.parent.type === "ObjectExpression" ? node.parent.properties : node.parent.elements; + + if (parentElements[0] && + parentElements[0].loc.start.line === parent.loc.start.line && + parentElements[0].loc.end.line !== parent.loc.start.line) { + + /* + * If the first element of the array spans multiple lines, don't increase the expected indentation of the rest. + * e.g. [{ + * foo: 1 + * }, + * { + * bar: 1 + * }] + * the second object is not indented. + */ + } else if (typeof options[parent.type] === "number") { + nodeIndent += options[parent.type] * indentSize; + } else { + nodeIndent = parentElements[0].loc.start.column; + } + } else if (parent.type === "CallExpression" || parent.type === "NewExpression") { + if (typeof options.CallExpression.arguments === "number") { + nodeIndent += options.CallExpression.arguments * indentSize; + } else if (options.CallExpression.arguments === "first") { + if (parent.arguments.indexOf(node) !== -1) { + nodeIndent = parent.arguments[0].loc.start.column; + } + } else { + nodeIndent += indentSize; + } + } else if (parent.type === "LogicalExpression" || parent.type === "ArrowFunctionExpression") { + nodeIndent += indentSize; + } + } + } else if (!parentVarNode && !isFirstArrayElementOnSameLine(parent) && parent.type !== "MemberExpression" && parent.type !== "ExpressionStatement" && parent.type !== "AssignmentExpression" && parent.type !== "Property") { + nodeIndent += indentSize; + } + + checkFirstNodeLineIndent(node, nodeIndent); + } else { + nodeIndent = getNodeIndent(node).goodChar; + } + + if (options[node.type] === "first") { + elementsIndent = elements.length ? elements[0].loc.start.column : 0; // If there are no elements, elementsIndent doesn't matter. + } else { + elementsIndent = nodeIndent + indentSize * options[node.type]; + } + + /* + * Check if the node is a multiple variable declaration; if so, then + * make sure indentation takes that into account. + */ + if (isNodeInVarOnTop(node, parentVarNode)) { + elementsIndent += indentSize * options.VariableDeclarator[parentVarNode.parent.kind]; + } + + checkNodesIndent(elements, elementsIndent); + + if (elements.length > 0) { + + // Skip last block line check if last item in same line + if (elements[elements.length - 1].loc.end.line === node.loc.end.line) { + return; + } + } + + checkLastNodeLineIndent(node, nodeIndent + + (isNodeInVarOnTop(node, parentVarNode) ? options.VariableDeclarator[parentVarNode.parent.kind] * indentSize : 0)); + } + + /** + * Check if the node or node body is a BlockStatement or not + * @param {ASTNode} node node to test + * @returns {boolean} True if it or its body is a block statement + */ + function isNodeBodyBlock(node) { + return node.type === "BlockStatement" || node.type === "ClassBody" || (node.body && node.body.type === "BlockStatement") || + (node.consequent && node.consequent.type === "BlockStatement"); + } + + /** + * Check indentation for blocks + * @param {ASTNode} node node to check + * @returns {void} + */ + function blockIndentationCheck(node) { + + // Skip inline blocks + if (isSingleLineNode(node)) { + return; + } + + if (node.parent && ( + node.parent.type === "FunctionExpression" || + node.parent.type === "FunctionDeclaration" || + node.parent.type === "ArrowFunctionExpression") + ) { + checkIndentInFunctionBlock(node); + return; + } + + let indent; + let nodesToCheck = []; + + /* + * For this statements we should check indent from statement beginning, + * not from the beginning of the block. + */ + const statementsWithProperties = [ + "IfStatement", "WhileStatement", "ForStatement", "ForInStatement", "ForOfStatement", "DoWhileStatement", "ClassDeclaration", "TryStatement" + ]; + + if (node.parent && statementsWithProperties.indexOf(node.parent.type) !== -1 && isNodeBodyBlock(node)) { + indent = getNodeIndent(node.parent).goodChar; + } else if (node.parent && node.parent.type === "CatchClause") { + indent = getNodeIndent(node.parent.parent).goodChar; + } else { + indent = getNodeIndent(node).goodChar; + } + + if (node.type === "IfStatement" && node.consequent.type !== "BlockStatement") { + nodesToCheck = [node.consequent]; + } else if (Array.isArray(node.body)) { + nodesToCheck = node.body; + } else { + nodesToCheck = [node.body]; + } + + if (nodesToCheck.length > 0) { + checkNodesIndent(nodesToCheck, indent + indentSize); + } + + if (node.type === "BlockStatement") { + checkLastNodeLineIndent(node, indent); + } + } + + /** + * Filter out the elements which are on the same line of each other or the node. + * basically have only 1 elements from each line except the variable declaration line. + * @param {ASTNode} node Variable declaration node + * @returns {ASTNode[]} Filtered elements + */ + function filterOutSameLineVars(node) { + return node.declarations.reduce((finalCollection, elem) => { + const lastElem = finalCollection[finalCollection.length - 1]; + + if ((elem.loc.start.line !== node.loc.start.line && !lastElem) || + (lastElem && lastElem.loc.start.line !== elem.loc.start.line)) { + finalCollection.push(elem); + } + + return finalCollection; + }, []); + } + + /** + * Check indentation for variable declarations + * @param {ASTNode} node node to examine + * @returns {void} + */ + function checkIndentInVariableDeclarations(node) { + const elements = filterOutSameLineVars(node); + const nodeIndent = getNodeIndent(node).goodChar; + const lastElement = elements[elements.length - 1]; + + const elementsIndent = nodeIndent + indentSize * options.VariableDeclarator[node.kind]; + + checkNodesIndent(elements, elementsIndent); + + // Only check the last line if there is any token after the last item + if (sourceCode.getLastToken(node).loc.end.line <= lastElement.loc.end.line) { + return; + } + + const tokenBeforeLastElement = sourceCode.getTokenBefore(lastElement); + + if (tokenBeforeLastElement.value === ",") { + + // Special case for comma-first syntax where the semicolon is indented + checkLastNodeLineIndent(node, getNodeIndent(tokenBeforeLastElement).goodChar); + } else { + checkLastNodeLineIndent(node, elementsIndent - indentSize); + } + } + + /** + * Check and decide whether to check for indentation for blockless nodes + * Scenarios are for or while statements without braces around them + * @param {ASTNode} node node to examine + * @returns {void} + */ + function blockLessNodes(node) { + if (node.body.type !== "BlockStatement") { + blockIndentationCheck(node); + } + } + + /** + * Returns the expected indentation for the case statement + * @param {ASTNode} node node to examine + * @param {int} [switchIndent] indent for switch statement + * @returns {int} indent size + */ + function expectedCaseIndent(node, switchIndent) { + const switchNode = (node.type === "SwitchStatement") ? node : node.parent; + let caseIndent; + + if (caseIndentStore[switchNode.loc.start.line]) { + return caseIndentStore[switchNode.loc.start.line]; + } + if (typeof switchIndent === "undefined") { + switchIndent = getNodeIndent(switchNode).goodChar; + } + + if (switchNode.cases.length > 0 && options.SwitchCase === 0) { + caseIndent = switchIndent; + } else { + caseIndent = switchIndent + (indentSize * options.SwitchCase); + } + + caseIndentStore[switchNode.loc.start.line] = caseIndent; + return caseIndent; + + } + + /** + * Checks wether a return statement is wrapped in () + * @param {ASTNode} node node to examine + * @returns {boolean} the result + */ + function isWrappedInParenthesis(node) { + const regex = /^return\s*?\(\s*?\);*?/; + + const statementWithoutArgument = sourceCode.getText(node).replace( + sourceCode.getText(node.argument), "" + ); + + return regex.test(statementWithoutArgument); + } + + return { + Program(node) { + if (node.body.length > 0) { + + // Root nodes should have no indent + checkNodesIndent(node.body, getNodeIndent(node).goodChar); + } + }, + + ClassBody: blockIndentationCheck, + + BlockStatement: blockIndentationCheck, + + WhileStatement: blockLessNodes, + + ForStatement: blockLessNodes, + + ForInStatement: blockLessNodes, + + ForOfStatement: blockLessNodes, + + DoWhileStatement: blockLessNodes, + + IfStatement(node) { + if (node.consequent.type !== "BlockStatement" && node.consequent.loc.start.line > node.loc.start.line) { + blockIndentationCheck(node); + } + }, + + VariableDeclaration(node) { + if (node.declarations[node.declarations.length - 1].loc.start.line > node.declarations[0].loc.start.line) { + checkIndentInVariableDeclarations(node); + } + }, + + ObjectExpression(node) { + checkIndentInArrayOrObjectBlock(node); + }, + + ArrayExpression(node) { + checkIndentInArrayOrObjectBlock(node); + }, + + MemberExpression(node) { + + if (typeof options.MemberExpression === "undefined") { + return; + } + + if (isSingleLineNode(node)) { + return; + } + + /* + * The typical layout of variable declarations and assignments + * alter the expectation of correct indentation. Skip them. + * TODO: Add appropriate configuration options for variable + * declarations and assignments. + */ + if (getParentNodeByType(node, "VariableDeclarator", ["FunctionExpression", "ArrowFunctionExpression"])) { + return; + } + + if (getParentNodeByType(node, "AssignmentExpression", ["FunctionExpression"])) { + return; + } + + const propertyIndent = getNodeIndent(node).goodChar + indentSize * options.MemberExpression; + + const checkNodes = [node.property]; + + const dot = sourceCode.getTokenBefore(node.property); + + if (dot.type === "Punctuator" && dot.value === ".") { + checkNodes.push(dot); + } + + checkNodesIndent(checkNodes, propertyIndent); + }, + + SwitchStatement(node) { + + // Switch is not a 'BlockStatement' + const switchIndent = getNodeIndent(node).goodChar; + const caseIndent = expectedCaseIndent(node, switchIndent); + + checkNodesIndent(node.cases, caseIndent); + + + checkLastNodeLineIndent(node, switchIndent); + }, + + SwitchCase(node) { + + // Skip inline cases + if (isSingleLineNode(node)) { + return; + } + const caseIndent = expectedCaseIndent(node); + + checkNodesIndent(node.consequent, caseIndent + indentSize); + }, + + FunctionDeclaration(node) { + if (isSingleLineNode(node)) { + return; + } + if (options.FunctionDeclaration.parameters === "first" && node.params.length) { + checkNodesIndent(node.params.slice(1), node.params[0].loc.start.column); + } else if (options.FunctionDeclaration.parameters !== null) { + checkNodesIndent(node.params, getNodeIndent(node).goodChar + indentSize * options.FunctionDeclaration.parameters); + } + }, + + FunctionExpression(node) { + if (isSingleLineNode(node)) { + return; + } + if (options.FunctionExpression.parameters === "first" && node.params.length) { + checkNodesIndent(node.params.slice(1), node.params[0].loc.start.column); + } else if (options.FunctionExpression.parameters !== null) { + checkNodesIndent(node.params, getNodeIndent(node).goodChar + indentSize * options.FunctionExpression.parameters); + } + }, + + ReturnStatement(node) { + if (isSingleLineNode(node)) { + return; + } + + const firstLineIndent = getNodeIndent(node).goodChar; + + // in case if return statement is wrapped in parenthesis + if (isWrappedInParenthesis(node)) { + checkLastReturnStatementLineIndent(node, firstLineIndent); + } else { + checkNodeIndent(node, firstLineIndent); + } + }, + + CallExpression(node) { + if (isSingleLineNode(node)) { + return; + } + if (options.CallExpression.arguments === "first" && node.arguments.length) { + checkNodesIndent(node.arguments.slice(1), node.arguments[0].loc.start.column); + } else if (options.CallExpression.arguments !== null) { + checkNodesIndent(node.arguments, getNodeIndent(node).goodChar + indentSize * options.CallExpression.arguments); + } + } + + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/indent.js b/tools/node_modules/eslint/lib/rules/indent.js new file mode 100644 index 0000000000..42cebf9ea5 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/indent.js @@ -0,0 +1,1522 @@ +/** + * @fileoverview This option sets a specific tab width for your code + * + * @author Teddy Katz + * @author Vitaly Puzrin + * @author Gyandeep Singh + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const lodash = require("lodash"); +const astUtils = require("../ast-utils"); +const createTree = require("functional-red-black-tree"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +const KNOWN_NODES = new Set([ + "AssignmentExpression", + "AssignmentPattern", + "ArrayExpression", + "ArrayPattern", + "ArrowFunctionExpression", + "AwaitExpression", + "BlockStatement", + "BinaryExpression", + "BreakStatement", + "CallExpression", + "CatchClause", + "ClassBody", + "ClassDeclaration", + "ClassExpression", + "ConditionalExpression", + "ContinueStatement", + "DoWhileStatement", + "DebuggerStatement", + "EmptyStatement", + "ExperimentalRestProperty", + "ExperimentalSpreadProperty", + "ExpressionStatement", + "ForStatement", + "ForInStatement", + "ForOfStatement", + "FunctionDeclaration", + "FunctionExpression", + "Identifier", + "IfStatement", + "Literal", + "LabeledStatement", + "LogicalExpression", + "MemberExpression", + "MetaProperty", + "MethodDefinition", + "NewExpression", + "ObjectExpression", + "ObjectPattern", + "Program", + "Property", + "RestElement", + "ReturnStatement", + "SequenceExpression", + "SpreadElement", + "Super", + "SwitchCase", + "SwitchStatement", + "TaggedTemplateExpression", + "TemplateElement", + "TemplateLiteral", + "ThisExpression", + "ThrowStatement", + "TryStatement", + "UnaryExpression", + "UpdateExpression", + "VariableDeclaration", + "VariableDeclarator", + "WhileStatement", + "WithStatement", + "YieldExpression", + "JSXIdentifier", + "JSXNamespacedName", + "JSXMemberExpression", + "JSXEmptyExpression", + "JSXExpressionContainer", + "JSXElement", + "JSXClosingElement", + "JSXOpeningElement", + "JSXAttribute", + "JSXSpreadAttribute", + "JSXText", + "ExportDefaultDeclaration", + "ExportNamedDeclaration", + "ExportAllDeclaration", + "ExportSpecifier", + "ImportDeclaration", + "ImportSpecifier", + "ImportDefaultSpecifier", + "ImportNamespaceSpecifier" +]); + +/* + * General rule strategy: + * 1. An OffsetStorage instance stores a map of desired offsets, where each token has a specified offset from another + * specified token or to the first column. + * 2. As the AST is traversed, modify the desired offsets of tokens accordingly. For example, when entering a + * BlockStatement, offset all of the tokens in the BlockStatement by 1 indent level from the opening curly + * brace of the BlockStatement. + * 3. After traversing the AST, calculate the expected indentation levels of every token according to the + * OffsetStorage container. + * 4. For each line, compare the expected indentation of the first token to the actual indentation in the file, + * and report the token if the two values are not equal. + */ + + +/** + * A mutable balanced binary search tree that stores (key, value) pairs. The keys are numeric, and must be unique. + * This is intended to be a generic wrapper around a balanced binary search tree library, so that the underlying implementation + * can easily be swapped out. + */ +class BinarySearchTree { + + /** + * Creates an empty tree + */ + constructor() { + this._rbTree = createTree(); + } + + /** + * Inserts an entry into the tree. + * @param {number} key The entry's key + * @param {*} value The entry's value + * @returns {void} + */ + insert(key, value) { + const iterator = this._rbTree.find(key); + + if (iterator.valid) { + this._rbTree = iterator.update(value); + } else { + this._rbTree = this._rbTree.insert(key, value); + } + } + + /** + * Finds the entry with the largest key less than or equal to the provided key + * @param {number} key The provided key + * @returns {{key: number, value: *}|null} The found entry, or null if no such entry exists. + */ + findLe(key) { + const iterator = this._rbTree.le(key); + + return iterator && { key: iterator.key, value: iterator.value }; + } + + /** + * Deletes all of the keys in the interval [start, end) + * @param {number} start The start of the range + * @param {number} end The end of the range + * @returns {void} + */ + deleteRange(start, end) { + + // Exit without traversing the tree if the range has zero size. + if (start === end) { + return; + } + const iterator = this._rbTree.ge(start); + + while (iterator.valid && iterator.key < end) { + this._rbTree = this._rbTree.remove(iterator.key); + iterator.next(); + } + } +} + +/** + * A helper class to get token-based info related to indentation + */ +class TokenInfo { + + /** + * @param {SourceCode} sourceCode A SourceCode object + */ + constructor(sourceCode) { + this.sourceCode = sourceCode; + this.firstTokensByLineNumber = sourceCode.tokensAndComments.reduce((map, token) => { + if (!map.has(token.loc.start.line)) { + map.set(token.loc.start.line, token); + } + if (!map.has(token.loc.end.line) && sourceCode.text.slice(token.range[1] - token.loc.end.column, token.range[1]).trim()) { + map.set(token.loc.end.line, token); + } + return map; + }, new Map()); + } + + /** + * Gets the first token on a given token's line + * @param {Token|ASTNode} token a node or token + * @returns {Token} The first token on the given line + */ + getFirstTokenOfLine(token) { + return this.firstTokensByLineNumber.get(token.loc.start.line); + } + + /** + * Determines whether a token is the first token in its line + * @param {Token} token The token + * @returns {boolean} `true` if the token is the first on its line + */ + isFirstTokenOfLine(token) { + return this.getFirstTokenOfLine(token) === token; + } + + /** + * Get the actual indent of a token + * @param {Token} token Token to examine. This should be the first token on its line. + * @returns {string} The indentation characters that precede the token + */ + getTokenIndent(token) { + return this.sourceCode.text.slice(token.range[0] - token.loc.start.column, token.range[0]); + } +} + +/** + * A class to store information on desired offsets of tokens from each other + */ +class OffsetStorage { + + /** + * @param {TokenInfo} tokenInfo a TokenInfo instance + * @param {number} indentSize The desired size of each indentation level + * @param {string} indentType The indentation character + */ + constructor(tokenInfo, indentSize, indentType) { + this._tokenInfo = tokenInfo; + this._indentSize = indentSize; + this._indentType = indentType; + + this._tree = new BinarySearchTree(); + this._tree.insert(0, { offset: 0, from: null, force: false }); + + this._lockedFirstTokens = new WeakMap(); + this._desiredIndentCache = new WeakMap(); + this._ignoredTokens = new WeakSet(); + } + + _getOffsetDescriptor(token) { + return this._tree.findLe(token.range[0]).value; + } + + /** + * Sets the offset column of token B to match the offset column of token A. + * **WARNING**: This matches a *column*, even if baseToken is not the first token on its line. In + * most cases, `setDesiredOffset` should be used instead. + * @param {Token} baseToken The first token + * @param {Token} offsetToken The second token, whose offset should be matched to the first token + * @returns {void} + */ + matchOffsetOf(baseToken, offsetToken) { + + /* + * lockedFirstTokens is a map from a token whose indentation is controlled by the "first" option to + * the token that it depends on. For example, with the `ArrayExpression: first` option, the first + * token of each element in the array after the first will be mapped to the first token of the first + * element. The desired indentation of each of these tokens is computed based on the desired indentation + * of the "first" element, rather than through the normal offset mechanism. + */ + this._lockedFirstTokens.set(offsetToken, baseToken); + } + + /** + * Sets the desired offset of a token. + * + * This uses a line-based offset collapsing behavior to handle tokens on the same line. + * For example, consider the following two cases: + * + * ( + * [ + * bar + * ] + * ) + * + * ([ + * bar + * ]) + * + * Based on the first case, it's clear that the `bar` token needs to have an offset of 1 indent level (4 spaces) from + * the `[` token, and the `[` token has to have an offset of 1 indent level from the `(` token. Since the `(` token is + * the first on its line (with an indent of 0 spaces), the `bar` token needs to be offset by 2 indent levels (8 spaces) + * from the start of its line. + * + * However, in the second case `bar` should only be indented by 4 spaces. This is because the offset of 1 indent level + * between the `(` and the `[` tokens gets "collapsed" because the two tokens are on the same line. As a result, the + * `(` token is mapped to the `[` token with an offset of 0, and the rule correctly decides that `bar` should be indented + * by 1 indent level from the start of the line. + * + * This is useful because rule listeners can usually just call `setDesiredOffset` for all the tokens in the node, + * without needing to check which lines those tokens are on. + * + * Note that since collapsing only occurs when two tokens are on the same line, there are a few cases where non-intuitive + * behavior can occur. For example, consider the following cases: + * + * foo( + * ). + * bar( + * baz + * ) + * + * foo( + * ).bar( + * baz + * ) + * + * Based on the first example, it would seem that `bar` should be offset by 1 indent level from `foo`, and `baz` + * should be offset by 1 indent level from `bar`. However, this is not correct, because it would result in `baz` + * being indented by 2 indent levels in the second case (since `foo`, `bar`, and `baz` are all on separate lines, no + * collapsing would occur). + * + * Instead, the correct way would be to offset `baz` by 1 level from `bar`, offset `bar` by 1 level from the `)`, and + * offset the `)` by 0 levels from `foo`. This ensures that the offset between `bar` and the `)` are correctly collapsed + * in the second case. + * + * @param {Token} token The token + * @param {Token} fromToken The token that `token` should be offset from + * @param {number} offset The desired indent level + * @returns {void} + */ + setDesiredOffset(token, fromToken, offset) { + return this.setDesiredOffsets(token.range, fromToken, offset); + } + + /** + * Sets the desired offset of all tokens in a range + * It's common for node listeners in this file to need to apply the same offset to a large, contiguous range of tokens. + * Moreover, the offset of any given token is usually updated multiple times (roughly once for each node that contains + * it). This means that the offset of each token is updated O(AST depth) times. + * It would not be performant to store and update the offsets for each token independently, because the rule would end + * up having a time complexity of O(number of tokens * AST depth), which is quite slow for large files. + * + * Instead, the offset tree is represented as a collection of contiguous offset ranges in a file. For example, the following + * list could represent the state of the offset tree at a given point: + * + * * Tokens starting in the interval [0, 15) are aligned with the beginning of the file + * * Tokens starting in the interval [15, 30) are offset by 1 indent level from the `bar` token + * * Tokens starting in the interval [30, 43) are offset by 1 indent level from the `foo` token + * * Tokens starting in the interval [43, 820) are offset by 2 indent levels from the `bar` token + * * Tokens starting in the interval [820, ∞) are offset by 1 indent level from the `baz` token + * + * The `setDesiredOffsets` methods inserts ranges like the ones above. The third line above would be inserted by using: + * `setDesiredOffsets([30, 43], fooToken, 1);` + * + * @param {[number, number]} range A [start, end] pair. All tokens with range[0] <= token.start < range[1] will have the offset applied. + * @param {Token} fromToken The token that this is offset from + * @param {number} offset The desired indent level + * @param {boolean} force `true` if this offset should not use the normal collapsing behavior. This should almost always be false. + * @returns {void} + */ + setDesiredOffsets(range, fromToken, offset, force) { + + /* + * Offset ranges are stored as a collection of nodes, where each node maps a numeric key to an offset + * descriptor. The tree for the example above would have the following nodes: + * + * * key: 0, value: { offset: 0, from: null } + * * key: 15, value: { offset: 1, from: barToken } + * * key: 30, value: { offset: 1, from: fooToken } + * * key: 43, value: { offset: 2, from: barToken } + * * key: 820, value: { offset: 1, from: bazToken } + * + * To find the offset descriptor for any given token, one needs to find the node with the largest key + * which is <= token.start. To make this operation fast, the nodes are stored in a balanced binary + * search tree indexed by key. + */ + + const descriptorToInsert = { offset, from: fromToken, force }; + + const descriptorAfterRange = this._tree.findLe(range[1]).value; + + const fromTokenIsInRange = fromToken && fromToken.range[0] >= range[0] && fromToken.range[1] <= range[1]; + const fromTokenDescriptor = fromTokenIsInRange && this._getOffsetDescriptor(fromToken); + + // First, remove any existing nodes in the range from the tree. + this._tree.deleteRange(range[0] + 1, range[1]); + + // Insert a new node into the tree for this range + this._tree.insert(range[0], descriptorToInsert); + + /* + * To avoid circular offset dependencies, keep the `fromToken` token mapped to whatever it was mapped to previously, + * even if it's in the current range. + */ + if (fromTokenIsInRange) { + this._tree.insert(fromToken.range[0], fromTokenDescriptor); + this._tree.insert(fromToken.range[1], descriptorToInsert); + } + + /* + * To avoid modifying the offset of tokens after the range, insert another node to keep the offset of the following + * tokens the same as it was before. + */ + this._tree.insert(range[1], descriptorAfterRange); + } + + /** + * Gets the desired indent of a token + * @param {Token} token The token + * @returns {string} The desired indent of the token + */ + getDesiredIndent(token) { + if (!this._desiredIndentCache.has(token)) { + + if (this._ignoredTokens.has(token)) { + + /* + * If the token is ignored, use the actual indent of the token as the desired indent. + * This ensures that no errors are reported for this token. + */ + this._desiredIndentCache.set( + token, + this._tokenInfo.getTokenIndent(token) + ); + } else if (this._lockedFirstTokens.has(token)) { + const firstToken = this._lockedFirstTokens.get(token); + + this._desiredIndentCache.set( + token, + + // (indentation for the first element's line) + this.getDesiredIndent(this._tokenInfo.getFirstTokenOfLine(firstToken)) + + + // (space between the start of the first element's line and the first element) + this._indentType.repeat(firstToken.loc.start.column - this._tokenInfo.getFirstTokenOfLine(firstToken).loc.start.column) + ); + } else { + const offsetInfo = this._getOffsetDescriptor(token); + const offset = ( + offsetInfo.from && + offsetInfo.from.loc.start.line === token.loc.start.line && + !offsetInfo.force + ) ? 0 : offsetInfo.offset * this._indentSize; + + this._desiredIndentCache.set( + token, + (offsetInfo.from ? this.getDesiredIndent(offsetInfo.from) : "") + this._indentType.repeat(offset) + ); + } + } + return this._desiredIndentCache.get(token); + } + + /** + * Ignores a token, preventing it from being reported. + * @param {Token} token The token + * @returns {void} + */ + ignoreToken(token) { + if (this._tokenInfo.isFirstTokenOfLine(token)) { + this._ignoredTokens.add(token); + } + } + + /** + * Gets the first token that the given token's indentation is dependent on + * @param {Token} token The token + * @returns {Token} The token that the given token depends on, or `null` if the given token is at the top level + */ + getFirstDependency(token) { + return this._getOffsetDescriptor(token).from; + } +} + +const ELEMENT_LIST_SCHEMA = { + oneOf: [ + { + type: "integer", + minimum: 0 + }, + { + enum: ["first", "off"] + } + ] +}; + +module.exports = { + meta: { + docs: { + description: "enforce consistent indentation", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + oneOf: [ + { + enum: ["tab"] + }, + { + type: "integer", + minimum: 0 + } + ] + }, + { + type: "object", + properties: { + SwitchCase: { + type: "integer", + minimum: 0 + }, + VariableDeclarator: { + oneOf: [ + { + type: "integer", + minimum: 0 + }, + { + type: "object", + properties: { + var: { + type: "integer", + minimum: 0 + }, + let: { + type: "integer", + minimum: 0 + }, + const: { + type: "integer", + minimum: 0 + } + }, + additionalProperties: false + } + ] + }, + outerIIFEBody: { + type: "integer", + minimum: 0 + }, + MemberExpression: { + oneOf: [ + { + type: "integer", + minimum: 0 + }, + { + enum: ["off"] + } + ] + }, + FunctionDeclaration: { + type: "object", + properties: { + parameters: ELEMENT_LIST_SCHEMA, + body: { + type: "integer", + minimum: 0 + } + }, + additionalProperties: false + }, + FunctionExpression: { + type: "object", + properties: { + parameters: ELEMENT_LIST_SCHEMA, + body: { + type: "integer", + minimum: 0 + } + }, + additionalProperties: false + }, + CallExpression: { + type: "object", + properties: { + arguments: ELEMENT_LIST_SCHEMA + }, + additionalProperties: false + }, + ArrayExpression: ELEMENT_LIST_SCHEMA, + ObjectExpression: ELEMENT_LIST_SCHEMA, + ImportDeclaration: ELEMENT_LIST_SCHEMA, + flatTernaryExpressions: { + type: "boolean" + }, + ignoredNodes: { + type: "array", + items: { + type: "string", + not: { + pattern: ":exit$" + } + } + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const DEFAULT_VARIABLE_INDENT = 1; + const DEFAULT_PARAMETER_INDENT = 1; + const DEFAULT_FUNCTION_BODY_INDENT = 1; + + let indentType = "space"; + let indentSize = 4; + const options = { + SwitchCase: 0, + VariableDeclarator: { + var: DEFAULT_VARIABLE_INDENT, + let: DEFAULT_VARIABLE_INDENT, + const: DEFAULT_VARIABLE_INDENT + }, + outerIIFEBody: 1, + FunctionDeclaration: { + parameters: DEFAULT_PARAMETER_INDENT, + body: DEFAULT_FUNCTION_BODY_INDENT + }, + FunctionExpression: { + parameters: DEFAULT_PARAMETER_INDENT, + body: DEFAULT_FUNCTION_BODY_INDENT + }, + CallExpression: { + arguments: DEFAULT_PARAMETER_INDENT + }, + MemberExpression: 1, + ArrayExpression: 1, + ObjectExpression: 1, + ImportDeclaration: 1, + flatTernaryExpressions: false, + ignoredNodes: [] + }; + + if (context.options.length) { + if (context.options[0] === "tab") { + indentSize = 1; + indentType = "tab"; + } else { + indentSize = context.options[0]; + indentType = "space"; + } + + if (context.options[1]) { + lodash.merge(options, context.options[1]); + + if (typeof options.VariableDeclarator === "number") { + options.VariableDeclarator = { + var: options.VariableDeclarator, + let: options.VariableDeclarator, + const: options.VariableDeclarator + }; + } + } + } + + const sourceCode = context.getSourceCode(); + const tokenInfo = new TokenInfo(sourceCode); + const offsets = new OffsetStorage(tokenInfo, indentSize, indentType === "space" ? " " : "\t"); + const parameterParens = new WeakSet(); + + /** + * Creates an error message for a line, given the expected/actual indentation. + * @param {int} expectedAmount The expected amount of indentation characters for this line + * @param {int} actualSpaces The actual number of indentation spaces that were found on this line + * @param {int} actualTabs The actual number of indentation tabs that were found on this line + * @returns {string} An error message for this line + */ + function createErrorMessage(expectedAmount, actualSpaces, actualTabs) { + const expectedStatement = `${expectedAmount} ${indentType}${expectedAmount === 1 ? "" : "s"}`; // e.g. "2 tabs" + const foundSpacesWord = `space${actualSpaces === 1 ? "" : "s"}`; // e.g. "space" + const foundTabsWord = `tab${actualTabs === 1 ? "" : "s"}`; // e.g. "tabs" + let foundStatement; + + if (actualSpaces > 0) { + + /* + * Abbreviate the message if the expected indentation is also spaces. + * e.g. 'Expected 4 spaces but found 2' rather than 'Expected 4 spaces but found 2 spaces' + */ + foundStatement = indentType === "space" ? actualSpaces : `${actualSpaces} ${foundSpacesWord}`; + } else if (actualTabs > 0) { + foundStatement = indentType === "tab" ? actualTabs : `${actualTabs} ${foundTabsWord}`; + } else { + foundStatement = "0"; + } + + return `Expected indentation of ${expectedStatement} but found ${foundStatement}.`; + } + + /** + * Reports a given indent violation + * @param {Token} token Token violating the indent rule + * @param {string} neededIndent Expected indentation string + * @returns {void} + */ + function report(token, neededIndent) { + const actualIndent = Array.from(tokenInfo.getTokenIndent(token)); + const numSpaces = actualIndent.filter(char => char === " ").length; + const numTabs = actualIndent.filter(char => char === "\t").length; + + context.report({ + node: token, + message: createErrorMessage(neededIndent.length, numSpaces, numTabs), + loc: { + start: { line: token.loc.start.line, column: 0 }, + end: { line: token.loc.start.line, column: token.loc.start.column } + }, + fix(fixer) { + const range = [token.range[0] - token.loc.start.column, token.range[0]]; + const newText = neededIndent; + + return fixer.replaceTextRange(range, newText); + } + }); + } + + /** + * Checks if a token's indentation is correct + * @param {Token} token Token to examine + * @param {string} desiredIndent Desired indentation of the string + * @returns {boolean} `true` if the token's indentation is correct + */ + function validateTokenIndent(token, desiredIndent) { + const indentation = tokenInfo.getTokenIndent(token); + + return indentation === desiredIndent || + + // To avoid conflicts with no-mixed-spaces-and-tabs, don't report mixed spaces and tabs. + indentation.includes(" ") && indentation.includes("\t"); + } + + /** + * Check to see if the node is a file level IIFE + * @param {ASTNode} node The function node to check. + * @returns {boolean} True if the node is the outer IIFE + */ + function isOuterIIFE(node) { + + /* + * Verify that the node is an IIFE + */ + if (!node.parent || node.parent.type !== "CallExpression" || node.parent.callee !== node) { + return false; + } + + /* + * Navigate legal ancestors to determine whether this IIFE is outer. + * A "legal ancestor" is an expression or statement that causes the function to get executed immediately. + * For example, `!(function(){})()` is an outer IIFE even though it is preceded by a ! operator. + */ + let statement = node.parent && node.parent.parent; + + while ( + statement.type === "UnaryExpression" && ["!", "~", "+", "-"].indexOf(statement.operator) > -1 || + statement.type === "AssignmentExpression" || + statement.type === "LogicalExpression" || + statement.type === "SequenceExpression" || + statement.type === "VariableDeclarator" + ) { + statement = statement.parent; + } + + return (statement.type === "ExpressionStatement" || statement.type === "VariableDeclaration") && statement.parent.type === "Program"; + } + + /** + * Check indentation for lists of elements (arrays, objects, function params) + * @param {ASTNode[]} elements List of elements that should be offset + * @param {Token} startToken The start token of the list that element should be aligned against, e.g. '[' + * @param {Token} endToken The end token of the list, e.g. ']' + * @param {number|string} offset The amount that the elements should be offset + * @returns {void} + */ + function addElementListIndent(elements, startToken, endToken, offset) { + + /** + * Gets the first token of a given element, including surrounding parentheses. + * @param {ASTNode} element A node in the `elements` list + * @returns {Token} The first token of this element + */ + function getFirstToken(element) { + let token = sourceCode.getTokenBefore(element); + + while (astUtils.isOpeningParenToken(token) && token !== startToken) { + token = sourceCode.getTokenBefore(token); + } + return sourceCode.getTokenAfter(token); + } + + // Run through all the tokens in the list, and offset them by one indent level (mainly for comments, other things will end up overridden) + offsets.setDesiredOffsets( + [startToken.range[1], endToken.range[0]], + startToken, + typeof offset === "number" ? offset : 1 + ); + offsets.setDesiredOffset(endToken, startToken, 0); + + // If the preference is "first" but there is no first element (e.g. sparse arrays w/ empty first slot), fall back to 1 level. + if (offset === "first" && elements.length && !elements[0]) { + return; + } + elements.forEach((element, index) => { + if (!element) { + + // Skip holes in arrays + return; + } + if (offset === "off") { + + // Ignore the first token of every element if the "off" option is used + offsets.ignoreToken(getFirstToken(element)); + } + + // Offset the following elements correctly relative to the first element + if (index === 0) { + return; + } + if (offset === "first" && tokenInfo.isFirstTokenOfLine(getFirstToken(element))) { + offsets.matchOffsetOf(getFirstToken(elements[0]), getFirstToken(element)); + } else { + const previousElement = elements[index - 1]; + const firstTokenOfPreviousElement = previousElement && getFirstToken(previousElement); + + if (previousElement && sourceCode.getLastToken(previousElement).loc.start.line > startToken.loc.end.line) { + offsets.setDesiredOffsets(element.range, firstTokenOfPreviousElement, 0); + } + } + }); + } + + /** + * Check and decide whether to check for indentation for blockless nodes + * Scenarios are for or while statements without braces around them + * @param {ASTNode} node node to examine + * @returns {void} + */ + function addBlocklessNodeIndent(node) { + if (node.type !== "BlockStatement") { + const lastParentToken = sourceCode.getTokenBefore(node, astUtils.isNotOpeningParenToken); + + let firstBodyToken = sourceCode.getFirstToken(node); + let lastBodyToken = sourceCode.getLastToken(node); + + while ( + astUtils.isOpeningParenToken(sourceCode.getTokenBefore(firstBodyToken)) && + astUtils.isClosingParenToken(sourceCode.getTokenAfter(lastBodyToken)) + ) { + firstBodyToken = sourceCode.getTokenBefore(firstBodyToken); + lastBodyToken = sourceCode.getTokenAfter(lastBodyToken); + } + + offsets.setDesiredOffsets([firstBodyToken.range[0], lastBodyToken.range[1]], lastParentToken, 1); + + /* + * For blockless nodes with semicolon-first style, don't indent the semicolon. + * e.g. + * if (foo) bar() + * ; [1, 2, 3].map(foo) + */ + const lastToken = sourceCode.getLastToken(node); + + if (node.type !== "EmptyStatement" && astUtils.isSemicolonToken(lastToken)) { + offsets.setDesiredOffset(lastToken, lastParentToken, 0); + } + } + } + + /** + * Checks the indentation for nodes that are like function calls (`CallExpression` and `NewExpression`) + * @param {ASTNode} node A CallExpression or NewExpression node + * @returns {void} + */ + function addFunctionCallIndent(node) { + let openingParen; + + if (node.arguments.length) { + openingParen = sourceCode.getFirstTokenBetween(node.callee, node.arguments[0], astUtils.isOpeningParenToken); + } else { + openingParen = sourceCode.getLastToken(node, 1); + } + const closingParen = sourceCode.getLastToken(node); + + parameterParens.add(openingParen); + parameterParens.add(closingParen); + offsets.setDesiredOffset(openingParen, sourceCode.getTokenBefore(openingParen), 0); + + addElementListIndent(node.arguments, openingParen, closingParen, options.CallExpression.arguments); + } + + /** + * Checks the indentation of parenthesized values, given a list of tokens in a program + * @param {Token[]} tokens A list of tokens + * @returns {void} + */ + function addParensIndent(tokens) { + const parenStack = []; + const parenPairs = []; + + tokens.forEach(nextToken => { + + // Accumulate a list of parenthesis pairs + if (astUtils.isOpeningParenToken(nextToken)) { + parenStack.push(nextToken); + } else if (astUtils.isClosingParenToken(nextToken)) { + parenPairs.unshift({ left: parenStack.pop(), right: nextToken }); + } + }); + + parenPairs.forEach(pair => { + const leftParen = pair.left; + const rightParen = pair.right; + + // We only want to handle parens around expressions, so exclude parentheses that are in function parameters and function call arguments. + if (!parameterParens.has(leftParen) && !parameterParens.has(rightParen)) { + const parenthesizedTokens = new Set(sourceCode.getTokensBetween(leftParen, rightParen)); + + parenthesizedTokens.forEach(token => { + if (!parenthesizedTokens.has(offsets.getFirstDependency(token))) { + offsets.setDesiredOffset(token, leftParen, 1); + } + }); + } + + offsets.setDesiredOffset(rightParen, leftParen, 0); + }); + } + + /** + * Ignore all tokens within an unknown node whose offset do not depend + * on another token's offset within the unknown node + * @param {ASTNode} node Unknown Node + * @returns {void} + */ + function ignoreNode(node) { + const unknownNodeTokens = new Set(sourceCode.getTokens(node, { includeComments: true })); + + unknownNodeTokens.forEach(token => { + if (!unknownNodeTokens.has(offsets.getFirstDependency(token))) { + const firstTokenOfLine = tokenInfo.getFirstTokenOfLine(token); + + if (token === firstTokenOfLine) { + offsets.ignoreToken(token); + } else { + offsets.setDesiredOffset(token, firstTokenOfLine, 0); + } + } + }); + } + + /** + * Check whether the given token is on the first line of a statement. + * @param {Token} token The token to check. + * @param {ASTNode} leafNode The expression node that the token belongs directly. + * @returns {boolean} `true` if the token is on the first line of a statement. + */ + function isOnFirstLineOfStatement(token, leafNode) { + let node = leafNode; + + while (node.parent && !node.parent.type.endsWith("Statement") && !node.parent.type.endsWith("Declaration")) { + node = node.parent; + } + node = node.parent; + + return !node || node.loc.start.line === token.loc.start.line; + } + + const baseOffsetListeners = { + "ArrayExpression, ArrayPattern"(node) { + const openingBracket = sourceCode.getFirstToken(node); + const closingBracket = sourceCode.getTokenAfter(lodash.findLast(node.elements) || openingBracket, astUtils.isClosingBracketToken); + + addElementListIndent(node.elements, openingBracket, closingBracket, options.ArrayExpression); + }, + + "ObjectExpression, ObjectPattern"(node) { + const openingCurly = sourceCode.getFirstToken(node); + const closingCurly = sourceCode.getTokenAfter( + node.properties.length ? node.properties[node.properties.length - 1] : openingCurly, + astUtils.isClosingBraceToken + ); + + addElementListIndent(node.properties, openingCurly, closingCurly, options.ObjectExpression); + }, + + ArrowFunctionExpression(node) { + const firstToken = sourceCode.getFirstToken(node); + + if (astUtils.isOpeningParenToken(firstToken)) { + const openingParen = firstToken; + const closingParen = sourceCode.getTokenBefore(node.body, astUtils.isClosingParenToken); + + parameterParens.add(openingParen); + parameterParens.add(closingParen); + addElementListIndent(node.params, openingParen, closingParen, options.FunctionExpression.parameters); + } + addBlocklessNodeIndent(node.body); + + let arrowToken; + + if (node.params.length) { + arrowToken = sourceCode.getTokenAfter(node.params[node.params.length - 1], astUtils.isArrowToken); + } else { + arrowToken = sourceCode.getFirstToken(node, astUtils.isArrowToken); + } + offsets.setDesiredOffset(arrowToken, sourceCode.getFirstToken(node), 0); + }, + + AssignmentExpression(node) { + const operator = sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator); + + offsets.setDesiredOffsets([operator.range[0], node.range[1]], sourceCode.getLastToken(node.left), 1); + offsets.ignoreToken(operator); + offsets.ignoreToken(sourceCode.getTokenAfter(operator)); + }, + + "BinaryExpression, LogicalExpression"(node) { + const operator = sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator); + + /* + * For backwards compatibility, don't check BinaryExpression indents, e.g. + * var foo = bar && + * baz; + */ + + const tokenAfterOperator = sourceCode.getTokenAfter(operator); + + offsets.ignoreToken(operator); + offsets.ignoreToken(tokenAfterOperator); + offsets.setDesiredOffset(tokenAfterOperator, operator, 0); + offsets.setDesiredOffsets([tokenAfterOperator.range[1], node.range[1]], tokenAfterOperator, 1); + }, + + "BlockStatement, ClassBody"(node) { + + let blockIndentLevel; + + if (node.parent && isOuterIIFE(node.parent)) { + blockIndentLevel = options.outerIIFEBody; + } else if (node.parent && (node.parent.type === "FunctionExpression" || node.parent.type === "ArrowFunctionExpression")) { + blockIndentLevel = options.FunctionExpression.body; + } else if (node.parent && node.parent.type === "FunctionDeclaration") { + blockIndentLevel = options.FunctionDeclaration.body; + } else { + blockIndentLevel = 1; + } + + /* + * For blocks that aren't lone statements, ensure that the opening curly brace + * is aligned with the parent. + */ + if (!astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type)) { + offsets.setDesiredOffset(sourceCode.getFirstToken(node), sourceCode.getFirstToken(node.parent), 0); + } + addElementListIndent(node.body, sourceCode.getFirstToken(node), sourceCode.getLastToken(node), blockIndentLevel); + }, + + CallExpression: addFunctionCallIndent, + + + "ClassDeclaration[superClass], ClassExpression[superClass]"(node) { + const classToken = sourceCode.getFirstToken(node); + const extendsToken = sourceCode.getTokenBefore(node.superClass, astUtils.isNotOpeningParenToken); + + offsets.setDesiredOffsets([extendsToken.range[0], node.body.range[0]], classToken, 1); + }, + + ConditionalExpression(node) { + const firstToken = sourceCode.getFirstToken(node); + + // `flatTernaryExpressions` option is for the following style: + // var a = + // foo > 0 ? bar : + // foo < 0 ? baz : + // /*else*/ qiz ; + if (!options.flatTernaryExpressions || + !astUtils.isTokenOnSameLine(node.test, node.consequent) || + isOnFirstLineOfStatement(firstToken, node) + ) { + const questionMarkToken = sourceCode.getFirstTokenBetween(node.test, node.consequent, token => token.type === "Punctuator" && token.value === "?"); + const colonToken = sourceCode.getFirstTokenBetween(node.consequent, node.alternate, token => token.type === "Punctuator" && token.value === ":"); + + const firstConsequentToken = sourceCode.getTokenAfter(questionMarkToken, { includeComments: true }); + const lastConsequentToken = sourceCode.getTokenBefore(colonToken, { includeComments: true }); + const firstAlternateToken = sourceCode.getTokenAfter(colonToken); + + offsets.setDesiredOffset(questionMarkToken, firstToken, 1); + offsets.setDesiredOffset(colonToken, firstToken, 1); + + offsets.setDesiredOffset(firstConsequentToken, firstToken, 1); + + /* + * The alternate and the consequent should usually have the same indentation. + * If they share part of a line, align the alternate against the first token of the consequent. + * This allows the alternate to be indented correctly in cases like this: + * foo ? ( + * bar + * ) : ( // this '(' is aligned with the '(' above, so it's considered to be aligned with `foo` + * baz // as a result, `baz` is offset by 1 rather than 2 + * ) + */ + if (lastConsequentToken.loc.end.line === firstAlternateToken.loc.start.line) { + offsets.setDesiredOffset(firstAlternateToken, firstConsequentToken, 0); + } else { + + /** + * If the alternate and consequent do not share part of a line, offset the alternate from the first + * token of the conditional expression. For example: + * foo ? bar + * : baz + * + * If `baz` were aligned with `bar` rather than being offset by 1 from `foo`, `baz` would end up + * having no expected indentation. + */ + offsets.setDesiredOffset(firstAlternateToken, firstToken, 1); + } + + offsets.setDesiredOffsets([questionMarkToken.range[1], colonToken.range[0]], firstConsequentToken, 0); + offsets.setDesiredOffsets([colonToken.range[1], node.range[1]], firstAlternateToken, 0); + } + }, + + "DoWhileStatement, WhileStatement, ForInStatement, ForOfStatement": node => addBlocklessNodeIndent(node.body), + + ExportNamedDeclaration(node) { + if (node.declaration === null) { + const closingCurly = sourceCode.getLastToken(node, astUtils.isClosingBraceToken); + + // Indent the specifiers in `export {foo, bar, baz}` + addElementListIndent(node.specifiers, sourceCode.getFirstToken(node, { skip: 1 }), closingCurly, 1); + + if (node.source) { + + // Indent everything after and including the `from` token in `export {foo, bar, baz} from 'qux'` + offsets.setDesiredOffsets([closingCurly.range[1], node.range[1]], sourceCode.getFirstToken(node), 1); + } + } + }, + + ForStatement(node) { + const forOpeningParen = sourceCode.getFirstToken(node, 1); + + if (node.init) { + offsets.setDesiredOffsets(node.init.range, forOpeningParen, 1); + } + if (node.test) { + offsets.setDesiredOffsets(node.test.range, forOpeningParen, 1); + } + if (node.update) { + offsets.setDesiredOffsets(node.update.range, forOpeningParen, 1); + } + addBlocklessNodeIndent(node.body); + }, + + "FunctionDeclaration, FunctionExpression"(node) { + const closingParen = sourceCode.getTokenBefore(node.body); + const openingParen = sourceCode.getTokenBefore(node.params.length ? node.params[0] : closingParen); + + parameterParens.add(openingParen); + parameterParens.add(closingParen); + addElementListIndent(node.params, openingParen, closingParen, options[node.type].parameters); + }, + + IfStatement(node) { + addBlocklessNodeIndent(node.consequent); + if (node.alternate && node.alternate.type !== "IfStatement") { + addBlocklessNodeIndent(node.alternate); + } + }, + + ImportDeclaration(node) { + if (node.specifiers.some(specifier => specifier.type === "ImportSpecifier")) { + const openingCurly = sourceCode.getFirstToken(node, astUtils.isOpeningBraceToken); + const closingCurly = sourceCode.getLastToken(node, astUtils.isClosingBraceToken); + + addElementListIndent(node.specifiers.filter(specifier => specifier.type === "ImportSpecifier"), openingCurly, closingCurly, options.ImportDeclaration); + } + + const fromToken = sourceCode.getLastToken(node, token => token.type === "Identifier" && token.value === "from"); + + if (fromToken) { + offsets.setDesiredOffsets([fromToken.range[0], node.range[1]], sourceCode.getFirstToken(node), 1); + } + }, + + "MemberExpression, JSXMemberExpression, MetaProperty"(node) { + const object = node.type === "MetaProperty" ? node.meta : node.object; + const firstNonObjectToken = sourceCode.getFirstTokenBetween(object, node.property, astUtils.isNotClosingParenToken); + const secondNonObjectToken = sourceCode.getTokenAfter(firstNonObjectToken); + + const objectParenCount = sourceCode.getTokensBetween(object, node.property, { filter: astUtils.isClosingParenToken }).length; + const firstObjectToken = objectParenCount + ? sourceCode.getTokenBefore(object, { skip: objectParenCount - 1 }) + : sourceCode.getFirstToken(object); + const lastObjectToken = sourceCode.getTokenBefore(firstNonObjectToken); + const firstPropertyToken = node.computed ? firstNonObjectToken : secondNonObjectToken; + + if (node.computed) { + + // For computed MemberExpressions, match the closing bracket with the opening bracket. + offsets.setDesiredOffset(sourceCode.getLastToken(node), firstNonObjectToken, 0); + offsets.setDesiredOffsets(node.property.range, firstNonObjectToken, 1); + } + + /* + * If the object ends on the same line that the property starts, match against the last token + * of the object, to ensure that the MemberExpression is not indented. + * + * Otherwise, match against the first token of the object, e.g. + * foo + * .bar + * .baz // <-- offset by 1 from `foo` + */ + const offsetBase = lastObjectToken.loc.end.line === firstPropertyToken.loc.start.line + ? lastObjectToken + : firstObjectToken; + + if (typeof options.MemberExpression === "number") { + + // Match the dot (for non-computed properties) or the opening bracket (for computed properties) against the object. + offsets.setDesiredOffset(firstNonObjectToken, offsetBase, options.MemberExpression); + + /* + * For computed MemberExpressions, match the first token of the property against the opening bracket. + * Otherwise, match the first token of the property against the object. + */ + offsets.setDesiredOffset(secondNonObjectToken, node.computed ? firstNonObjectToken : offsetBase, options.MemberExpression); + } else { + + // If the MemberExpression option is off, ignore the dot and the first token of the property. + offsets.ignoreToken(firstNonObjectToken); + offsets.ignoreToken(secondNonObjectToken); + + // To ignore the property indentation, ensure that the property tokens depend on the ignored tokens. + offsets.setDesiredOffset(firstNonObjectToken, offsetBase, 0); + offsets.setDesiredOffset(secondNonObjectToken, firstNonObjectToken, 0); + } + }, + + NewExpression(node) { + + // Only indent the arguments if the NewExpression has parens (e.g. `new Foo(bar)` or `new Foo()`, but not `new Foo` + if (node.arguments.length > 0 || + astUtils.isClosingParenToken(sourceCode.getLastToken(node)) && + astUtils.isOpeningParenToken(sourceCode.getLastToken(node, 1))) { + addFunctionCallIndent(node); + } + }, + + Property(node) { + if (!node.shorthand && !node.method && node.kind === "init") { + const colon = sourceCode.getFirstTokenBetween(node.key, node.value, astUtils.isColonToken); + + offsets.ignoreToken(sourceCode.getTokenAfter(colon)); + } + }, + + SwitchStatement(node) { + const openingCurly = sourceCode.getTokenAfter(node.discriminant, astUtils.isOpeningBraceToken); + const closingCurly = sourceCode.getLastToken(node); + const caseKeywords = node.cases.map(switchCase => sourceCode.getFirstToken(switchCase)); + + offsets.setDesiredOffsets([openingCurly.range[1], closingCurly.range[0]], openingCurly, options.SwitchCase); + + node.cases.forEach((switchCase, index) => { + const caseKeyword = caseKeywords[index]; + + if (!(switchCase.consequent.length === 1 && switchCase.consequent[0].type === "BlockStatement")) { + const tokenAfterCurrentCase = index === node.cases.length - 1 ? closingCurly : caseKeywords[index + 1]; + + offsets.setDesiredOffsets([caseKeyword.range[1], tokenAfterCurrentCase.range[0]], caseKeyword, 1); + } + }); + + if (node.cases.length) { + sourceCode.getTokensBetween( + node.cases[node.cases.length - 1], + closingCurly, + { includeComments: true, filter: astUtils.isCommentToken } + ).forEach(token => offsets.ignoreToken(token)); + } + }, + + TemplateLiteral(node) { + node.expressions.forEach((expression, index) => { + const previousQuasi = node.quasis[index]; + const nextQuasi = node.quasis[index + 1]; + const tokenToAlignFrom = previousQuasi.loc.start.line === previousQuasi.loc.end.line ? sourceCode.getFirstToken(previousQuasi) : null; + + offsets.setDesiredOffsets([previousQuasi.range[1], nextQuasi.range[0]], tokenToAlignFrom, 1); + offsets.setDesiredOffset(sourceCode.getFirstToken(nextQuasi), tokenToAlignFrom, 0); + }); + }, + + VariableDeclaration(node) { + const variableIndent = options.VariableDeclarator.hasOwnProperty(node.kind) ? options.VariableDeclarator[node.kind] : DEFAULT_VARIABLE_INDENT; + + if (node.declarations[node.declarations.length - 1].loc.start.line > node.loc.start.line) { + + /* + * VariableDeclarator indentation is a bit different from other forms of indentation, in that the + * indentation of an opening bracket sometimes won't match that of a closing bracket. For example, + * the following indentations are correct: + * + * var foo = { + * ok: true + * }; + * + * var foo = { + * ok: true, + * }, + * bar = 1; + * + * Account for when exiting the AST (after indentations have already been set for the nodes in + * the declaration) by manually increasing the indentation level of the tokens in this declarator + * on the same line as the start of the declaration, provided that there are declarators that + * follow this one. + */ + const firstToken = sourceCode.getFirstToken(node); + + offsets.setDesiredOffsets(node.range, firstToken, variableIndent, true); + } else { + offsets.setDesiredOffsets(node.range, sourceCode.getFirstToken(node), variableIndent); + } + const lastToken = sourceCode.getLastToken(node); + + if (astUtils.isSemicolonToken(lastToken)) { + offsets.ignoreToken(lastToken); + } + }, + + VariableDeclarator(node) { + if (node.init) { + const equalOperator = sourceCode.getTokenBefore(node.init, astUtils.isNotOpeningParenToken); + const tokenAfterOperator = sourceCode.getTokenAfter(equalOperator); + + offsets.ignoreToken(equalOperator); + offsets.ignoreToken(tokenAfterOperator); + offsets.setDesiredOffsets([tokenAfterOperator.range[0], node.range[1]], equalOperator, 1); + offsets.setDesiredOffset(equalOperator, sourceCode.getLastToken(node.id), 0); + } + }, + + "JSXAttribute[value]"(node) { + const equalsToken = sourceCode.getFirstTokenBetween(node.name, node.value, token => token.type === "Punctuator" && token.value === "="); + + offsets.setDesiredOffsets([equalsToken.range[0], node.value.range[1]], sourceCode.getFirstToken(node.name), 1); + }, + + JSXElement(node) { + if (node.closingElement) { + addElementListIndent(node.children, sourceCode.getFirstToken(node.openingElement), sourceCode.getFirstToken(node.closingElement), 1); + } + }, + + JSXOpeningElement(node) { + const firstToken = sourceCode.getFirstToken(node); + let closingToken; + + if (node.selfClosing) { + closingToken = sourceCode.getLastToken(node, { skip: 1 }); + offsets.setDesiredOffset(sourceCode.getLastToken(node), closingToken, 0); + } else { + closingToken = sourceCode.getLastToken(node); + } + offsets.setDesiredOffsets(node.name.range, sourceCode.getFirstToken(node)); + addElementListIndent(node.attributes, firstToken, closingToken, 1); + }, + + JSXClosingElement(node) { + const firstToken = sourceCode.getFirstToken(node); + + offsets.setDesiredOffsets(node.name.range, firstToken, 1); + offsets.setDesiredOffset(sourceCode.getLastToken(node), firstToken, 0); + }, + + JSXExpressionContainer(node) { + const openingCurly = sourceCode.getFirstToken(node); + const closingCurly = sourceCode.getLastToken(node); + + offsets.setDesiredOffsets( + [openingCurly.range[1], closingCurly.range[0]], + openingCurly, + 1 + ); + offsets.setDesiredOffset(closingCurly, openingCurly, 0); + } + }; + + const listenerCallQueue = []; + + /* + * To ignore the indentation of a node: + * 1. Don't call the node's listener when entering it (if it has a listener) + * 2. Call `ignoreNode` on the node sometime after exiting it and before validating offsets. + */ + const offsetListeners = lodash.mapValues( + baseOffsetListeners, + + /* + * Offset listener calls are deferred until traversal is finished, and are called as + * part of the final `Program:exit` listener. This is necessary because a node might + * be matched by multiple selectors. + * + * Example: Suppose there is an offset listener for `Identifier`, and the user has + * specified in configuration that `MemberExpression > Identifier` should be ignored. + * Due to selector specificity rules, the `Identifier` listener will get called first. However, + * if a given Identifier node is supposed to be ignored, then the `Identifier` offset listener + * should not have been called at all. Without doing extra selector matching, we don't know + * whether the Identifier matches the `MemberExpression > Identifier` selector until the + * `MemberExpression > Identifier` listener is called. + * + * To avoid this, the `Identifier` listener isn't called until traversal finishes and all + * ignored nodes are known. + */ + listener => + node => + listenerCallQueue.push({ listener, node }) + ); + + // For each ignored node selector, set up a listener to collect it into the `ignoredNodes` set. + const ignoredNodes = new Set(); + const addToIgnoredNodes = ignoredNodes.add.bind(ignoredNodes); + + const ignoredNodeListeners = options.ignoredNodes.reduce( + (listeners, ignoredSelector) => Object.assign(listeners, { [ignoredSelector]: addToIgnoredNodes }), + {} + ); + + /* + * Join the listeners, and add a listener to verify that all tokens actually have the correct indentation + * at the end. + * + * Using Object.assign will cause some offset listeners to be overwritten if the same selector also appears + * in `ignoredNodeListeners`. This isn't a problem because all of the matching nodes will be ignored, + * so those listeners wouldn't be called anyway. + */ + return Object.assign( + offsetListeners, + ignoredNodeListeners, + { + "*:exit"(node) { + + // If a node's type is nonstandard, we can't tell how its children should be offset, so ignore it. + if (!KNOWN_NODES.has(node.type)) { + ignoredNodes.add(node); + } + }, + "Program:exit"() { + + // Invoke the queued offset listeners for the nodes that aren't ignored. + listenerCallQueue + .filter(nodeInfo => !ignoredNodes.has(nodeInfo.node)) + .forEach(nodeInfo => nodeInfo.listener(nodeInfo.node)); + + // Update the offsets for ignored nodes to prevent their child tokens from being reported. + ignoredNodes.forEach(ignoreNode); + + addParensIndent(sourceCode.ast.tokens); + + /* + * Create a Map from (tokenOrComment) => (precedingToken). + * This is necessary because sourceCode.getTokenBefore does not handle a comment as an argument correctly. + */ + const precedingTokens = sourceCode.ast.comments.reduce((commentMap, comment) => { + const tokenOrCommentBefore = sourceCode.getTokenBefore(comment, { includeComments: true }); + + return commentMap.set(comment, commentMap.has(tokenOrCommentBefore) ? commentMap.get(tokenOrCommentBefore) : tokenOrCommentBefore); + }, new WeakMap()); + + sourceCode.lines.forEach((line, lineIndex) => { + const lineNumber = lineIndex + 1; + + if (!tokenInfo.firstTokensByLineNumber.has(lineNumber)) { + + // Don't check indentation on blank lines + return; + } + + const firstTokenOfLine = tokenInfo.firstTokensByLineNumber.get(lineNumber); + + if (firstTokenOfLine.loc.start.line !== lineNumber) { + + // Don't check the indentation of multi-line tokens (e.g. template literals or block comments) twice. + return; + } + + // If the token matches the expected expected indentation, don't report it. + if (validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine))) { + return; + } + + if (astUtils.isCommentToken(firstTokenOfLine)) { + const tokenBefore = precedingTokens.get(firstTokenOfLine); + const tokenAfter = tokenBefore ? sourceCode.getTokenAfter(tokenBefore) : sourceCode.ast.tokens[0]; + + // If a comment matches the expected indentation of the token immediately before or after, don't report it. + if ( + tokenBefore && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenBefore)) || + tokenAfter && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenAfter)) + ) { + return; + } + } + + // Otherwise, report the token/comment. + report(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine)); + }); + } + } + ); + } +}; diff --git a/tools/node_modules/eslint/lib/rules/init-declarations.js b/tools/node_modules/eslint/lib/rules/init-declarations.js new file mode 100644 index 0000000000..1f53f3dfc4 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/init-declarations.js @@ -0,0 +1,137 @@ +/** + * @fileoverview A rule to control the style of variable initializations. + * @author Colin Ihrig + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether or not a given node is a for loop. + * @param {ASTNode} block - A node to check. + * @returns {boolean} `true` when the node is a for loop. + */ +function isForLoop(block) { + return block.type === "ForInStatement" || + block.type === "ForOfStatement" || + block.type === "ForStatement"; +} + +/** + * Checks whether or not a given declarator node has its initializer. + * @param {ASTNode} node - A declarator node to check. + * @returns {boolean} `true` when the node has its initializer. + */ +function isInitialized(node) { + const declaration = node.parent; + const block = declaration.parent; + + if (isForLoop(block)) { + if (block.type === "ForStatement") { + return block.init === declaration; + } + return block.left === declaration; + } + return Boolean(node.init); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require or disallow initialization in variable declarations", + category: "Variables", + recommended: false + }, + + schema: { + anyOf: [ + { + type: "array", + items: [ + { + enum: ["always"] + } + ], + minItems: 0, + maxItems: 1 + }, + { + type: "array", + items: [ + { + enum: ["never"] + }, + { + type: "object", + properties: { + ignoreForLoopInit: { + type: "boolean" + } + }, + additionalProperties: false + } + ], + minItems: 0, + maxItems: 2 + } + ] + } + }, + + create(context) { + + const MODE_ALWAYS = "always", + MODE_NEVER = "never"; + + const mode = context.options[0] || MODE_ALWAYS; + const params = context.options[1] || {}; + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + "VariableDeclaration:exit"(node) { + + const kind = node.kind, + declarations = node.declarations; + + for (let i = 0; i < declarations.length; ++i) { + const declaration = declarations[i], + id = declaration.id, + initialized = isInitialized(declaration), + isIgnoredForLoop = params.ignoreForLoopInit && isForLoop(node.parent); + + if (id.type !== "Identifier") { + continue; + } + + if (mode === MODE_ALWAYS && !initialized) { + context.report({ + node: declaration, + message: "Variable '{{idName}}' should be initialized on declaration.", + data: { + idName: id.name + } + }); + } else if (mode === MODE_NEVER && kind !== "const" && initialized && !isIgnoredForLoop) { + context.report({ + node: declaration, + message: "Variable '{{idName}}' should not be initialized on declaration.", + data: { + idName: id.name + } + }); + } + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/jsx-quotes.js b/tools/node_modules/eslint/lib/rules/jsx-quotes.js new file mode 100644 index 0000000000..5653922d94 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/jsx-quotes.js @@ -0,0 +1,89 @@ +/** + * @fileoverview A rule to ensure consistent quotes used in jsx syntax. + * @author Mathias Schreck <https://github.com/lo1tuma> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Constants +//------------------------------------------------------------------------------ + +const QUOTE_SETTINGS = { + "prefer-double": { + quote: "\"", + description: "singlequote", + convert(str) { + return str.replace(/'/g, "\""); + } + }, + "prefer-single": { + quote: "'", + description: "doublequote", + convert(str) { + return str.replace(/"/g, "'"); + } + } +}; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce the consistent use of either double or single quotes in JSX attributes", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + enum: ["prefer-single", "prefer-double"] + } + ] + }, + + create(context) { + const quoteOption = context.options[0] || "prefer-double", + setting = QUOTE_SETTINGS[quoteOption]; + + /** + * Checks if the given string literal node uses the expected quotes + * @param {ASTNode} node - A string literal node. + * @returns {boolean} Whether or not the string literal used the expected quotes. + * @public + */ + function usesExpectedQuotes(node) { + return node.value.indexOf(setting.quote) !== -1 || astUtils.isSurroundedBy(node.raw, setting.quote); + } + + return { + JSXAttribute(node) { + const attributeValue = node.value; + + if (attributeValue && astUtils.isStringLiteral(attributeValue) && !usesExpectedQuotes(attributeValue)) { + context.report({ + node: attributeValue, + message: "Unexpected usage of {{description}}.", + data: { + description: setting.description + }, + fix(fixer) { + return fixer.replaceText(attributeValue, setting.convert(attributeValue.raw)); + } + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/key-spacing.js b/tools/node_modules/eslint/lib/rules/key-spacing.js new file mode 100644 index 0000000000..1c62ad4289 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/key-spacing.js @@ -0,0 +1,641 @@ +/** + * @fileoverview Rule to specify spacing of object literal keys and values + * @author Brandon Mills + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether a string contains a line terminator as defined in + * http://www.ecma-international.org/ecma-262/5.1/#sec-7.3 + * @param {string} str String to test. + * @returns {boolean} True if str contains a line terminator. + */ +function containsLineTerminator(str) { + return astUtils.LINEBREAK_MATCHER.test(str); +} + +/** + * Gets the last element of an array. + * @param {Array} arr An array. + * @returns {any} Last element of arr. + */ +function last(arr) { + return arr[arr.length - 1]; +} + +/** + * Checks whether a node is contained on a single line. + * @param {ASTNode} node AST Node being evaluated. + * @returns {boolean} True if the node is a single line. + */ +function isSingleLine(node) { + return (node.loc.end.line === node.loc.start.line); +} + +/** + * Initializes a single option property from the configuration with defaults for undefined values + * @param {Object} toOptions Object to be initialized + * @param {Object} fromOptions Object to be initialized from + * @returns {Object} The object with correctly initialized options and values + */ +function initOptionProperty(toOptions, fromOptions) { + toOptions.mode = fromOptions.mode || "strict"; + + // Set value of beforeColon + if (typeof fromOptions.beforeColon !== "undefined") { + toOptions.beforeColon = +fromOptions.beforeColon; + } else { + toOptions.beforeColon = 0; + } + + // Set value of afterColon + if (typeof fromOptions.afterColon !== "undefined") { + toOptions.afterColon = +fromOptions.afterColon; + } else { + toOptions.afterColon = 1; + } + + // Set align if exists + if (typeof fromOptions.align !== "undefined") { + if (typeof fromOptions.align === "object") { + toOptions.align = fromOptions.align; + } else { // "string" + toOptions.align = { + on: fromOptions.align, + mode: toOptions.mode, + beforeColon: toOptions.beforeColon, + afterColon: toOptions.afterColon + }; + } + } + + return toOptions; +} + +/** + * Initializes all the option values (singleLine, multiLine and align) from the configuration with defaults for undefined values + * @param {Object} toOptions Object to be initialized + * @param {Object} fromOptions Object to be initialized from + * @returns {Object} The object with correctly initialized options and values + */ +function initOptions(toOptions, fromOptions) { + if (typeof fromOptions.align === "object") { + + // Initialize the alignment configuration + toOptions.align = initOptionProperty({}, fromOptions.align); + toOptions.align.on = fromOptions.align.on || "colon"; + toOptions.align.mode = fromOptions.align.mode || "strict"; + + toOptions.multiLine = initOptionProperty({}, (fromOptions.multiLine || fromOptions)); + toOptions.singleLine = initOptionProperty({}, (fromOptions.singleLine || fromOptions)); + + } else { // string or undefined + toOptions.multiLine = initOptionProperty({}, (fromOptions.multiLine || fromOptions)); + toOptions.singleLine = initOptionProperty({}, (fromOptions.singleLine || fromOptions)); + + // If alignment options are defined in multiLine, pull them out into the general align configuration + if (toOptions.multiLine.align) { + toOptions.align = { + on: toOptions.multiLine.align.on, + mode: toOptions.multiLine.align.mode || toOptions.multiLine.mode, + beforeColon: toOptions.multiLine.align.beforeColon, + afterColon: toOptions.multiLine.align.afterColon + }; + } + } + + return toOptions; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +const messages = { + key: "{{error}} space after {{computed}}key '{{key}}'.", + value: "{{error}} space before value for {{computed}}key '{{key}}'." +}; + +module.exports = { + meta: { + docs: { + description: "enforce consistent spacing between keys and values in object literal properties", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [{ + anyOf: [ + { + type: "object", + properties: { + align: { + anyOf: [ + { + enum: ["colon", "value"] + }, + { + type: "object", + properties: { + mode: { + enum: ["strict", "minimum"] + }, + on: { + enum: ["colon", "value"] + }, + beforeColon: { + type: "boolean" + }, + afterColon: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + mode: { + enum: ["strict", "minimum"] + }, + beforeColon: { + type: "boolean" + }, + afterColon: { + type: "boolean" + } + }, + additionalProperties: false + }, + { + type: "object", + properties: { + singleLine: { + type: "object", + properties: { + mode: { + enum: ["strict", "minimum"] + }, + beforeColon: { + type: "boolean" + }, + afterColon: { + type: "boolean" + } + }, + additionalProperties: false + }, + multiLine: { + type: "object", + properties: { + align: { + anyOf: [ + { + enum: ["colon", "value"] + }, + { + type: "object", + properties: { + mode: { + enum: ["strict", "minimum"] + }, + on: { + enum: ["colon", "value"] + }, + beforeColon: { + type: "boolean" + }, + afterColon: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + mode: { + enum: ["strict", "minimum"] + }, + beforeColon: { + type: "boolean" + }, + afterColon: { + type: "boolean" + } + }, + additionalProperties: false + } + }, + additionalProperties: false + }, + { + type: "object", + properties: { + singleLine: { + type: "object", + properties: { + mode: { + enum: ["strict", "minimum"] + }, + beforeColon: { + type: "boolean" + }, + afterColon: { + type: "boolean" + } + }, + additionalProperties: false + }, + multiLine: { + type: "object", + properties: { + mode: { + enum: ["strict", "minimum"] + }, + beforeColon: { + type: "boolean" + }, + afterColon: { + type: "boolean" + } + }, + additionalProperties: false + }, + align: { + type: "object", + properties: { + mode: { + enum: ["strict", "minimum"] + }, + on: { + enum: ["colon", "value"] + }, + beforeColon: { + type: "boolean" + }, + afterColon: { + type: "boolean" + } + }, + additionalProperties: false + } + }, + additionalProperties: false + } + ] + }] + }, + + create(context) { + + /** + * OPTIONS + * "key-spacing": [2, { + * beforeColon: false, + * afterColon: true, + * align: "colon" // Optional, or "value" + * } + */ + const options = context.options[0] || {}, + ruleOptions = initOptions({}, options), + multiLineOptions = ruleOptions.multiLine, + singleLineOptions = ruleOptions.singleLine, + alignmentOptions = ruleOptions.align || null; + + const sourceCode = context.getSourceCode(); + + /** + * Checks whether a property is a member of the property group it follows. + * @param {ASTNode} lastMember The last Property known to be in the group. + * @param {ASTNode} candidate The next Property that might be in the group. + * @returns {boolean} True if the candidate property is part of the group. + */ + function continuesPropertyGroup(lastMember, candidate) { + const groupEndLine = lastMember.loc.start.line, + candidateStartLine = candidate.loc.start.line; + + if (candidateStartLine - groupEndLine <= 1) { + return true; + } + + /* + * Check that the first comment is adjacent to the end of the group, the + * last comment is adjacent to the candidate property, and that successive + * comments are adjacent to each other. + */ + const leadingComments = sourceCode.getCommentsBefore(candidate); + + if ( + leadingComments.length && + leadingComments[0].loc.start.line - groupEndLine <= 1 && + candidateStartLine - last(leadingComments).loc.end.line <= 1 + ) { + for (let i = 1; i < leadingComments.length; i++) { + if (leadingComments[i].loc.start.line - leadingComments[i - 1].loc.end.line > 1) { + return false; + } + } + return true; + } + + return false; + } + + /** + * Determines if the given property is key-value property. + * @param {ASTNode} property Property node to check. + * @returns {boolean} Whether the property is a key-value property. + */ + function isKeyValueProperty(property) { + return !( + (property.method || + property.shorthand || + property.kind !== "init" || property.type !== "Property") // Could be "ExperimentalSpreadProperty" or "SpreadProperty" + ); + } + + /** + * Starting from the given a node (a property.key node here) looks forward + * until it finds the last token before a colon punctuator and returns it. + * @param {ASTNode} node The node to start looking from. + * @returns {ASTNode} The last token before a colon punctuator. + */ + function getLastTokenBeforeColon(node) { + const colonToken = sourceCode.getTokenAfter(node, astUtils.isColonToken); + + return sourceCode.getTokenBefore(colonToken); + } + + /** + * Starting from the given a node (a property.key node here) looks forward + * until it finds the colon punctuator and returns it. + * @param {ASTNode} node The node to start looking from. + * @returns {ASTNode} The colon punctuator. + */ + function getNextColon(node) { + return sourceCode.getTokenAfter(node, astUtils.isColonToken); + } + + /** + * Gets an object literal property's key as the identifier name or string value. + * @param {ASTNode} property Property node whose key to retrieve. + * @returns {string} The property's key. + */ + function getKey(property) { + const key = property.key; + + if (property.computed) { + return sourceCode.getText().slice(key.range[0], key.range[1]); + } + + return property.key.name || property.key.value; + } + + /** + * Reports an appropriately-formatted error if spacing is incorrect on one + * side of the colon. + * @param {ASTNode} property Key-value pair in an object literal. + * @param {string} side Side being verified - either "key" or "value". + * @param {string} whitespace Actual whitespace string. + * @param {int} expected Expected whitespace length. + * @param {string} mode Value of the mode as "strict" or "minimum" + * @returns {void} + */ + function report(property, side, whitespace, expected, mode) { + const diff = whitespace.length - expected, + nextColon = getNextColon(property.key), + tokenBeforeColon = sourceCode.getTokenBefore(nextColon, { includeComments: true }), + tokenAfterColon = sourceCode.getTokenAfter(nextColon, { includeComments: true }), + isKeySide = side === "key", + locStart = isKeySide ? tokenBeforeColon.loc.start : tokenAfterColon.loc.start, + isExtra = diff > 0, + diffAbs = Math.abs(diff), + spaces = Array(diffAbs + 1).join(" "); + + if (( + diff && mode === "strict" || + diff < 0 && mode === "minimum" || + diff > 0 && !expected && mode === "minimum") && + !(expected && containsLineTerminator(whitespace)) + ) { + let fix; + + if (isExtra) { + let range; + + // Remove whitespace + if (isKeySide) { + range = [tokenBeforeColon.range[1], tokenBeforeColon.range[1] + diffAbs]; + } else { + range = [tokenAfterColon.range[0] - diffAbs, tokenAfterColon.range[0]]; + } + fix = function(fixer) { + return fixer.removeRange(range); + }; + } else { + + // Add whitespace + if (isKeySide) { + fix = function(fixer) { + return fixer.insertTextAfter(tokenBeforeColon, spaces); + }; + } else { + fix = function(fixer) { + return fixer.insertTextBefore(tokenAfterColon, spaces); + }; + } + } + + context.report({ + node: property[side], + loc: locStart, + message: messages[side], + data: { + error: isExtra ? "Extra" : "Missing", + computed: property.computed ? "computed " : "", + key: getKey(property) + }, + fix + }); + } + } + + /** + * Gets the number of characters in a key, including quotes around string + * keys and braces around computed property keys. + * @param {ASTNode} property Property of on object literal. + * @returns {int} Width of the key. + */ + function getKeyWidth(property) { + const startToken = sourceCode.getFirstToken(property); + const endToken = getLastTokenBeforeColon(property.key); + + return endToken.range[1] - startToken.range[0]; + } + + /** + * Gets the whitespace around the colon in an object literal property. + * @param {ASTNode} property Property node from an object literal. + * @returns {Object} Whitespace before and after the property's colon. + */ + function getPropertyWhitespace(property) { + const whitespace = /(\s*):(\s*)/.exec(sourceCode.getText().slice( + property.key.range[1], property.value.range[0] + )); + + if (whitespace) { + return { + beforeColon: whitespace[1], + afterColon: whitespace[2] + }; + } + return null; + } + + /** + * Creates groups of properties. + * @param {ASTNode} node ObjectExpression node being evaluated. + * @returns {Array.<ASTNode[]>} Groups of property AST node lists. + */ + function createGroups(node) { + if (node.properties.length === 1) { + return [node.properties]; + } + + return node.properties.reduce((groups, property) => { + const currentGroup = last(groups), + prev = last(currentGroup); + + if (!prev || continuesPropertyGroup(prev, property)) { + currentGroup.push(property); + } else { + groups.push([property]); + } + + return groups; + }, [ + [] + ]); + } + + /** + * Verifies correct vertical alignment of a group of properties. + * @param {ASTNode[]} properties List of Property AST nodes. + * @returns {void} + */ + function verifyGroupAlignment(properties) { + const length = properties.length, + widths = properties.map(getKeyWidth), // Width of keys, including quotes + align = alignmentOptions.on; // "value" or "colon" + let targetWidth = Math.max.apply(null, widths), + beforeColon, afterColon, mode; + + if (alignmentOptions && length > 1) { // When aligning values within a group, use the alignment configuration. + beforeColon = alignmentOptions.beforeColon; + afterColon = alignmentOptions.afterColon; + mode = alignmentOptions.mode; + } else { + beforeColon = multiLineOptions.beforeColon; + afterColon = multiLineOptions.afterColon; + mode = alignmentOptions.mode; + } + + // Conditionally include one space before or after colon + targetWidth += (align === "colon" ? beforeColon : afterColon); + + for (let i = 0; i < length; i++) { + const property = properties[i]; + const whitespace = getPropertyWhitespace(property); + + if (whitespace) { // Object literal getters/setters lack a colon + const width = widths[i]; + + if (align === "value") { + report(property, "key", whitespace.beforeColon, beforeColon, mode); + report(property, "value", whitespace.afterColon, targetWidth - width, mode); + } else { // align = "colon" + report(property, "key", whitespace.beforeColon, targetWidth - width, mode); + report(property, "value", whitespace.afterColon, afterColon, mode); + } + } + } + } + + /** + * Verifies vertical alignment, taking into account groups of properties. + * @param {ASTNode} node ObjectExpression node being evaluated. + * @returns {void} + */ + function verifyAlignment(node) { + createGroups(node).forEach(group => { + verifyGroupAlignment(group.filter(isKeyValueProperty)); + }); + } + + /** + * Verifies spacing of property conforms to specified options. + * @param {ASTNode} node Property node being evaluated. + * @param {Object} lineOptions Configured singleLine or multiLine options + * @returns {void} + */ + function verifySpacing(node, lineOptions) { + const actual = getPropertyWhitespace(node); + + if (actual) { // Object literal getters/setters lack colons + report(node, "key", actual.beforeColon, lineOptions.beforeColon, lineOptions.mode); + report(node, "value", actual.afterColon, lineOptions.afterColon, lineOptions.mode); + } + } + + /** + * Verifies spacing of each property in a list. + * @param {ASTNode[]} properties List of Property AST nodes. + * @returns {void} + */ + function verifyListSpacing(properties) { + const length = properties.length; + + for (let i = 0; i < length; i++) { + verifySpacing(properties[i], singleLineOptions); + } + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + if (alignmentOptions) { // Verify vertical alignment + + return { + ObjectExpression(node) { + if (isSingleLine(node)) { + verifyListSpacing(node.properties.filter(isKeyValueProperty)); + } else { + verifyAlignment(node); + } + } + }; + + } + + // Obey beforeColon and afterColon in each property as configured + return { + Property(node) { + verifySpacing(node, isSingleLine(node.parent) ? singleLineOptions : multiLineOptions); + } + }; + + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/keyword-spacing.js b/tools/node_modules/eslint/lib/rules/keyword-spacing.js new file mode 100644 index 0000000000..218cfd02be --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/keyword-spacing.js @@ -0,0 +1,584 @@ +/** + * @fileoverview Rule to enforce spacing before and after keywords. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"), + keywords = require("../util/keywords"); + +//------------------------------------------------------------------------------ +// Constants +//------------------------------------------------------------------------------ + +const PREV_TOKEN = /^[)\]}>]$/; +const NEXT_TOKEN = /^(?:[([{<~!]|\+\+?|--?)$/; +const PREV_TOKEN_M = /^[)\]}>*]$/; +const NEXT_TOKEN_M = /^[{*]$/; +const TEMPLATE_OPEN_PAREN = /\$\{$/; +const TEMPLATE_CLOSE_PAREN = /^\}/; +const CHECK_TYPE = /^(?:JSXElement|RegularExpression|String|Template)$/; +const KEYS = keywords.concat(["as", "async", "await", "from", "get", "let", "of", "set", "yield"]); + +// check duplications. +(function() { + KEYS.sort(); + for (let i = 1; i < KEYS.length; ++i) { + if (KEYS[i] === KEYS[i - 1]) { + throw new Error(`Duplication was found in the keyword list: ${KEYS[i]}`); + } + } +}()); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether or not a given token is a "Template" token ends with "${". + * + * @param {Token} token - A token to check. + * @returns {boolean} `true` if the token is a "Template" token ends with "${". + */ +function isOpenParenOfTemplate(token) { + return token.type === "Template" && TEMPLATE_OPEN_PAREN.test(token.value); +} + +/** + * Checks whether or not a given token is a "Template" token starts with "}". + * + * @param {Token} token - A token to check. + * @returns {boolean} `true` if the token is a "Template" token starts with "}". + */ +function isCloseParenOfTemplate(token) { + return token.type === "Template" && TEMPLATE_CLOSE_PAREN.test(token.value); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent spacing before and after keywords", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + type: "object", + properties: { + before: { type: "boolean" }, + after: { type: "boolean" }, + overrides: { + type: "object", + properties: KEYS.reduce((retv, key) => { + retv[key] = { + type: "object", + properties: { + before: { type: "boolean" }, + after: { type: "boolean" } + }, + additionalProperties: false + }; + return retv; + }, {}), + additionalProperties: false + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + /** + * Reports a given token if there are not space(s) before the token. + * + * @param {Token} token - A token to report. + * @param {RegExp|undefined} pattern - Optional. A pattern of the previous + * token to check. + * @returns {void} + */ + function expectSpaceBefore(token, pattern) { + pattern = pattern || PREV_TOKEN; + + const prevToken = sourceCode.getTokenBefore(token); + + if (prevToken && + (CHECK_TYPE.test(prevToken.type) || pattern.test(prevToken.value)) && + !isOpenParenOfTemplate(prevToken) && + astUtils.isTokenOnSameLine(prevToken, token) && + !sourceCode.isSpaceBetweenTokens(prevToken, token) + ) { + context.report({ + loc: token.loc.start, + message: "Expected space(s) before \"{{value}}\".", + data: token, + fix(fixer) { + return fixer.insertTextBefore(token, " "); + } + }); + } + } + + /** + * Reports a given token if there are space(s) before the token. + * + * @param {Token} token - A token to report. + * @param {RegExp|undefined} pattern - Optional. A pattern of the previous + * token to check. + * @returns {void} + */ + function unexpectSpaceBefore(token, pattern) { + pattern = pattern || PREV_TOKEN; + + const prevToken = sourceCode.getTokenBefore(token); + + if (prevToken && + (CHECK_TYPE.test(prevToken.type) || pattern.test(prevToken.value)) && + !isOpenParenOfTemplate(prevToken) && + astUtils.isTokenOnSameLine(prevToken, token) && + sourceCode.isSpaceBetweenTokens(prevToken, token) + ) { + context.report({ + loc: token.loc.start, + message: "Unexpected space(s) before \"{{value}}\".", + data: token, + fix(fixer) { + return fixer.removeRange([prevToken.range[1], token.range[0]]); + } + }); + } + } + + /** + * Reports a given token if there are not space(s) after the token. + * + * @param {Token} token - A token to report. + * @param {RegExp|undefined} pattern - Optional. A pattern of the next + * token to check. + * @returns {void} + */ + function expectSpaceAfter(token, pattern) { + pattern = pattern || NEXT_TOKEN; + + const nextToken = sourceCode.getTokenAfter(token); + + if (nextToken && + (CHECK_TYPE.test(nextToken.type) || pattern.test(nextToken.value)) && + !isCloseParenOfTemplate(nextToken) && + astUtils.isTokenOnSameLine(token, nextToken) && + !sourceCode.isSpaceBetweenTokens(token, nextToken) + ) { + context.report({ + loc: token.loc.start, + message: "Expected space(s) after \"{{value}}\".", + data: token, + fix(fixer) { + return fixer.insertTextAfter(token, " "); + } + }); + } + } + + /** + * Reports a given token if there are space(s) after the token. + * + * @param {Token} token - A token to report. + * @param {RegExp|undefined} pattern - Optional. A pattern of the next + * token to check. + * @returns {void} + */ + function unexpectSpaceAfter(token, pattern) { + pattern = pattern || NEXT_TOKEN; + + const nextToken = sourceCode.getTokenAfter(token); + + if (nextToken && + (CHECK_TYPE.test(nextToken.type) || pattern.test(nextToken.value)) && + !isCloseParenOfTemplate(nextToken) && + astUtils.isTokenOnSameLine(token, nextToken) && + sourceCode.isSpaceBetweenTokens(token, nextToken) + ) { + context.report({ + loc: token.loc.start, + message: "Unexpected space(s) after \"{{value}}\".", + data: token, + fix(fixer) { + return fixer.removeRange([token.range[1], nextToken.range[0]]); + } + }); + } + } + + /** + * Parses the option object and determines check methods for each keyword. + * + * @param {Object|undefined} options - The option object to parse. + * @returns {Object} - Normalized option object. + * Keys are keywords (there are for every keyword). + * Values are instances of `{"before": function, "after": function}`. + */ + function parseOptions(options) { + const before = !options || options.before !== false; + const after = !options || options.after !== false; + const defaultValue = { + before: before ? expectSpaceBefore : unexpectSpaceBefore, + after: after ? expectSpaceAfter : unexpectSpaceAfter + }; + const overrides = (options && options.overrides) || {}; + const retv = Object.create(null); + + for (let i = 0; i < KEYS.length; ++i) { + const key = KEYS[i]; + const override = overrides[key]; + + if (override) { + const thisBefore = ("before" in override) ? override.before : before; + const thisAfter = ("after" in override) ? override.after : after; + + retv[key] = { + before: thisBefore ? expectSpaceBefore : unexpectSpaceBefore, + after: thisAfter ? expectSpaceAfter : unexpectSpaceAfter + }; + } else { + retv[key] = defaultValue; + } + } + + return retv; + } + + const checkMethodMap = parseOptions(context.options[0]); + + /** + * Reports a given token if usage of spacing followed by the token is + * invalid. + * + * @param {Token} token - A token to report. + * @param {RegExp|undefined} pattern - Optional. A pattern of the previous + * token to check. + * @returns {void} + */ + function checkSpacingBefore(token, pattern) { + checkMethodMap[token.value].before(token, pattern); + } + + /** + * Reports a given token if usage of spacing preceded by the token is + * invalid. + * + * @param {Token} token - A token to report. + * @param {RegExp|undefined} pattern - Optional. A pattern of the next + * token to check. + * @returns {void} + */ + function checkSpacingAfter(token, pattern) { + checkMethodMap[token.value].after(token, pattern); + } + + /** + * Reports a given token if usage of spacing around the token is invalid. + * + * @param {Token} token - A token to report. + * @returns {void} + */ + function checkSpacingAround(token) { + checkSpacingBefore(token); + checkSpacingAfter(token); + } + + /** + * Reports the first token of a given node if the first token is a keyword + * and usage of spacing around the token is invalid. + * + * @param {ASTNode|null} node - A node to report. + * @returns {void} + */ + function checkSpacingAroundFirstToken(node) { + const firstToken = node && sourceCode.getFirstToken(node); + + if (firstToken && firstToken.type === "Keyword") { + checkSpacingAround(firstToken); + } + } + + /** + * Reports the first token of a given node if the first token is a keyword + * and usage of spacing followed by the token is invalid. + * + * This is used for unary operators (e.g. `typeof`), `function`, and `super`. + * Other rules are handling usage of spacing preceded by those keywords. + * + * @param {ASTNode|null} node - A node to report. + * @returns {void} + */ + function checkSpacingBeforeFirstToken(node) { + const firstToken = node && sourceCode.getFirstToken(node); + + if (firstToken && firstToken.type === "Keyword") { + checkSpacingBefore(firstToken); + } + } + + /** + * Reports the previous token of a given node if the token is a keyword and + * usage of spacing around the token is invalid. + * + * @param {ASTNode|null} node - A node to report. + * @returns {void} + */ + function checkSpacingAroundTokenBefore(node) { + if (node) { + const token = sourceCode.getTokenBefore(node, astUtils.isKeywordToken); + + checkSpacingAround(token); + } + } + + /** + * Reports `async` or `function` keywords of a given node if usage of + * spacing around those keywords is invalid. + * + * @param {ASTNode} node - A node to report. + * @returns {void} + */ + function checkSpacingForFunction(node) { + const firstToken = node && sourceCode.getFirstToken(node); + + if (firstToken && + ((firstToken.type === "Keyword" && firstToken.value === "function") || + firstToken.value === "async") + ) { + checkSpacingBefore(firstToken); + } + } + + /** + * Reports `class` and `extends` keywords of a given node if usage of + * spacing around those keywords is invalid. + * + * @param {ASTNode} node - A node to report. + * @returns {void} + */ + function checkSpacingForClass(node) { + checkSpacingAroundFirstToken(node); + checkSpacingAroundTokenBefore(node.superClass); + } + + /** + * Reports `if` and `else` keywords of a given node if usage of spacing + * around those keywords is invalid. + * + * @param {ASTNode} node - A node to report. + * @returns {void} + */ + function checkSpacingForIfStatement(node) { + checkSpacingAroundFirstToken(node); + checkSpacingAroundTokenBefore(node.alternate); + } + + /** + * Reports `try`, `catch`, and `finally` keywords of a given node if usage + * of spacing around those keywords is invalid. + * + * @param {ASTNode} node - A node to report. + * @returns {void} + */ + function checkSpacingForTryStatement(node) { + checkSpacingAroundFirstToken(node); + checkSpacingAroundFirstToken(node.handler); + checkSpacingAroundTokenBefore(node.finalizer); + } + + /** + * Reports `do` and `while` keywords of a given node if usage of spacing + * around those keywords is invalid. + * + * @param {ASTNode} node - A node to report. + * @returns {void} + */ + function checkSpacingForDoWhileStatement(node) { + checkSpacingAroundFirstToken(node); + checkSpacingAroundTokenBefore(node.test); + } + + /** + * Reports `for` and `in` keywords of a given node if usage of spacing + * around those keywords is invalid. + * + * @param {ASTNode} node - A node to report. + * @returns {void} + */ + function checkSpacingForForInStatement(node) { + checkSpacingAroundFirstToken(node); + checkSpacingAroundTokenBefore(node.right); + } + + /** + * Reports `for` and `of` keywords of a given node if usage of spacing + * around those keywords is invalid. + * + * @param {ASTNode} node - A node to report. + * @returns {void} + */ + function checkSpacingForForOfStatement(node) { + checkSpacingAroundFirstToken(node); + checkSpacingAround(sourceCode.getTokenBefore(node.right, astUtils.isNotOpeningParenToken)); + } + + /** + * Reports `import`, `export`, `as`, and `from` keywords of a given node if + * usage of spacing around those keywords is invalid. + * + * This rule handles the `*` token in module declarations. + * + * import*as A from "./a"; /*error Expected space(s) after "import". + * error Expected space(s) before "as". + * + * @param {ASTNode} node - A node to report. + * @returns {void} + */ + function checkSpacingForModuleDeclaration(node) { + const firstToken = sourceCode.getFirstToken(node); + + checkSpacingBefore(firstToken, PREV_TOKEN_M); + checkSpacingAfter(firstToken, NEXT_TOKEN_M); + + if (node.source) { + const fromToken = sourceCode.getTokenBefore(node.source); + + checkSpacingBefore(fromToken, PREV_TOKEN_M); + checkSpacingAfter(fromToken, NEXT_TOKEN_M); + } + } + + /** + * Reports `as` keyword of a given node if usage of spacing around this + * keyword is invalid. + * + * @param {ASTNode} node - A node to report. + * @returns {void} + */ + function checkSpacingForImportNamespaceSpecifier(node) { + const asToken = sourceCode.getFirstToken(node, 1); + + checkSpacingBefore(asToken, PREV_TOKEN_M); + } + + /** + * Reports `static`, `get`, and `set` keywords of a given node if usage of + * spacing around those keywords is invalid. + * + * @param {ASTNode} node - A node to report. + * @returns {void} + */ + function checkSpacingForProperty(node) { + if (node.static) { + checkSpacingAroundFirstToken(node); + } + if (node.kind === "get" || + node.kind === "set" || + ( + (node.method || node.type === "MethodDefinition") && + node.value.async + ) + ) { + const token = sourceCode.getTokenBefore( + node.key, + tok => { + switch (tok.value) { + case "get": + case "set": + case "async": + return true; + default: + return false; + } + } + ); + + if (!token) { + throw new Error("Failed to find token get, set, or async beside method name"); + } + + + checkSpacingAround(token); + } + } + + /** + * Reports `await` keyword of a given node if usage of spacing before + * this keyword is invalid. + * + * @param {ASTNode} node - A node to report. + * @returns {void} + */ + function checkSpacingForAwaitExpression(node) { + checkSpacingBefore(sourceCode.getFirstToken(node)); + } + + return { + + // Statements + DebuggerStatement: checkSpacingAroundFirstToken, + WithStatement: checkSpacingAroundFirstToken, + + // Statements - Control flow + BreakStatement: checkSpacingAroundFirstToken, + ContinueStatement: checkSpacingAroundFirstToken, + ReturnStatement: checkSpacingAroundFirstToken, + ThrowStatement: checkSpacingAroundFirstToken, + TryStatement: checkSpacingForTryStatement, + + // Statements - Choice + IfStatement: checkSpacingForIfStatement, + SwitchStatement: checkSpacingAroundFirstToken, + SwitchCase: checkSpacingAroundFirstToken, + + // Statements - Loops + DoWhileStatement: checkSpacingForDoWhileStatement, + ForInStatement: checkSpacingForForInStatement, + ForOfStatement: checkSpacingForForOfStatement, + ForStatement: checkSpacingAroundFirstToken, + WhileStatement: checkSpacingAroundFirstToken, + + // Statements - Declarations + ClassDeclaration: checkSpacingForClass, + ExportNamedDeclaration: checkSpacingForModuleDeclaration, + ExportDefaultDeclaration: checkSpacingAroundFirstToken, + ExportAllDeclaration: checkSpacingForModuleDeclaration, + FunctionDeclaration: checkSpacingForFunction, + ImportDeclaration: checkSpacingForModuleDeclaration, + VariableDeclaration: checkSpacingAroundFirstToken, + + // Expressions + ArrowFunctionExpression: checkSpacingForFunction, + AwaitExpression: checkSpacingForAwaitExpression, + ClassExpression: checkSpacingForClass, + FunctionExpression: checkSpacingForFunction, + NewExpression: checkSpacingBeforeFirstToken, + Super: checkSpacingBeforeFirstToken, + ThisExpression: checkSpacingBeforeFirstToken, + UnaryExpression: checkSpacingBeforeFirstToken, + YieldExpression: checkSpacingBeforeFirstToken, + + // Others + ImportNamespaceSpecifier: checkSpacingForImportNamespaceSpecifier, + MethodDefinition: checkSpacingForProperty, + Property: checkSpacingForProperty + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/line-comment-position.js b/tools/node_modules/eslint/lib/rules/line-comment-position.js new file mode 100644 index 0000000000..0df806cca8 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/line-comment-position.js @@ -0,0 +1,115 @@ +/** + * @fileoverview Rule to enforce the position of line comments + * @author Alberto RodrÃguez + */ +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce position of line comments", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + oneOf: [ + { + enum: ["above", "beside"] + }, + { + type: "object", + properties: { + position: { + enum: ["above", "beside"] + }, + ignorePattern: { + type: "string" + }, + applyDefaultPatterns: { + type: "boolean" + }, + applyDefaultIgnorePatterns: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + const options = context.options[0]; + + let above, + ignorePattern, + applyDefaultIgnorePatterns = true; + + if (!options || typeof options === "string") { + above = !options || options === "above"; + + } else { + above = options.position === "above"; + ignorePattern = options.ignorePattern; + + if (options.hasOwnProperty("applyDefaultIgnorePatterns")) { + applyDefaultIgnorePatterns = options.applyDefaultIgnorePatterns !== false; + } else { + applyDefaultIgnorePatterns = options.applyDefaultPatterns !== false; + } + } + + const defaultIgnoreRegExp = astUtils.COMMENTS_IGNORE_PATTERN; + const fallThroughRegExp = /^\s*falls?\s?through/; + const customIgnoreRegExp = new RegExp(ignorePattern); + const sourceCode = context.getSourceCode(); + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + Program() { + const comments = sourceCode.getAllComments(); + + comments.filter(token => token.type === "Line").forEach(node => { + if (applyDefaultIgnorePatterns && (defaultIgnoreRegExp.test(node.value) || fallThroughRegExp.test(node.value))) { + return; + } + + if (ignorePattern && customIgnoreRegExp.test(node.value)) { + return; + } + + const previous = sourceCode.getTokenBefore(node, { includeComments: true }); + const isOnSameLine = previous && previous.loc.end.line === node.loc.start.line; + + if (above) { + if (isOnSameLine) { + context.report({ + node, + message: "Expected comment to be above code." + }); + } + } else { + if (!isOnSameLine) { + context.report({ + node, + message: "Expected comment to be beside code." + }); + } + } + }); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/linebreak-style.js b/tools/node_modules/eslint/lib/rules/linebreak-style.js new file mode 100644 index 0000000000..907bc02ec4 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/linebreak-style.js @@ -0,0 +1,96 @@ +/** + * @fileoverview Rule to enforce a single linebreak style. + * @author Erik Mueller + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent linebreak style", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + enum: ["unix", "windows"] + } + ] + }, + + create(context) { + + const EXPECTED_LF_MSG = "Expected linebreaks to be 'LF' but found 'CRLF'.", + EXPECTED_CRLF_MSG = "Expected linebreaks to be 'CRLF' but found 'LF'."; + + const sourceCode = context.getSourceCode(); + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Builds a fix function that replaces text at the specified range in the source text. + * @param {int[]} range The range to replace + * @param {string} text The text to insert. + * @returns {Function} Fixer function + * @private + */ + function createFix(range, text) { + return function(fixer) { + return fixer.replaceTextRange(range, text); + }; + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + Program: function checkForlinebreakStyle(node) { + const linebreakStyle = context.options[0] || "unix", + expectedLF = linebreakStyle === "unix", + expectedLFChars = expectedLF ? "\n" : "\r\n", + source = sourceCode.getText(), + pattern = astUtils.createGlobalLinebreakMatcher(); + let match; + + let i = 0; + + while ((match = pattern.exec(source)) !== null) { + i++; + if (match[0] === expectedLFChars) { + continue; + } + + const index = match.index; + const range = [index, index + match[0].length]; + + context.report({ + node, + loc: { + line: i, + column: sourceCode.lines[i - 1].length + }, + message: expectedLF ? EXPECTED_LF_MSG : EXPECTED_CRLF_MSG, + fix: createFix(range, expectedLFChars) + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/lines-around-comment.js b/tools/node_modules/eslint/lib/rules/lines-around-comment.js new file mode 100644 index 0000000000..3df9cb86f4 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/lines-around-comment.js @@ -0,0 +1,397 @@ +/** + * @fileoverview Enforces empty lines around comments. + * @author Jamund Ferguson + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const lodash = require("lodash"), + astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Return an array with with any line numbers that are empty. + * @param {Array} lines An array of each line of the file. + * @returns {Array} An array of line numbers. + */ +function getEmptyLineNums(lines) { + const emptyLines = lines.map((line, i) => ({ + code: line.trim(), + num: i + 1 + })).filter(line => !line.code).map(line => line.num); + + return emptyLines; +} + +/** + * Return an array with with any line numbers that contain comments. + * @param {Array} comments An array of comment tokens. + * @returns {Array} An array of line numbers. + */ +function getCommentLineNums(comments) { + const lines = []; + + comments.forEach(token => { + const start = token.loc.start.line; + const end = token.loc.end.line; + + lines.push(start, end); + }); + return lines; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require empty lines around comments", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + type: "object", + properties: { + beforeBlockComment: { + type: "boolean" + }, + afterBlockComment: { + type: "boolean" + }, + beforeLineComment: { + type: "boolean" + }, + afterLineComment: { + type: "boolean" + }, + allowBlockStart: { + type: "boolean" + }, + allowBlockEnd: { + type: "boolean" + }, + allowClassStart: { + type: "boolean" + }, + allowClassEnd: { + type: "boolean" + }, + allowObjectStart: { + type: "boolean" + }, + allowObjectEnd: { + type: "boolean" + }, + allowArrayStart: { + type: "boolean" + }, + allowArrayEnd: { + type: "boolean" + }, + ignorePattern: { + type: "string" + }, + applyDefaultIgnorePatterns: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + const options = context.options[0] ? Object.assign({}, context.options[0]) : {}; + const ignorePattern = options.ignorePattern; + const defaultIgnoreRegExp = astUtils.COMMENTS_IGNORE_PATTERN; + const customIgnoreRegExp = new RegExp(ignorePattern); + const applyDefaultIgnorePatterns = options.applyDefaultIgnorePatterns !== false; + + + options.beforeLineComment = options.beforeLineComment || false; + options.afterLineComment = options.afterLineComment || false; + options.beforeBlockComment = typeof options.beforeBlockComment !== "undefined" ? options.beforeBlockComment : true; + options.afterBlockComment = options.afterBlockComment || false; + options.allowBlockStart = options.allowBlockStart || false; + options.allowBlockEnd = options.allowBlockEnd || false; + + const sourceCode = context.getSourceCode(); + + const lines = sourceCode.lines, + numLines = lines.length + 1, + comments = sourceCode.getAllComments(), + commentLines = getCommentLineNums(comments), + emptyLines = getEmptyLineNums(lines), + commentAndEmptyLines = commentLines.concat(emptyLines); + + /** + * Returns whether or not comments are on lines starting with or ending with code + * @param {token} token The comment token to check. + * @returns {boolean} True if the comment is not alone. + */ + function codeAroundComment(token) { + let currentToken = token; + + do { + currentToken = sourceCode.getTokenBefore(currentToken, { includeComments: true }); + } while (currentToken && astUtils.isCommentToken(currentToken)); + + if (currentToken && astUtils.isTokenOnSameLine(currentToken, token)) { + return true; + } + + currentToken = token; + do { + currentToken = sourceCode.getTokenAfter(currentToken, { includeComments: true }); + } while (currentToken && astUtils.isCommentToken(currentToken)); + + if (currentToken && astUtils.isTokenOnSameLine(token, currentToken)) { + return true; + } + + return false; + } + + /** + * Returns whether or not comments are inside a node type or not. + * @param {ASTNode} parent The Comment parent node. + * @param {string} nodeType The parent type to check against. + * @returns {boolean} True if the comment is inside nodeType. + */ + function isParentNodeType(parent, nodeType) { + return parent.type === nodeType || + (parent.body && parent.body.type === nodeType) || + (parent.consequent && parent.consequent.type === nodeType); + } + + /** + * Returns the parent node that contains the given token. + * @param {token} token The token to check. + * @returns {ASTNode} The parent node that contains the given token. + */ + function getParentNodeOfToken(token) { + return sourceCode.getNodeByRangeIndex(token.range[0]); + } + + /** + * Returns whether or not comments are at the parent start or not. + * @param {token} token The Comment token. + * @param {string} nodeType The parent type to check against. + * @returns {boolean} True if the comment is at parent start. + */ + function isCommentAtParentStart(token, nodeType) { + const parent = getParentNodeOfToken(token); + + return parent && isParentNodeType(parent, nodeType) && + token.loc.start.line - parent.loc.start.line === 1; + } + + /** + * Returns whether or not comments are at the parent end or not. + * @param {token} token The Comment token. + * @param {string} nodeType The parent type to check against. + * @returns {boolean} True if the comment is at parent end. + */ + function isCommentAtParentEnd(token, nodeType) { + const parent = getParentNodeOfToken(token); + + return parent && isParentNodeType(parent, nodeType) && + parent.loc.end.line - token.loc.end.line === 1; + } + + /** + * Returns whether or not comments are at the block start or not. + * @param {token} token The Comment token. + * @returns {boolean} True if the comment is at block start. + */ + function isCommentAtBlockStart(token) { + return isCommentAtParentStart(token, "ClassBody") || isCommentAtParentStart(token, "BlockStatement") || isCommentAtParentStart(token, "SwitchCase"); + } + + /** + * Returns whether or not comments are at the block end or not. + * @param {token} token The Comment token. + * @returns {boolean} True if the comment is at block end. + */ + function isCommentAtBlockEnd(token) { + return isCommentAtParentEnd(token, "ClassBody") || isCommentAtParentEnd(token, "BlockStatement") || isCommentAtParentEnd(token, "SwitchCase") || isCommentAtParentEnd(token, "SwitchStatement"); + } + + /** + * Returns whether or not comments are at the class start or not. + * @param {token} token The Comment token. + * @returns {boolean} True if the comment is at class start. + */ + function isCommentAtClassStart(token) { + return isCommentAtParentStart(token, "ClassBody"); + } + + /** + * Returns whether or not comments are at the class end or not. + * @param {token} token The Comment token. + * @returns {boolean} True if the comment is at class end. + */ + function isCommentAtClassEnd(token) { + return isCommentAtParentEnd(token, "ClassBody"); + } + + /** + * Returns whether or not comments are at the object start or not. + * @param {token} token The Comment token. + * @returns {boolean} True if the comment is at object start. + */ + function isCommentAtObjectStart(token) { + return isCommentAtParentStart(token, "ObjectExpression") || isCommentAtParentStart(token, "ObjectPattern"); + } + + /** + * Returns whether or not comments are at the object end or not. + * @param {token} token The Comment token. + * @returns {boolean} True if the comment is at object end. + */ + function isCommentAtObjectEnd(token) { + return isCommentAtParentEnd(token, "ObjectExpression") || isCommentAtParentEnd(token, "ObjectPattern"); + } + + /** + * Returns whether or not comments are at the array start or not. + * @param {token} token The Comment token. + * @returns {boolean} True if the comment is at array start. + */ + function isCommentAtArrayStart(token) { + return isCommentAtParentStart(token, "ArrayExpression") || isCommentAtParentStart(token, "ArrayPattern"); + } + + /** + * Returns whether or not comments are at the array end or not. + * @param {token} token The Comment token. + * @returns {boolean} True if the comment is at array end. + */ + function isCommentAtArrayEnd(token) { + return isCommentAtParentEnd(token, "ArrayExpression") || isCommentAtParentEnd(token, "ArrayPattern"); + } + + /** + * Checks if a comment token has lines around it (ignores inline comments) + * @param {token} token The Comment token. + * @param {Object} opts Options to determine the newline. + * @param {boolean} opts.after Should have a newline after this line. + * @param {boolean} opts.before Should have a newline before this line. + * @returns {void} + */ + function checkForEmptyLine(token, opts) { + if (applyDefaultIgnorePatterns && defaultIgnoreRegExp.test(token.value)) { + return; + } + + if (ignorePattern && customIgnoreRegExp.test(token.value)) { + return; + } + + let after = opts.after, + before = opts.before; + + const prevLineNum = token.loc.start.line - 1, + nextLineNum = token.loc.end.line + 1, + commentIsNotAlone = codeAroundComment(token); + + const blockStartAllowed = options.allowBlockStart && + isCommentAtBlockStart(token) && + !(options.allowClassStart === false && + isCommentAtClassStart(token)), + blockEndAllowed = options.allowBlockEnd && isCommentAtBlockEnd(token) && !(options.allowClassEnd === false && isCommentAtClassEnd(token)), + classStartAllowed = options.allowClassStart && isCommentAtClassStart(token), + classEndAllowed = options.allowClassEnd && isCommentAtClassEnd(token), + objectStartAllowed = options.allowObjectStart && isCommentAtObjectStart(token), + objectEndAllowed = options.allowObjectEnd && isCommentAtObjectEnd(token), + arrayStartAllowed = options.allowArrayStart && isCommentAtArrayStart(token), + arrayEndAllowed = options.allowArrayEnd && isCommentAtArrayEnd(token); + + const exceptionStartAllowed = blockStartAllowed || classStartAllowed || objectStartAllowed || arrayStartAllowed; + const exceptionEndAllowed = blockEndAllowed || classEndAllowed || objectEndAllowed || arrayEndAllowed; + + // ignore top of the file and bottom of the file + if (prevLineNum < 1) { + before = false; + } + if (nextLineNum >= numLines) { + after = false; + } + + // we ignore all inline comments + if (commentIsNotAlone) { + return; + } + + const previousTokenOrComment = sourceCode.getTokenBefore(token, { includeComments: true }); + const nextTokenOrComment = sourceCode.getTokenAfter(token, { includeComments: true }); + + // check for newline before + if (!exceptionStartAllowed && before && !lodash.includes(commentAndEmptyLines, prevLineNum) && + !(astUtils.isCommentToken(previousTokenOrComment) && astUtils.isTokenOnSameLine(previousTokenOrComment, token))) { + const lineStart = token.range[0] - token.loc.start.column; + const range = [lineStart, lineStart]; + + context.report({ + node: token, + message: "Expected line before comment.", + fix(fixer) { + return fixer.insertTextBeforeRange(range, "\n"); + } + }); + } + + // check for newline after + if (!exceptionEndAllowed && after && !lodash.includes(commentAndEmptyLines, nextLineNum) && + !(astUtils.isCommentToken(nextTokenOrComment) && astUtils.isTokenOnSameLine(token, nextTokenOrComment))) { + context.report({ + node: token, + message: "Expected line after comment.", + fix(fixer) { + return fixer.insertTextAfter(token, "\n"); + } + }); + } + + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + Program() { + comments.forEach(token => { + if (token.type === "Line") { + if (options.beforeLineComment || options.afterLineComment) { + checkForEmptyLine(token, { + after: options.afterLineComment, + before: options.beforeLineComment + }); + } + } else if (token.type === "Block") { + if (options.beforeBlockComment || options.afterBlockComment) { + checkForEmptyLine(token, { + after: options.afterBlockComment, + before: options.beforeBlockComment + }); + } + } + }); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/lines-around-directive.js b/tools/node_modules/eslint/lib/rules/lines-around-directive.js new file mode 100644 index 0000000000..b560009f71 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/lines-around-directive.js @@ -0,0 +1,193 @@ +/** + * @fileoverview Require or disallow newlines around directives. + * @author Kai Cataldo + * @deprecated + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require or disallow newlines around directives", + category: "Stylistic Issues", + recommended: false, + replacedBy: ["padding-line-between-statements"] + }, + schema: [{ + oneOf: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + before: { + enum: ["always", "never"] + }, + after: { + enum: ["always", "never"] + } + }, + additionalProperties: false, + minProperties: 2 + } + ] + }], + fixable: "whitespace", + deprecated: true + }, + + create(context) { + const sourceCode = context.getSourceCode(); + const config = context.options[0] || "always"; + const expectLineBefore = typeof config === "string" ? config : config.before; + const expectLineAfter = typeof config === "string" ? config : config.after; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Check if node is preceded by a blank newline. + * @param {ASTNode} node Node to check. + * @returns {boolean} Whether or not the passed in node is preceded by a blank newline. + */ + function hasNewlineBefore(node) { + const tokenBefore = sourceCode.getTokenBefore(node, { includeComments: true }); + const tokenLineBefore = tokenBefore ? tokenBefore.loc.end.line : 0; + + return node.loc.start.line - tokenLineBefore >= 2; + } + + /** + * Gets the last token of a node that is on the same line as the rest of the node. + * This will usually be the last token of the node, but it will be the second-to-last token if the node has a trailing + * semicolon on a different line. + * @param {ASTNode} node A directive node + * @returns {Token} The last token of the node on the line + */ + function getLastTokenOnLine(node) { + const lastToken = sourceCode.getLastToken(node); + const secondToLastToken = sourceCode.getTokenBefore(lastToken); + + return astUtils.isSemicolonToken(lastToken) && lastToken.loc.start.line > secondToLastToken.loc.end.line + ? secondToLastToken + : lastToken; + } + + /** + * Check if node is followed by a blank newline. + * @param {ASTNode} node Node to check. + * @returns {boolean} Whether or not the passed in node is followed by a blank newline. + */ + function hasNewlineAfter(node) { + const lastToken = getLastTokenOnLine(node); + const tokenAfter = sourceCode.getTokenAfter(lastToken, { includeComments: true }); + + return tokenAfter.loc.start.line - lastToken.loc.end.line >= 2; + } + + /** + * Report errors for newlines around directives. + * @param {ASTNode} node Node to check. + * @param {string} location Whether the error was found before or after the directive. + * @param {boolean} expected Whether or not a newline was expected or unexpected. + * @returns {void} + */ + function reportError(node, location, expected) { + context.report({ + node, + message: "{{expected}} newline {{location}} \"{{value}}\" directive.", + data: { + expected: expected ? "Expected" : "Unexpected", + value: node.expression.value, + location + }, + fix(fixer) { + const lastToken = getLastTokenOnLine(node); + + if (expected) { + return location === "before" ? fixer.insertTextBefore(node, "\n") : fixer.insertTextAfter(lastToken, "\n"); + } + return fixer.removeRange(location === "before" ? [node.range[0] - 1, node.range[0]] : [lastToken.range[1], lastToken.range[1] + 1]); + } + }); + } + + /** + * Check lines around directives in node + * @param {ASTNode} node - node to check + * @returns {void} + */ + function checkDirectives(node) { + const directives = astUtils.getDirectivePrologue(node); + + if (!directives.length) { + return; + } + + const firstDirective = directives[0]; + const leadingComments = sourceCode.getCommentsBefore(firstDirective); + + /* + * Only check before the first directive if it is preceded by a comment or if it is at the top of + * the file and expectLineBefore is set to "never". This is to not force a newline at the top of + * the file if there are no comments as well as for compatibility with padded-blocks. + */ + if (leadingComments.length) { + if (expectLineBefore === "always" && !hasNewlineBefore(firstDirective)) { + reportError(firstDirective, "before", true); + } + + if (expectLineBefore === "never" && hasNewlineBefore(firstDirective)) { + reportError(firstDirective, "before", false); + } + } else if ( + node.type === "Program" && + expectLineBefore === "never" && + !leadingComments.length && + hasNewlineBefore(firstDirective) + ) { + reportError(firstDirective, "before", false); + } + + const lastDirective = directives[directives.length - 1]; + const statements = node.type === "Program" ? node.body : node.body.body; + + /* + * Do not check after the last directive if the body only + * contains a directive prologue and isn't followed by a comment to ensure + * this rule behaves well with padded-blocks. + */ + if (lastDirective === statements[statements.length - 1] && !lastDirective.trailingComments) { + return; + } + + if (expectLineAfter === "always" && !hasNewlineAfter(lastDirective)) { + reportError(lastDirective, "after", true); + } + + if (expectLineAfter === "never" && hasNewlineAfter(lastDirective)) { + reportError(lastDirective, "after", false); + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + Program: checkDirectives, + FunctionDeclaration: checkDirectives, + FunctionExpression: checkDirectives, + ArrowFunctionExpression: checkDirectives + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/lines-between-class-members.js b/tools/node_modules/eslint/lib/rules/lines-between-class-members.js new file mode 100644 index 0000000000..85e8c69358 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/lines-between-class-members.js @@ -0,0 +1,91 @@ +/** + * @fileoverview Rule to check empty newline between class members + * @author 薛定谔的猫<hh_2013@foxmail.com> + */ +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require or disallow an empty line between class members", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + exceptAfterSingleLine: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + const options = []; + + options[0] = context.options[0] || "always"; + options[1] = context.options[1] || { exceptAfterSingleLine: false }; + + const ALWAYS_MESSAGE = "Expected blank line between class members."; + const NEVER_MESSAGE = "Unexpected blank line between class members."; + + const sourceCode = context.getSourceCode(); + + /** + * Checks if there is padding between two tokens + * @param {Token} first The first token + * @param {Token} second The second token + * @returns {boolean} True if there is at least a line between the tokens + */ + function isPaddingBetweenTokens(first, second) { + return second.loc.start.line - first.loc.end.line >= 2; + } + + return { + ClassBody(node) { + const body = node.body; + + for (let i = 0; i < body.length - 1; i++) { + const curFirst = sourceCode.getFirstToken(body[i]); + const curLast = sourceCode.getLastToken(body[i]); + const comments = sourceCode.getCommentsBefore(body[i + 1]); + const nextFirst = comments.length ? comments[0] : sourceCode.getFirstToken(body[i + 1]); + const isPadded = isPaddingBetweenTokens(curLast, nextFirst); + const isMulti = !astUtils.isTokenOnSameLine(curFirst, curLast); + const skip = !isMulti && options[1].exceptAfterSingleLine; + + + if ((options[0] === "always" && !skip && !isPadded) || + (options[0] === "never" && isPadded)) { + context.report({ + node: body[i + 1], + message: isPadded ? NEVER_MESSAGE : ALWAYS_MESSAGE, + fix(fixer) { + return isPadded + ? fixer.replaceTextRange([curLast.range[1], nextFirst.range[0]], "\n") + : fixer.insertTextAfter(curLast, "\n"); + } + }); + } + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/max-depth.js b/tools/node_modules/eslint/lib/rules/max-depth.js new file mode 100644 index 0000000000..74c13ffa9f --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/max-depth.js @@ -0,0 +1,148 @@ +/** + * @fileoverview A rule to set the maximum depth block can be nested in a function. + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce a maximum depth that blocks can be nested", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + oneOf: [ + { + type: "integer", + minimum: 0 + }, + { + type: "object", + properties: { + maximum: { + type: "integer", + minimum: 0 + }, + max: { + type: "integer", + minimum: 0 + } + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + const functionStack = [], + option = context.options[0]; + let maxDepth = 4; + + if (typeof option === "object" && option.hasOwnProperty("maximum") && typeof option.maximum === "number") { + maxDepth = option.maximum; + } + if (typeof option === "object" && option.hasOwnProperty("max") && typeof option.max === "number") { + maxDepth = option.max; + } + if (typeof option === "number") { + maxDepth = option; + } + + /** + * When parsing a new function, store it in our function stack + * @returns {void} + * @private + */ + function startFunction() { + functionStack.push(0); + } + + /** + * When parsing is done then pop out the reference + * @returns {void} + * @private + */ + function endFunction() { + functionStack.pop(); + } + + /** + * Save the block and Evaluate the node + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function pushBlock(node) { + const len = ++functionStack[functionStack.length - 1]; + + if (len > maxDepth) { + context.report({ node, message: "Blocks are nested too deeply ({{depth}}).", data: { depth: len } }); + } + } + + /** + * Pop the saved block + * @returns {void} + * @private + */ + function popBlock() { + functionStack[functionStack.length - 1]--; + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + Program: startFunction, + FunctionDeclaration: startFunction, + FunctionExpression: startFunction, + ArrowFunctionExpression: startFunction, + + IfStatement(node) { + if (node.parent.type !== "IfStatement") { + pushBlock(node); + } + }, + SwitchStatement: pushBlock, + TryStatement: pushBlock, + DoWhileStatement: pushBlock, + WhileStatement: pushBlock, + WithStatement: pushBlock, + ForStatement: pushBlock, + ForInStatement: pushBlock, + ForOfStatement: pushBlock, + + "IfStatement:exit": popBlock, + "SwitchStatement:exit": popBlock, + "TryStatement:exit": popBlock, + "DoWhileStatement:exit": popBlock, + "WhileStatement:exit": popBlock, + "WithStatement:exit": popBlock, + "ForStatement:exit": popBlock, + "ForInStatement:exit": popBlock, + "ForOfStatement:exit": popBlock, + + "FunctionDeclaration:exit": endFunction, + "FunctionExpression:exit": endFunction, + "ArrowFunctionExpression:exit": endFunction, + "Program:exit": endFunction + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/max-len.js b/tools/node_modules/eslint/lib/rules/max-len.js new file mode 100644 index 0000000000..27d549c9c5 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/max-len.js @@ -0,0 +1,365 @@ +/** + * @fileoverview Rule to check for max length on a line. + * @author Matt DuVall <http://www.mattduvall.com> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Constants +//------------------------------------------------------------------------------ + +const OPTIONS_SCHEMA = { + type: "object", + properties: { + code: { + type: "integer", + minimum: 0 + }, + comments: { + type: "integer", + minimum: 0 + }, + tabWidth: { + type: "integer", + minimum: 0 + }, + ignorePattern: { + type: "string" + }, + ignoreComments: { + type: "boolean" + }, + ignoreStrings: { + type: "boolean" + }, + ignoreUrls: { + type: "boolean" + }, + ignoreTemplateLiterals: { + type: "boolean" + }, + ignoreRegExpLiterals: { + type: "boolean" + }, + ignoreTrailingComments: { + type: "boolean" + } + }, + additionalProperties: false +}; + +const OPTIONS_OR_INTEGER_SCHEMA = { + anyOf: [ + OPTIONS_SCHEMA, + { + type: "integer", + minimum: 0 + } + ] +}; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce a maximum line length", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + OPTIONS_OR_INTEGER_SCHEMA, + OPTIONS_OR_INTEGER_SCHEMA, + OPTIONS_SCHEMA + ] + }, + + create(context) { + + /* + * Inspired by http://tools.ietf.org/html/rfc3986#appendix-B, however: + * - They're matching an entire string that we know is a URI + * - We're matching part of a string where we think there *might* be a URL + * - We're only concerned about URLs, as picking out any URI would cause + * too many false positives + * - We don't care about matching the entire URL, any small segment is fine + */ + const URL_REGEXP = /[^:/?#]:\/\/[^?#]/; + + const sourceCode = context.getSourceCode(); + + /** + * Computes the length of a line that may contain tabs. The width of each + * tab will be the number of spaces to the next tab stop. + * @param {string} line The line. + * @param {int} tabWidth The width of each tab stop in spaces. + * @returns {int} The computed line length. + * @private + */ + function computeLineLength(line, tabWidth) { + let extraCharacterCount = 0; + + line.replace(/\t/g, (match, offset) => { + const totalOffset = offset + extraCharacterCount, + previousTabStopOffset = tabWidth ? totalOffset % tabWidth : 0, + spaceCount = tabWidth - previousTabStopOffset; + + extraCharacterCount += spaceCount - 1; // -1 for the replaced tab + }); + return Array.from(line).length + extraCharacterCount; + } + + // The options object must be the last option specified… + const lastOption = context.options[context.options.length - 1]; + const options = typeof lastOption === "object" ? Object.create(lastOption) : {}; + + // …but max code length… + if (typeof context.options[0] === "number") { + options.code = context.options[0]; + } + + // …and tabWidth can be optionally specified directly as integers. + if (typeof context.options[1] === "number") { + options.tabWidth = context.options[1]; + } + + const maxLength = options.code || 80, + tabWidth = options.tabWidth || 4, + ignoreComments = options.ignoreComments || false, + ignoreStrings = options.ignoreStrings || false, + ignoreTemplateLiterals = options.ignoreTemplateLiterals || false, + ignoreRegExpLiterals = options.ignoreRegExpLiterals || false, + ignoreTrailingComments = options.ignoreTrailingComments || options.ignoreComments || false, + ignoreUrls = options.ignoreUrls || false, + maxCommentLength = options.comments; + let ignorePattern = options.ignorePattern || null; + + if (ignorePattern) { + ignorePattern = new RegExp(ignorePattern); + } + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Tells if a given comment is trailing: it starts on the current line and + * extends to or past the end of the current line. + * @param {string} line The source line we want to check for a trailing comment on + * @param {number} lineNumber The one-indexed line number for line + * @param {ASTNode} comment The comment to inspect + * @returns {boolean} If the comment is trailing on the given line + */ + function isTrailingComment(line, lineNumber, comment) { + return comment && + (comment.loc.start.line === lineNumber && lineNumber <= comment.loc.end.line) && + (comment.loc.end.line > lineNumber || comment.loc.end.column === line.length); + } + + /** + * Tells if a comment encompasses the entire line. + * @param {string} line The source line with a trailing comment + * @param {number} lineNumber The one-indexed line number this is on + * @param {ASTNode} comment The comment to remove + * @returns {boolean} If the comment covers the entire line + */ + function isFullLineComment(line, lineNumber, comment) { + const start = comment.loc.start, + end = comment.loc.end, + isFirstTokenOnLine = !line.slice(0, comment.loc.start.column).trim(); + + return comment && + (start.line < lineNumber || (start.line === lineNumber && isFirstTokenOnLine)) && + (end.line > lineNumber || (end.line === lineNumber && end.column === line.length)); + } + + /** + * Gets the line after the comment and any remaining trailing whitespace is + * stripped. + * @param {string} line The source line with a trailing comment + * @param {ASTNode} comment The comment to remove + * @returns {string} Line without comment and trailing whitepace + */ + function stripTrailingComment(line, comment) { + + // loc.column is zero-indexed + return line.slice(0, comment.loc.start.column).replace(/\s+$/, ""); + } + + /** + * Ensure that an array exists at [key] on `object`, and add `value` to it. + * + * @param {Object} object the object to mutate + * @param {string} key the object's key + * @param {*} value the value to add + * @returns {void} + * @private + */ + function ensureArrayAndPush(object, key, value) { + if (!Array.isArray(object[key])) { + object[key] = []; + } + object[key].push(value); + } + + /** + * Retrieves an array containing all strings (" or ') in the source code. + * + * @returns {ASTNode[]} An array of string nodes. + */ + function getAllStrings() { + return sourceCode.ast.tokens.filter(token => token.type === "String"); + } + + /** + * Retrieves an array containing all template literals in the source code. + * + * @returns {ASTNode[]} An array of template literal nodes. + */ + function getAllTemplateLiterals() { + return sourceCode.ast.tokens.filter(token => token.type === "Template"); + } + + + /** + * Retrieves an array containing all RegExp literals in the source code. + * + * @returns {ASTNode[]} An array of RegExp literal nodes. + */ + function getAllRegExpLiterals() { + return sourceCode.ast.tokens.filter(token => token.type === "RegularExpression"); + } + + + /** + * A reducer to group an AST node by line number, both start and end. + * + * @param {Object} acc the accumulator + * @param {ASTNode} node the AST node in question + * @returns {Object} the modified accumulator + * @private + */ + function groupByLineNumber(acc, node) { + for (let i = node.loc.start.line; i <= node.loc.end.line; ++i) { + ensureArrayAndPush(acc, i, node); + } + return acc; + } + + /** + * Check the program for max length + * @param {ASTNode} node Node to examine + * @returns {void} + * @private + */ + function checkProgramForMaxLength(node) { + + // split (honors line-ending) + const lines = sourceCode.lines, + + // list of comments to ignore + comments = ignoreComments || maxCommentLength || ignoreTrailingComments ? sourceCode.getAllComments() : []; + + // we iterate over comments in parallel with the lines + let commentsIndex = 0; + + const strings = getAllStrings(); + const stringsByLine = strings.reduce(groupByLineNumber, {}); + + const templateLiterals = getAllTemplateLiterals(); + const templateLiteralsByLine = templateLiterals.reduce(groupByLineNumber, {}); + + const regExpLiterals = getAllRegExpLiterals(); + const regExpLiteralsByLine = regExpLiterals.reduce(groupByLineNumber, {}); + + lines.forEach((line, i) => { + + // i is zero-indexed, line numbers are one-indexed + const lineNumber = i + 1; + + /* + * if we're checking comment length; we need to know whether this + * line is a comment + */ + let lineIsComment = false; + + /* + * We can short-circuit the comment checks if we're already out of + * comments to check. + */ + if (commentsIndex < comments.length) { + let comment = null; + + // iterate over comments until we find one past the current line + do { + comment = comments[++commentsIndex]; + } while (comment && comment.loc.start.line <= lineNumber); + + // and step back by one + comment = comments[--commentsIndex]; + + if (isFullLineComment(line, lineNumber, comment)) { + lineIsComment = true; + } else if (ignoreTrailingComments && isTrailingComment(line, lineNumber, comment)) { + line = stripTrailingComment(line, comment); + } + } + if (ignorePattern && ignorePattern.test(line) || + ignoreUrls && URL_REGEXP.test(line) || + ignoreStrings && stringsByLine[lineNumber] || + ignoreTemplateLiterals && templateLiteralsByLine[lineNumber] || + ignoreRegExpLiterals && regExpLiteralsByLine[lineNumber] + ) { + + // ignore this line + return; + } + + const lineLength = computeLineLength(line, tabWidth); + const commentLengthApplies = lineIsComment && maxCommentLength; + + if (lineIsComment && ignoreComments) { + return; + } + + if (commentLengthApplies) { + if (lineLength > maxCommentLength) { + context.report({ + node, + loc: { line: lineNumber, column: 0 }, + message: "Line {{lineNumber}} exceeds the maximum comment line length of {{maxCommentLength}}.", + data: { + lineNumber: i + 1, + maxCommentLength + } + }); + } + } else if (lineLength > maxLength) { + context.report({ + node, + loc: { line: lineNumber, column: 0 }, + message: "Line {{lineNumber}} exceeds the maximum line length of {{maxLength}}.", + data: { + lineNumber: i + 1, + maxLength + } + }); + } + }); + } + + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + Program: checkProgramForMaxLength + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/max-lines.js b/tools/node_modules/eslint/lib/rules/max-lines.js new file mode 100644 index 0000000000..297c75dc13 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/max-lines.js @@ -0,0 +1,144 @@ +/** + * @fileoverview enforce a maximum file length + * @author Alberto RodrÃguez + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const lodash = require("lodash"); +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce a maximum number of lines per file", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + oneOf: [ + { + type: "integer", + minimum: 0 + }, + { + type: "object", + properties: { + max: { + type: "integer", + minimum: 0 + }, + skipComments: { + type: "boolean" + }, + skipBlankLines: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + const option = context.options[0]; + let max = 300; + + if (typeof option === "object" && option.hasOwnProperty("max") && typeof option.max === "number") { + max = option.max; + } + + if (typeof option === "number") { + max = option; + } + + const skipComments = option && option.skipComments; + const skipBlankLines = option && option.skipBlankLines; + + const sourceCode = context.getSourceCode(); + + /** + * Returns whether or not a token is a comment node type + * @param {Token} token The token to check + * @returns {boolean} True if the token is a comment node + */ + function isCommentNodeType(token) { + return token && (token.type === "Block" || token.type === "Line"); + } + + /** + * Returns the line numbers of a comment that don't have any code on the same line + * @param {Node} comment The comment node to check + * @returns {int[]} The line numbers + */ + function getLinesWithoutCode(comment) { + let start = comment.loc.start.line; + let end = comment.loc.end.line; + + let token; + + token = comment; + do { + token = sourceCode.getTokenBefore(token, { includeComments: true }); + } while (isCommentNodeType(token)); + + if (token && astUtils.isTokenOnSameLine(token, comment)) { + start += 1; + } + + token = comment; + do { + token = sourceCode.getTokenAfter(token, { includeComments: true }); + } while (isCommentNodeType(token)); + + if (token && astUtils.isTokenOnSameLine(comment, token)) { + end -= 1; + } + + if (start <= end) { + return lodash.range(start, end + 1); + } + return []; + } + + return { + "Program:exit"() { + let lines = sourceCode.lines.map((text, i) => ({ lineNumber: i + 1, text })); + + if (skipBlankLines) { + lines = lines.filter(l => l.text.trim() !== ""); + } + + if (skipComments) { + const comments = sourceCode.getAllComments(); + + const commentLines = lodash.flatten(comments.map(comment => getLinesWithoutCode(comment))); + + lines = lines.filter(l => !lodash.includes(commentLines, l.lineNumber)); + } + + if (lines.length > max) { + context.report({ + loc: { line: 1, column: 0 }, + message: "File must be at most {{max}} lines long. It's {{actual}} lines long.", + data: { + max, + actual: lines.length + } + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/max-nested-callbacks.js b/tools/node_modules/eslint/lib/rules/max-nested-callbacks.js new file mode 100644 index 0000000000..a89f49ae02 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/max-nested-callbacks.js @@ -0,0 +1,112 @@ +/** + * @fileoverview Rule to enforce a maximum number of nested callbacks. + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce a maximum depth that callbacks can be nested", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + oneOf: [ + { + type: "integer", + minimum: 0 + }, + { + type: "object", + properties: { + maximum: { + type: "integer", + minimum: 0 + }, + max: { + type: "integer", + minimum: 0 + } + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + + //-------------------------------------------------------------------------- + // Constants + //-------------------------------------------------------------------------- + const option = context.options[0]; + let THRESHOLD = 10; + + if (typeof option === "object" && option.hasOwnProperty("maximum") && typeof option.maximum === "number") { + THRESHOLD = option.maximum; + } + if (typeof option === "object" && option.hasOwnProperty("max") && typeof option.max === "number") { + THRESHOLD = option.max; + } + if (typeof option === "number") { + THRESHOLD = option; + } + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + const callbackStack = []; + + /** + * Checks a given function node for too many callbacks. + * @param {ASTNode} node The node to check. + * @returns {void} + * @private + */ + function checkFunction(node) { + const parent = node.parent; + + if (parent.type === "CallExpression") { + callbackStack.push(node); + } + + if (callbackStack.length > THRESHOLD) { + const opts = { num: callbackStack.length, max: THRESHOLD }; + + context.report({ node, message: "Too many nested callbacks ({{num}}). Maximum allowed is {{max}}.", data: opts }); + } + } + + /** + * Pops the call stack. + * @returns {void} + * @private + */ + function popStack() { + callbackStack.pop(); + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + ArrowFunctionExpression: checkFunction, + "ArrowFunctionExpression:exit": popStack, + + FunctionExpression: checkFunction, + "FunctionExpression:exit": popStack + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/max-params.js b/tools/node_modules/eslint/lib/rules/max-params.js new file mode 100644 index 0000000000..85838adacc --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/max-params.js @@ -0,0 +1,96 @@ +/** + * @fileoverview Rule to flag when a function has too many parameters + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const lodash = require("lodash"); + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce a maximum number of parameters in function definitions", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + oneOf: [ + { + type: "integer", + minimum: 0 + }, + { + type: "object", + properties: { + maximum: { + type: "integer", + minimum: 0 + }, + max: { + type: "integer", + minimum: 0 + } + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + + const option = context.options[0]; + let numParams = 3; + + if (typeof option === "object" && option.hasOwnProperty("maximum") && typeof option.maximum === "number") { + numParams = option.maximum; + } + if (typeof option === "object" && option.hasOwnProperty("max") && typeof option.max === "number") { + numParams = option.max; + } + if (typeof option === "number") { + numParams = option; + } + + /** + * Checks a function to see if it has too many parameters. + * @param {ASTNode} node The node to check. + * @returns {void} + * @private + */ + function checkFunction(node) { + if (node.params.length > numParams) { + context.report({ + node, + message: "{{name}} has too many parameters ({{count}}). Maximum allowed is {{max}}.", + data: { + name: lodash.upperFirst(astUtils.getFunctionNameWithKind(node)), + count: node.params.length, + max: numParams + } + }); + } + } + + return { + FunctionDeclaration: checkFunction, + ArrowFunctionExpression: checkFunction, + FunctionExpression: checkFunction + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/max-statements-per-line.js b/tools/node_modules/eslint/lib/rules/max-statements-per-line.js new file mode 100644 index 0000000000..3d18da4ee1 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/max-statements-per-line.js @@ -0,0 +1,194 @@ +/** + * @fileoverview Specify the maximum number of statements allowed per line. + * @author Kenneth Williams + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce a maximum number of statements allowed per line", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + max: { + type: "integer", + minimum: 1 + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + const sourceCode = context.getSourceCode(), + options = context.options[0] || {}, + maxStatementsPerLine = typeof options.max !== "undefined" ? options.max : 1, + message = "This line has {{numberOfStatementsOnThisLine}} {{statements}}. Maximum allowed is {{maxStatementsPerLine}}."; + + let lastStatementLine = 0, + numberOfStatementsOnThisLine = 0, + firstExtraStatement; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + const SINGLE_CHILD_ALLOWED = /^(?:(?:DoWhile|For|ForIn|ForOf|If|Labeled|While)Statement|Export(?:Default|Named)Declaration)$/; + + /** + * Reports with the first extra statement, and clears it. + * + * @returns {void} + */ + function reportFirstExtraStatementAndClear() { + if (firstExtraStatement) { + context.report({ + node: firstExtraStatement, + message, + data: { + numberOfStatementsOnThisLine, + maxStatementsPerLine, + statements: numberOfStatementsOnThisLine === 1 ? "statement" : "statements" + } + }); + } + firstExtraStatement = null; + } + + /** + * Gets the actual last token of a given node. + * + * @param {ASTNode} node - A node to get. This is a node except EmptyStatement. + * @returns {Token} The actual last token. + */ + function getActualLastToken(node) { + return sourceCode.getLastToken(node, astUtils.isNotSemicolonToken); + } + + /** + * Addresses a given node. + * It updates the state of this rule, then reports the node if the node violated this rule. + * + * @param {ASTNode} node - A node to check. + * @returns {void} + */ + function enterStatement(node) { + const line = node.loc.start.line; + + /* + * Skip to allow non-block statements if this is direct child of control statements. + * `if (a) foo();` is counted as 1. + * But `if (a) foo(); else foo();` should be counted as 2. + */ + if (SINGLE_CHILD_ALLOWED.test(node.parent.type) && + node.parent.alternate !== node + ) { + return; + } + + // Update state. + if (line === lastStatementLine) { + numberOfStatementsOnThisLine += 1; + } else { + reportFirstExtraStatementAndClear(); + numberOfStatementsOnThisLine = 1; + lastStatementLine = line; + } + + // Reports if the node violated this rule. + if (numberOfStatementsOnThisLine === maxStatementsPerLine + 1) { + firstExtraStatement = firstExtraStatement || node; + } + } + + /** + * Updates the state of this rule with the end line of leaving node to check with the next statement. + * + * @param {ASTNode} node - A node to check. + * @returns {void} + */ + function leaveStatement(node) { + const line = getActualLastToken(node).loc.end.line; + + // Update state. + if (line !== lastStatementLine) { + reportFirstExtraStatementAndClear(); + numberOfStatementsOnThisLine = 1; + lastStatementLine = line; + } + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + BreakStatement: enterStatement, + ClassDeclaration: enterStatement, + ContinueStatement: enterStatement, + DebuggerStatement: enterStatement, + DoWhileStatement: enterStatement, + ExpressionStatement: enterStatement, + ForInStatement: enterStatement, + ForOfStatement: enterStatement, + ForStatement: enterStatement, + FunctionDeclaration: enterStatement, + IfStatement: enterStatement, + ImportDeclaration: enterStatement, + LabeledStatement: enterStatement, + ReturnStatement: enterStatement, + SwitchStatement: enterStatement, + ThrowStatement: enterStatement, + TryStatement: enterStatement, + VariableDeclaration: enterStatement, + WhileStatement: enterStatement, + WithStatement: enterStatement, + ExportNamedDeclaration: enterStatement, + ExportDefaultDeclaration: enterStatement, + ExportAllDeclaration: enterStatement, + + "BreakStatement:exit": leaveStatement, + "ClassDeclaration:exit": leaveStatement, + "ContinueStatement:exit": leaveStatement, + "DebuggerStatement:exit": leaveStatement, + "DoWhileStatement:exit": leaveStatement, + "ExpressionStatement:exit": leaveStatement, + "ForInStatement:exit": leaveStatement, + "ForOfStatement:exit": leaveStatement, + "ForStatement:exit": leaveStatement, + "FunctionDeclaration:exit": leaveStatement, + "IfStatement:exit": leaveStatement, + "ImportDeclaration:exit": leaveStatement, + "LabeledStatement:exit": leaveStatement, + "ReturnStatement:exit": leaveStatement, + "SwitchStatement:exit": leaveStatement, + "ThrowStatement:exit": leaveStatement, + "TryStatement:exit": leaveStatement, + "VariableDeclaration:exit": leaveStatement, + "WhileStatement:exit": leaveStatement, + "WithStatement:exit": leaveStatement, + "ExportNamedDeclaration:exit": leaveStatement, + "ExportDefaultDeclaration:exit": leaveStatement, + "ExportAllDeclaration:exit": leaveStatement, + "Program:exit": reportFirstExtraStatementAndClear + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/max-statements.js b/tools/node_modules/eslint/lib/rules/max-statements.js new file mode 100644 index 0000000000..f98aa3a21d --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/max-statements.js @@ -0,0 +1,170 @@ +/** + * @fileoverview A rule to set the maximum number of statements in a function. + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const lodash = require("lodash"); + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce a maximum number of statements allowed in function blocks", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + oneOf: [ + { + type: "integer", + minimum: 0 + }, + { + type: "object", + properties: { + maximum: { + type: "integer", + minimum: 0 + }, + max: { + type: "integer", + minimum: 0 + } + }, + additionalProperties: false + } + ] + }, + { + type: "object", + properties: { + ignoreTopLevelFunctions: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + const functionStack = [], + option = context.options[0], + ignoreTopLevelFunctions = context.options[1] && context.options[1].ignoreTopLevelFunctions || false, + topLevelFunctions = []; + let maxStatements = 10; + + if (typeof option === "object" && option.hasOwnProperty("maximum") && typeof option.maximum === "number") { + maxStatements = option.maximum; + } + if (typeof option === "object" && option.hasOwnProperty("max") && typeof option.max === "number") { + maxStatements = option.max; + } + if (typeof option === "number") { + maxStatements = option; + } + + /** + * Reports a node if it has too many statements + * @param {ASTNode} node node to evaluate + * @param {int} count Number of statements in node + * @param {int} max Maximum number of statements allowed + * @returns {void} + * @private + */ + function reportIfTooManyStatements(node, count, max) { + if (count > max) { + const name = lodash.upperFirst(astUtils.getFunctionNameWithKind(node)); + + context.report({ + node, + message: "{{name}} has too many statements ({{count}}). Maximum allowed is {{max}}.", + data: { name, count, max } + }); + } + } + + /** + * When parsing a new function, store it in our function stack + * @returns {void} + * @private + */ + function startFunction() { + functionStack.push(0); + } + + /** + * Evaluate the node at the end of function + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function endFunction(node) { + const count = functionStack.pop(); + + if (ignoreTopLevelFunctions && functionStack.length === 0) { + topLevelFunctions.push({ node, count }); + } else { + reportIfTooManyStatements(node, count, maxStatements); + } + } + + /** + * Increment the count of the functions + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function countStatements(node) { + functionStack[functionStack.length - 1] += node.body.length; + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + FunctionDeclaration: startFunction, + FunctionExpression: startFunction, + ArrowFunctionExpression: startFunction, + + BlockStatement: countStatements, + + "FunctionDeclaration:exit": endFunction, + "FunctionExpression:exit": endFunction, + "ArrowFunctionExpression:exit": endFunction, + + "Program:exit"() { + if (topLevelFunctions.length === 1) { + return; + } + + topLevelFunctions.forEach(element => { + const count = element.count; + const node = element.node; + + reportIfTooManyStatements(node, count, maxStatements); + }); + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/multiline-comment-style.js b/tools/node_modules/eslint/lib/rules/multiline-comment-style.js new file mode 100644 index 0000000000..db4f768443 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/multiline-comment-style.js @@ -0,0 +1,294 @@ +/** + * @fileoverview enforce a particular style for multiline comments + * @author Teddy Katz + */ +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce a particular style for multiline comments", + category: "Stylistic Issues", + recommended: false + }, + fixable: "whitespace", + schema: [{ enum: ["starred-block", "separate-lines", "bare-block"] }] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + const option = context.options[0] || "starred-block"; + + const EXPECTED_BLOCK_ERROR = "Expected a block comment instead of consecutive line comments."; + const START_NEWLINE_ERROR = "Expected a linebreak after '/*'."; + const END_NEWLINE_ERROR = "Expected a linebreak before '*/'."; + const MISSING_STAR_ERROR = "Expected a '*' at the start of this line."; + const ALIGNMENT_ERROR = "Expected this line to be aligned with the start of the comment."; + const EXPECTED_LINES_ERROR = "Expected multiple line comments instead of a block comment."; + + //---------------------------------------------------------------------- + // Helpers + //---------------------------------------------------------------------- + + /** + * Gets a list of comment lines in a group + * @param {Token[]} commentGroup A group of comments, containing either multiple line comments or a single block comment + * @returns {string[]} A list of comment lines + */ + function getCommentLines(commentGroup) { + if (commentGroup[0].type === "Line") { + return commentGroup.map(comment => comment.value); + } + return commentGroup[0].value + .split(astUtils.LINEBREAK_MATCHER) + .map(line => line.replace(/^\s*\*?/, "")); + } + + /** + * Converts a comment into starred-block form + * @param {Token} firstComment The first comment of the group being converted + * @param {string[]} commentLinesList A list of lines to appear in the new starred-block comment + * @returns {string} A representation of the comment value in starred-block form, excluding start and end markers + */ + function convertToStarredBlock(firstComment, commentLinesList) { + const initialOffset = sourceCode.text.slice(firstComment.range[0] - firstComment.loc.start.column, firstComment.range[0]); + const starredLines = commentLinesList.map(line => `${initialOffset} *${line}`); + + return `\n${starredLines.join("\n")}\n${initialOffset} `; + } + + /** + * Converts a comment into separate-line form + * @param {Token} firstComment The first comment of the group being converted + * @param {string[]} commentLinesList A list of lines to appear in the new starred-block comment + * @returns {string} A representation of the comment value in separate-line form + */ + function convertToSeparateLines(firstComment, commentLinesList) { + const initialOffset = sourceCode.text.slice(firstComment.range[0] - firstComment.loc.start.column, firstComment.range[0]); + const separateLines = commentLinesList.map(line => `// ${line.trim()}`); + + return separateLines.join(`\n${initialOffset}`); + } + + /** + * Converts a comment into bare-block form + * @param {Token} firstComment The first comment of the group being converted + * @param {string[]} commentLinesList A list of lines to appear in the new starred-block comment + * @returns {string} A representation of the comment value in bare-block form + */ + function convertToBlock(firstComment, commentLinesList) { + const initialOffset = sourceCode.text.slice(firstComment.range[0] - firstComment.loc.start.column, firstComment.range[0]); + const blockLines = commentLinesList.map(line => line.trim()); + + return `/* ${blockLines.join(`\n${initialOffset} `)} */`; + } + + /** + * Check a comment is JSDoc form + * @param {Token[]} commentGroup A group of comments, containing either multiple line comments or a single block comment + * @returns {boolean} if commentGroup is JSDoc form, return true + */ + function isJSDoc(commentGroup) { + const lines = commentGroup[0].value.split(astUtils.LINEBREAK_MATCHER); + + return commentGroup[0].type === "Block" && + /^\*\s*$/.test(lines[0]) && + lines.slice(1, -1).every(line => /^\s* /.test(line)) && + /^\s*$/.test(lines[lines.length - 1]); + } + + /** + * Each method checks a group of comments to see if it's valid according to the given option. + * @param {Token[]} commentGroup A list of comments that appear together. This will either contain a single + * block comment or multiple line comments. + * @returns {void} + */ + const commentGroupCheckers = { + "starred-block"(commentGroup) { + const commentLines = getCommentLines(commentGroup); + + if (commentLines.some(value => value.includes("*/"))) { + return; + } + + if (commentGroup.length > 1) { + context.report({ + loc: { + start: commentGroup[0].loc.start, + end: commentGroup[commentGroup.length - 1].loc.end + }, + message: EXPECTED_BLOCK_ERROR, + fix(fixer) { + const range = [commentGroup[0].range[0], commentGroup[commentGroup.length - 1].range[1]]; + const starredBlock = `/*${convertToStarredBlock(commentGroup[0], commentLines)}*/`; + + return commentLines.some(value => value.startsWith("/")) + ? null + : fixer.replaceTextRange(range, starredBlock); + } + }); + } else { + const block = commentGroup[0]; + const lines = block.value.split(astUtils.LINEBREAK_MATCHER); + const expectedLinePrefix = `${sourceCode.text.slice(block.range[0] - block.loc.start.column, block.range[0])} *`; + + if (!/^\*?\s*$/.test(lines[0])) { + const start = block.value.startsWith("*") ? block.range[0] + 1 : block.range[0]; + + context.report({ + loc: { + start: block.loc.start, + end: { line: block.loc.start.line, column: block.loc.start.column + 2 } + }, + message: START_NEWLINE_ERROR, + fix: fixer => fixer.insertTextAfterRange([start, start + 2], `\n${expectedLinePrefix}`) + }); + } + + if (!/^\s*$/.test(lines[lines.length - 1])) { + context.report({ + loc: { + start: { line: block.loc.end.line, column: block.loc.end.column - 2 }, + end: block.loc.end + }, + message: END_NEWLINE_ERROR, + fix: fixer => fixer.replaceTextRange([block.range[1] - 2, block.range[1]], `\n${expectedLinePrefix}/`) + }); + } + + for (let lineNumber = block.loc.start.line + 1; lineNumber <= block.loc.end.line; lineNumber++) { + const lineText = sourceCode.lines[lineNumber - 1]; + + if (!lineText.startsWith(expectedLinePrefix)) { + context.report({ + loc: { + start: { line: lineNumber, column: 0 }, + end: { line: lineNumber, column: sourceCode.lines[lineNumber - 1].length } + }, + message: /^\s*\*/.test(lineText) + ? ALIGNMENT_ERROR + : MISSING_STAR_ERROR, + fix(fixer) { + const lineStartIndex = sourceCode.getIndexFromLoc({ line: lineNumber, column: 0 }); + const linePrefixLength = lineText.match(/^\s*\*? ?/)[0].length; + const commentStartIndex = lineStartIndex + linePrefixLength; + + const replacementText = lineNumber === block.loc.end.line || lineText.length === linePrefixLength + ? expectedLinePrefix + : `${expectedLinePrefix} `; + + return fixer.replaceTextRange([lineStartIndex, commentStartIndex], replacementText); + } + }); + } + } + } + }, + "separate-lines"(commentGroup) { + if (!isJSDoc(commentGroup) && commentGroup[0].type === "Block") { + const commentLines = getCommentLines(commentGroup); + const block = commentGroup[0]; + const tokenAfter = sourceCode.getTokenAfter(block, { includeComments: true }); + + if (tokenAfter && block.loc.end.line === tokenAfter.loc.start.line) { + return; + } + + context.report({ + loc: { + start: block.loc.start, + end: { line: block.loc.start.line, column: block.loc.start.column + 2 } + }, + message: EXPECTED_LINES_ERROR, + fix(fixer) { + return fixer.replaceText(block, convertToSeparateLines(block, commentLines.filter(line => line))); + } + }); + } + }, + "bare-block"(commentGroup) { + if (!isJSDoc(commentGroup)) { + const commentLines = getCommentLines(commentGroup); + + // disallows consecutive line comments in favor of using a block comment. + if (commentGroup[0].type === "Line" && commentLines.length > 1 && + !commentLines.some(value => value.includes("*/"))) { + context.report({ + loc: { + start: commentGroup[0].loc.start, + end: commentGroup[commentGroup.length - 1].loc.end + }, + message: EXPECTED_BLOCK_ERROR, + fix(fixer) { + const range = [commentGroup[0].range[0], commentGroup[commentGroup.length - 1].range[1]]; + const block = convertToBlock(commentGroup[0], commentLines.filter(line => line)); + + return fixer.replaceTextRange(range, block); + } + }); + } + + // prohibits block comments from having a * at the beginning of each line. + if (commentGroup[0].type === "Block") { + const block = commentGroup[0]; + const lines = block.value.split(astUtils.LINEBREAK_MATCHER).filter(line => line.trim()); + + if (lines.length > 0 && lines.every(line => /^\s*\*/.test(line))) { + context.report({ + loc: { + start: block.loc.start, + end: { line: block.loc.start.line, column: block.loc.start.column + 2 } + }, + message: EXPECTED_BLOCK_ERROR, + fix(fixer) { + return fixer.replaceText(block, convertToBlock(block, commentLines.filter(line => line))); + } + }); + } + } + } + } + }; + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + return { + Program() { + return sourceCode.getAllComments() + .filter(comment => comment.type !== "Shebang") + .filter(comment => !astUtils.COMMENTS_IGNORE_PATTERN.test(comment.value)) + .filter(comment => { + const tokenBefore = sourceCode.getTokenBefore(comment, { includeComments: true }); + + return !tokenBefore || tokenBefore.loc.end.line < comment.loc.start.line; + }) + .reduce((commentGroups, comment, index, commentList) => { + const tokenBefore = sourceCode.getTokenBefore(comment, { includeComments: true }); + + if ( + comment.type === "Line" && + index && commentList[index - 1].type === "Line" && + tokenBefore && tokenBefore.loc.end.line === comment.loc.start.line - 1 && + tokenBefore === commentList[index - 1] + ) { + commentGroups[commentGroups.length - 1].push(comment); + } else { + commentGroups.push([comment]); + } + + return commentGroups; + }, []) + .filter(commentGroup => !(commentGroup.length === 1 && commentGroup[0].loc.start.line === commentGroup[0].loc.end.line)) + .forEach(commentGroupCheckers[option]); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/multiline-ternary.js b/tools/node_modules/eslint/lib/rules/multiline-ternary.js new file mode 100644 index 0000000000..a74f241d86 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/multiline-ternary.js @@ -0,0 +1,89 @@ +/** + * @fileoverview Enforce newlines between operands of ternary expressions + * @author Kai Cataldo + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce newlines between operands of ternary expressions", + category: "Stylistic Issues", + recommended: false + }, + schema: [ + { + enum: ["always", "always-multiline", "never"] + } + ] + }, + + create(context) { + const option = context.options[0]; + const multiline = option !== "never"; + const allowSingleLine = option === "always-multiline"; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Tests whether node is preceded by supplied tokens + * @param {ASTNode} node - node to check + * @param {ASTNode} parentNode - parent of node to report + * @param {boolean} expected - whether newline was expected or not + * @returns {void} + * @private + */ + function reportError(node, parentNode, expected) { + context.report({ + node, + message: "{{expected}} newline between {{typeOfError}} of ternary expression.", + data: { + expected: expected ? "Expected" : "Unexpected", + typeOfError: node === parentNode.test ? "test and consequent" : "consequent and alternate" + } + }); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + ConditionalExpression(node) { + const areTestAndConsequentOnSameLine = astUtils.isTokenOnSameLine(node.test, node.consequent); + const areConsequentAndAlternateOnSameLine = astUtils.isTokenOnSameLine(node.consequent, node.alternate); + + if (!multiline) { + if (!areTestAndConsequentOnSameLine) { + reportError(node.test, node, false); + } + + if (!areConsequentAndAlternateOnSameLine) { + reportError(node.consequent, node, false); + } + } else { + if (allowSingleLine && node.loc.start.line === node.loc.end.line) { + return; + } + + if (areTestAndConsequentOnSameLine) { + reportError(node.test, node, true); + } + + if (areConsequentAndAlternateOnSameLine) { + reportError(node.consequent, node, true); + } + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/new-cap.js b/tools/node_modules/eslint/lib/rules/new-cap.js new file mode 100644 index 0000000000..f01e8f90ac --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/new-cap.js @@ -0,0 +1,272 @@ +/** + * @fileoverview Rule to flag use of constructors without capital letters + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const CAPS_ALLOWED = [ + "Array", + "Boolean", + "Date", + "Error", + "Function", + "Number", + "Object", + "RegExp", + "String", + "Symbol" +]; + +/** + * Ensure that if the key is provided, it must be an array. + * @param {Object} obj Object to check with `key`. + * @param {string} key Object key to check on `obj`. + * @param {*} fallback If obj[key] is not present, this will be returned. + * @returns {string[]} Returns obj[key] if it's an Array, otherwise `fallback` + */ +function checkArray(obj, key, fallback) { + + /* istanbul ignore if */ + if (Object.prototype.hasOwnProperty.call(obj, key) && !Array.isArray(obj[key])) { + throw new TypeError(`${key}, if provided, must be an Array`); + } + return obj[key] || fallback; +} + +/** + * A reducer function to invert an array to an Object mapping the string form of the key, to `true`. + * @param {Object} map Accumulator object for the reduce. + * @param {string} key Object key to set to `true`. + * @returns {Object} Returns the updated Object for further reduction. + */ +function invert(map, key) { + map[key] = true; + return map; +} + +/** + * Creates an object with the cap is new exceptions as its keys and true as their values. + * @param {Object} config Rule configuration + * @returns {Object} Object with cap is new exceptions. + */ +function calculateCapIsNewExceptions(config) { + let capIsNewExceptions = checkArray(config, "capIsNewExceptions", CAPS_ALLOWED); + + if (capIsNewExceptions !== CAPS_ALLOWED) { + capIsNewExceptions = capIsNewExceptions.concat(CAPS_ALLOWED); + } + + return capIsNewExceptions.reduce(invert, {}); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require constructor names to begin with a capital letter", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + newIsCap: { + type: "boolean" + }, + capIsNew: { + type: "boolean" + }, + newIsCapExceptions: { + type: "array", + items: { + type: "string" + } + }, + newIsCapExceptionPattern: { + type: "string" + }, + capIsNewExceptions: { + type: "array", + items: { + type: "string" + } + }, + capIsNewExceptionPattern: { + type: "string" + }, + properties: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + const config = context.options[0] ? Object.assign({}, context.options[0]) : {}; + + config.newIsCap = config.newIsCap !== false; + config.capIsNew = config.capIsNew !== false; + const skipProperties = config.properties === false; + + const newIsCapExceptions = checkArray(config, "newIsCapExceptions", []).reduce(invert, {}); + const newIsCapExceptionPattern = config.newIsCapExceptionPattern ? new RegExp(config.newIsCapExceptionPattern) : null; + + const capIsNewExceptions = calculateCapIsNewExceptions(config); + const capIsNewExceptionPattern = config.capIsNewExceptionPattern ? new RegExp(config.capIsNewExceptionPattern) : null; + + const listeners = {}; + + const sourceCode = context.getSourceCode(); + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Get exact callee name from expression + * @param {ASTNode} node CallExpression or NewExpression node + * @returns {string} name + */ + function extractNameFromExpression(node) { + + let name = ""; + + if (node.callee.type === "MemberExpression") { + const property = node.callee.property; + + if (property.type === "Literal" && (typeof property.value === "string")) { + name = property.value; + } else if (property.type === "Identifier" && !node.callee.computed) { + name = property.name; + } + } else { + name = node.callee.name; + } + return name; + } + + /** + * Returns the capitalization state of the string - + * Whether the first character is uppercase, lowercase, or non-alphabetic + * @param {string} str String + * @returns {string} capitalization state: "non-alpha", "lower", or "upper" + */ + function getCap(str) { + const firstChar = str.charAt(0); + + const firstCharLower = firstChar.toLowerCase(); + const firstCharUpper = firstChar.toUpperCase(); + + if (firstCharLower === firstCharUpper) { + + // char has no uppercase variant, so it's non-alphabetic + return "non-alpha"; + } + if (firstChar === firstCharLower) { + return "lower"; + } + return "upper"; + + } + + /** + * Check if capitalization is allowed for a CallExpression + * @param {Object} allowedMap Object mapping calleeName to a Boolean + * @param {ASTNode} node CallExpression node + * @param {string} calleeName Capitalized callee name from a CallExpression + * @param {Object} pattern RegExp object from options pattern + * @returns {boolean} Returns true if the callee may be capitalized + */ + function isCapAllowed(allowedMap, node, calleeName, pattern) { + const sourceText = sourceCode.getText(node.callee); + + if (allowedMap[calleeName] || allowedMap[sourceText]) { + return true; + } + + if (pattern && pattern.test(sourceText)) { + return true; + } + + if (calleeName === "UTC" && node.callee.type === "MemberExpression") { + + // allow if callee is Date.UTC + return node.callee.object.type === "Identifier" && + node.callee.object.name === "Date"; + } + + return skipProperties && node.callee.type === "MemberExpression"; + } + + /** + * Reports the given message for the given node. The location will be the start of the property or the callee. + * @param {ASTNode} node CallExpression or NewExpression node. + * @param {string} message The message to report. + * @returns {void} + */ + function report(node, message) { + let callee = node.callee; + + if (callee.type === "MemberExpression") { + callee = callee.property; + } + + context.report({ node, loc: callee.loc.start, message }); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + if (config.newIsCap) { + listeners.NewExpression = function(node) { + + const constructorName = extractNameFromExpression(node); + + if (constructorName) { + const capitalization = getCap(constructorName); + const isAllowed = capitalization !== "lower" || isCapAllowed(newIsCapExceptions, node, constructorName, newIsCapExceptionPattern); + + if (!isAllowed) { + report(node, "A constructor name should not start with a lowercase letter."); + } + } + }; + } + + if (config.capIsNew) { + listeners.CallExpression = function(node) { + + const calleeName = extractNameFromExpression(node); + + if (calleeName) { + const capitalization = getCap(calleeName); + const isAllowed = capitalization !== "upper" || isCapAllowed(capIsNewExceptions, node, calleeName, capIsNewExceptionPattern); + + if (!isAllowed) { + report(node, "A function with a name starting with an uppercase letter should only be used as a constructor."); + } + } + }; + } + + return listeners; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/new-parens.js b/tools/node_modules/eslint/lib/rules/new-parens.js new file mode 100644 index 0000000000..aa0f7fe931 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/new-parens.js @@ -0,0 +1,58 @@ +/** + * @fileoverview Rule to flag when using constructor without parentheses + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require parentheses when invoking a constructor with no arguments", + category: "Stylistic Issues", + recommended: false + }, + + schema: [], + + fixable: "code" + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + return { + NewExpression(node) { + if (node.arguments.length !== 0) { + return; // shortcut: if there are arguments, there have to be parens + } + + const lastToken = sourceCode.getLastToken(node); + const hasLastParen = lastToken && astUtils.isClosingParenToken(lastToken); + const hasParens = hasLastParen && astUtils.isOpeningParenToken(sourceCode.getTokenBefore(lastToken)); + + if (!hasParens) { + context.report({ + node, + message: "Missing '()' invoking a constructor.", + fix: fixer => fixer.insertTextAfter(node, "()") + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/newline-after-var.js b/tools/node_modules/eslint/lib/rules/newline-after-var.js new file mode 100644 index 0000000000..80f73c836f --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/newline-after-var.js @@ -0,0 +1,254 @@ +/** + * @fileoverview Rule to check empty newline after "var" statement + * @author Gopal Venkatesan + * @deprecated + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require or disallow an empty line after variable declarations", + category: "Stylistic Issues", + recommended: false, + replacedBy: ["padding-line-between-statements"] + }, + + schema: [ + { + enum: ["never", "always"] + } + ], + + fixable: "whitespace", + + deprecated: true + }, + + create(context) { + + const ALWAYS_MESSAGE = "Expected blank line after variable declarations.", + NEVER_MESSAGE = "Unexpected blank line after variable declarations."; + + const sourceCode = context.getSourceCode(); + + // Default `mode` to "always". + const mode = context.options[0] === "never" ? "never" : "always"; + + // Cache starting and ending line numbers of comments for faster lookup + const commentEndLine = sourceCode.getAllComments().reduce((result, token) => { + result[token.loc.start.line] = token.loc.end.line; + return result; + }, {}); + + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Gets a token from the given node to compare line to the next statement. + * + * In general, the token is the last token of the node. However, the token is the second last token if the following conditions satisfy. + * + * - The last token is semicolon. + * - The semicolon is on a different line from the previous token of the semicolon. + * + * This behavior would address semicolon-less style code. e.g.: + * + * var foo = 1 + * + * ;(a || b).doSomething() + * + * @param {ASTNode} node - The node to get. + * @returns {Token} The token to compare line to the next statement. + */ + function getLastToken(node) { + const lastToken = sourceCode.getLastToken(node); + + if (lastToken.type === "Punctuator" && lastToken.value === ";") { + const prevToken = sourceCode.getTokenBefore(lastToken); + + if (prevToken.loc.end.line !== lastToken.loc.start.line) { + return prevToken; + } + } + + return lastToken; + } + + /** + * Determine if provided keyword is a variable declaration + * @private + * @param {string} keyword - keyword to test + * @returns {boolean} True if `keyword` is a type of var + */ + function isVar(keyword) { + return keyword === "var" || keyword === "let" || keyword === "const"; + } + + /** + * Determine if provided keyword is a variant of for specifiers + * @private + * @param {string} keyword - keyword to test + * @returns {boolean} True if `keyword` is a variant of for specifier + */ + function isForTypeSpecifier(keyword) { + return keyword === "ForStatement" || keyword === "ForInStatement" || keyword === "ForOfStatement"; + } + + /** + * Determine if provided keyword is an export specifiers + * @private + * @param {string} nodeType - nodeType to test + * @returns {boolean} True if `nodeType` is an export specifier + */ + function isExportSpecifier(nodeType) { + return nodeType === "ExportNamedDeclaration" || nodeType === "ExportSpecifier" || + nodeType === "ExportDefaultDeclaration" || nodeType === "ExportAllDeclaration"; + } + + /** + * Determine if provided node is the last of their parent block. + * @private + * @param {ASTNode} node - node to test + * @returns {boolean} True if `node` is last of their parent block. + */ + function isLastNode(node) { + const token = sourceCode.getTokenAfter(node); + + return !token || (token.type === "Punctuator" && token.value === "}"); + } + + /** + * Gets the last line of a group of consecutive comments + * @param {number} commentStartLine The starting line of the group + * @returns {number} The number of the last comment line of the group + */ + function getLastCommentLineOfBlock(commentStartLine) { + const currentCommentEnd = commentEndLine[commentStartLine]; + + return commentEndLine[currentCommentEnd + 1] ? getLastCommentLineOfBlock(currentCommentEnd + 1) : currentCommentEnd; + } + + /** + * Determine if a token starts more than one line after a comment ends + * @param {token} token The token being checked + * @param {integer} commentStartLine The line number on which the comment starts + * @returns {boolean} True if `token` does not start immediately after a comment + */ + function hasBlankLineAfterComment(token, commentStartLine) { + return token.loc.start.line > getLastCommentLineOfBlock(commentStartLine) + 1; + } + + /** + * Checks that a blank line exists after a variable declaration when mode is + * set to "always", or checks that there is no blank line when mode is set + * to "never" + * @private + * @param {ASTNode} node - `VariableDeclaration` node to test + * @returns {void} + */ + function checkForBlankLine(node) { + + /* + * lastToken is the last token on the node's line. It will usually also be the last token of the node, but it will + * sometimes be second-last if there is a semicolon on a different line. + */ + const lastToken = getLastToken(node), + + /* + * If lastToken is the last token of the node, nextToken should be the token after the node. Otherwise, nextToken + * is the last token of the node. + */ + nextToken = lastToken === sourceCode.getLastToken(node) ? sourceCode.getTokenAfter(node) : sourceCode.getLastToken(node), + nextLineNum = lastToken.loc.end.line + 1; + + // Ignore if there is no following statement + if (!nextToken) { + return; + } + + // Ignore if parent of node is a for variant + if (isForTypeSpecifier(node.parent.type)) { + return; + } + + // Ignore if parent of node is an export specifier + if (isExportSpecifier(node.parent.type)) { + return; + } + + /* + * Some coding styles use multiple `var` statements, so do nothing if + * the next token is a `var` statement. + */ + if (nextToken.type === "Keyword" && isVar(nextToken.value)) { + return; + } + + // Ignore if it is last statement in a block + if (isLastNode(node)) { + return; + } + + // Next statement is not a `var`... + const noNextLineToken = nextToken.loc.start.line > nextLineNum; + const hasNextLineComment = (typeof commentEndLine[nextLineNum] !== "undefined"); + + if (mode === "never" && noNextLineToken && !hasNextLineComment) { + context.report({ + node, + message: NEVER_MESSAGE, + data: { identifier: node.name }, + fix(fixer) { + const linesBetween = sourceCode.getText().slice(lastToken.range[1], nextToken.range[0]).split(astUtils.LINEBREAK_MATCHER); + + return fixer.replaceTextRange([lastToken.range[1], nextToken.range[0]], `${linesBetween.slice(0, -1).join("")}\n${linesBetween[linesBetween.length - 1]}`); + } + }); + } + + // Token on the next line, or comment without blank line + if ( + mode === "always" && ( + !noNextLineToken || + hasNextLineComment && !hasBlankLineAfterComment(nextToken, nextLineNum) + ) + ) { + context.report({ + node, + message: ALWAYS_MESSAGE, + data: { identifier: node.name }, + fix(fixer) { + if ((noNextLineToken ? getLastCommentLineOfBlock(nextLineNum) : lastToken.loc.end.line) === nextToken.loc.start.line) { + return fixer.insertTextBefore(nextToken, "\n\n"); + } + + return fixer.insertTextBeforeRange([nextToken.range[0] - nextToken.loc.start.column, nextToken.range[1]], "\n"); + } + }); + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + VariableDeclaration: checkForBlankLine + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/newline-before-return.js b/tools/node_modules/eslint/lib/rules/newline-before-return.js new file mode 100644 index 0000000000..02bd66db13 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/newline-before-return.js @@ -0,0 +1,210 @@ +/** + * @fileoverview Rule to require newlines before `return` statement + * @author Kai Cataldo + * @deprecated + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require an empty line before `return` statements", + category: "Stylistic Issues", + recommended: false, + replacedBy: ["padding-line-between-statements"] + }, + fixable: "whitespace", + schema: [], + deprecated: true + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Tests whether node is preceded by supplied tokens + * @param {ASTNode} node - node to check + * @param {array} testTokens - array of tokens to test against + * @returns {boolean} Whether or not the node is preceded by one of the supplied tokens + * @private + */ + function isPrecededByTokens(node, testTokens) { + const tokenBefore = sourceCode.getTokenBefore(node); + + return testTokens.some(token => tokenBefore.value === token); + } + + /** + * Checks whether node is the first node after statement or in block + * @param {ASTNode} node - node to check + * @returns {boolean} Whether or not the node is the first node after statement or in block + * @private + */ + function isFirstNode(node) { + const parentType = node.parent.type; + + if (node.parent.body) { + return Array.isArray(node.parent.body) + ? node.parent.body[0] === node + : node.parent.body === node; + } + + if (parentType === "IfStatement") { + return isPrecededByTokens(node, ["else", ")"]); + } + if (parentType === "DoWhileStatement") { + return isPrecededByTokens(node, ["do"]); + } + if (parentType === "SwitchCase") { + return isPrecededByTokens(node, [":"]); + } + return isPrecededByTokens(node, [")"]); + + } + + /** + * Returns the number of lines of comments that precede the node + * @param {ASTNode} node - node to check for overlapping comments + * @param {number} lineNumTokenBefore - line number of previous token, to check for overlapping comments + * @returns {number} Number of lines of comments that precede the node + * @private + */ + function calcCommentLines(node, lineNumTokenBefore) { + const comments = sourceCode.getCommentsBefore(node); + let numLinesComments = 0; + + if (!comments.length) { + return numLinesComments; + } + + comments.forEach(comment => { + numLinesComments++; + + if (comment.type === "Block") { + numLinesComments += comment.loc.end.line - comment.loc.start.line; + } + + // avoid counting lines with inline comments twice + if (comment.loc.start.line === lineNumTokenBefore) { + numLinesComments--; + } + + if (comment.loc.end.line === node.loc.start.line) { + numLinesComments--; + } + }); + + return numLinesComments; + } + + /** + * Returns the line number of the token before the node that is passed in as an argument + * @param {ASTNode} node - The node to use as the start of the calculation + * @returns {number} Line number of the token before `node` + * @private + */ + function getLineNumberOfTokenBefore(node) { + const tokenBefore = sourceCode.getTokenBefore(node); + let lineNumTokenBefore; + + /** + * Global return (at the beginning of a script) is a special case. + * If there is no token before `return`, then we expect no line + * break before the return. Comments are allowed to occupy lines + * before the global return, just no blank lines. + * Setting lineNumTokenBefore to zero in that case results in the + * desired behavior. + */ + if (tokenBefore) { + lineNumTokenBefore = tokenBefore.loc.end.line; + } else { + lineNumTokenBefore = 0; // global return at beginning of script + } + + return lineNumTokenBefore; + } + + /** + * Checks whether node is preceded by a newline + * @param {ASTNode} node - node to check + * @returns {boolean} Whether or not the node is preceded by a newline + * @private + */ + function hasNewlineBefore(node) { + const lineNumNode = node.loc.start.line; + const lineNumTokenBefore = getLineNumberOfTokenBefore(node); + const commentLines = calcCommentLines(node, lineNumTokenBefore); + + return (lineNumNode - lineNumTokenBefore - commentLines) > 1; + } + + /** + * Checks whether it is safe to apply a fix to a given return statement. + * + * The fix is not considered safe if the given return statement has leading comments, + * as we cannot safely determine if the newline should be added before or after the comments. + * For more information, see: https://github.com/eslint/eslint/issues/5958#issuecomment-222767211 + * + * @param {ASTNode} node - The return statement node to check. + * @returns {boolean} `true` if it can fix the node. + * @private + */ + function canFix(node) { + const leadingComments = sourceCode.getCommentsBefore(node); + const lastLeadingComment = leadingComments[leadingComments.length - 1]; + const tokenBefore = sourceCode.getTokenBefore(node); + + if (leadingComments.length === 0) { + return true; + } + + /* + * if the last leading comment ends in the same line as the previous token and + * does not share a line with the `return` node, we can consider it safe to fix. + * Example: + * function a() { + * var b; //comment + * return; + * } + */ + if (lastLeadingComment.loc.end.line === tokenBefore.loc.end.line && + lastLeadingComment.loc.end.line !== node.loc.start.line) { + return true; + } + + return false; + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + ReturnStatement(node) { + if (!isFirstNode(node) && !hasNewlineBefore(node)) { + context.report({ + node, + message: "Expected newline before return statement.", + fix(fixer) { + if (canFix(node)) { + const tokenBefore = sourceCode.getTokenBefore(node); + const newlines = node.loc.start.line === tokenBefore.loc.end.line ? "\n\n" : "\n"; + + return fixer.insertTextBefore(node, newlines); + } + return null; + } + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/newline-per-chained-call.js b/tools/node_modules/eslint/lib/rules/newline-per-chained-call.js new file mode 100644 index 0000000000..356c4baf32 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/newline-per-chained-call.js @@ -0,0 +1,103 @@ +/** + * @fileoverview Rule to ensure newline per method call when chaining calls + * @author Rajendra Patil + * @author Burak Yigit Kaya + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require a newline after each call in a method chain", + category: "Stylistic Issues", + recommended: false + }, + fixable: "whitespace", + schema: [{ + type: "object", + properties: { + ignoreChainWithDepth: { + type: "integer", + minimum: 1, + maximum: 10 + } + }, + additionalProperties: false + }] + }, + + create(context) { + + const options = context.options[0] || {}, + ignoreChainWithDepth = options.ignoreChainWithDepth || 2; + + const sourceCode = context.getSourceCode(); + + /** + * Get the prefix of a given MemberExpression node. + * If the MemberExpression node is a computed value it returns a + * left bracket. If not it returns a period. + * + * @param {ASTNode} node - A MemberExpression node to get + * @returns {string} The prefix of the node. + */ + function getPrefix(node) { + return node.computed ? "[" : "."; + } + + /** + * Gets the property text of a given MemberExpression node. + * If the text is multiline, this returns only the first line. + * + * @param {ASTNode} node - A MemberExpression node to get. + * @returns {string} The property text of the node. + */ + function getPropertyText(node) { + const prefix = getPrefix(node); + const lines = sourceCode.getText(node.property).split(astUtils.LINEBREAK_MATCHER); + const suffix = node.computed && lines.length === 1 ? "]" : ""; + + return prefix + lines[0] + suffix; + } + + return { + "CallExpression:exit"(node) { + if (!node.callee || node.callee.type !== "MemberExpression") { + return; + } + + const callee = node.callee; + let parent = callee.object; + let depth = 1; + + while (parent && parent.callee) { + depth += 1; + parent = parent.callee.object; + } + + if (depth > ignoreChainWithDepth && astUtils.isTokenOnSameLine(callee.object, callee.property)) { + context.report({ + node: callee.property, + loc: callee.property.loc.start, + message: "Expected line break before `{{callee}}`.", + data: { + callee: getPropertyText(callee) + }, + fix(fixer) { + const firstTokenAfterObject = sourceCode.getTokenAfter(callee.object, astUtils.isNotClosingParenToken); + + return fixer.insertTextBefore(firstTokenAfterObject, "\n"); + } + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-alert.js b/tools/node_modules/eslint/lib/rules/no-alert.js new file mode 100644 index 0000000000..bc1087253b --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-alert.js @@ -0,0 +1,123 @@ +/** + * @fileoverview Rule to flag use of alert, confirm, prompt + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const getPropertyName = require("../ast-utils").getStaticPropertyName; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks if the given name is a prohibited identifier. + * @param {string} name The name to check + * @returns {boolean} Whether or not the name is prohibited. + */ +function isProhibitedIdentifier(name) { + return /^(alert|confirm|prompt)$/.test(name); +} + +/** + * Reports the given node and identifier name. + * @param {RuleContext} context The ESLint rule context. + * @param {ASTNode} node The node to report on. + * @param {string} identifierName The name of the identifier. + * @returns {void} + */ +function report(context, node, identifierName) { + context.report(node, "Unexpected {{name}}.", { name: identifierName }); +} + +/** + * Finds the eslint-scope reference in the given scope. + * @param {Object} scope The scope to search. + * @param {ASTNode} node The identifier node. + * @returns {Reference|null} Returns the found reference or null if none were found. + */ +function findReference(scope, node) { + const references = scope.references.filter(reference => reference.identifier.range[0] === node.range[0] && + reference.identifier.range[1] === node.range[1]); + + if (references.length === 1) { + return references[0]; + } + return null; +} + +/** + * Checks if the given identifier node is shadowed in the given scope. + * @param {Object} scope The current scope. + * @param {string} node The identifier node to check + * @returns {boolean} Whether or not the name is shadowed. + */ +function isShadowed(scope, node) { + const reference = findReference(scope, node); + + return reference && reference.resolved && reference.resolved.defs.length > 0; +} + +/** + * Checks if the given identifier node is a ThisExpression in the global scope or the global window property. + * @param {Object} scope The current scope. + * @param {string} node The identifier node to check + * @returns {boolean} Whether or not the node is a reference to the global object. + */ +function isGlobalThisReferenceOrGlobalWindow(scope, node) { + if (scope.type === "global" && node.type === "ThisExpression") { + return true; + } + if (node.name === "window") { + return !isShadowed(scope, node); + } + + return false; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow the use of `alert`, `confirm`, and `prompt`", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + return { + CallExpression(node) { + const callee = node.callee, + currentScope = context.getScope(); + + // without window. + if (callee.type === "Identifier") { + const identifierName = callee.name; + + if (!isShadowed(currentScope, callee) && isProhibitedIdentifier(callee.name)) { + report(context, node, identifierName); + } + + } else if (callee.type === "MemberExpression" && isGlobalThisReferenceOrGlobalWindow(currentScope, callee.object)) { + const identifierName = getPropertyName(callee); + + if (isProhibitedIdentifier(identifierName)) { + report(context, node, identifierName); + } + } + + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-array-constructor.js b/tools/node_modules/eslint/lib/rules/no-array-constructor.js new file mode 100644 index 0000000000..70dc8b4cd5 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-array-constructor.js @@ -0,0 +1,47 @@ +/** + * @fileoverview Disallow construction of dense arrays using the Array constructor + * @author Matt DuVall <http://www.mattduvall.com/> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `Array` constructors", + category: "Stylistic Issues", + recommended: false + }, + + schema: [] + }, + + create(context) { + + /** + * Disallow construction of dense arrays using the Array constructor + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function check(node) { + if ( + node.arguments.length !== 1 && + node.callee.type === "Identifier" && + node.callee.name === "Array" + ) { + context.report({ node, message: "The array literal notation [] is preferrable." }); + } + } + + return { + CallExpression: check, + NewExpression: check + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-await-in-loop.js b/tools/node_modules/eslint/lib/rules/no-await-in-loop.js new file mode 100644 index 0000000000..d1ed92b704 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-await-in-loop.js @@ -0,0 +1,83 @@ +/** + * @fileoverview Rule to disallow uses of await inside of loops. + * @author Nat Mote (nmote) + */ +"use strict"; + +// Node types which are considered loops. +const loopTypes = new Set([ + "ForStatement", + "ForOfStatement", + "ForInStatement", + "WhileStatement", + "DoWhileStatement" +]); + +/* + * Node types at which we should stop looking for loops. For example, it is fine to declare an async + * function within a loop, and use await inside of that. + */ +const boundaryTypes = new Set([ + "FunctionDeclaration", + "FunctionExpression", + "ArrowFunctionExpression" +]); + +module.exports = { + meta: { + docs: { + description: "disallow `await` inside of loops", + category: "Possible Errors", + recommended: false + }, + schema: [] + }, + create(context) { + return { + AwaitExpression(node) { + const ancestors = context.getAncestors(); + + // Reverse so that we can traverse from the deepest node upwards. + ancestors.reverse(); + + /* + * Create a set of all the ancestors plus this node so that we can check + * if this use of await appears in the body of the loop as opposed to + * the right-hand side of a for...of, for example. + */ + const ancestorSet = new Set(ancestors).add(node); + + for (let i = 0; i < ancestors.length; i++) { + const ancestor = ancestors[i]; + + if (boundaryTypes.has(ancestor.type)) { + + /* + * Short-circuit out if we encounter a boundary type. Loops above + * this do not matter. + */ + return; + } + if (loopTypes.has(ancestor.type)) { + + /* + * Only report if we are actually in the body or another part that gets executed on + * every iteration. + */ + if ( + ancestorSet.has(ancestor.body) || + ancestorSet.has(ancestor.test) || + ancestorSet.has(ancestor.update) + ) { + context.report({ + node, + message: "Unexpected `await` inside a loop." + }); + return; + } + } + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-bitwise.js b/tools/node_modules/eslint/lib/rules/no-bitwise.js new file mode 100644 index 0000000000..f062ad2669 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-bitwise.js @@ -0,0 +1,111 @@ +/** + * @fileoverview Rule to flag bitwise identifiers + * @author Nicholas C. Zakas + */ + +"use strict"; + +/* + * + * Set of bitwise operators. + * + */ +const BITWISE_OPERATORS = [ + "^", "|", "&", "<<", ">>", ">>>", + "^=", "|=", "&=", "<<=", ">>=", ">>>=", + "~" +]; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow bitwise operators", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + allow: { + type: "array", + items: { + enum: BITWISE_OPERATORS + }, + uniqueItems: true + }, + int32Hint: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const options = context.options[0] || {}; + const allowed = options.allow || []; + const int32Hint = options.int32Hint === true; + + /** + * Reports an unexpected use of a bitwise operator. + * @param {ASTNode} node Node which contains the bitwise operator. + * @returns {void} + */ + function report(node) { + context.report({ node, message: "Unexpected use of '{{operator}}'.", data: { operator: node.operator } }); + } + + /** + * Checks if the given node has a bitwise operator. + * @param {ASTNode} node The node to check. + * @returns {boolean} Whether or not the node has a bitwise operator. + */ + function hasBitwiseOperator(node) { + return BITWISE_OPERATORS.indexOf(node.operator) !== -1; + } + + /** + * Checks if exceptions were provided, e.g. `{ allow: ['~', '|'] }`. + * @param {ASTNode} node The node to check. + * @returns {boolean} Whether or not the node has a bitwise operator. + */ + function allowedOperator(node) { + return allowed.indexOf(node.operator) !== -1; + } + + /** + * Checks if the given bitwise operator is used for integer typecasting, i.e. "|0" + * @param {ASTNode} node The node to check. + * @returns {boolean} whether the node is used in integer typecasting. + */ + function isInt32Hint(node) { + return int32Hint && node.operator === "|" && node.right && + node.right.type === "Literal" && node.right.value === 0; + } + + /** + * Report if the given node contains a bitwise operator. + * @param {ASTNode} node The node to check. + * @returns {void} + */ + function checkNodeForBitwiseOperator(node) { + if (hasBitwiseOperator(node) && !allowedOperator(node) && !isInt32Hint(node)) { + report(node); + } + } + + return { + AssignmentExpression: checkNodeForBitwiseOperator, + BinaryExpression: checkNodeForBitwiseOperator, + UnaryExpression: checkNodeForBitwiseOperator + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-buffer-constructor.js b/tools/node_modules/eslint/lib/rules/no-buffer-constructor.js new file mode 100644 index 0000000000..1521ff2847 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-buffer-constructor.js @@ -0,0 +1,37 @@ +/** + * @fileoverview disallow use of the Buffer() constructor + * @author Teddy Katz + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow use of the Buffer() constructor", + category: "Node.js and CommonJS", + recommended: false + }, + schema: [] + }, + + create(context) { + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + return { + "CallExpression[callee.name='Buffer'], NewExpression[callee.name='Buffer']"(node) { + context.report({ + node, + message: "{{example}} is deprecated. Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe() instead.", + data: { example: node.type === "CallExpression" ? "Buffer()" : "new Buffer()" } + }); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-caller.js b/tools/node_modules/eslint/lib/rules/no-caller.js new file mode 100644 index 0000000000..55a37b7d86 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-caller.js @@ -0,0 +1,39 @@ +/** + * @fileoverview Rule to flag use of arguments.callee and arguments.caller. + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow the use of `arguments.caller` or `arguments.callee`", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + + return { + + MemberExpression(node) { + const objectName = node.object.name, + propertyName = node.property.name; + + if (objectName === "arguments" && !node.computed && propertyName && propertyName.match(/^calle[er]$/)) { + context.report({ node, message: "Avoid arguments.{{property}}.", data: { property: propertyName } }); + } + + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-case-declarations.js b/tools/node_modules/eslint/lib/rules/no-case-declarations.js new file mode 100644 index 0000000000..e801c6bb6e --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-case-declarations.js @@ -0,0 +1,57 @@ +/** + * @fileoverview Rule to flag use of an lexical declarations inside a case clause + * @author Erik Arvidsson + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow lexical declarations in case clauses", + category: "Best Practices", + recommended: true + }, + + schema: [] + }, + + create(context) { + + /** + * Checks whether or not a node is a lexical declaration. + * @param {ASTNode} node A direct child statement of a switch case. + * @returns {boolean} Whether or not the node is a lexical declaration. + */ + function isLexicalDeclaration(node) { + switch (node.type) { + case "FunctionDeclaration": + case "ClassDeclaration": + return true; + case "VariableDeclaration": + return node.kind !== "var"; + default: + return false; + } + } + + return { + SwitchCase(node) { + for (let i = 0; i < node.consequent.length; i++) { + const statement = node.consequent[i]; + + if (isLexicalDeclaration(statement)) { + context.report({ + node, + message: "Unexpected lexical declaration in case block." + }); + } + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-catch-shadow.js b/tools/node_modules/eslint/lib/rules/no-catch-shadow.js new file mode 100644 index 0000000000..bef61902e9 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-catch-shadow.js @@ -0,0 +1,69 @@ +/** + * @fileoverview Rule to flag variable leak in CatchClauses in IE 8 and earlier + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `catch` clause parameters from shadowing variables in the outer scope", + category: "Variables", + recommended: false + }, + + schema: [] + }, + + create(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Check if the parameters are been shadowed + * @param {Object} scope current scope + * @param {string} name parameter name + * @returns {boolean} True is its been shadowed + */ + function paramIsShadowing(scope, name) { + return astUtils.getVariableByName(scope, name) !== null; + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + + CatchClause(node) { + let scope = context.getScope(); + + /* + * When ecmaVersion >= 6, CatchClause creates its own scope + * so start from one upper scope to exclude the current node + */ + if (scope.block === node) { + scope = scope.upper; + } + + if (paramIsShadowing(scope, node.param.name)) { + context.report({ node, message: "Value of '{{name}}' may be overwritten in IE 8 and earlier.", data: { name: node.param.name } }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-class-assign.js b/tools/node_modules/eslint/lib/rules/no-class-assign.js new file mode 100644 index 0000000000..4b0443abc7 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-class-assign.js @@ -0,0 +1,54 @@ +/** + * @fileoverview A rule to disallow modifying variables of class declarations + * @author Toru Nagashima + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow reassigning class members", + category: "ECMAScript 6", + recommended: true + }, + + schema: [] + }, + + create(context) { + + /** + * Finds and reports references that are non initializer and writable. + * @param {Variable} variable - A variable to check. + * @returns {void} + */ + function checkVariable(variable) { + astUtils.getModifyingReferences(variable.references).forEach(reference => { + context.report({ node: reference.identifier, message: "'{{name}}' is a class.", data: { name: reference.identifier.name } }); + + }); + } + + /** + * Finds and reports references that are non initializer and writable. + * @param {ASTNode} node - A ClassDeclaration/ClassExpression node to check. + * @returns {void} + */ + function checkForClass(node) { + context.getDeclaredVariables(node).forEach(checkVariable); + } + + return { + ClassDeclaration: checkForClass, + ClassExpression: checkForClass + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-compare-neg-zero.js b/tools/node_modules/eslint/lib/rules/no-compare-neg-zero.js new file mode 100644 index 0000000000..604e221919 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-compare-neg-zero.js @@ -0,0 +1,53 @@ +/** + * @fileoverview The rule should warn against code that tries to compare against -0. + * @author Aladdin-ADD <hh_2013@foxmail.com> + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow comparing against -0", + category: "Possible Errors", + recommended: true + }, + fixable: null, + schema: [] + }, + + create(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Checks a given node is -0 + * + * @param {ASTNode} node - A node to check. + * @returns {boolean} `true` if the node is -0. + */ + function isNegZero(node) { + return node.type === "UnaryExpression" && node.operator === "-" && node.argument.type === "Literal" && node.argument.value === 0; + } + const OPERATORS_TO_CHECK = new Set([">", ">=", "<", "<=", "==", "===", "!=", "!=="]); + + return { + BinaryExpression(node) { + if (OPERATORS_TO_CHECK.has(node.operator)) { + if (isNegZero(node.left) || isNegZero(node.right)) { + context.report({ + node, + message: "Do not use the '{{operator}}' operator to compare against -0.", + data: { operator: node.operator } + }); + } + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-cond-assign.js b/tools/node_modules/eslint/lib/rules/no-cond-assign.js new file mode 100644 index 0000000000..7c031c13f0 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-cond-assign.js @@ -0,0 +1,139 @@ +/** + * @fileoverview Rule to flag assignment in a conditional statement's test expression + * @author Stephen Murray <spmurrayzzz> + */ +"use strict"; + +const astUtils = require("../ast-utils"); + +const NODE_DESCRIPTIONS = { + DoWhileStatement: "a 'do...while' statement", + ForStatement: "a 'for' statement", + IfStatement: "an 'if' statement", + WhileStatement: "a 'while' statement" +}; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow assignment operators in conditional expressions", + category: "Possible Errors", + recommended: true + }, + + schema: [ + { + enum: ["except-parens", "always"] + } + ] + }, + + create(context) { + + const prohibitAssign = (context.options[0] || "except-parens"); + + const sourceCode = context.getSourceCode(); + + /** + * Check whether an AST node is the test expression for a conditional statement. + * @param {!Object} node The node to test. + * @returns {boolean} `true` if the node is the text expression for a conditional statement; otherwise, `false`. + */ + function isConditionalTestExpression(node) { + return node.parent && + node.parent.test && + node === node.parent.test; + } + + /** + * Given an AST node, perform a bottom-up search for the first ancestor that represents a conditional statement. + * @param {!Object} node The node to use at the start of the search. + * @returns {?Object} The closest ancestor node that represents a conditional statement. + */ + function findConditionalAncestor(node) { + let currentAncestor = node; + + do { + if (isConditionalTestExpression(currentAncestor)) { + return currentAncestor.parent; + } + } while ((currentAncestor = currentAncestor.parent) && !astUtils.isFunction(currentAncestor)); + + return null; + } + + /** + * Check whether the code represented by an AST node is enclosed in two sets of parentheses. + * @param {!Object} node The node to test. + * @returns {boolean} `true` if the code is enclosed in two sets of parentheses; otherwise, `false`. + */ + function isParenthesisedTwice(node) { + const previousToken = sourceCode.getTokenBefore(node, 1), + nextToken = sourceCode.getTokenAfter(node, 1); + + return astUtils.isParenthesised(sourceCode, node) && + astUtils.isOpeningParenToken(previousToken) && previousToken.range[1] <= node.range[0] && + astUtils.isClosingParenToken(nextToken) && nextToken.range[0] >= node.range[1]; + } + + /** + * Check a conditional statement's test expression for top-level assignments that are not enclosed in parentheses. + * @param {!Object} node The node for the conditional statement. + * @returns {void} + */ + function testForAssign(node) { + if (node.test && + (node.test.type === "AssignmentExpression") && + (node.type === "ForStatement" + ? !astUtils.isParenthesised(sourceCode, node.test) + : !isParenthesisedTwice(node.test) + ) + ) { + + // must match JSHint's error message + context.report({ + node, + loc: node.test.loc.start, + message: "Expected a conditional expression and instead saw an assignment." + }); + } + } + + /** + * Check whether an assignment expression is descended from a conditional statement's test expression. + * @param {!Object} node The node for the assignment expression. + * @returns {void} + */ + function testForConditionalAncestor(node) { + const ancestor = findConditionalAncestor(node); + + if (ancestor) { + context.report({ + node: ancestor, + message: "Unexpected assignment within {{type}}.", + data: { + type: NODE_DESCRIPTIONS[ancestor.type] || ancestor.type + } + }); + } + } + + if (prohibitAssign === "always") { + return { + AssignmentExpression: testForConditionalAncestor + }; + } + + return { + DoWhileStatement: testForAssign, + ForStatement: testForAssign, + IfStatement: testForAssign, + WhileStatement: testForAssign + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-confusing-arrow.js b/tools/node_modules/eslint/lib/rules/no-confusing-arrow.js new file mode 100644 index 0000000000..fc69ca39a9 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-confusing-arrow.js @@ -0,0 +1,76 @@ +/** + * @fileoverview A rule to warn against using arrow functions when they could be + * confused with comparisions + * @author Jxck <https://github.com/Jxck> + */ + +"use strict"; + +const astUtils = require("../ast-utils.js"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether or not a node is a conditional expression. + * @param {ASTNode} node - node to test + * @returns {boolean} `true` if the node is a conditional expression. + */ +function isConditional(node) { + return node && node.type === "ConditionalExpression"; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow arrow functions where they could be confused with comparisons", + category: "ECMAScript 6", + recommended: false + }, + + fixable: "code", + + schema: [{ + type: "object", + properties: { + allowParens: { type: "boolean" } + }, + additionalProperties: false + }] + }, + + create(context) { + const config = context.options[0] || {}; + const sourceCode = context.getSourceCode(); + + /** + * Reports if an arrow function contains an ambiguous conditional. + * @param {ASTNode} node - A node to check and report. + * @returns {void} + */ + function checkArrowFunc(node) { + const body = node.body; + + if (isConditional(body) && !(config.allowParens && astUtils.isParenthesised(sourceCode, body))) { + context.report({ + node, + message: "Arrow function used ambiguously with a conditional expression.", + fix(fixer) { + + // if `allowParens` is not set to true dont bother wrapping in parens + return config.allowParens && fixer.replaceText(node.body, `(${sourceCode.getText(node.body)})`); + } + }); + } + } + + return { + ArrowFunctionExpression: checkArrowFunc + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-console.js b/tools/node_modules/eslint/lib/rules/no-console.js new file mode 100644 index 0000000000..f5a3a235e6 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-console.js @@ -0,0 +1,131 @@ +/** + * @fileoverview Rule to flag use of console object + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow the use of `console`", + category: "Possible Errors", + recommended: true + }, + + schema: [ + { + type: "object", + properties: { + allow: { + type: "array", + items: { + type: "string" + }, + minItems: 1, + uniqueItems: true + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const options = context.options[0] || {}; + const allowed = options.allow || []; + + /** + * Checks whether the given reference is 'console' or not. + * + * @param {eslint-scope.Reference} reference - The reference to check. + * @returns {boolean} `true` if the reference is 'console'. + */ + function isConsole(reference) { + const id = reference.identifier; + + return id && id.name === "console"; + } + + /** + * Checks whether the property name of the given MemberExpression node + * is allowed by options or not. + * + * @param {ASTNode} node - The MemberExpression node to check. + * @returns {boolean} `true` if the property name of the node is allowed. + */ + function isAllowed(node) { + const propertyName = astUtils.getStaticPropertyName(node); + + return propertyName && allowed.indexOf(propertyName) !== -1; + } + + /** + * Checks whether the given reference is a member access which is not + * allowed by options or not. + * + * @param {eslint-scope.Reference} reference - The reference to check. + * @returns {boolean} `true` if the reference is a member access which + * is not allowed by options. + */ + function isMemberAccessExceptAllowed(reference) { + const node = reference.identifier; + const parent = node.parent; + + return ( + parent.type === "MemberExpression" && + parent.object === node && + !isAllowed(parent) + ); + } + + /** + * Reports the given reference as a violation. + * + * @param {eslint-scope.Reference} reference - The reference to report. + * @returns {void} + */ + function report(reference) { + const node = reference.identifier.parent; + + context.report({ + node, + loc: node.loc, + message: "Unexpected console statement." + }); + } + + return { + "Program:exit"() { + const scope = context.getScope(); + const consoleVar = astUtils.getVariableByName(scope, "console"); + const shadowed = consoleVar && consoleVar.defs.length > 0; + + /* + * 'scope.through' includes all references to undefined + * variables. If the variable 'console' is not defined, it uses + * 'scope.through'. + */ + const references = consoleVar + ? consoleVar.references + : scope.through.filter(isConsole); + + if (!shadowed) { + references + .filter(isMemberAccessExceptAllowed) + .forEach(report); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-const-assign.js b/tools/node_modules/eslint/lib/rules/no-const-assign.js new file mode 100644 index 0000000000..db1848a981 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-const-assign.js @@ -0,0 +1,47 @@ +/** + * @fileoverview A rule to disallow modifying variables that are declared using `const` + * @author Toru Nagashima + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow reassigning `const` variables", + category: "ECMAScript 6", + recommended: true + }, + + schema: [] + }, + + create(context) { + + /** + * Finds and reports references that are non initializer and writable. + * @param {Variable} variable - A variable to check. + * @returns {void} + */ + function checkVariable(variable) { + astUtils.getModifyingReferences(variable.references).forEach(reference => { + context.report({ node: reference.identifier, message: "'{{name}}' is constant.", data: { name: reference.identifier.name } }); + }); + } + + return { + VariableDeclaration(node) { + if (node.kind === "const") { + context.getDeclaredVariables(node).forEach(checkVariable); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-constant-condition.js b/tools/node_modules/eslint/lib/rules/no-constant-condition.js new file mode 100644 index 0000000000..0cd445dfdb --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-constant-condition.js @@ -0,0 +1,210 @@ +/** + * @fileoverview Rule to flag use constant conditions + * @author Christian Schulz <http://rndm.de> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow constant expressions in conditions", + category: "Possible Errors", + recommended: true + }, + + schema: [ + { + type: "object", + properties: { + checkLoops: { + type: "boolean" + } + }, + additionalProperties: false + } + + ] + }, + + create(context) { + const options = context.options[0] || {}, + checkLoops = options.checkLoops !== false, + loopSetStack = []; + + let loopsInCurrentScope = new Set(); + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + + /** + * Checks if a branch node of LogicalExpression short circuits the whole condition + * @param {ASTNode} node The branch of main condition which needs to be checked + * @param {string} operator The operator of the main LogicalExpression. + * @returns {boolean} true when condition short circuits whole condition + */ + function isLogicalIdentity(node, operator) { + switch (node.type) { + case "Literal": + return (operator === "||" && node.value === true) || + (operator === "&&" && node.value === false); + + case "UnaryExpression": + return (operator === "&&" && node.operator === "void"); + + case "LogicalExpression": + return isLogicalIdentity(node.left, node.operator) || + isLogicalIdentity(node.right, node.operator); + + // no default + } + return false; + } + + /** + * Checks if a node has a constant truthiness value. + * @param {ASTNode} node The AST node to check. + * @param {boolean} inBooleanPosition `false` if checking branch of a condition. + * `true` in all other cases + * @returns {Bool} true when node's truthiness is constant + * @private + */ + function isConstant(node, inBooleanPosition) { + switch (node.type) { + case "Literal": + case "ArrowFunctionExpression": + case "FunctionExpression": + case "ObjectExpression": + case "ArrayExpression": + return true; + + case "UnaryExpression": + if (node.operator === "void") { + return true; + } + + return (node.operator === "typeof" && inBooleanPosition) || + isConstant(node.argument, true); + + case "BinaryExpression": + return isConstant(node.left, false) && + isConstant(node.right, false) && + node.operator !== "in"; + + case "LogicalExpression": { + const isLeftConstant = isConstant(node.left, inBooleanPosition); + const isRightConstant = isConstant(node.right, inBooleanPosition); + const isLeftShortCircuit = (isLeftConstant && isLogicalIdentity(node.left, node.operator)); + const isRightShortCircuit = (isRightConstant && isLogicalIdentity(node.right, node.operator)); + + return (isLeftConstant && isRightConstant) || isLeftShortCircuit || isRightShortCircuit; + } + + case "AssignmentExpression": + return (node.operator === "=") && isConstant(node.right, inBooleanPosition); + + case "SequenceExpression": + return isConstant(node.expressions[node.expressions.length - 1], inBooleanPosition); + + // no default + } + return false; + } + + /** + * Tracks when the given node contains a constant condition. + * @param {ASTNode} node The AST node to check. + * @returns {void} + * @private + */ + function trackConstantConditionLoop(node) { + if (node.test && isConstant(node.test, true)) { + loopsInCurrentScope.add(node); + } + } + + /** + * Reports when the set contains the given constant condition node + * @param {ASTNode} node The AST node to check. + * @returns {void} + * @private + */ + function checkConstantConditionLoopInSet(node) { + if (loopsInCurrentScope.has(node)) { + loopsInCurrentScope.delete(node); + context.report({ node: node.test, message: "Unexpected constant condition." }); + } + } + + /** + * Reports when the given node contains a constant condition. + * @param {ASTNode} node The AST node to check. + * @returns {void} + * @private + */ + function reportIfConstant(node) { + if (node.test && isConstant(node.test, true)) { + context.report({ node: node.test, message: "Unexpected constant condition." }); + } + } + + /** + * Stores current set of constant loops in loopSetStack temporarily + * and uses a new set to track constant loops + * @returns {void} + * @private + */ + function enterFunction() { + loopSetStack.push(loopsInCurrentScope); + loopsInCurrentScope = new Set(); + } + + /** + * Reports when the set still contains stored constant conditions + * @param {ASTNode} node The AST node to check. + * @returns {void} + * @private + */ + function exitFunction() { + loopsInCurrentScope = loopSetStack.pop(); + } + + /** + * Checks node when checkLoops option is enabled + * @param {ASTNode} node The AST node to check. + * @returns {void} + * @private + */ + function checkLoop(node) { + if (checkLoops) { + trackConstantConditionLoop(node); + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + ConditionalExpression: reportIfConstant, + IfStatement: reportIfConstant, + WhileStatement: checkLoop, + "WhileStatement:exit": checkConstantConditionLoopInSet, + DoWhileStatement: checkLoop, + "DoWhileStatement:exit": checkConstantConditionLoopInSet, + ForStatement: checkLoop, + "ForStatement > .test": node => checkLoop(node.parent), + "ForStatement:exit": checkConstantConditionLoopInSet, + FunctionDeclaration: enterFunction, + "FunctionDeclaration:exit": exitFunction, + YieldExpression: () => loopsInCurrentScope.clear() + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-continue.js b/tools/node_modules/eslint/lib/rules/no-continue.js new file mode 100644 index 0000000000..2615fba13e --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-continue.js @@ -0,0 +1,32 @@ +/** + * @fileoverview Rule to flag use of continue statement + * @author Borislav Zhivkov + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `continue` statements", + category: "Stylistic Issues", + recommended: false + }, + + schema: [] + }, + + create(context) { + + return { + ContinueStatement(node) { + context.report({ node, message: "Unexpected use of continue statement." }); + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-control-regex.js b/tools/node_modules/eslint/lib/rules/no-control-regex.js new file mode 100644 index 0000000000..14981f4ab1 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-control-regex.js @@ -0,0 +1,127 @@ +/** + * @fileoverview Rule to forbid control charactes from regular expressions. + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow control characters in regular expressions", + category: "Possible Errors", + recommended: true + }, + + schema: [] + }, + + create(context) { + + /** + * Get the regex expression + * @param {ASTNode} node node to evaluate + * @returns {*} Regex if found else null + * @private + */ + function getRegExp(node) { + if (node.value instanceof RegExp) { + return node.value; + } + if (typeof node.value === "string") { + + const parent = context.getAncestors().pop(); + + if ((parent.type === "NewExpression" || parent.type === "CallExpression") && + parent.callee.type === "Identifier" && parent.callee.name === "RegExp" + ) { + + // there could be an invalid regular expression string + try { + return new RegExp(node.value); + } catch (ex) { + return null; + } + } + } + + return null; + } + + + const controlChar = /[\x00-\x1f]/g; // eslint-disable-line no-control-regex + const consecutiveSlashes = /\\+/g; + const consecutiveSlashesAtEnd = /\\+$/g; + const stringControlChar = /\\x[01][0-9a-f]/ig; + const stringControlCharWithoutSlash = /x[01][0-9a-f]/ig; + + /** + * Return a list of the control characters in the given regex string + * @param {string} regexStr regex as string to check + * @returns {array} returns a list of found control characters on given string + * @private + */ + function getControlCharacters(regexStr) { + + // check control characters, if RegExp object used + const controlChars = regexStr.match(controlChar) || []; + + let stringControlChars = []; + + // check substr, if regex literal used + const subStrIndex = regexStr.search(stringControlChar); + + if (subStrIndex > -1) { + + // is it escaped, check backslash count + const possibleEscapeCharacters = regexStr.slice(0, subStrIndex).match(consecutiveSlashesAtEnd); + + const hasControlChars = possibleEscapeCharacters === null || !(possibleEscapeCharacters[0].length % 2); + + if (hasControlChars) { + stringControlChars = regexStr.slice(subStrIndex, -1) + .split(consecutiveSlashes) + .filter(Boolean) + .map(x => { + const match = x.match(stringControlCharWithoutSlash) || [x]; + + return `\\${match[0]}`; + }); + } + } + + return controlChars.map(x => { + const hexCode = `0${x.charCodeAt(0).toString(16)}`.slice(-2); + + return `\\x${hexCode}`; + }).concat(stringControlChars); + } + + return { + Literal(node) { + const regex = getRegExp(node); + + if (regex) { + const computedValue = regex.toString(); + + const controlCharacters = getControlCharacters(computedValue); + + if (controlCharacters.length > 0) { + context.report({ + node, + message: "Unexpected control character(s) in regular expression: {{controlChars}}.", + data: { + controlChars: controlCharacters.join(", ") + } + }); + } + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-debugger.js b/tools/node_modules/eslint/lib/rules/no-debugger.js new file mode 100644 index 0000000000..d79cb18166 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-debugger.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Rule to flag use of a debugger statement + * @author Nicholas C. Zakas + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow the use of `debugger`", + category: "Possible Errors", + recommended: true + }, + fixable: "code", + schema: [] + }, + + create(context) { + + return { + DebuggerStatement(node) { + context.report({ + node, + message: "Unexpected 'debugger' statement.", + fix(fixer) { + if (astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type)) { + return fixer.remove(node); + } + return null; + } + }); + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-delete-var.js b/tools/node_modules/eslint/lib/rules/no-delete-var.js new file mode 100644 index 0000000000..adc1b5bb9c --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-delete-var.js @@ -0,0 +1,35 @@ +/** + * @fileoverview Rule to flag when deleting variables + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow deleting variables", + category: "Variables", + recommended: true + }, + + schema: [] + }, + + create(context) { + + return { + + UnaryExpression(node) { + if (node.operator === "delete" && node.argument.type === "Identifier") { + context.report({ node, message: "Variables should not be deleted." }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-div-regex.js b/tools/node_modules/eslint/lib/rules/no-div-regex.js new file mode 100644 index 0000000000..84a9b9a3aa --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-div-regex.js @@ -0,0 +1,38 @@ +/** + * @fileoverview Rule to check for ambiguous div operator in regexes + * @author Matt DuVall <http://www.mattduvall.com> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow division operators explicitly at the beginning of regular expressions", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + return { + + Literal(node) { + const token = sourceCode.getFirstToken(node); + + if (token.type === "RegularExpression" && token.value[1] === "=") { + context.report({ node, message: "A regular expression literal can be confused with '/='." }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-dupe-args.js b/tools/node_modules/eslint/lib/rules/no-dupe-args.js new file mode 100644 index 0000000000..c932be01d7 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-dupe-args.js @@ -0,0 +1,73 @@ +/** + * @fileoverview Rule to flag duplicate arguments + * @author Jamund Ferguson + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow duplicate arguments in `function` definitions", + category: "Possible Errors", + recommended: true + }, + + schema: [] + }, + + create(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Checks whether or not a given definition is a parameter's. + * @param {eslint-scope.DefEntry} def - A definition to check. + * @returns {boolean} `true` if the definition is a parameter's. + */ + function isParameter(def) { + return def.type === "Parameter"; + } + + /** + * Determines if a given node has duplicate parameters. + * @param {ASTNode} node The node to check. + * @returns {void} + * @private + */ + function checkParams(node) { + const variables = context.getDeclaredVariables(node); + + for (let i = 0; i < variables.length; ++i) { + const variable = variables[i]; + + // Checks and reports duplications. + const defs = variable.defs.filter(isParameter); + + if (defs.length >= 2) { + context.report({ + node, + message: "Duplicate param '{{name}}'.", + data: { name: variable.name } + }); + } + } + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + FunctionDeclaration: checkParams, + FunctionExpression: checkParams + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-dupe-class-members.js b/tools/node_modules/eslint/lib/rules/no-dupe-class-members.js new file mode 100644 index 0000000000..07b999fab1 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-dupe-class-members.js @@ -0,0 +1,109 @@ +/** + * @fileoverview A rule to disallow duplicate name in class members. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow duplicate class members", + category: "ECMAScript 6", + recommended: true + }, + + schema: [] + }, + + create(context) { + let stack = []; + + /** + * Gets state of a given member name. + * @param {string} name - A name of a member. + * @param {boolean} isStatic - A flag which specifies that is a static member. + * @returns {Object} A state of a given member name. + * - retv.init {boolean} A flag which shows the name is declared as normal member. + * - retv.get {boolean} A flag which shows the name is declared as getter. + * - retv.set {boolean} A flag which shows the name is declared as setter. + */ + function getState(name, isStatic) { + const stateMap = stack[stack.length - 1]; + const key = `$${name}`; // to avoid "__proto__". + + if (!stateMap[key]) { + stateMap[key] = { + nonStatic: { init: false, get: false, set: false }, + static: { init: false, get: false, set: false } + }; + } + + return stateMap[key][isStatic ? "static" : "nonStatic"]; + } + + /** + * Gets the name text of a given node. + * + * @param {ASTNode} node - A node to get the name. + * @returns {string} The name text of the node. + */ + function getName(node) { + switch (node.type) { + case "Identifier": return node.name; + case "Literal": return String(node.value); + + /* istanbul ignore next: syntax error */ + default: return ""; + } + } + + return { + + // Initializes the stack of state of member declarations. + Program() { + stack = []; + }, + + // Initializes state of member declarations for the class. + ClassBody() { + stack.push(Object.create(null)); + }, + + // Disposes the state for the class. + "ClassBody:exit"() { + stack.pop(); + }, + + // Reports the node if its name has been declared already. + MethodDefinition(node) { + if (node.computed) { + return; + } + + const name = getName(node.key); + const state = getState(name, node.static); + let isDuplicate = false; + + if (node.kind === "get") { + isDuplicate = (state.init || state.get); + state.get = true; + } else if (node.kind === "set") { + isDuplicate = (state.init || state.set); + state.set = true; + } else { + isDuplicate = (state.init || state.get || state.set); + state.init = true; + } + + if (isDuplicate) { + context.report({ node, message: "Duplicate name '{{name}}'.", data: { name } }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-dupe-keys.js b/tools/node_modules/eslint/lib/rules/no-dupe-keys.js new file mode 100644 index 0000000000..0120d0b38c --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-dupe-keys.js @@ -0,0 +1,135 @@ +/** + * @fileoverview Rule to flag use of duplicate keys in an object. + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const GET_KIND = /^(?:init|get)$/; +const SET_KIND = /^(?:init|set)$/; + +/** + * The class which stores properties' information of an object. + */ +class ObjectInfo { + + /** + * @param {ObjectInfo|null} upper - The information of the outer object. + * @param {ASTNode} node - The ObjectExpression node of this information. + */ + constructor(upper, node) { + this.upper = upper; + this.node = node; + this.properties = new Map(); + } + + /** + * Gets the information of the given Property node. + * @param {ASTNode} node - The Property node to get. + * @returns {{get: boolean, set: boolean}} The information of the property. + */ + getPropertyInfo(node) { + const name = astUtils.getStaticPropertyName(node); + + if (!this.properties.has(name)) { + this.properties.set(name, { get: false, set: false }); + } + return this.properties.get(name); + } + + /** + * Checks whether the given property has been defined already or not. + * @param {ASTNode} node - The Property node to check. + * @returns {boolean} `true` if the property has been defined. + */ + isPropertyDefined(node) { + const entry = this.getPropertyInfo(node); + + return ( + (GET_KIND.test(node.kind) && entry.get) || + (SET_KIND.test(node.kind) && entry.set) + ); + } + + /** + * Defines the given property. + * @param {ASTNode} node - The Property node to define. + * @returns {void} + */ + defineProperty(node) { + const entry = this.getPropertyInfo(node); + + if (GET_KIND.test(node.kind)) { + entry.get = true; + } + if (SET_KIND.test(node.kind)) { + entry.set = true; + } + } +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow duplicate keys in object literals", + category: "Possible Errors", + recommended: true + }, + + schema: [] + }, + + create(context) { + let info = null; + + return { + ObjectExpression(node) { + info = new ObjectInfo(info, node); + }, + "ObjectExpression:exit"() { + info = info.upper; + }, + + Property(node) { + const name = astUtils.getStaticPropertyName(node); + + // Skip destructuring. + if (node.parent.type !== "ObjectExpression") { + return; + } + + // Skip if the name is not static. + if (!name) { + return; + } + + // Reports if the name is defined already. + if (info.isPropertyDefined(node)) { + context.report({ + node: info.node, + loc: node.key.loc, + message: "Duplicate key '{{name}}'.", + data: { name } + }); + } + + // Update info. + info.defineProperty(node); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-duplicate-case.js b/tools/node_modules/eslint/lib/rules/no-duplicate-case.js new file mode 100644 index 0000000000..07823f284c --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-duplicate-case.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Rule to disallow a duplicate case label. + * @author Dieter Oberkofler + * @author Burak Yigit Kaya + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow duplicate case labels", + category: "Possible Errors", + recommended: true + }, + + schema: [] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + return { + SwitchStatement(node) { + const mapping = {}; + + node.cases.forEach(switchCase => { + const key = sourceCode.getText(switchCase.test); + + if (mapping[key]) { + context.report({ node: switchCase, message: "Duplicate case label." }); + } else { + mapping[key] = switchCase; + } + }); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-duplicate-imports.js b/tools/node_modules/eslint/lib/rules/no-duplicate-imports.js new file mode 100644 index 0000000000..d12c3a56df --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-duplicate-imports.js @@ -0,0 +1,137 @@ +/** + * @fileoverview Restrict usage of duplicate imports. + * @author Simen Bekkhus + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +/** + * Returns the name of the module imported or re-exported. + * + * @param {ASTNode} node - A node to get. + * @returns {string} the name of the module, or empty string if no name. + */ +function getValue(node) { + if (node && node.source && node.source.value) { + return node.source.value.trim(); + } + + return ""; +} + +/** + * Checks if the name of the import or export exists in the given array, and reports if so. + * + * @param {RuleContext} context - The ESLint rule context object. + * @param {ASTNode} node - A node to get. + * @param {string} value - The name of the imported or exported module. + * @param {string[]} array - The array containing other imports or exports in the file. + * @param {string} message - A message to be reported after the name of the module + * + * @returns {void} No return value + */ +function checkAndReport(context, node, value, array, message) { + if (array.indexOf(value) !== -1) { + context.report({ + node, + message: "'{{module}}' {{message}}", + data: { + module: value, + message + } + }); + } +} + +/** + * @callback nodeCallback + * @param {ASTNode} node - A node to handle. + */ + +/** + * Returns a function handling the imports of a given file + * + * @param {RuleContext} context - The ESLint rule context object. + * @param {boolean} includeExports - Whether or not to check for exports in addition to imports. + * @param {string[]} importsInFile - The array containing other imports in the file. + * @param {string[]} exportsInFile - The array containing other exports in the file. + * + * @returns {nodeCallback} A function passed to ESLint to handle the statement. + */ +function handleImports(context, includeExports, importsInFile, exportsInFile) { + return function(node) { + const value = getValue(node); + + if (value) { + checkAndReport(context, node, value, importsInFile, "import is duplicated."); + + if (includeExports) { + checkAndReport(context, node, value, exportsInFile, "import is duplicated as export."); + } + + importsInFile.push(value); + } + }; +} + +/** + * Returns a function handling the exports of a given file + * + * @param {RuleContext} context - The ESLint rule context object. + * @param {string[]} importsInFile - The array containing other imports in the file. + * @param {string[]} exportsInFile - The array containing other exports in the file. + * + * @returns {nodeCallback} A function passed to ESLint to handle the statement. + */ +function handleExports(context, importsInFile, exportsInFile) { + return function(node) { + const value = getValue(node); + + if (value) { + checkAndReport(context, node, value, exportsInFile, "export is duplicated."); + checkAndReport(context, node, value, importsInFile, "export is duplicated as import."); + + exportsInFile.push(value); + } + }; +} + +module.exports = { + meta: { + docs: { + description: "disallow duplicate module imports", + category: "ECMAScript 6", + recommended: false + }, + + schema: [{ + type: "object", + properties: { + includeExports: { + type: "boolean" + } + }, + additionalProperties: false + }] + }, + + create(context) { + const includeExports = (context.options[0] || {}).includeExports, + importsInFile = [], + exportsInFile = []; + + const handlers = { + ImportDeclaration: handleImports(context, includeExports, importsInFile, exportsInFile) + }; + + if (includeExports) { + handlers.ExportNamedDeclaration = handleExports(context, importsInFile, exportsInFile); + handlers.ExportAllDeclaration = handleExports(context, importsInFile, exportsInFile); + } + + return handlers; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-else-return.js b/tools/node_modules/eslint/lib/rules/no-else-return.js new file mode 100644 index 0000000000..deeff41ab8 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-else-return.js @@ -0,0 +1,276 @@ +/** + * @fileoverview Rule to flag `else` after a `return` in `if` + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); +const FixTracker = require("../util/fix-tracker"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `else` blocks after `return` statements in `if` statements", + category: "Best Practices", + recommended: false + }, + + schema: [{ + type: "object", + properties: { + allowElseIf: { + type: "boolean" + } + }, + additionalProperties: false + }], + fixable: "code" + }, + + create(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Display the context report if rule is violated + * + * @param {Node} node The 'else' node + * @returns {void} + */ + function displayReport(node) { + context.report({ + node, + message: "Unnecessary 'else' after 'return'.", + fix: fixer => { + const sourceCode = context.getSourceCode(); + const startToken = sourceCode.getFirstToken(node); + const elseToken = sourceCode.getTokenBefore(startToken); + const source = sourceCode.getText(node); + const lastIfToken = sourceCode.getTokenBefore(elseToken); + let fixedSource, firstTokenOfElseBlock; + + if (startToken.type === "Punctuator" && startToken.value === "{") { + firstTokenOfElseBlock = sourceCode.getTokenAfter(startToken); + } else { + firstTokenOfElseBlock = startToken; + } + + /* + * If the if block does not have curly braces and does not end in a semicolon + * and the else block starts with (, [, /, +, ` or -, then it is not + * safe to remove the else keyword, because ASI will not add a semicolon + * after the if block + */ + const ifBlockMaybeUnsafe = node.parent.consequent.type !== "BlockStatement" && lastIfToken.value !== ";"; + const elseBlockUnsafe = /^[([/+`-]/.test(firstTokenOfElseBlock.value); + + if (ifBlockMaybeUnsafe && elseBlockUnsafe) { + return null; + } + + const endToken = sourceCode.getLastToken(node); + const lastTokenOfElseBlock = sourceCode.getTokenBefore(endToken); + + if (lastTokenOfElseBlock.value !== ";") { + const nextToken = sourceCode.getTokenAfter(endToken); + + const nextTokenUnsafe = nextToken && /^[([/+`-]/.test(nextToken.value); + const nextTokenOnSameLine = nextToken && nextToken.loc.start.line === lastTokenOfElseBlock.loc.start.line; + + /* + * If the else block contents does not end in a semicolon, + * and the else block starts with (, [, /, +, ` or -, then it is not + * safe to remove the else block, because ASI will not add a semicolon + * after the remaining else block contents + */ + if (nextTokenUnsafe || (nextTokenOnSameLine && nextToken.value !== "}")) { + return null; + } + } + + if (startToken.type === "Punctuator" && startToken.value === "{") { + fixedSource = source.slice(1, -1); + } else { + fixedSource = source; + } + + /* + * Extend the replacement range to include the entire + * function to avoid conflicting with no-useless-return. + * https://github.com/eslint/eslint/issues/8026 + */ + return new FixTracker(fixer, sourceCode) + .retainEnclosingFunction(node) + .replaceTextRange([elseToken.range[0], node.range[1]], fixedSource); + } + }); + } + + /** + * Check to see if the node is a ReturnStatement + * + * @param {Node} node The node being evaluated + * @returns {boolean} True if node is a return + */ + function checkForReturn(node) { + return node.type === "ReturnStatement"; + } + + /** + * Naive return checking, does not iterate through the whole + * BlockStatement because we make the assumption that the ReturnStatement + * will be the last node in the body of the BlockStatement. + * + * @param {Node} node The consequent/alternate node + * @returns {boolean} True if it has a return + */ + function naiveHasReturn(node) { + if (node.type === "BlockStatement") { + const body = node.body, + lastChildNode = body[body.length - 1]; + + return lastChildNode && checkForReturn(lastChildNode); + } + return checkForReturn(node); + } + + /** + * Check to see if the node is valid for evaluation, + * meaning it has an else. + * + * @param {Node} node The node being evaluated + * @returns {boolean} True if the node is valid + */ + function hasElse(node) { + return node.alternate && node.consequent; + } + + /** + * If the consequent is an IfStatement, check to see if it has an else + * and both its consequent and alternate path return, meaning this is + * a nested case of rule violation. If-Else not considered currently. + * + * @param {Node} node The consequent node + * @returns {boolean} True if this is a nested rule violation + */ + function checkForIf(node) { + return node.type === "IfStatement" && hasElse(node) && + naiveHasReturn(node.alternate) && naiveHasReturn(node.consequent); + } + + /** + * Check the consequent/body node to make sure it is not + * a ReturnStatement or an IfStatement that returns on both + * code paths. + * + * @param {Node} node The consequent or body node + * @param {Node} alternate The alternate node + * @returns {boolean} `true` if it is a Return/If node that always returns. + */ + function checkForReturnOrIf(node) { + return checkForReturn(node) || checkForIf(node); + } + + + /** + * Check whether a node returns in every codepath. + * @param {Node} node The node to be checked + * @returns {boolean} `true` if it returns on every codepath. + */ + function alwaysReturns(node) { + if (node.type === "BlockStatement") { + + // If we have a BlockStatement, check each consequent body node. + return node.body.some(checkForReturnOrIf); + } + + /* + * If not a block statement, make sure the consequent isn't a + * ReturnStatement or an IfStatement with returns on both paths. + */ + return checkForReturnOrIf(node); + } + + + /** + * Check the if statement, but don't catch else-if blocks. + * @returns {void} + * @param {Node} node The node for the if statement to check + * @private + */ + function checkIfWithoutElse(node) { + const parent = node.parent; + let consequents, + alternate; + + /* + * Fixing this would require splitting one statement into two, so no error should + * be reported if this node is in a position where only one statement is allowed. + */ + if (!astUtils.STATEMENT_LIST_PARENTS.has(parent.type)) { + return; + } + + for (consequents = []; node.type === "IfStatement"; node = node.alternate) { + if (!node.alternate) { + return; + } + consequents.push(node.consequent); + alternate = node.alternate; + } + + if (consequents.every(alwaysReturns)) { + displayReport(alternate); + } + } + + /** + * Check the if statement + * @returns {void} + * @param {Node} node The node for the if statement to check + * @private + */ + function checkIfWithElse(node) { + const parent = node.parent; + + + /* + * Fixing this would require splitting one statement into two, so no error should + * be reported if this node is in a position where only one statement is allowed. + */ + if (!astUtils.STATEMENT_LIST_PARENTS.has(parent.type)) { + return; + } + + const alternate = node.alternate; + + if (alternate && alwaysReturns(node.consequent)) { + displayReport(alternate); + } + } + + const allowElseIf = !(context.options[0] && context.options[0].allowElseIf === false); + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + + "IfStatement:exit": allowElseIf ? checkIfWithoutElse : checkIfWithElse + + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-empty-character-class.js b/tools/node_modules/eslint/lib/rules/no-empty-character-class.js new file mode 100644 index 0000000000..0ea7c5a0d1 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-empty-character-class.js @@ -0,0 +1,57 @@ +/** + * @fileoverview Rule to flag the use of empty character classes in regular expressions + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/* + * plain-English description of the following regexp: + * 0. `^` fix the match at the beginning of the string + * 1. `\/`: the `/` that begins the regexp + * 2. `([^\\[]|\\.|\[([^\\\]]|\\.)+\])*`: regexp contents; 0 or more of the following + * 2.0. `[^\\[]`: any character that's not a `\` or a `[` (anything but escape sequences and character classes) + * 2.1. `\\.`: an escape sequence + * 2.2. `\[([^\\\]]|\\.)+\]`: a character class that isn't empty + * 3. `\/` the `/` that ends the regexp + * 4. `[gimuy]*`: optional regexp flags + * 5. `$`: fix the match at the end of the string + */ +const regex = /^\/([^\\[]|\\.|\[([^\\\]]|\\.)+])*\/[gimuy]*$/; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow empty character classes in regular expressions", + category: "Possible Errors", + recommended: true + }, + + schema: [] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + return { + + Literal(node) { + const token = sourceCode.getFirstToken(node); + + if (token.type === "RegularExpression" && !regex.test(token.value)) { + context.report({ node, message: "Empty class." }); + } + } + + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-empty-function.js b/tools/node_modules/eslint/lib/rules/no-empty-function.js new file mode 100644 index 0000000000..38c915c33f --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-empty-function.js @@ -0,0 +1,160 @@ +/** + * @fileoverview Rule to disallow empty functions. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const ALLOW_OPTIONS = Object.freeze([ + "functions", + "arrowFunctions", + "generatorFunctions", + "methods", + "generatorMethods", + "getters", + "setters", + "constructors" +]); + +/** + * Gets the kind of a given function node. + * + * @param {ASTNode} node - A function node to get. This is one of + * an ArrowFunctionExpression, a FunctionDeclaration, or a + * FunctionExpression. + * @returns {string} The kind of the function. This is one of "functions", + * "arrowFunctions", "generatorFunctions", "asyncFunctions", "methods", + * "generatorMethods", "asyncMethods", "getters", "setters", and + * "constructors". + */ +function getKind(node) { + const parent = node.parent; + let kind = ""; + + if (node.type === "ArrowFunctionExpression") { + return "arrowFunctions"; + } + + // Detects main kind. + if (parent.type === "Property") { + if (parent.kind === "get") { + return "getters"; + } + if (parent.kind === "set") { + return "setters"; + } + kind = parent.method ? "methods" : "functions"; + + } else if (parent.type === "MethodDefinition") { + if (parent.kind === "get") { + return "getters"; + } + if (parent.kind === "set") { + return "setters"; + } + if (parent.kind === "constructor") { + return "constructors"; + } + kind = "methods"; + + } else { + kind = "functions"; + } + + // Detects prefix. + let prefix = ""; + + if (node.generator) { + prefix = "generator"; + } else if (node.async) { + prefix = "async"; + } else { + return kind; + } + return prefix + kind[0].toUpperCase() + kind.slice(1); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow empty functions", + category: "Best Practices", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + allow: { + type: "array", + items: { enum: ALLOW_OPTIONS }, + uniqueItems: true + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const options = context.options[0] || {}; + const allowed = options.allow || []; + + const sourceCode = context.getSourceCode(); + + /** + * Reports a given function node if the node matches the following patterns. + * + * - Not allowed by options. + * - The body is empty. + * - The body doesn't have any comments. + * + * @param {ASTNode} node - A function node to report. This is one of + * an ArrowFunctionExpression, a FunctionDeclaration, or a + * FunctionExpression. + * @returns {void} + */ + function reportIfEmpty(node) { + const kind = getKind(node); + const name = astUtils.getFunctionNameWithKind(node); + const innerComments = sourceCode.getTokens(node.body, { + includeComments: true, + filter: astUtils.isCommentToken + }); + + if (allowed.indexOf(kind) === -1 && + node.body.type === "BlockStatement" && + node.body.body.length === 0 && + innerComments.length === 0 + ) { + context.report({ + node, + loc: node.body.loc.start, + message: "Unexpected empty {{name}}.", + data: { name } + }); + } + } + + return { + ArrowFunctionExpression: reportIfEmpty, + FunctionDeclaration: reportIfEmpty, + FunctionExpression: reportIfEmpty + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-empty-pattern.js b/tools/node_modules/eslint/lib/rules/no-empty-pattern.js new file mode 100644 index 0000000000..11f50b5414 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-empty-pattern.js @@ -0,0 +1,36 @@ +/** + * @fileoverview Rule to disallow an empty pattern + * @author Alberto RodrÃguez + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow empty destructuring patterns", + category: "Best Practices", + recommended: true + }, + + schema: [] + }, + + create(context) { + return { + ObjectPattern(node) { + if (node.properties.length === 0) { + context.report({ node, message: "Unexpected empty object pattern." }); + } + }, + ArrayPattern(node) { + if (node.elements.length === 0) { + context.report({ node, message: "Unexpected empty array pattern." }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-empty.js b/tools/node_modules/eslint/lib/rules/no-empty.js new file mode 100644 index 0000000000..b71b8582a3 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-empty.js @@ -0,0 +1,78 @@ +/** + * @fileoverview Rule to flag use of an empty block statement + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow empty block statements", + category: "Possible Errors", + recommended: true + }, + + schema: [ + { + type: "object", + properties: { + allowEmptyCatch: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const options = context.options[0] || {}, + allowEmptyCatch = options.allowEmptyCatch || false; + + const sourceCode = context.getSourceCode(); + + return { + BlockStatement(node) { + + // if the body is not empty, we can just return immediately + if (node.body.length !== 0) { + return; + } + + // a function is generally allowed to be empty + if (astUtils.isFunction(node.parent)) { + return; + } + + if (allowEmptyCatch && node.parent.type === "CatchClause") { + return; + } + + // any other block is only allowed to be empty, if it contains a comment + if (sourceCode.getCommentsInside(node).length > 0) { + return; + } + + context.report({ node, message: "Empty block statement." }); + }, + + SwitchStatement(node) { + + if (typeof node.cases === "undefined" || node.cases.length === 0) { + context.report({ node, message: "Empty switch statement." }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-eq-null.js b/tools/node_modules/eslint/lib/rules/no-eq-null.js new file mode 100644 index 0000000000..7e915a8c72 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-eq-null.js @@ -0,0 +1,39 @@ +/** + * @fileoverview Rule to flag comparisons to null without a type-checking + * operator. + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `null` comparisons without type-checking operators", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + + return { + + BinaryExpression(node) { + const badOperator = node.operator === "==" || node.operator === "!="; + + if (node.right.type === "Literal" && node.right.raw === "null" && badOperator || + node.left.type === "Literal" && node.left.raw === "null" && badOperator) { + context.report({ node, message: "Use ‘===’ to compare with ‘null’." }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-eval.js b/tools/node_modules/eslint/lib/rules/no-eval.js new file mode 100644 index 0000000000..ee5f577f47 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-eval.js @@ -0,0 +1,308 @@ +/** + * @fileoverview Rule to flag use of eval() statement + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const candidatesOfGlobalObject = Object.freeze([ + "global", + "window" +]); + +/** + * Checks a given node is a Identifier node of the specified name. + * + * @param {ASTNode} node - A node to check. + * @param {string} name - A name to check. + * @returns {boolean} `true` if the node is a Identifier node of the name. + */ +function isIdentifier(node, name) { + return node.type === "Identifier" && node.name === name; +} + +/** + * Checks a given node is a Literal node of the specified string value. + * + * @param {ASTNode} node - A node to check. + * @param {string} name - A name to check. + * @returns {boolean} `true` if the node is a Literal node of the name. + */ +function isConstant(node, name) { + switch (node.type) { + case "Literal": + return node.value === name; + + case "TemplateLiteral": + return ( + node.expressions.length === 0 && + node.quasis[0].value.cooked === name + ); + + default: + return false; + } +} + +/** + * Checks a given node is a MemberExpression node which has the specified name's + * property. + * + * @param {ASTNode} node - A node to check. + * @param {string} name - A name to check. + * @returns {boolean} `true` if the node is a MemberExpression node which has + * the specified name's property + */ +function isMember(node, name) { + return ( + node.type === "MemberExpression" && + (node.computed ? isConstant : isIdentifier)(node.property, name) + ); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow the use of `eval()`", + category: "Best Practices", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + allowIndirect: { type: "boolean" } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const allowIndirect = Boolean( + context.options[0] && + context.options[0].allowIndirect + ); + const sourceCode = context.getSourceCode(); + let funcInfo = null; + + /** + * Pushs a variable scope (Program or Function) information to the stack. + * + * This is used in order to check whether or not `this` binding is a + * reference to the global object. + * + * @param {ASTNode} node - A node of the scope. This is one of Program, + * FunctionDeclaration, FunctionExpression, and ArrowFunctionExpression. + * @returns {void} + */ + function enterVarScope(node) { + const strict = context.getScope().isStrict; + + funcInfo = { + upper: funcInfo, + node, + strict, + defaultThis: false, + initialized: strict + }; + } + + /** + * Pops a variable scope from the stack. + * + * @returns {void} + */ + function exitVarScope() { + funcInfo = funcInfo.upper; + } + + /** + * Reports a given node. + * + * `node` is `Identifier` or `MemberExpression`. + * The parent of `node` might be `CallExpression`. + * + * The location of the report is always `eval` `Identifier` (or possibly + * `Literal`). The type of the report is `CallExpression` if the parent is + * `CallExpression`. Otherwise, it's the given node type. + * + * @param {ASTNode} node - A node to report. + * @returns {void} + */ + function report(node) { + let locationNode = node; + const parent = node.parent; + + if (node.type === "MemberExpression") { + locationNode = node.property; + } + if (parent.type === "CallExpression" && parent.callee === node) { + node = parent; + } + + context.report({ + node, + loc: locationNode.loc.start, + message: "eval can be harmful." + }); + } + + /** + * Reports accesses of `eval` via the global object. + * + * @param {eslint-scope.Scope} globalScope - The global scope. + * @returns {void} + */ + function reportAccessingEvalViaGlobalObject(globalScope) { + for (let i = 0; i < candidatesOfGlobalObject.length; ++i) { + const name = candidatesOfGlobalObject[i]; + const variable = astUtils.getVariableByName(globalScope, name); + + if (!variable) { + continue; + } + + const references = variable.references; + + for (let j = 0; j < references.length; ++j) { + const identifier = references[j].identifier; + let node = identifier.parent; + + // To detect code like `window.window.eval`. + while (isMember(node, name)) { + node = node.parent; + } + + // Reports. + if (isMember(node, "eval")) { + report(node); + } + } + } + } + + /** + * Reports all accesses of `eval` (excludes direct calls to eval). + * + * @param {eslint-scope.Scope} globalScope - The global scope. + * @returns {void} + */ + function reportAccessingEval(globalScope) { + const variable = astUtils.getVariableByName(globalScope, "eval"); + + if (!variable) { + return; + } + + const references = variable.references; + + for (let i = 0; i < references.length; ++i) { + const reference = references[i]; + const id = reference.identifier; + + if (id.name === "eval" && !astUtils.isCallee(id)) { + + // Is accessing to eval (excludes direct calls to eval) + report(id); + } + } + } + + if (allowIndirect) { + + // Checks only direct calls to eval. It's simple! + return { + "CallExpression:exit"(node) { + const callee = node.callee; + + if (isIdentifier(callee, "eval")) { + report(callee); + } + } + }; + } + + return { + "CallExpression:exit"(node) { + const callee = node.callee; + + if (isIdentifier(callee, "eval")) { + report(callee); + } + }, + + Program(node) { + const scope = context.getScope(), + features = context.parserOptions.ecmaFeatures || {}, + strict = + scope.isStrict || + node.sourceType === "module" || + (features.globalReturn && scope.childScopes[0].isStrict); + + funcInfo = { + upper: null, + node, + strict, + defaultThis: true, + initialized: true + }; + }, + + "Program:exit"() { + const globalScope = context.getScope(); + + exitVarScope(); + reportAccessingEval(globalScope); + reportAccessingEvalViaGlobalObject(globalScope); + }, + + FunctionDeclaration: enterVarScope, + "FunctionDeclaration:exit": exitVarScope, + FunctionExpression: enterVarScope, + "FunctionExpression:exit": exitVarScope, + ArrowFunctionExpression: enterVarScope, + "ArrowFunctionExpression:exit": exitVarScope, + + ThisExpression(node) { + if (!isMember(node.parent, "eval")) { + return; + } + + /* + * `this.eval` is found. + * Checks whether or not the value of `this` is the global object. + */ + if (!funcInfo.initialized) { + funcInfo.initialized = true; + funcInfo.defaultThis = astUtils.isDefaultThisBinding( + funcInfo.node, + sourceCode + ); + } + + if (!funcInfo.strict && funcInfo.defaultThis) { + + // `this.eval` is possible built-in `eval`. + report(node.parent); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-ex-assign.js b/tools/node_modules/eslint/lib/rules/no-ex-assign.js new file mode 100644 index 0000000000..20869d5cd1 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-ex-assign.js @@ -0,0 +1,45 @@ +/** + * @fileoverview Rule to flag assignment of the exception parameter + * @author Stephen Murray <spmurrayzzz> + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow reassigning exceptions in `catch` clauses", + category: "Possible Errors", + recommended: true + }, + + schema: [] + }, + + create(context) { + + /** + * Finds and reports references that are non initializer and writable. + * @param {Variable} variable - A variable to check. + * @returns {void} + */ + function checkVariable(variable) { + astUtils.getModifyingReferences(variable.references).forEach(reference => { + context.report({ node: reference.identifier, message: "Do not assign to the exception parameter." }); + }); + } + + return { + CatchClause(node) { + context.getDeclaredVariables(node).forEach(checkVariable); + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-extend-native.js b/tools/node_modules/eslint/lib/rules/no-extend-native.js new file mode 100644 index 0000000000..c550cf5da5 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-extend-native.js @@ -0,0 +1,174 @@ +/** + * @fileoverview Rule to flag adding properties to native object's prototypes. + * @author David Nelson + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); +const globals = require("globals"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const propertyDefinitionMethods = new Set(["defineProperty", "defineProperties"]); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow extending native types", + category: "Best Practices", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + exceptions: { + type: "array", + items: { + type: "string" + }, + uniqueItems: true + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + const config = context.options[0] || {}; + const exceptions = new Set(config.exceptions || []); + const modifiedBuiltins = new Set( + Object.keys(globals.builtin) + .filter(builtin => builtin[0].toUpperCase() === builtin[0]) + .filter(builtin => !exceptions.has(builtin)) + ); + + /** + * Reports a lint error for the given node. + * @param {ASTNode} node The node to report. + * @param {string} builtin The name of the native builtin being extended. + * @returns {void} + */ + function reportNode(node, builtin) { + context.report({ + node, + message: "{{builtin}} prototype is read only, properties should not be added.", + data: { + builtin + } + }); + } + + /** + * Check to see if the `prototype` property of the given object + * identifier node is being accessed. + * @param {ASTNode} identifierNode The Identifier representing the object + * to check. + * @returns {boolean} True if the identifier is the object of a + * MemberExpression and its `prototype` property is being accessed, + * false otherwise. + */ + function isPrototypePropertyAccessed(identifierNode) { + return Boolean( + identifierNode && + identifierNode.parent && + identifierNode.parent.type === "MemberExpression" && + identifierNode.parent.object === identifierNode && + astUtils.getStaticPropertyName(identifierNode.parent) === "prototype" + ); + } + + /** + * Checks that an identifier is an object of a prototype whose member + * is being assigned in an AssignmentExpression. + * Example: Object.prototype.foo = "bar" + * @param {ASTNode} identifierNode The identifier to check. + * @returns {boolean} True if the identifier's prototype is modified. + */ + function isInPrototypePropertyAssignment(identifierNode) { + return Boolean( + isPrototypePropertyAccessed(identifierNode) && + identifierNode.parent.parent.type === "MemberExpression" && + identifierNode.parent.parent.parent.type === "AssignmentExpression" && + identifierNode.parent.parent.parent.left === identifierNode.parent.parent + ); + } + + /** + * Checks that an identifier is an object of a prototype whose member + * is being extended via the Object.defineProperty() or + * Object.defineProperties() methods. + * Example: Object.defineProperty(Array.prototype, "foo", ...) + * Example: Object.defineProperties(Array.prototype, ...) + * @param {ASTNode} identifierNode The identifier to check. + * @returns {boolean} True if the identifier's prototype is modified. + */ + function isInDefinePropertyCall(identifierNode) { + return Boolean( + isPrototypePropertyAccessed(identifierNode) && + identifierNode.parent.parent.type === "CallExpression" && + identifierNode.parent.parent.arguments[0] === identifierNode.parent && + identifierNode.parent.parent.callee.type === "MemberExpression" && + identifierNode.parent.parent.callee.object.type === "Identifier" && + identifierNode.parent.parent.callee.object.name === "Object" && + identifierNode.parent.parent.callee.property.type === "Identifier" && + propertyDefinitionMethods.has(identifierNode.parent.parent.callee.property.name) + ); + } + + /** + * Check to see if object prototype access is part of a prototype + * extension. There are three ways a prototype can be extended: + * 1. Assignment to prototype property (Object.prototype.foo = 1) + * 2. Object.defineProperty()/Object.defineProperties() on a prototype + * If prototype extension is detected, report the AssignmentExpression + * or CallExpression node. + * @param {ASTNode} identifierNode The Identifier representing the object + * which prototype is being accessed and possibly extended. + * @returns {void} + */ + function checkAndReportPrototypeExtension(identifierNode) { + if (isInPrototypePropertyAssignment(identifierNode)) { + + // Identifier --> MemberExpression --> MemberExpression --> AssignmentExpression + reportNode(identifierNode.parent.parent.parent, identifierNode.name); + } else if (isInDefinePropertyCall(identifierNode)) { + + // Identifier --> MemberExpression --> CallExpression + reportNode(identifierNode.parent.parent, identifierNode.name); + } + } + + return { + + "Program:exit"() { + const globalScope = context.getScope(); + + modifiedBuiltins.forEach(builtin => { + const builtinVar = globalScope.set.get(builtin); + + if (builtinVar && builtinVar.references) { + builtinVar.references + .map(ref => ref.identifier) + .forEach(checkAndReportPrototypeExtension); + } + }); + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-extra-bind.js b/tools/node_modules/eslint/lib/rules/no-extra-bind.js new file mode 100644 index 0000000000..2d22eff245 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-extra-bind.js @@ -0,0 +1,145 @@ +/** + * @fileoverview Rule to flag unnecessary bind calls + * @author Bence Dányi <bence@danyi.me> + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow unnecessary calls to `.bind()`", + category: "Best Practices", + recommended: false + }, + + schema: [], + + fixable: "code" + }, + + create(context) { + let scopeInfo = null; + + /** + * Reports a given function node. + * + * @param {ASTNode} node - A node to report. This is a FunctionExpression or + * an ArrowFunctionExpression. + * @returns {void} + */ + function report(node) { + context.report({ + node: node.parent.parent, + message: "The function binding is unnecessary.", + loc: node.parent.property.loc.start, + fix(fixer) { + const firstTokenToRemove = context.getSourceCode() + .getFirstTokenBetween(node.parent.object, node.parent.property, astUtils.isNotClosingParenToken); + + return fixer.removeRange([firstTokenToRemove.range[0], node.parent.parent.range[1]]); + } + }); + } + + /** + * Checks whether or not a given function node is the callee of `.bind()` + * method. + * + * e.g. `(function() {}.bind(foo))` + * + * @param {ASTNode} node - A node to report. This is a FunctionExpression or + * an ArrowFunctionExpression. + * @returns {boolean} `true` if the node is the callee of `.bind()` method. + */ + function isCalleeOfBindMethod(node) { + const parent = node.parent; + const grandparent = parent.parent; + + return ( + grandparent && + grandparent.type === "CallExpression" && + grandparent.callee === parent && + grandparent.arguments.length === 1 && + parent.type === "MemberExpression" && + parent.object === node && + astUtils.getStaticPropertyName(parent) === "bind" + ); + } + + /** + * Adds a scope information object to the stack. + * + * @param {ASTNode} node - A node to add. This node is a FunctionExpression + * or a FunctionDeclaration node. + * @returns {void} + */ + function enterFunction(node) { + scopeInfo = { + isBound: isCalleeOfBindMethod(node), + thisFound: false, + upper: scopeInfo + }; + } + + /** + * Removes the scope information object from the top of the stack. + * At the same time, this reports the function node if the function has + * `.bind()` and the `this` keywords found. + * + * @param {ASTNode} node - A node to remove. This node is a + * FunctionExpression or a FunctionDeclaration node. + * @returns {void} + */ + function exitFunction(node) { + if (scopeInfo.isBound && !scopeInfo.thisFound) { + report(node); + } + + scopeInfo = scopeInfo.upper; + } + + /** + * Reports a given arrow function if the function is callee of `.bind()` + * method. + * + * @param {ASTNode} node - A node to report. This node is an + * ArrowFunctionExpression. + * @returns {void} + */ + function exitArrowFunction(node) { + if (isCalleeOfBindMethod(node)) { + report(node); + } + } + + /** + * Set the mark as the `this` keyword was found in this scope. + * + * @returns {void} + */ + function markAsThisFound() { + if (scopeInfo) { + scopeInfo.thisFound = true; + } + } + + return { + "ArrowFunctionExpression:exit": exitArrowFunction, + FunctionDeclaration: enterFunction, + "FunctionDeclaration:exit": exitFunction, + FunctionExpression: enterFunction, + "FunctionExpression:exit": exitFunction, + ThisExpression: markAsThisFound + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-extra-boolean-cast.js b/tools/node_modules/eslint/lib/rules/no-extra-boolean-cast.js new file mode 100644 index 0000000000..47ca7e22fe --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-extra-boolean-cast.js @@ -0,0 +1,122 @@ +/** + * @fileoverview Rule to flag unnecessary double negation in Boolean contexts + * @author Brandon Mills + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow unnecessary boolean casts", + category: "Possible Errors", + recommended: true + }, + + schema: [], + + fixable: "code" + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + // Node types which have a test which will coerce values to booleans. + const BOOLEAN_NODE_TYPES = [ + "IfStatement", + "DoWhileStatement", + "WhileStatement", + "ConditionalExpression", + "ForStatement" + ]; + + /** + * Check if a node is in a context where its value would be coerced to a boolean at runtime. + * + * @param {Object} node The node + * @param {Object} parent Its parent + * @returns {boolean} If it is in a boolean context + */ + function isInBooleanContext(node, parent) { + return ( + (BOOLEAN_NODE_TYPES.indexOf(parent.type) !== -1 && + node === parent.test) || + + // !<bool> + (parent.type === "UnaryExpression" && + parent.operator === "!") + ); + } + + + return { + UnaryExpression(node) { + const ancestors = context.getAncestors(), + parent = ancestors.pop(), + grandparent = ancestors.pop(); + + // Exit early if it's guaranteed not to match + if (node.operator !== "!" || + parent.type !== "UnaryExpression" || + parent.operator !== "!") { + return; + } + + if (isInBooleanContext(parent, grandparent) || + + // Boolean(<bool>) and new Boolean(<bool>) + ((grandparent.type === "CallExpression" || grandparent.type === "NewExpression") && + grandparent.callee.type === "Identifier" && + grandparent.callee.name === "Boolean") + ) { + context.report({ + node, + message: "Redundant double negation.", + fix: fixer => fixer.replaceText(parent, sourceCode.getText(node.argument)) + }); + } + }, + CallExpression(node) { + const parent = node.parent; + + if (node.callee.type !== "Identifier" || node.callee.name !== "Boolean") { + return; + } + + if (isInBooleanContext(node, parent)) { + context.report({ + node, + message: "Redundant Boolean call.", + fix: fixer => { + if (!node.arguments.length) { + return fixer.replaceText(parent, "true"); + } + + if (node.arguments.length > 1 || node.arguments[0].type === "SpreadElement") { + return null; + } + + const argument = node.arguments[0]; + + if (astUtils.getPrecedence(argument) < astUtils.getPrecedence(node.parent)) { + return fixer.replaceText(node, `(${sourceCode.getText(argument)})`); + } + return fixer.replaceText(node, sourceCode.getText(argument)); + } + }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-extra-label.js b/tools/node_modules/eslint/lib/rules/no-extra-label.js new file mode 100644 index 0000000000..b89267de93 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-extra-label.js @@ -0,0 +1,140 @@ +/** + * @fileoverview Rule to disallow unnecessary labels + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow unnecessary labels", + category: "Best Practices", + recommended: false + }, + + schema: [], + + fixable: "code" + }, + + create(context) { + const sourceCode = context.getSourceCode(); + let scopeInfo = null; + + /** + * Creates a new scope with a breakable statement. + * + * @param {ASTNode} node - A node to create. This is a BreakableStatement. + * @returns {void} + */ + function enterBreakableStatement(node) { + scopeInfo = { + label: node.parent.type === "LabeledStatement" ? node.parent.label : null, + breakable: true, + upper: scopeInfo + }; + } + + /** + * Removes the top scope of the stack. + * + * @returns {void} + */ + function exitBreakableStatement() { + scopeInfo = scopeInfo.upper; + } + + /** + * Creates a new scope with a labeled statement. + * + * This ignores it if the body is a breakable statement. + * In this case it's handled in the `enterBreakableStatement` function. + * + * @param {ASTNode} node - A node to create. This is a LabeledStatement. + * @returns {void} + */ + function enterLabeledStatement(node) { + if (!astUtils.isBreakableStatement(node.body)) { + scopeInfo = { + label: node.label, + breakable: false, + upper: scopeInfo + }; + } + } + + /** + * Removes the top scope of the stack. + * + * This ignores it if the body is a breakable statement. + * In this case it's handled in the `exitBreakableStatement` function. + * + * @param {ASTNode} node - A node. This is a LabeledStatement. + * @returns {void} + */ + function exitLabeledStatement(node) { + if (!astUtils.isBreakableStatement(node.body)) { + scopeInfo = scopeInfo.upper; + } + } + + /** + * Reports a given control node if it's unnecessary. + * + * @param {ASTNode} node - A node. This is a BreakStatement or a + * ContinueStatement. + * @returns {void} + */ + function reportIfUnnecessary(node) { + if (!node.label) { + return; + } + + const labelNode = node.label; + + for (let info = scopeInfo; info !== null; info = info.upper) { + if (info.breakable || info.label && info.label.name === labelNode.name) { + if (info.breakable && info.label && info.label.name === labelNode.name) { + context.report({ + node: labelNode, + message: "This label '{{name}}' is unnecessary.", + data: labelNode, + fix: fixer => fixer.removeRange([sourceCode.getFirstToken(node).range[1], labelNode.range[1]]) + }); + } + return; + } + } + } + + return { + WhileStatement: enterBreakableStatement, + "WhileStatement:exit": exitBreakableStatement, + DoWhileStatement: enterBreakableStatement, + "DoWhileStatement:exit": exitBreakableStatement, + ForStatement: enterBreakableStatement, + "ForStatement:exit": exitBreakableStatement, + ForInStatement: enterBreakableStatement, + "ForInStatement:exit": exitBreakableStatement, + ForOfStatement: enterBreakableStatement, + "ForOfStatement:exit": exitBreakableStatement, + SwitchStatement: enterBreakableStatement, + "SwitchStatement:exit": exitBreakableStatement, + LabeledStatement: enterLabeledStatement, + "LabeledStatement:exit": exitLabeledStatement, + BreakStatement: reportIfUnnecessary, + ContinueStatement: reportIfUnnecessary + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-extra-parens.js b/tools/node_modules/eslint/lib/rules/no-extra-parens.js new file mode 100644 index 0000000000..d8e0df64a7 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-extra-parens.js @@ -0,0 +1,745 @@ +/** + * @fileoverview Disallow parenthesising higher precedence subexpressions. + * @author Michael Ficarra + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils.js"); + +module.exports = { + meta: { + docs: { + description: "disallow unnecessary parentheses", + category: "Possible Errors", + recommended: false + }, + + fixable: "code", + + schema: { + anyOf: [ + { + type: "array", + items: [ + { + enum: ["functions"] + } + ], + minItems: 0, + maxItems: 1 + }, + { + type: "array", + items: [ + { + enum: ["all"] + }, + { + type: "object", + properties: { + conditionalAssign: { type: "boolean" }, + nestedBinaryExpressions: { type: "boolean" }, + returnAssign: { type: "boolean" }, + ignoreJSX: { enum: ["none", "all", "single-line", "multi-line"] }, + enforceForArrowConditionals: { type: "boolean" } + }, + additionalProperties: false + } + ], + minItems: 0, + maxItems: 2 + } + ] + } + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + const tokensToIgnore = new WeakSet(); + const isParenthesised = astUtils.isParenthesised.bind(astUtils, sourceCode); + const precedence = astUtils.getPrecedence; + const ALL_NODES = context.options[0] !== "functions"; + const EXCEPT_COND_ASSIGN = ALL_NODES && context.options[1] && context.options[1].conditionalAssign === false; + const NESTED_BINARY = ALL_NODES && context.options[1] && context.options[1].nestedBinaryExpressions === false; + const EXCEPT_RETURN_ASSIGN = ALL_NODES && context.options[1] && context.options[1].returnAssign === false; + const IGNORE_JSX = ALL_NODES && context.options[1] && context.options[1].ignoreJSX; + const IGNORE_ARROW_CONDITIONALS = ALL_NODES && context.options[1] && + context.options[1].enforceForArrowConditionals === false; + + const PRECEDENCE_OF_ASSIGNMENT_EXPR = precedence({ type: "AssignmentExpression" }); + const PRECEDENCE_OF_UPDATE_EXPR = precedence({ type: "UpdateExpression" }); + + /** + * Determines if this rule should be enforced for a node given the current configuration. + * @param {ASTNode} node - The node to be checked. + * @returns {boolean} True if the rule should be enforced for this node. + * @private + */ + function ruleApplies(node) { + if (node.type === "JSXElement") { + const isSingleLine = node.loc.start.line === node.loc.end.line; + + switch (IGNORE_JSX) { + + // Exclude this JSX element from linting + case "all": + return false; + + // Exclude this JSX element if it is multi-line element + case "multi-line": + return isSingleLine; + + // Exclude this JSX element if it is single-line element + case "single-line": + return !isSingleLine; + + // Nothing special to be done for JSX elements + case "none": + break; + + // no default + } + } + + return ALL_NODES || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression"; + } + + /** + * Determines if a node is surrounded by parentheses twice. + * @param {ASTNode} node - The node to be checked. + * @returns {boolean} True if the node is doubly parenthesised. + * @private + */ + function isParenthesisedTwice(node) { + const previousToken = sourceCode.getTokenBefore(node, 1), + nextToken = sourceCode.getTokenAfter(node, 1); + + return isParenthesised(node) && previousToken && nextToken && + astUtils.isOpeningParenToken(previousToken) && previousToken.range[1] <= node.range[0] && + astUtils.isClosingParenToken(nextToken) && nextToken.range[0] >= node.range[1]; + } + + /** + * Determines if a node is surrounded by (potentially) invalid parentheses. + * @param {ASTNode} node - The node to be checked. + * @returns {boolean} True if the node is incorrectly parenthesised. + * @private + */ + function hasExcessParens(node) { + return ruleApplies(node) && isParenthesised(node); + } + + /** + * Determines if a node that is expected to be parenthesised is surrounded by + * (potentially) invalid extra parentheses. + * @param {ASTNode} node - The node to be checked. + * @returns {boolean} True if the node is has an unexpected extra pair of parentheses. + * @private + */ + function hasDoubleExcessParens(node) { + return ruleApplies(node) && isParenthesisedTwice(node); + } + + /** + * Determines if a node test expression is allowed to have a parenthesised assignment + * @param {ASTNode} node - The node to be checked. + * @returns {boolean} True if the assignment can be parenthesised. + * @private + */ + function isCondAssignException(node) { + return EXCEPT_COND_ASSIGN && node.test.type === "AssignmentExpression"; + } + + /** + * Determines if a node is in a return statement + * @param {ASTNode} node - The node to be checked. + * @returns {boolean} True if the node is in a return statement. + * @private + */ + function isInReturnStatement(node) { + while (node) { + if (node.type === "ReturnStatement" || + (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement")) { + return true; + } + node = node.parent; + } + + return false; + } + + /** + * Determines if a constructor function is newed-up with parens + * @param {ASTNode} newExpression - The NewExpression node to be checked. + * @returns {boolean} True if the constructor is called with parens. + * @private + */ + function isNewExpressionWithParens(newExpression) { + const lastToken = sourceCode.getLastToken(newExpression); + const penultimateToken = sourceCode.getTokenBefore(lastToken); + + return newExpression.arguments.length > 0 || astUtils.isOpeningParenToken(penultimateToken) && astUtils.isClosingParenToken(lastToken); + } + + /** + * Determines if a node is or contains an assignment expression + * @param {ASTNode} node - The node to be checked. + * @returns {boolean} True if the node is or contains an assignment expression. + * @private + */ + function containsAssignment(node) { + if (node.type === "AssignmentExpression") { + return true; + } + if (node.type === "ConditionalExpression" && + (node.consequent.type === "AssignmentExpression" || node.alternate.type === "AssignmentExpression")) { + return true; + } + if ((node.left && node.left.type === "AssignmentExpression") || + (node.right && node.right.type === "AssignmentExpression")) { + return true; + } + + return false; + } + + /** + * Determines if a node is contained by or is itself a return statement and is allowed to have a parenthesised assignment + * @param {ASTNode} node - The node to be checked. + * @returns {boolean} True if the assignment can be parenthesised. + * @private + */ + function isReturnAssignException(node) { + if (!EXCEPT_RETURN_ASSIGN || !isInReturnStatement(node)) { + return false; + } + + if (node.type === "ReturnStatement") { + return node.argument && containsAssignment(node.argument); + } + if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") { + return containsAssignment(node.body); + } + return containsAssignment(node); + + } + + /** + * Determines if a node following a [no LineTerminator here] restriction is + * surrounded by (potentially) invalid extra parentheses. + * @param {Token} token - The token preceding the [no LineTerminator here] restriction. + * @param {ASTNode} node - The node to be checked. + * @returns {boolean} True if the node is incorrectly parenthesised. + * @private + */ + function hasExcessParensNoLineTerminator(token, node) { + if (token.loc.end.line === node.loc.start.line) { + return hasExcessParens(node); + } + + return hasDoubleExcessParens(node); + } + + /** + * Determines whether a node should be preceded by an additional space when removing parens + * @param {ASTNode} node node to evaluate; must be surrounded by parentheses + * @returns {boolean} `true` if a space should be inserted before the node + * @private + */ + function requiresLeadingSpace(node) { + const leftParenToken = sourceCode.getTokenBefore(node); + const tokenBeforeLeftParen = sourceCode.getTokenBefore(node, 1); + const firstToken = sourceCode.getFirstToken(node); + + return tokenBeforeLeftParen && + tokenBeforeLeftParen.range[1] === leftParenToken.range[0] && + leftParenToken.range[1] === firstToken.range[0] && + !astUtils.canTokensBeAdjacent(tokenBeforeLeftParen, firstToken); + } + + /** + * Determines whether a node should be followed by an additional space when removing parens + * @param {ASTNode} node node to evaluate; must be surrounded by parentheses + * @returns {boolean} `true` if a space should be inserted after the node + * @private + */ + function requiresTrailingSpace(node) { + const nextTwoTokens = sourceCode.getTokensAfter(node, { count: 2 }); + const rightParenToken = nextTwoTokens[0]; + const tokenAfterRightParen = nextTwoTokens[1]; + const tokenBeforeRightParen = sourceCode.getLastToken(node); + + return rightParenToken && tokenAfterRightParen && + !sourceCode.isSpaceBetweenTokens(rightParenToken, tokenAfterRightParen) && + !astUtils.canTokensBeAdjacent(tokenBeforeRightParen, tokenAfterRightParen); + } + + /** + * Determines if a given expression node is an IIFE + * @param {ASTNode} node The node to check + * @returns {boolean} `true` if the given node is an IIFE + */ + function isIIFE(node) { + return node.type === "CallExpression" && node.callee.type === "FunctionExpression"; + } + + /** + * Report the node + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function report(node) { + const leftParenToken = sourceCode.getTokenBefore(node); + const rightParenToken = sourceCode.getTokenAfter(node); + + if (!isParenthesisedTwice(node)) { + if (tokensToIgnore.has(sourceCode.getFirstToken(node))) { + return; + } + + if (isIIFE(node) && !isParenthesised(node.callee)) { + return; + } + } + + context.report({ + node, + loc: leftParenToken.loc.start, + message: "Gratuitous parentheses around expression.", + fix(fixer) { + const parenthesizedSource = sourceCode.text.slice(leftParenToken.range[1], rightParenToken.range[0]); + + return fixer.replaceTextRange([ + leftParenToken.range[0], + rightParenToken.range[1] + ], (requiresLeadingSpace(node) ? " " : "") + parenthesizedSource + (requiresTrailingSpace(node) ? " " : "")); + } + }); + } + + /** + * Evaluate Unary update + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function checkUnaryUpdate(node) { + if (node.type === "UnaryExpression" && node.argument.type === "BinaryExpression" && node.argument.operator === "**") { + return; + } + + if (hasExcessParens(node.argument) && precedence(node.argument) >= precedence(node)) { + report(node.argument); + } + } + + /** + * Check if a member expression contains a call expression + * @param {ASTNode} node MemberExpression node to evaluate + * @returns {boolean} true if found, false if not + */ + function doesMemberExpressionContainCallExpression(node) { + let currentNode = node.object; + let currentNodeType = node.object.type; + + while (currentNodeType === "MemberExpression") { + currentNode = currentNode.object; + currentNodeType = currentNode.type; + } + + return currentNodeType === "CallExpression"; + } + + /** + * Evaluate a new call + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function checkCallNew(node) { + const callee = node.callee; + + if (hasExcessParens(callee) && precedence(callee) >= precedence(node)) { + const hasNewParensException = callee.type === "NewExpression" && !isNewExpressionWithParens(callee); + + if ( + hasDoubleExcessParens(callee) || + !isIIFE(node) && !hasNewParensException && !( + + /* + * Allow extra parens around a new expression if + * there are intervening parentheses. + */ + callee.type === "MemberExpression" && + doesMemberExpressionContainCallExpression(callee) + ) + ) { + report(node.callee); + } + } + if (node.arguments.length === 1) { + if (hasDoubleExcessParens(node.arguments[0]) && precedence(node.arguments[0]) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) { + report(node.arguments[0]); + } + } else { + node.arguments + .filter(arg => hasExcessParens(arg) && precedence(arg) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) + .forEach(report); + } + } + + /** + * Evaluate binary logicals + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function checkBinaryLogical(node) { + const prec = precedence(node); + const leftPrecedence = precedence(node.left); + const rightPrecedence = precedence(node.right); + const isExponentiation = node.operator === "**"; + const shouldSkipLeft = (NESTED_BINARY && (node.left.type === "BinaryExpression" || node.left.type === "LogicalExpression")) || + node.left.type === "UnaryExpression" && isExponentiation; + const shouldSkipRight = NESTED_BINARY && (node.right.type === "BinaryExpression" || node.right.type === "LogicalExpression"); + + if (!shouldSkipLeft && hasExcessParens(node.left) && (leftPrecedence > prec || (leftPrecedence === prec && !isExponentiation))) { + report(node.left); + } + if (!shouldSkipRight && hasExcessParens(node.right) && (rightPrecedence > prec || (rightPrecedence === prec && isExponentiation))) { + report(node.right); + } + } + + /** + * Check the parentheses around the super class of the given class definition. + * @param {ASTNode} node The node of class declarations to check. + * @returns {void} + */ + function checkClass(node) { + if (!node.superClass) { + return; + } + + /* + * If `node.superClass` is a LeftHandSideExpression, parentheses are extra. + * Otherwise, parentheses are needed. + */ + const hasExtraParens = precedence(node.superClass) > PRECEDENCE_OF_UPDATE_EXPR + ? hasExcessParens(node.superClass) + : hasDoubleExcessParens(node.superClass); + + if (hasExtraParens) { + report(node.superClass); + } + } + + /** + * Check the parentheses around the argument of the given spread operator. + * @param {ASTNode} node The node of spread elements/properties to check. + * @returns {void} + */ + function checkSpreadOperator(node) { + const hasExtraParens = precedence(node.argument) >= PRECEDENCE_OF_ASSIGNMENT_EXPR + ? hasExcessParens(node.argument) + : hasDoubleExcessParens(node.argument); + + if (hasExtraParens) { + report(node.argument); + } + } + + /** + * Checks the parentheses for an ExpressionStatement or ExportDefaultDeclaration + * @param {ASTNode} node The ExpressionStatement.expression or ExportDefaultDeclaration.declaration node + * @returns {void} + */ + function checkExpressionOrExportStatement(node) { + const firstToken = isParenthesised(node) ? sourceCode.getTokenBefore(node) : sourceCode.getFirstToken(node); + const secondToken = sourceCode.getTokenAfter(firstToken, astUtils.isNotOpeningParenToken); + const thirdToken = secondToken ? sourceCode.getTokenAfter(secondToken) : null; + + if ( + astUtils.isOpeningParenToken(firstToken) && + ( + astUtils.isOpeningBraceToken(secondToken) || + secondToken.type === "Keyword" && ( + secondToken.value === "function" || + secondToken.value === "class" || + secondToken.value === "let" && astUtils.isOpeningBracketToken(sourceCode.getTokenAfter(secondToken, astUtils.isNotClosingParenToken)) + ) || + secondToken && secondToken.type === "Identifier" && secondToken.value === "async" && thirdToken && thirdToken.type === "Keyword" && thirdToken.value === "function" + ) + ) { + tokensToIgnore.add(secondToken); + } + + if (hasExcessParens(node)) { + report(node); + } + } + + return { + ArrayExpression(node) { + node.elements + .filter(e => e && hasExcessParens(e) && precedence(e) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) + .forEach(report); + }, + + ArrowFunctionExpression(node) { + if (isReturnAssignException(node)) { + return; + } + + if (node.body.type === "ConditionalExpression" && + IGNORE_ARROW_CONDITIONALS && + !isParenthesisedTwice(node.body) + ) { + return; + } + + if (node.body.type !== "BlockStatement") { + const firstBodyToken = sourceCode.getFirstToken(node.body, astUtils.isNotOpeningParenToken); + const tokenBeforeFirst = sourceCode.getTokenBefore(firstBodyToken); + + if (astUtils.isOpeningParenToken(tokenBeforeFirst) && astUtils.isOpeningBraceToken(firstBodyToken)) { + tokensToIgnore.add(firstBodyToken); + } + if (hasExcessParens(node.body) && precedence(node.body) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) { + report(node.body); + } + } + }, + + AssignmentExpression(node) { + if (isReturnAssignException(node)) { + return; + } + + if (hasExcessParens(node.right) && precedence(node.right) >= precedence(node)) { + report(node.right); + } + }, + + BinaryExpression: checkBinaryLogical, + CallExpression: checkCallNew, + + ConditionalExpression(node) { + if (isReturnAssignException(node)) { + return; + } + + if (hasExcessParens(node.test) && precedence(node.test) >= precedence({ type: "LogicalExpression", operator: "||" })) { + report(node.test); + } + + if (hasExcessParens(node.consequent) && precedence(node.consequent) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) { + report(node.consequent); + } + + if (hasExcessParens(node.alternate) && precedence(node.alternate) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) { + report(node.alternate); + } + }, + + DoWhileStatement(node) { + if (hasDoubleExcessParens(node.test) && !isCondAssignException(node)) { + report(node.test); + } + }, + + ExportDefaultDeclaration: node => checkExpressionOrExportStatement(node.declaration), + ExpressionStatement: node => checkExpressionOrExportStatement(node.expression), + + "ForInStatement, ForOfStatement"(node) { + if (node.left.type !== "VariableDeclarator") { + const firstLeftToken = sourceCode.getFirstToken(node.left, astUtils.isNotOpeningParenToken); + + if ( + firstLeftToken.value === "let" && ( + + /* + * If `let` is the only thing on the left side of the loop, it's the loop variable: `for ((let) of foo);` + * Removing it will cause a syntax error, because it will be parsed as the start of a VariableDeclarator. + */ + firstLeftToken.range[1] === node.left.range[1] || + + /* + * If `let` is followed by a `[` token, it's a property access on the `let` value: `for ((let[foo]) of bar);` + * Removing it will cause the property access to be parsed as a destructuring declaration of `foo` instead. + */ + astUtils.isOpeningBracketToken( + sourceCode.getTokenAfter(firstLeftToken, astUtils.isNotClosingParenToken) + ) + ) + ) { + tokensToIgnore.add(firstLeftToken); + } + } + if (hasExcessParens(node.right)) { + report(node.right); + } + if (hasExcessParens(node.left)) { + report(node.left); + } + }, + + ForStatement(node) { + if (node.init && hasExcessParens(node.init)) { + report(node.init); + } + + if (node.test && hasExcessParens(node.test) && !isCondAssignException(node)) { + report(node.test); + } + + if (node.update && hasExcessParens(node.update)) { + report(node.update); + } + }, + + IfStatement(node) { + if (hasDoubleExcessParens(node.test) && !isCondAssignException(node)) { + report(node.test); + } + }, + + LogicalExpression: checkBinaryLogical, + + MemberExpression(node) { + const nodeObjHasExcessParens = hasExcessParens(node.object); + + if ( + nodeObjHasExcessParens && + precedence(node.object) >= precedence(node) && + ( + node.computed || + !( + astUtils.isDecimalInteger(node.object) || + + // RegExp literal is allowed to have parens (#1589) + (node.object.type === "Literal" && node.object.regex) + ) + ) + ) { + report(node.object); + } + + if (nodeObjHasExcessParens && + node.object.type === "CallExpression" && + node.parent.type !== "NewExpression") { + report(node.object); + } + + if (node.computed && hasExcessParens(node.property)) { + report(node.property); + } + }, + + NewExpression: checkCallNew, + + ObjectExpression(node) { + node.properties + .filter(property => { + const value = property.value; + + return value && hasExcessParens(value) && precedence(value) >= PRECEDENCE_OF_ASSIGNMENT_EXPR; + }).forEach(property => report(property.value)); + }, + + ReturnStatement(node) { + const returnToken = sourceCode.getFirstToken(node); + + if (isReturnAssignException(node)) { + return; + } + + if (node.argument && + hasExcessParensNoLineTerminator(returnToken, node.argument) && + + // RegExp literal is allowed to have parens (#1589) + !(node.argument.type === "Literal" && node.argument.regex)) { + report(node.argument); + } + }, + + SequenceExpression(node) { + node.expressions + .filter(e => hasExcessParens(e) && precedence(e) >= precedence(node)) + .forEach(report); + }, + + SwitchCase(node) { + if (node.test && hasExcessParens(node.test)) { + report(node.test); + } + }, + + SwitchStatement(node) { + if (hasDoubleExcessParens(node.discriminant)) { + report(node.discriminant); + } + }, + + ThrowStatement(node) { + const throwToken = sourceCode.getFirstToken(node); + + if (hasExcessParensNoLineTerminator(throwToken, node.argument)) { + report(node.argument); + } + }, + + UnaryExpression: checkUnaryUpdate, + UpdateExpression: checkUnaryUpdate, + AwaitExpression: checkUnaryUpdate, + + VariableDeclarator(node) { + if (node.init && hasExcessParens(node.init) && + precedence(node.init) >= PRECEDENCE_OF_ASSIGNMENT_EXPR && + + // RegExp literal is allowed to have parens (#1589) + !(node.init.type === "Literal" && node.init.regex)) { + report(node.init); + } + }, + + WhileStatement(node) { + if (hasDoubleExcessParens(node.test) && !isCondAssignException(node)) { + report(node.test); + } + }, + + WithStatement(node) { + if (hasDoubleExcessParens(node.object)) { + report(node.object); + } + }, + + YieldExpression(node) { + if (node.argument) { + const yieldToken = sourceCode.getFirstToken(node); + + if ((precedence(node.argument) >= precedence(node) && + hasExcessParensNoLineTerminator(yieldToken, node.argument)) || + hasDoubleExcessParens(node.argument)) { + report(node.argument); + } + } + }, + + ClassDeclaration: checkClass, + ClassExpression: checkClass, + + SpreadElement: checkSpreadOperator, + SpreadProperty: checkSpreadOperator, + ExperimentalSpreadProperty: checkSpreadOperator + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-extra-semi.js b/tools/node_modules/eslint/lib/rules/no-extra-semi.js new file mode 100644 index 0000000000..acd312b32b --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-extra-semi.js @@ -0,0 +1,120 @@ +/** + * @fileoverview Rule to flag use of unnecessary semicolons + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const FixTracker = require("../util/fix-tracker"); +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow unnecessary semicolons", + category: "Possible Errors", + recommended: true + }, + + fixable: "code", + schema: [] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + /** + * Reports an unnecessary semicolon error. + * @param {Node|Token} nodeOrToken - A node or a token to be reported. + * @returns {void} + */ + function report(nodeOrToken) { + context.report({ + node: nodeOrToken, + message: "Unnecessary semicolon.", + fix(fixer) { + + /* + * Expand the replacement range to include the surrounding + * tokens to avoid conflicting with semi. + * https://github.com/eslint/eslint/issues/7928 + */ + return new FixTracker(fixer, context.getSourceCode()) + .retainSurroundingTokens(nodeOrToken) + .remove(nodeOrToken); + } + }); + } + + /** + * Checks for a part of a class body. + * This checks tokens from a specified token to a next MethodDefinition or the end of class body. + * + * @param {Token} firstToken - The first token to check. + * @returns {void} + */ + function checkForPartOfClassBody(firstToken) { + for (let token = firstToken; + token.type === "Punctuator" && !astUtils.isClosingBraceToken(token); + token = sourceCode.getTokenAfter(token) + ) { + if (astUtils.isSemicolonToken(token)) { + report(token); + } + } + } + + return { + + /** + * Reports this empty statement, except if the parent node is a loop. + * @param {Node} node - A EmptyStatement node to be reported. + * @returns {void} + */ + EmptyStatement(node) { + const parent = node.parent, + allowedParentTypes = [ + "ForStatement", + "ForInStatement", + "ForOfStatement", + "WhileStatement", + "DoWhileStatement", + "IfStatement", + "LabeledStatement", + "WithStatement" + ]; + + if (allowedParentTypes.indexOf(parent.type) === -1) { + report(node); + } + }, + + /** + * Checks tokens from the head of this class body to the first MethodDefinition or the end of this class body. + * @param {Node} node - A ClassBody node to check. + * @returns {void} + */ + ClassBody(node) { + checkForPartOfClassBody(sourceCode.getFirstToken(node, 1)); // 0 is `{`. + }, + + /** + * Checks tokens from this MethodDefinition to the next MethodDefinition or the end of this class body. + * @param {Node} node - A MethodDefinition node of the start point. + * @returns {void} + */ + MethodDefinition(node) { + checkForPartOfClassBody(sourceCode.getTokenAfter(node)); + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-fallthrough.js b/tools/node_modules/eslint/lib/rules/no-fallthrough.js new file mode 100644 index 0000000000..082e8431d6 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-fallthrough.js @@ -0,0 +1,135 @@ +/** + * @fileoverview Rule to flag fall-through cases in switch statements. + * @author Matt DuVall <http://mattduvall.com/> + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const lodash = require("lodash"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const DEFAULT_FALLTHROUGH_COMMENT = /falls?\s?through/i; + +/** + * Checks whether or not a given node has a fallthrough comment. + * @param {ASTNode} node - A SwitchCase node to get comments. + * @param {RuleContext} context - A rule context which stores comments. + * @param {RegExp} fallthroughCommentPattern - A pattern to match comment to. + * @returns {boolean} `true` if the node has a valid fallthrough comment. + */ +function hasFallthroughComment(node, context, fallthroughCommentPattern) { + const sourceCode = context.getSourceCode(); + const comment = lodash.last(sourceCode.getCommentsBefore(node)); + + return Boolean(comment && fallthroughCommentPattern.test(comment.value)); +} + +/** + * Checks whether or not a given code path segment is reachable. + * @param {CodePathSegment} segment - A CodePathSegment to check. + * @returns {boolean} `true` if the segment is reachable. + */ +function isReachable(segment) { + return segment.reachable; +} + +/** + * Checks whether a node and a token are separated by blank lines + * @param {ASTNode} node - The node to check + * @param {Token} token - The token to compare against + * @returns {boolean} `true` if there are blank lines between node and token + */ +function hasBlankLinesBetween(node, token) { + return token.loc.start.line > node.loc.end.line + 1; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow fallthrough of `case` statements", + category: "Best Practices", + recommended: true + }, + + schema: [ + { + type: "object", + properties: { + commentPattern: { + type: "string" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const options = context.options[0] || {}; + let currentCodePath = null; + const sourceCode = context.getSourceCode(); + + /* + * We need to use leading comments of the next SwitchCase node because + * trailing comments is wrong if semicolons are omitted. + */ + let fallthroughCase = null; + let fallthroughCommentPattern = null; + + if (options.commentPattern) { + fallthroughCommentPattern = new RegExp(options.commentPattern); + } else { + fallthroughCommentPattern = DEFAULT_FALLTHROUGH_COMMENT; + } + + return { + onCodePathStart(codePath) { + currentCodePath = codePath; + }, + onCodePathEnd() { + currentCodePath = currentCodePath.upper; + }, + + SwitchCase(node) { + + /* + * Checks whether or not there is a fallthrough comment. + * And reports the previous fallthrough node if that does not exist. + */ + if (fallthroughCase && !hasFallthroughComment(node, context, fallthroughCommentPattern)) { + context.report({ + message: "Expected a 'break' statement before '{{type}}'.", + data: { type: node.test ? "case" : "default" }, + node + }); + } + fallthroughCase = null; + }, + + "SwitchCase:exit"(node) { + const nextToken = sourceCode.getTokenAfter(node); + + /* + * `reachable` meant fall through because statements preceded by + * `break`, `return`, or `throw` are unreachable. + * And allows empty cases and the last case. + */ + if (currentCodePath.currentSegments.some(isReachable) && + (node.consequent.length > 0 || hasBlankLinesBetween(node, nextToken)) && + lodash.last(node.parent.cases) !== node) { + fallthroughCase = node; + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-floating-decimal.js b/tools/node_modules/eslint/lib/rules/no-floating-decimal.js new file mode 100644 index 0000000000..dfba453a49 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-floating-decimal.js @@ -0,0 +1,64 @@ +/** + * @fileoverview Rule to flag use of a leading/trailing decimal point in a numeric literal + * @author James Allardice + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow leading or trailing decimal points in numeric literals", + category: "Best Practices", + recommended: false + }, + + schema: [], + + fixable: "code" + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + return { + Literal(node) { + + if (typeof node.value === "number") { + if (node.raw.startsWith(".")) { + context.report({ + node, + message: "A leading decimal point can be confused with a dot.", + fix(fixer) { + const tokenBefore = sourceCode.getTokenBefore(node); + const needsSpaceBefore = tokenBefore && + tokenBefore.range[1] === node.range[0] && + !astUtils.canTokensBeAdjacent(tokenBefore, `0${node.raw}`); + + return fixer.insertTextBefore(node, needsSpaceBefore ? " 0" : "0"); + } + }); + } + if (node.raw.indexOf(".") === node.raw.length - 1) { + context.report({ + node, + message: "A trailing decimal point can be confused with a dot.", + fix: fixer => fixer.insertTextAfter(node, "0") + }); + } + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-func-assign.js b/tools/node_modules/eslint/lib/rules/no-func-assign.js new file mode 100644 index 0000000000..ea86365b29 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-func-assign.js @@ -0,0 +1,63 @@ +/** + * @fileoverview Rule to flag use of function declaration identifiers as variables. + * @author Ian Christian Myers + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow reassigning `function` declarations", + category: "Possible Errors", + recommended: true + }, + + schema: [] + }, + + create(context) { + + /** + * Reports a reference if is non initializer and writable. + * @param {References} references - Collection of reference to check. + * @returns {void} + */ + function checkReference(references) { + astUtils.getModifyingReferences(references).forEach(reference => { + context.report({ node: reference.identifier, message: "'{{name}}' is a function.", data: { name: reference.identifier.name } }); + }); + } + + /** + * Finds and reports references that are non initializer and writable. + * @param {Variable} variable - A variable to check. + * @returns {void} + */ + function checkVariable(variable) { + if (variable.defs[0].type === "FunctionName") { + checkReference(variable.references); + } + } + + /** + * Checks parameters of a given function node. + * @param {ASTNode} node - A function node to check. + * @returns {void} + */ + function checkForFunction(node) { + context.getDeclaredVariables(node).forEach(checkVariable); + } + + return { + FunctionDeclaration: checkForFunction, + FunctionExpression: checkForFunction + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-global-assign.js b/tools/node_modules/eslint/lib/rules/no-global-assign.js new file mode 100644 index 0000000000..679650cb70 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-global-assign.js @@ -0,0 +1,85 @@ +/** + * @fileoverview Rule to disallow assignments to native objects or read-only global variables + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow assignments to native objects or read-only global variables", + category: "Best Practices", + recommended: true + }, + + schema: [ + { + type: "object", + properties: { + exceptions: { + type: "array", + items: { type: "string" }, + uniqueItems: true + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const config = context.options[0]; + const exceptions = (config && config.exceptions) || []; + + /** + * Reports write references. + * @param {Reference} reference - A reference to check. + * @param {int} index - The index of the reference in the references. + * @param {Reference[]} references - The array that the reference belongs to. + * @returns {void} + */ + function checkReference(reference, index, references) { + const identifier = reference.identifier; + + if (reference.init === false && + reference.isWrite() && + + /* + * Destructuring assignments can have multiple default value, + * so possibly there are multiple writeable references for the same identifier. + */ + (index === 0 || references[index - 1].identifier !== identifier) + ) { + context.report({ + node: identifier, + message: "Read-only global '{{name}}' should not be modified.", + data: identifier + }); + } + } + + /** + * Reports write references if a given variable is read-only builtin. + * @param {Variable} variable - A variable to check. + * @returns {void} + */ + function checkVariable(variable) { + if (variable.writeable === false && exceptions.indexOf(variable.name) === -1) { + variable.references.forEach(checkReference); + } + } + + return { + Program() { + const globalScope = context.getScope(); + + globalScope.variables.forEach(checkVariable); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-implicit-coercion.js b/tools/node_modules/eslint/lib/rules/no-implicit-coercion.js new file mode 100644 index 0000000000..24e04858f0 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-implicit-coercion.js @@ -0,0 +1,292 @@ +/** + * @fileoverview A rule to disallow the type conversions with shorter notations. + * @author Toru Nagashima + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const INDEX_OF_PATTERN = /^(?:i|lastI)ndexOf$/; +const ALLOWABLE_OPERATORS = ["~", "!!", "+", "*"]; + +/** + * Parses and normalizes an option object. + * @param {Object} options - An option object to parse. + * @returns {Object} The parsed and normalized option object. + */ +function parseOptions(options) { + options = options || {}; + return { + boolean: "boolean" in options ? Boolean(options.boolean) : true, + number: "number" in options ? Boolean(options.number) : true, + string: "string" in options ? Boolean(options.string) : true, + allow: options.allow || [] + }; +} + +/** + * Checks whether or not a node is a double logical nigating. + * @param {ASTNode} node - An UnaryExpression node to check. + * @returns {boolean} Whether or not the node is a double logical nigating. + */ +function isDoubleLogicalNegating(node) { + return ( + node.operator === "!" && + node.argument.type === "UnaryExpression" && + node.argument.operator === "!" + ); +} + +/** + * Checks whether or not a node is a binary negating of `.indexOf()` method calling. + * @param {ASTNode} node - An UnaryExpression node to check. + * @returns {boolean} Whether or not the node is a binary negating of `.indexOf()` method calling. + */ +function isBinaryNegatingOfIndexOf(node) { + return ( + node.operator === "~" && + node.argument.type === "CallExpression" && + node.argument.callee.type === "MemberExpression" && + node.argument.callee.property.type === "Identifier" && + INDEX_OF_PATTERN.test(node.argument.callee.property.name) + ); +} + +/** + * Checks whether or not a node is a multiplying by one. + * @param {BinaryExpression} node - A BinaryExpression node to check. + * @returns {boolean} Whether or not the node is a multiplying by one. + */ +function isMultiplyByOne(node) { + return node.operator === "*" && ( + node.left.type === "Literal" && node.left.value === 1 || + node.right.type === "Literal" && node.right.value === 1 + ); +} + +/** + * Checks whether the result of a node is numeric or not + * @param {ASTNode} node The node to test + * @returns {boolean} true if the node is a number literal or a `Number()`, `parseInt` or `parseFloat` call + */ +function isNumeric(node) { + return ( + node.type === "Literal" && typeof node.value === "number" || + node.type === "CallExpression" && ( + node.callee.name === "Number" || + node.callee.name === "parseInt" || + node.callee.name === "parseFloat" + ) + ); +} + +/** + * Returns the first non-numeric operand in a BinaryExpression. Designed to be + * used from bottom to up since it walks up the BinaryExpression trees using + * node.parent to find the result. + * @param {BinaryExpression} node The BinaryExpression node to be walked up on + * @returns {ASTNode|null} The first non-numeric item in the BinaryExpression tree or null + */ +function getNonNumericOperand(node) { + const left = node.left, + right = node.right; + + if (right.type !== "BinaryExpression" && !isNumeric(right)) { + return right; + } + + if (left.type !== "BinaryExpression" && !isNumeric(left)) { + return left; + } + + return null; +} + +/** + * Checks whether a node is an empty string literal or not. + * @param {ASTNode} node The node to check. + * @returns {boolean} Whether or not the passed in node is an + * empty string literal or not. + */ +function isEmptyString(node) { + return astUtils.isStringLiteral(node) && (node.value === "" || (node.type === "TemplateLiteral" && node.quasis.length === 1 && node.quasis[0].value.cooked === "")); +} + +/** + * Checks whether or not a node is a concatenating with an empty string. + * @param {ASTNode} node - A BinaryExpression node to check. + * @returns {boolean} Whether or not the node is a concatenating with an empty string. + */ +function isConcatWithEmptyString(node) { + return node.operator === "+" && ( + (isEmptyString(node.left) && !astUtils.isStringLiteral(node.right)) || + (isEmptyString(node.right) && !astUtils.isStringLiteral(node.left)) + ); +} + +/** + * Checks whether or not a node is appended with an empty string. + * @param {ASTNode} node - An AssignmentExpression node to check. + * @returns {boolean} Whether or not the node is appended with an empty string. + */ +function isAppendEmptyString(node) { + return node.operator === "+=" && isEmptyString(node.right); +} + +/** + * Returns the operand that is not an empty string from a flagged BinaryExpression. + * @param {ASTNode} node - The flagged BinaryExpression node to check. + * @returns {ASTNode} The operand that is not an empty string from a flagged BinaryExpression. + */ +function getNonEmptyOperand(node) { + return isEmptyString(node.left) ? node.right : node.left; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow shorthand type conversions", + category: "Best Practices", + recommended: false + }, + + fixable: "code", + schema: [{ + type: "object", + properties: { + boolean: { + type: "boolean" + }, + number: { + type: "boolean" + }, + string: { + type: "boolean" + }, + allow: { + type: "array", + items: { + enum: ALLOWABLE_OPERATORS + }, + uniqueItems: true + } + }, + additionalProperties: false + }] + }, + + create(context) { + const options = parseOptions(context.options[0]); + const sourceCode = context.getSourceCode(); + + /** + * Reports an error and autofixes the node + * @param {ASTNode} node - An ast node to report the error on. + * @param {string} recommendation - The recommended code for the issue + * @param {bool} shouldFix - Whether this report should fix the node + * @returns {void} + */ + function report(node, recommendation, shouldFix) { + shouldFix = typeof shouldFix === "undefined" ? true : shouldFix; + + context.report({ + node, + message: "use `{{recommendation}}` instead.", + data: { + recommendation + }, + fix(fixer) { + if (!shouldFix) { + return null; + } + + const tokenBefore = sourceCode.getTokenBefore(node); + + if ( + tokenBefore && + tokenBefore.range[1] === node.range[0] && + !astUtils.canTokensBeAdjacent(tokenBefore, recommendation) + ) { + return fixer.replaceText(node, ` ${recommendation}`); + } + return fixer.replaceText(node, recommendation); + } + }); + } + + return { + UnaryExpression(node) { + let operatorAllowed; + + // !!foo + operatorAllowed = options.allow.indexOf("!!") >= 0; + if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) { + const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`; + + report(node, recommendation); + } + + // ~foo.indexOf(bar) + operatorAllowed = options.allow.indexOf("~") >= 0; + if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) { + const recommendation = `${sourceCode.getText(node.argument)} !== -1`; + + report(node, recommendation, false); + } + + // +foo + operatorAllowed = options.allow.indexOf("+") >= 0; + if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) { + const recommendation = `Number(${sourceCode.getText(node.argument)})`; + + report(node, recommendation); + } + }, + + // Use `:exit` to prevent double reporting + "BinaryExpression:exit"(node) { + let operatorAllowed; + + // 1 * foo + operatorAllowed = options.allow.indexOf("*") >= 0; + const nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && getNonNumericOperand(node); + + if (nonNumericOperand) { + const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`; + + report(node, recommendation); + } + + // "" + foo + operatorAllowed = options.allow.indexOf("+") >= 0; + if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) { + const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`; + + report(node, recommendation); + } + }, + + AssignmentExpression(node) { + + // foo += "" + const operatorAllowed = options.allow.indexOf("+") >= 0; + + if (!operatorAllowed && options.string && isAppendEmptyString(node)) { + const code = sourceCode.getText(getNonEmptyOperand(node)); + const recommendation = `${code} = String(${code})`; + + report(node, recommendation); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-implicit-globals.js b/tools/node_modules/eslint/lib/rules/no-implicit-globals.js new file mode 100644 index 0000000000..f0962cdc7a --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-implicit-globals.js @@ -0,0 +1,55 @@ +/** + * @fileoverview Rule to check for implicit global variables and functions. + * @author Joshua Peek + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow variable and `function` declarations in the global scope", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + return { + Program() { + const scope = context.getScope(); + + scope.variables.forEach(variable => { + if (variable.writeable) { + return; + } + + variable.defs.forEach(def => { + if (def.type === "FunctionName" || (def.type === "Variable" && def.parent.kind === "var")) { + context.report({ node: def.node, message: "Implicit global variable, assign as global property instead." }); + } + }); + }); + + scope.implicit.variables.forEach(variable => { + const scopeVariable = scope.set.get(variable.name); + + if (scopeVariable && scopeVariable.writeable) { + return; + } + + variable.defs.forEach(def => { + context.report({ node: def.node, message: "Implicit global variable, assign as global property instead." }); + }); + }); + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-implied-eval.js b/tools/node_modules/eslint/lib/rules/no-implied-eval.js new file mode 100644 index 0000000000..cfb16dbf73 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-implied-eval.js @@ -0,0 +1,161 @@ +/** + * @fileoverview Rule to flag use of implied eval via setTimeout and setInterval + * @author James Allardice + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow the use of `eval()`-like methods", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + const CALLEE_RE = /^(setTimeout|setInterval|execScript)$/; + + /* + * Figures out if we should inspect a given binary expression. Is a stack + * of stacks, where the first element in each substack is a CallExpression. + */ + const impliedEvalAncestorsStack = []; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Get the last element of an array, without modifying arr, like pop(), but non-destructive. + * @param {array} arr What to inspect + * @returns {*} The last element of arr + * @private + */ + function last(arr) { + return arr ? arr[arr.length - 1] : null; + } + + /** + * Checks if the given MemberExpression node is a potentially implied eval identifier on window. + * @param {ASTNode} node The MemberExpression node to check. + * @returns {boolean} Whether or not the given node is potentially an implied eval. + * @private + */ + function isImpliedEvalMemberExpression(node) { + const object = node.object, + property = node.property, + hasImpliedEvalName = CALLEE_RE.test(property.name) || CALLEE_RE.test(property.value); + + return object.name === "window" && hasImpliedEvalName; + } + + /** + * Determines if a node represents a call to a potentially implied eval. + * + * This checks the callee name and that there's an argument, but not the type of the argument. + * + * @param {ASTNode} node The CallExpression to check. + * @returns {boolean} True if the node matches, false if not. + * @private + */ + function isImpliedEvalCallExpression(node) { + const isMemberExpression = (node.callee.type === "MemberExpression"), + isIdentifier = (node.callee.type === "Identifier"), + isImpliedEvalCallee = + (isIdentifier && CALLEE_RE.test(node.callee.name)) || + (isMemberExpression && isImpliedEvalMemberExpression(node.callee)); + + return isImpliedEvalCallee && node.arguments.length; + } + + /** + * Checks that the parent is a direct descendent of an potential implied eval CallExpression, and if the parent is a CallExpression, that we're the first argument. + * @param {ASTNode} node The node to inspect the parent of. + * @returns {boolean} Was the parent a direct descendent, and is the child therefore potentially part of a dangerous argument? + * @private + */ + function hasImpliedEvalParent(node) { + + // make sure our parent is marked + return node.parent === last(last(impliedEvalAncestorsStack)) && + + // if our parent is a CallExpression, make sure we're the first argument + (node.parent.type !== "CallExpression" || node === node.parent.arguments[0]); + } + + /** + * Checks if our parent is marked as part of an implied eval argument. If + * so, collapses the top of impliedEvalAncestorsStack and reports on the + * original CallExpression. + * @param {ASTNode} node The CallExpression to check. + * @returns {boolean} True if the node matches, false if not. + * @private + */ + function checkString(node) { + if (hasImpliedEvalParent(node)) { + + // remove the entire substack, to avoid duplicate reports + const substack = impliedEvalAncestorsStack.pop(); + + context.report({ node: substack[0], message: "Implied eval. Consider passing a function instead of a string." }); + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + CallExpression(node) { + if (isImpliedEvalCallExpression(node)) { + + // call expressions create a new substack + impliedEvalAncestorsStack.push([node]); + } + }, + + "CallExpression:exit"(node) { + if (node === last(last(impliedEvalAncestorsStack))) { + + /* + * Destroys the entire sub-stack, rather than just using + * last(impliedEvalAncestorsStack).pop(), as a CallExpression is + * always the bottom of a impliedEvalAncestorsStack substack. + */ + impliedEvalAncestorsStack.pop(); + } + }, + + BinaryExpression(node) { + if (node.operator === "+" && hasImpliedEvalParent(node)) { + last(impliedEvalAncestorsStack).push(node); + } + }, + + "BinaryExpression:exit"(node) { + if (node === last(last(impliedEvalAncestorsStack))) { + last(impliedEvalAncestorsStack).pop(); + } + }, + + Literal(node) { + if (typeof node.value === "string") { + checkString(node); + } + }, + + TemplateLiteral(node) { + checkString(node); + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-inline-comments.js b/tools/node_modules/eslint/lib/rules/no-inline-comments.js new file mode 100644 index 0000000000..42b4753dfd --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-inline-comments.js @@ -0,0 +1,65 @@ +/** + * @fileoverview Enforces or disallows inline comments. + * @author Greg Cochard + */ +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow inline comments after code", + category: "Stylistic Issues", + recommended: false + }, + + schema: [] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + /** + * Will check that comments are not on lines starting with or ending with code + * @param {ASTNode} node The comment node to check + * @private + * @returns {void} + */ + function testCodeAroundComment(node) { + + // Get the whole line and cut it off at the start of the comment + const startLine = String(sourceCode.lines[node.loc.start.line - 1]); + const endLine = String(sourceCode.lines[node.loc.end.line - 1]); + + const preamble = startLine.slice(0, node.loc.start.column).trim(); + + // Also check after the comment + const postamble = endLine.slice(node.loc.end.column).trim(); + + // Check that this comment isn't an ESLint directive + const isDirective = astUtils.isDirectiveComment(node); + + // Should be empty if there was only whitespace around the comment + if (!isDirective && (preamble || postamble)) { + context.report({ node, message: "Unexpected comment inline with code." }); + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + Program() { + const comments = sourceCode.getAllComments(); + + comments.filter(token => token.type !== "Shebang").forEach(testCodeAroundComment); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-inner-declarations.js b/tools/node_modules/eslint/lib/rules/no-inner-declarations.js new file mode 100644 index 0000000000..28aa5b4b5c --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-inner-declarations.js @@ -0,0 +1,89 @@ +/** + * @fileoverview Rule to enforce declarations in program or function body root. + * @author Brandon Mills + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow variable or `function` declarations in nested blocks", + category: "Possible Errors", + recommended: true + }, + + schema: [ + { + enum: ["functions", "both"] + } + ] + }, + + create(context) { + + /** + * Find the nearest Program or Function ancestor node. + * @returns {Object} Ancestor's type and distance from node. + */ + function nearestBody() { + const ancestors = context.getAncestors(); + let ancestor = ancestors.pop(), + generation = 1; + + while (ancestor && ["Program", "FunctionDeclaration", + "FunctionExpression", "ArrowFunctionExpression" + ].indexOf(ancestor.type) < 0) { + generation += 1; + ancestor = ancestors.pop(); + } + + return { + + // Type of containing ancestor + type: ancestor.type, + + // Separation between ancestor and node + distance: generation + }; + } + + /** + * Ensure that a given node is at a program or function body's root. + * @param {ASTNode} node Declaration node to check. + * @returns {void} + */ + function check(node) { + const body = nearestBody(), + valid = ((body.type === "Program" && body.distance === 1) || + body.distance === 2); + + if (!valid) { + context.report({ + node, + message: "Move {{type}} declaration to {{body}} root.", + data: { + type: (node.type === "FunctionDeclaration" ? "function" : "variable"), + body: (body.type === "Program" ? "program" : "function body") + } + }); + } + } + + return { + + FunctionDeclaration: check, + VariableDeclaration(node) { + if (context.options[0] === "both" && node.kind === "var") { + check(node); + } + } + + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-invalid-regexp.js b/tools/node_modules/eslint/lib/rules/no-invalid-regexp.js new file mode 100644 index 0000000000..45596f7ee8 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-invalid-regexp.js @@ -0,0 +1,106 @@ +/** + * @fileoverview Validate strings passed to the RegExp constructor + * @author Michael Ficarra + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const espree = require("espree"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow invalid regular expression strings in `RegExp` constructors", + category: "Possible Errors", + recommended: true + }, + + schema: [{ + type: "object", + properties: { + allowConstructorFlags: { + type: "array", + items: { + type: "string" + } + } + }, + additionalProperties: false + }] + }, + + create(context) { + + const options = context.options[0]; + let allowedFlags = ""; + + if (options && options.allowConstructorFlags) { + allowedFlags = options.allowConstructorFlags.join(""); + } + + /** + * Check if node is a string + * @param {ASTNode} node node to evaluate + * @returns {boolean} True if its a string + * @private + */ + function isString(node) { + return node && node.type === "Literal" && typeof node.value === "string"; + } + + /** + * Validate strings passed to the RegExp constructor + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function check(node) { + if (node.callee.type === "Identifier" && node.callee.name === "RegExp" && isString(node.arguments[0])) { + let flags = isString(node.arguments[1]) ? node.arguments[1].value : ""; + + if (allowedFlags) { + flags = flags.replace(new RegExp(`[${allowedFlags}]`, "gi"), ""); + } + + try { + void new RegExp(node.arguments[0].value); + } catch (e) { + context.report({ + node, + message: "{{message}}.", + data: e + }); + } + + if (flags) { + + try { + espree.parse(`/./${flags}`, context.parserOptions); + } catch (ex) { + context.report({ + node, + message: "Invalid flags supplied to RegExp constructor '{{flags}}'.", + data: { + flags + } + }); + } + } + + } + } + + return { + CallExpression: check, + NewExpression: check + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-invalid-this.js b/tools/node_modules/eslint/lib/rules/no-invalid-this.js new file mode 100644 index 0000000000..5a0a62f7a1 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-invalid-this.js @@ -0,0 +1,123 @@ +/** + * @fileoverview A rule to disallow `this` keywords outside of classes or class-like objects. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `this` keywords outside of classes or class-like objects", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + const stack = [], + sourceCode = context.getSourceCode(); + + /** + * Gets the current checking context. + * + * The return value has a flag that whether or not `this` keyword is valid. + * The flag is initialized when got at the first time. + * + * @returns {{valid: boolean}} + * an object which has a flag that whether or not `this` keyword is valid. + */ + stack.getCurrent = function() { + const current = this[this.length - 1]; + + if (!current.init) { + current.init = true; + current.valid = !astUtils.isDefaultThisBinding( + current.node, + sourceCode + ); + } + return current; + }; + + /** + * Pushs new checking context into the stack. + * + * The checking context is not initialized yet. + * Because most functions don't have `this` keyword. + * When `this` keyword was found, the checking context is initialized. + * + * @param {ASTNode} node - A function node that was entered. + * @returns {void} + */ + function enterFunction(node) { + + // `this` can be invalid only under strict mode. + stack.push({ + init: !context.getScope().isStrict, + node, + valid: true + }); + } + + /** + * Pops the current checking context from the stack. + * @returns {void} + */ + function exitFunction() { + stack.pop(); + } + + return { + + /* + * `this` is invalid only under strict mode. + * Modules is always strict mode. + */ + Program(node) { + const scope = context.getScope(), + features = context.parserOptions.ecmaFeatures || {}; + + stack.push({ + init: true, + node, + valid: !( + scope.isStrict || + node.sourceType === "module" || + (features.globalReturn && scope.childScopes[0].isStrict) + ) + }); + }, + + "Program:exit"() { + stack.pop(); + }, + + FunctionDeclaration: enterFunction, + "FunctionDeclaration:exit": exitFunction, + FunctionExpression: enterFunction, + "FunctionExpression:exit": exitFunction, + + // Reports if `this` of the current context is invalid. + ThisExpression(node) { + const current = stack.getCurrent(); + + if (current && !current.valid) { + context.report({ node, message: "Unexpected 'this'." }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-irregular-whitespace.js b/tools/node_modules/eslint/lib/rules/no-irregular-whitespace.js new file mode 100644 index 0000000000..cfbdfd1a5e --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-irregular-whitespace.js @@ -0,0 +1,236 @@ +/** + * @fileoverview Rule to disalow whitespace that is not a tab or space, whitespace inside strings and comments are allowed + * @author Jonathan Kingston + * @author Christophe Porteneuve + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Constants +//------------------------------------------------------------------------------ + +const ALL_IRREGULARS = /[\f\v\u0085\ufeff\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000\u2028\u2029]/; +const IRREGULAR_WHITESPACE = /[\f\v\u0085\ufeff\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000]+/mg; +const IRREGULAR_LINE_TERMINATORS = /[\u2028\u2029]/mg; +const LINE_BREAK = astUtils.createGlobalLinebreakMatcher(); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow irregular whitespace outside of strings and comments", + category: "Possible Errors", + recommended: true + }, + + schema: [ + { + type: "object", + properties: { + skipComments: { + type: "boolean" + }, + skipStrings: { + type: "boolean" + }, + skipTemplates: { + type: "boolean" + }, + skipRegExps: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + // Module store of errors that we have found + let errors = []; + + // Lookup the `skipComments` option, which defaults to `false`. + const options = context.options[0] || {}; + const skipComments = !!options.skipComments; + const skipStrings = options.skipStrings !== false; + const skipRegExps = !!options.skipRegExps; + const skipTemplates = !!options.skipTemplates; + + const sourceCode = context.getSourceCode(); + const commentNodes = sourceCode.getAllComments(); + + /** + * Removes errors that occur inside a string node + * @param {ASTNode} node to check for matching errors. + * @returns {void} + * @private + */ + function removeWhitespaceError(node) { + const locStart = node.loc.start; + const locEnd = node.loc.end; + + errors = errors.filter(error => { + const errorLoc = error[1]; + + if (errorLoc.line >= locStart.line && errorLoc.line <= locEnd.line) { + if (errorLoc.column >= locStart.column && (errorLoc.column <= locEnd.column || errorLoc.line < locEnd.line)) { + return false; + } + } + return true; + }); + } + + /** + * Checks identifier or literal nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors + * @param {ASTNode} node to check for matching errors. + * @returns {void} + * @private + */ + function removeInvalidNodeErrorsInIdentifierOrLiteral(node) { + const shouldCheckStrings = skipStrings && (typeof node.value === "string"); + const shouldCheckRegExps = skipRegExps && (node.value instanceof RegExp); + + if (shouldCheckStrings || shouldCheckRegExps) { + + // If we have irregular characters remove them from the errors list + if (ALL_IRREGULARS.test(node.raw)) { + removeWhitespaceError(node); + } + } + } + + /** + * Checks template string literal nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors + * @param {ASTNode} node to check for matching errors. + * @returns {void} + * @private + */ + function removeInvalidNodeErrorsInTemplateLiteral(node) { + if (typeof node.value.raw === "string") { + if (ALL_IRREGULARS.test(node.value.raw)) { + removeWhitespaceError(node); + } + } + } + + /** + * Checks comment nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors + * @param {ASTNode} node to check for matching errors. + * @returns {void} + * @private + */ + function removeInvalidNodeErrorsInComment(node) { + if (ALL_IRREGULARS.test(node.value)) { + removeWhitespaceError(node); + } + } + + /** + * Checks the program source for irregular whitespace + * @param {ASTNode} node The program node + * @returns {void} + * @private + */ + function checkForIrregularWhitespace(node) { + const sourceLines = sourceCode.lines; + + sourceLines.forEach((sourceLine, lineIndex) => { + const lineNumber = lineIndex + 1; + let match; + + while ((match = IRREGULAR_WHITESPACE.exec(sourceLine)) !== null) { + const location = { + line: lineNumber, + column: match.index + }; + + errors.push([node, location, "Irregular whitespace not allowed."]); + } + }); + } + + /** + * Checks the program source for irregular line terminators + * @param {ASTNode} node The program node + * @returns {void} + * @private + */ + function checkForIrregularLineTerminators(node) { + const source = sourceCode.getText(), + sourceLines = sourceCode.lines, + linebreaks = source.match(LINE_BREAK); + let lastLineIndex = -1, + match; + + while ((match = IRREGULAR_LINE_TERMINATORS.exec(source)) !== null) { + const lineIndex = linebreaks.indexOf(match[0], lastLineIndex + 1) || 0; + const location = { + line: lineIndex + 1, + column: sourceLines[lineIndex].length + }; + + errors.push([node, location, "Irregular whitespace not allowed."]); + lastLineIndex = lineIndex; + } + } + + /** + * A no-op function to act as placeholder for comment accumulation when the `skipComments` option is `false`. + * @returns {void} + * @private + */ + function noop() {} + + const nodes = {}; + + if (ALL_IRREGULARS.test(sourceCode.getText())) { + nodes.Program = function(node) { + + /* + * As we can easily fire warnings for all white space issues with + * all the source its simpler to fire them here. + * This means we can check all the application code without having + * to worry about issues caused in the parser tokens. + * When writing this code also evaluating per node was missing out + * connecting tokens in some cases. + * We can later filter the errors when they are found to be not an + * issue in nodes we don't care about. + */ + checkForIrregularWhitespace(node); + checkForIrregularLineTerminators(node); + }; + + nodes.Identifier = removeInvalidNodeErrorsInIdentifierOrLiteral; + nodes.Literal = removeInvalidNodeErrorsInIdentifierOrLiteral; + nodes.TemplateElement = skipTemplates ? removeInvalidNodeErrorsInTemplateLiteral : noop; + nodes["Program:exit"] = function() { + if (skipComments) { + + // First strip errors occurring in comment nodes. + commentNodes.forEach(removeInvalidNodeErrorsInComment); + } + + // If we have any errors remaining report on them + errors.forEach(error => { + context.report.apply(context, error); + }); + }; + } else { + nodes.Program = noop; + } + + return nodes; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-iterator.js b/tools/node_modules/eslint/lib/rules/no-iterator.js new file mode 100644 index 0000000000..3677dd94d9 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-iterator.js @@ -0,0 +1,38 @@ +/** + * @fileoverview Rule to flag usage of __iterator__ property + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow the use of the `__iterator__` property", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + + return { + + MemberExpression(node) { + + if (node.property && + (node.property.type === "Identifier" && node.property.name === "__iterator__" && !node.computed) || + (node.property.type === "Literal" && node.property.value === "__iterator__")) { + context.report({ node, message: "Reserved name '__iterator__'." }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-label-var.js b/tools/node_modules/eslint/lib/rules/no-label-var.js new file mode 100644 index 0000000000..954066aef3 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-label-var.js @@ -0,0 +1,69 @@ +/** + * @fileoverview Rule to flag labels that are the same as an identifier + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow labels that share a name with a variable", + category: "Variables", + recommended: false + }, + + schema: [] + }, + + create(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Check if the identifier is present inside current scope + * @param {Object} scope current scope + * @param {string} name To evaluate + * @returns {boolean} True if its present + * @private + */ + function findIdentifier(scope, name) { + return astUtils.getVariableByName(scope, name) !== null; + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + + LabeledStatement(node) { + + // Fetch the innermost scope. + const scope = context.getScope(); + + /* + * Recursively find the identifier walking up the scope, starting + * with the innermost scope. + */ + if (findIdentifier(scope, node.label.name)) { + context.report({ node, message: "Found identifier with same name as label." }); + } + } + + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-labels.js b/tools/node_modules/eslint/lib/rules/no-labels.js new file mode 100644 index 0000000000..101092a667 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-labels.js @@ -0,0 +1,141 @@ +/** + * @fileoverview Disallow Labeled Statements + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow labeled statements", + category: "Best Practices", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + allowLoop: { + type: "boolean" + }, + allowSwitch: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const options = context.options[0]; + const allowLoop = Boolean(options && options.allowLoop); + const allowSwitch = Boolean(options && options.allowSwitch); + let scopeInfo = null; + + /** + * Gets the kind of a given node. + * + * @param {ASTNode} node - A node to get. + * @returns {string} The kind of the node. + */ + function getBodyKind(node) { + if (astUtils.isLoop(node)) { + return "loop"; + } + if (node.type === "SwitchStatement") { + return "switch"; + } + return "other"; + } + + /** + * Checks whether the label of a given kind is allowed or not. + * + * @param {string} kind - A kind to check. + * @returns {boolean} `true` if the kind is allowed. + */ + function isAllowed(kind) { + switch (kind) { + case "loop": return allowLoop; + case "switch": return allowSwitch; + default: return false; + } + } + + /** + * Checks whether a given name is a label of a loop or not. + * + * @param {string} label - A name of a label to check. + * @returns {boolean} `true` if the name is a label of a loop. + */ + function getKind(label) { + let info = scopeInfo; + + while (info) { + if (info.label === label) { + return info.kind; + } + info = info.upper; + } + + /* istanbul ignore next: syntax error */ + return "other"; + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + LabeledStatement(node) { + scopeInfo = { + label: node.label.name, + kind: getBodyKind(node.body), + upper: scopeInfo + }; + }, + + "LabeledStatement:exit"(node) { + if (!isAllowed(scopeInfo.kind)) { + context.report({ + node, + message: "Unexpected labeled statement." + }); + } + + scopeInfo = scopeInfo.upper; + }, + + BreakStatement(node) { + if (node.label && !isAllowed(getKind(node.label.name))) { + context.report({ + node, + message: "Unexpected label in break statement." + }); + } + }, + + ContinueStatement(node) { + if (node.label && !isAllowed(getKind(node.label.name))) { + context.report({ + node, + message: "Unexpected label in continue statement." + }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-lone-blocks.js b/tools/node_modules/eslint/lib/rules/no-lone-blocks.js new file mode 100644 index 0000000000..2b5666e213 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-lone-blocks.js @@ -0,0 +1,112 @@ +/** + * @fileoverview Rule to flag blocks with no reason to exist + * @author Brandon Mills + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow unnecessary nested blocks", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + + // A stack of lone blocks to be checked for block-level bindings + const loneBlocks = []; + let ruleDef; + + /** + * Reports a node as invalid. + * @param {ASTNode} node - The node to be reported. + * @returns {void} + */ + function report(node) { + const message = node.parent.type === "BlockStatement" ? "Nested block is redundant." : "Block is redundant."; + + context.report({ node, message }); + } + + /** + * Checks for any ocurrence of a BlockStatement in a place where lists of statements can appear + * @param {ASTNode} node The node to check + * @returns {boolean} True if the node is a lone block. + */ + function isLoneBlock(node) { + return node.parent.type === "BlockStatement" || + node.parent.type === "Program" || + + // Don't report blocks in switch cases if the block is the only statement of the case. + node.parent.type === "SwitchCase" && !(node.parent.consequent[0] === node && node.parent.consequent.length === 1); + } + + /** + * Checks the enclosing block of the current node for block-level bindings, + * and "marks it" as valid if any. + * @returns {void} + */ + function markLoneBlock() { + if (loneBlocks.length === 0) { + return; + } + + const block = context.getAncestors().pop(); + + if (loneBlocks[loneBlocks.length - 1] === block) { + loneBlocks.pop(); + } + } + + // Default rule definition: report all lone blocks + ruleDef = { + BlockStatement(node) { + if (isLoneBlock(node)) { + report(node); + } + } + }; + + // ES6: report blocks without block-level bindings + if (context.parserOptions.ecmaVersion >= 6) { + ruleDef = { + BlockStatement(node) { + if (isLoneBlock(node)) { + loneBlocks.push(node); + } + }, + "BlockStatement:exit"(node) { + if (loneBlocks.length > 0 && loneBlocks[loneBlocks.length - 1] === node) { + loneBlocks.pop(); + report(node); + } + } + }; + + ruleDef.VariableDeclaration = function(node) { + if (node.kind === "let" || node.kind === "const") { + markLoneBlock(); + } + }; + + ruleDef.FunctionDeclaration = function() { + if (context.getScope().isStrict) { + markLoneBlock(); + } + }; + + ruleDef.ClassDeclaration = markLoneBlock; + } + + return ruleDef; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-lonely-if.js b/tools/node_modules/eslint/lib/rules/no-lonely-if.js new file mode 100644 index 0000000000..db127d1945 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-lonely-if.js @@ -0,0 +1,83 @@ +/** + * @fileoverview Rule to disallow if as the only statmenet in an else block + * @author Brandon Mills + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `if` statements as the only statement in `else` blocks", + category: "Stylistic Issues", + recommended: false + }, + + schema: [], + + fixable: "code" + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + return { + IfStatement(node) { + const ancestors = context.getAncestors(), + parent = ancestors.pop(), + grandparent = ancestors.pop(); + + if (parent && parent.type === "BlockStatement" && + parent.body.length === 1 && grandparent && + grandparent.type === "IfStatement" && + parent === grandparent.alternate) { + context.report({ + node, + message: "Unexpected if as the only statement in an else block.", + fix(fixer) { + const openingElseCurly = sourceCode.getFirstToken(parent); + const closingElseCurly = sourceCode.getLastToken(parent); + const elseKeyword = sourceCode.getTokenBefore(openingElseCurly); + const tokenAfterElseBlock = sourceCode.getTokenAfter(closingElseCurly); + const lastIfToken = sourceCode.getLastToken(node.consequent); + const sourceText = sourceCode.getText(); + + if (sourceText.slice(openingElseCurly.range[1], + node.range[0]).trim() || sourceText.slice(node.range[1], closingElseCurly.range[0]).trim()) { + + // Don't fix if there are any non-whitespace characters interfering (e.g. comments) + return null; + } + + if ( + node.consequent.type !== "BlockStatement" && lastIfToken.value !== ";" && tokenAfterElseBlock && + ( + node.consequent.loc.end.line === tokenAfterElseBlock.loc.start.line || + /^[([/+`-]/.test(tokenAfterElseBlock.value) || + lastIfToken.value === "++" || + lastIfToken.value === "--" + ) + ) { + + /* + * If the `if` statement has no block, and is not followed by a semicolon, make sure that fixing + * the issue would not change semantics due to ASI. If this would happen, don't do a fix. + */ + return null; + } + + return fixer.replaceTextRange( + [openingElseCurly.range[0], closingElseCurly.range[1]], + (elseKeyword.range[1] === openingElseCurly.range[0] ? " " : "") + sourceCode.getText(node) + ); + } + }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-loop-func.js b/tools/node_modules/eslint/lib/rules/no-loop-func.js new file mode 100644 index 0000000000..c97e0c3c5a --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-loop-func.js @@ -0,0 +1,201 @@ +/** + * @fileoverview Rule to flag creation of function inside a loop + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Gets the containing loop node of a specified node. + * + * We don't need to check nested functions, so this ignores those. + * `Scope.through` contains references of nested functions. + * + * @param {ASTNode} node - An AST node to get. + * @returns {ASTNode|null} The containing loop node of the specified node, or + * `null`. + */ +function getContainingLoopNode(node) { + let parent = node.parent; + + while (parent) { + switch (parent.type) { + case "WhileStatement": + case "DoWhileStatement": + return parent; + + case "ForStatement": + + // `init` is outside of the loop. + if (parent.init !== node) { + return parent; + } + break; + + case "ForInStatement": + case "ForOfStatement": + + // `right` is outside of the loop. + if (parent.right !== node) { + return parent; + } + break; + + case "ArrowFunctionExpression": + case "FunctionExpression": + case "FunctionDeclaration": + + // We don't need to check nested functions. + return null; + + default: + break; + } + + node = parent; + parent = node.parent; + } + + return null; +} + +/** + * Gets the containing loop node of a given node. + * If the loop was nested, this returns the most outer loop. + * + * @param {ASTNode} node - A node to get. This is a loop node. + * @param {ASTNode|null} excludedNode - A node that the result node should not + * include. + * @returns {ASTNode} The most outer loop node. + */ +function getTopLoopNode(node, excludedNode) { + let retv = node; + const border = excludedNode ? excludedNode.range[1] : 0; + + while (node && node.range[0] >= border) { + retv = node; + node = getContainingLoopNode(node); + } + + return retv; +} + +/** + * Checks whether a given reference which refers to an upper scope's variable is + * safe or not. + * + * @param {ASTNode} loopNode - A containing loop node. + * @param {eslint-scope.Reference} reference - A reference to check. + * @returns {boolean} `true` if the reference is safe or not. + */ +function isSafe(loopNode, reference) { + const variable = reference.resolved; + const definition = variable && variable.defs[0]; + const declaration = definition && definition.parent; + const kind = (declaration && declaration.type === "VariableDeclaration") + ? declaration.kind + : ""; + + // Variables which are declared by `const` is safe. + if (kind === "const") { + return true; + } + + /* + * Variables which are declared by `let` in the loop is safe. + * It's a different instance from the next loop step's. + */ + if (kind === "let" && + declaration.range[0] > loopNode.range[0] && + declaration.range[1] < loopNode.range[1] + ) { + return true; + } + + /* + * WriteReferences which exist after this border are unsafe because those + * can modify the variable. + */ + const border = getTopLoopNode( + loopNode, + (kind === "let") ? declaration : null + ).range[0]; + + /** + * Checks whether a given reference is safe or not. + * The reference is every reference of the upper scope's variable we are + * looking now. + * + * It's safeafe if the reference matches one of the following condition. + * - is readonly. + * - doesn't exist inside a local function and after the border. + * + * @param {eslint-scope.Reference} upperRef - A reference to check. + * @returns {boolean} `true` if the reference is safe. + */ + function isSafeReference(upperRef) { + const id = upperRef.identifier; + + return ( + !upperRef.isWrite() || + variable.scope.variableScope === upperRef.from.variableScope && + id.range[0] < border + ); + } + + return Boolean(variable) && variable.references.every(isSafeReference); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `function` declarations and expressions inside loop statements", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + + /** + * Reports functions which match the following condition: + * + * - has a loop node in ancestors. + * - has any references which refers to an unsafe variable. + * + * @param {ASTNode} node The AST node to check. + * @returns {boolean} Whether or not the node is within a loop. + */ + function checkForLoops(node) { + const loopNode = getContainingLoopNode(node); + + if (!loopNode) { + return; + } + + const references = context.getScope().through; + + if (references.length > 0 && + !references.every(isSafe.bind(null, loopNode)) + ) { + context.report({ node, message: "Don't make functions within a loop." }); + } + } + + return { + ArrowFunctionExpression: checkForLoops, + FunctionExpression: checkForLoops, + FunctionDeclaration: checkForLoops + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-magic-numbers.js b/tools/node_modules/eslint/lib/rules/no-magic-numbers.js new file mode 100644 index 0000000000..796ecff0f8 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-magic-numbers.js @@ -0,0 +1,149 @@ +/** + * @fileoverview Rule to flag statements that use magic numbers (adapted from https://github.com/danielstjules/buddy.js) + * @author Vincent Lemeunier + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow magic numbers", + category: "Best Practices", + recommended: false + }, + + schema: [{ + type: "object", + properties: { + detectObjects: { + type: "boolean" + }, + enforceConst: { + type: "boolean" + }, + ignore: { + type: "array", + items: { + type: "number" + }, + uniqueItems: true + }, + ignoreArrayIndexes: { + type: "boolean" + } + }, + additionalProperties: false + }] + }, + + create(context) { + const config = context.options[0] || {}, + detectObjects = !!config.detectObjects, + enforceConst = !!config.enforceConst, + ignore = config.ignore || [], + ignoreArrayIndexes = !!config.ignoreArrayIndexes; + + /** + * Returns whether the node is number literal + * @param {Node} node - the node literal being evaluated + * @returns {boolean} true if the node is a number literal + */ + function isNumber(node) { + return typeof node.value === "number"; + } + + /** + * Returns whether the number should be ignored + * @param {number} num - the number + * @returns {boolean} true if the number should be ignored + */ + function shouldIgnoreNumber(num) { + return ignore.indexOf(num) !== -1; + } + + /** + * Returns whether the number should be ignored when used as a radix within parseInt() or Number.parseInt() + * @param {ASTNode} parent - the non-"UnaryExpression" parent + * @param {ASTNode} node - the node literal being evaluated + * @returns {boolean} true if the number should be ignored + */ + function shouldIgnoreParseInt(parent, node) { + return parent.type === "CallExpression" && node === parent.arguments[1] && + (parent.callee.name === "parseInt" || + parent.callee.type === "MemberExpression" && + parent.callee.object.name === "Number" && + parent.callee.property.name === "parseInt"); + } + + /** + * Returns whether the number should be ignored when used to define a JSX prop + * @param {ASTNode} parent - the non-"UnaryExpression" parent + * @returns {boolean} true if the number should be ignored + */ + function shouldIgnoreJSXNumbers(parent) { + return parent.type.indexOf("JSX") === 0; + } + + /** + * Returns whether the number should be ignored when used as an array index with enabled 'ignoreArrayIndexes' option. + * @param {ASTNode} parent - the non-"UnaryExpression" parent. + * @returns {boolean} true if the number should be ignored + */ + function shouldIgnoreArrayIndexes(parent) { + return parent.type === "MemberExpression" && ignoreArrayIndexes; + } + + return { + Literal(node) { + let parent = node.parent, + value = node.value, + raw = node.raw; + const okTypes = detectObjects ? [] : ["ObjectExpression", "Property", "AssignmentExpression"]; + + if (!isNumber(node)) { + return; + } + + // For negative magic numbers: update the value and parent node + if (parent.type === "UnaryExpression" && parent.operator === "-") { + node = parent; + parent = node.parent; + value = -value; + raw = `-${raw}`; + } + + if (shouldIgnoreNumber(value) || + shouldIgnoreParseInt(parent, node) || + shouldIgnoreArrayIndexes(parent) || + shouldIgnoreJSXNumbers(parent)) { + return; + } + + if (parent.type === "VariableDeclarator") { + if (enforceConst && parent.parent.kind !== "const") { + context.report({ + node, + message: "Number constants declarations must use 'const'." + }); + } + } else if ( + okTypes.indexOf(parent.type) === -1 || + (parent.type === "AssignmentExpression" && parent.left.type === "Identifier") + ) { + context.report({ + node, + message: "No magic number: {{raw}}.", + data: { + raw + } + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-mixed-operators.js b/tools/node_modules/eslint/lib/rules/no-mixed-operators.js new file mode 100644 index 0000000000..9f1fbc9a6d --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-mixed-operators.js @@ -0,0 +1,209 @@ +/** + * @fileoverview Rule to disallow mixed binary operators. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils.js"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const ARITHMETIC_OPERATORS = ["+", "-", "*", "/", "%", "**"]; +const BITWISE_OPERATORS = ["&", "|", "^", "~", "<<", ">>", ">>>"]; +const COMPARISON_OPERATORS = ["==", "!=", "===", "!==", ">", ">=", "<", "<="]; +const LOGICAL_OPERATORS = ["&&", "||"]; +const RELATIONAL_OPERATORS = ["in", "instanceof"]; +const ALL_OPERATORS = [].concat( + ARITHMETIC_OPERATORS, + BITWISE_OPERATORS, + COMPARISON_OPERATORS, + LOGICAL_OPERATORS, + RELATIONAL_OPERATORS +); +const DEFAULT_GROUPS = [ + ARITHMETIC_OPERATORS, + BITWISE_OPERATORS, + COMPARISON_OPERATORS, + LOGICAL_OPERATORS, + RELATIONAL_OPERATORS +]; +const TARGET_NODE_TYPE = /^(?:Binary|Logical)Expression$/; + +/** + * Normalizes options. + * + * @param {Object|undefined} options - A options object to normalize. + * @returns {Object} Normalized option object. + */ +function normalizeOptions(options) { + const hasGroups = (options && options.groups && options.groups.length > 0); + const groups = hasGroups ? options.groups : DEFAULT_GROUPS; + const allowSamePrecedence = (options && options.allowSamePrecedence) !== false; + + return { + groups, + allowSamePrecedence + }; +} + +/** + * Checks whether any group which includes both given operator exists or not. + * + * @param {Array.<string[]>} groups - A list of groups to check. + * @param {string} left - An operator. + * @param {string} right - Another operator. + * @returns {boolean} `true` if such group existed. + */ +function includesBothInAGroup(groups, left, right) { + return groups.some(group => group.indexOf(left) !== -1 && group.indexOf(right) !== -1); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow mixed binary operators", + category: "Stylistic Issues", + recommended: false + }, + schema: [ + { + type: "object", + properties: { + groups: { + type: "array", + items: { + type: "array", + items: { enum: ALL_OPERATORS }, + minItems: 2, + uniqueItems: true + }, + uniqueItems: true + }, + allowSamePrecedence: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + const options = normalizeOptions(context.options[0]); + + /** + * Checks whether a given node should be ignored by options or not. + * + * @param {ASTNode} node - A node to check. This is a BinaryExpression + * node or a LogicalExpression node. This parent node is one of + * them, too. + * @returns {boolean} `true` if the node should be ignored. + */ + function shouldIgnore(node) { + const a = node; + const b = node.parent; + + return ( + !includesBothInAGroup(options.groups, a.operator, b.operator) || + ( + options.allowSamePrecedence && + astUtils.getPrecedence(a) === astUtils.getPrecedence(b) + ) + ); + } + + /** + * Checks whether the operator of a given node is mixed with parent + * node's operator or not. + * + * @param {ASTNode} node - A node to check. This is a BinaryExpression + * node or a LogicalExpression node. This parent node is one of + * them, too. + * @returns {boolean} `true` if the node was mixed. + */ + function isMixedWithParent(node) { + return ( + node.operator !== node.parent.operator && + !astUtils.isParenthesised(sourceCode, node) + ); + } + + /** + * Gets the operator token of a given node. + * + * @param {ASTNode} node - A node to check. This is a BinaryExpression + * node or a LogicalExpression node. + * @returns {Token} The operator token of the node. + */ + function getOperatorToken(node) { + return sourceCode.getTokenAfter(node.left, astUtils.isNotClosingParenToken); + } + + /** + * Reports both the operator of a given node and the operator of the + * parent node. + * + * @param {ASTNode} node - A node to check. This is a BinaryExpression + * node or a LogicalExpression node. This parent node is one of + * them, too. + * @returns {void} + */ + function reportBothOperators(node) { + const parent = node.parent; + const left = (parent.left === node) ? node : parent; + const right = (parent.left !== node) ? node : parent; + const message = + "Unexpected mix of '{{leftOperator}}' and '{{rightOperator}}'."; + const data = { + leftOperator: left.operator, + rightOperator: right.operator + }; + + context.report({ + node: left, + loc: getOperatorToken(left).loc.start, + message, + data + }); + context.report({ + node: right, + loc: getOperatorToken(right).loc.start, + message, + data + }); + } + + /** + * Checks between the operator of this node and the operator of the + * parent node. + * + * @param {ASTNode} node - A node to check. + * @returns {void} + */ + function check(node) { + if (TARGET_NODE_TYPE.test(node.parent.type) && + isMixedWithParent(node) && + !shouldIgnore(node) + ) { + reportBothOperators(node); + } + } + + return { + BinaryExpression: check, + LogicalExpression: check + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-mixed-requires.js b/tools/node_modules/eslint/lib/rules/no-mixed-requires.js new file mode 100644 index 0000000000..171052a52a --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-mixed-requires.js @@ -0,0 +1,220 @@ +/** + * @fileoverview Rule to enforce grouped require statements for Node.JS + * @author Raphael Pigulla + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `require` calls to be mixed with regular variable declarations", + category: "Node.js and CommonJS", + recommended: false + }, + + schema: [ + { + oneOf: [ + { + type: "boolean" + }, + { + type: "object", + properties: { + grouping: { + type: "boolean" + }, + allowCall: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + + const options = context.options[0]; + let grouping = false, + allowCall = false; + + if (typeof options === "object") { + grouping = options.grouping; + allowCall = options.allowCall; + } else { + grouping = !!options; + } + + /** + * Returns the list of built-in modules. + * + * @returns {string[]} An array of built-in Node.js modules. + */ + function getBuiltinModules() { + + /* + * This list is generated using: + * `require("repl")._builtinLibs.concat('repl').sort()` + * This particular list is as per nodejs v0.12.2 and iojs v0.7.1 + */ + return [ + "assert", "buffer", "child_process", "cluster", "crypto", + "dgram", "dns", "domain", "events", "fs", "http", "https", + "net", "os", "path", "punycode", "querystring", "readline", + "repl", "smalloc", "stream", "string_decoder", "tls", "tty", + "url", "util", "v8", "vm", "zlib" + ]; + } + + const BUILTIN_MODULES = getBuiltinModules(); + + const DECL_REQUIRE = "require", + DECL_UNINITIALIZED = "uninitialized", + DECL_OTHER = "other"; + + const REQ_CORE = "core", + REQ_FILE = "file", + REQ_MODULE = "module", + REQ_COMPUTED = "computed"; + + /** + * Determines the type of a declaration statement. + * @param {ASTNode} initExpression The init node of the VariableDeclarator. + * @returns {string} The type of declaration represented by the expression. + */ + function getDeclarationType(initExpression) { + if (!initExpression) { + + // "var x;" + return DECL_UNINITIALIZED; + } + + if (initExpression.type === "CallExpression" && + initExpression.callee.type === "Identifier" && + initExpression.callee.name === "require" + ) { + + // "var x = require('util');" + return DECL_REQUIRE; + } + if (allowCall && + initExpression.type === "CallExpression" && + initExpression.callee.type === "CallExpression" + ) { + + // "var x = require('diagnose')('sub-module');" + return getDeclarationType(initExpression.callee); + } + if (initExpression.type === "MemberExpression") { + + // "var x = require('glob').Glob;" + return getDeclarationType(initExpression.object); + } + + // "var x = 42;" + return DECL_OTHER; + } + + /** + * Determines the type of module that is loaded via require. + * @param {ASTNode} initExpression The init node of the VariableDeclarator. + * @returns {string} The module type. + */ + function inferModuleType(initExpression) { + if (initExpression.type === "MemberExpression") { + + // "var x = require('glob').Glob;" + return inferModuleType(initExpression.object); + } + if (initExpression.arguments.length === 0) { + + // "var x = require();" + return REQ_COMPUTED; + } + + const arg = initExpression.arguments[0]; + + if (arg.type !== "Literal" || typeof arg.value !== "string") { + + // "var x = require(42);" + return REQ_COMPUTED; + } + + if (BUILTIN_MODULES.indexOf(arg.value) !== -1) { + + // "var fs = require('fs');" + return REQ_CORE; + } + if (/^\.{0,2}\//.test(arg.value)) { + + // "var utils = require('./utils');" + return REQ_FILE; + } + + // "var async = require('async');" + return REQ_MODULE; + + } + + /** + * Check if the list of variable declarations is mixed, i.e. whether it + * contains both require and other declarations. + * @param {ASTNode} declarations The list of VariableDeclarators. + * @returns {boolean} True if the declarations are mixed, false if not. + */ + function isMixed(declarations) { + const contains = {}; + + declarations.forEach(declaration => { + const type = getDeclarationType(declaration.init); + + contains[type] = true; + }); + + return !!( + contains[DECL_REQUIRE] && + (contains[DECL_UNINITIALIZED] || contains[DECL_OTHER]) + ); + } + + /** + * Check if all require declarations in the given list are of the same + * type. + * @param {ASTNode} declarations The list of VariableDeclarators. + * @returns {boolean} True if the declarations are grouped, false if not. + */ + function isGrouped(declarations) { + const found = {}; + + declarations.forEach(declaration => { + if (getDeclarationType(declaration.init) === DECL_REQUIRE) { + found[inferModuleType(declaration.init)] = true; + } + }); + + return Object.keys(found).length <= 1; + } + + + return { + + VariableDeclaration(node) { + + if (isMixed(node.declarations)) { + context.report({ node, message: "Do not mix 'require' and other declarations." }); + } else if (grouping && !isGrouped(node.declarations)) { + context.report({ node, message: "Do not mix core, module, file and computed requires." }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-mixed-spaces-and-tabs.js b/tools/node_modules/eslint/lib/rules/no-mixed-spaces-and-tabs.js new file mode 100644 index 0000000000..2b8e89d3c8 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-mixed-spaces-and-tabs.js @@ -0,0 +1,143 @@ +/** + * @fileoverview Disallow mixed spaces and tabs for indentation + * @author Jary Niebur + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow mixed spaces and tabs for indentation", + category: "Stylistic Issues", + recommended: true + }, + + schema: [ + { + enum: ["smart-tabs", true, false] + } + ] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + let smartTabs; + const ignoredLocs = []; + + switch (context.options[0]) { + case true: // Support old syntax, maybe add deprecation warning here + case "smart-tabs": + smartTabs = true; + break; + default: + smartTabs = false; + } + + /** + * Determines if a given line and column are before a location. + * @param {Location} loc The location object from an AST node. + * @param {int} line The line to check. + * @param {int} column The column to check. + * @returns {boolean} True if the line and column are before the location, false if not. + * @private + */ + function beforeLoc(loc, line, column) { + if (line < loc.start.line) { + return true; + } + return line === loc.start.line && column < loc.start.column; + } + + /** + * Determines if a given line and column are after a location. + * @param {Location} loc The location object from an AST node. + * @param {int} line The line to check. + * @param {int} column The column to check. + * @returns {boolean} True if the line and column are after the location, false if not. + * @private + */ + function afterLoc(loc, line, column) { + if (line > loc.end.line) { + return true; + } + return line === loc.end.line && column > loc.end.column; + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + TemplateElement(node) { + ignoredLocs.push(node.loc); + }, + + "Program:exit"(node) { + + /* + * At least one space followed by a tab + * or the reverse before non-tab/-space + * characters begin. + */ + let regex = /^(?=[\t ]*(\t | \t))/; + const lines = sourceCode.lines, + comments = sourceCode.getAllComments(); + + comments.forEach(comment => { + ignoredLocs.push(comment.loc); + }); + + ignoredLocs.sort((first, second) => { + if (beforeLoc(first, second.start.line, second.start.column)) { + return 1; + } + + if (beforeLoc(second, first.start.line, second.start.column)) { + return -1; + } + + return 0; + }); + + if (smartTabs) { + + /* + * At least one space followed by a tab + * before non-tab/-space characters begin. + */ + regex = /^(?=[\t ]* \t)/; + } + + lines.forEach((line, i) => { + const match = regex.exec(line); + + if (match) { + const lineNumber = i + 1, + column = match.index + 1; + + for (let j = 0; j < ignoredLocs.length; j++) { + if (beforeLoc(ignoredLocs[j], lineNumber, column)) { + continue; + } + if (afterLoc(ignoredLocs[j], lineNumber, column)) { + continue; + } + + return; + } + + context.report({ node, loc: { line: lineNumber, column }, message: "Mixed spaces and tabs." }); + } + }); + } + + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-multi-assign.js b/tools/node_modules/eslint/lib/rules/no-multi-assign.js new file mode 100644 index 0000000000..164869f6dd --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-multi-assign.js @@ -0,0 +1,41 @@ +/** + * @fileoverview Rule to check use of chained assignment expressions + * @author Stewart Rand + */ + +"use strict"; + + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow use of chained assignment expressions", + category: "Stylistic Issues", + recommended: false + }, + schema: [] + }, + + create(context) { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + AssignmentExpression(node) { + if (["AssignmentExpression", "VariableDeclarator"].indexOf(node.parent.type) !== -1) { + context.report({ + node, + message: "Unexpected chained assignment." + }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-multi-spaces.js b/tools/node_modules/eslint/lib/rules/no-multi-spaces.js new file mode 100644 index 0000000000..84f1b50189 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-multi-spaces.js @@ -0,0 +1,130 @@ +/** + * @fileoverview Disallow use of multiple spaces. + * @author Nicholas C. Zakas + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow multiple spaces", + category: "Best Practices", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + type: "object", + properties: { + exceptions: { + type: "object", + patternProperties: { + "^([A-Z][a-z]*)+$": { + type: "boolean" + } + }, + additionalProperties: false + }, + ignoreEOLComments: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + const options = context.options[0] || {}; + const ignoreEOLComments = options.ignoreEOLComments; + const exceptions = Object.assign({ Property: true }, options.exceptions); + const hasExceptions = Object.keys(exceptions).filter(key => exceptions[key]).length > 0; + + /** + * Formats value of given comment token for error message by truncating its length. + * @param {Token} token comment token + * @returns {string} formatted value + * @private + */ + function formatReportedCommentValue(token) { + const valueLines = token.value.split("\n"); + const value = valueLines[0]; + const formattedValue = `${value.slice(0, 12)}...`; + + return valueLines.length === 1 && value.length <= 12 ? value : formattedValue; + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + Program() { + sourceCode.tokensAndComments.forEach((leftToken, leftIndex, tokensAndComments) => { + if (leftIndex === tokensAndComments.length - 1) { + return; + } + const rightToken = tokensAndComments[leftIndex + 1]; + + // Ignore tokens that don't have 2 spaces between them or are on different lines + if ( + !sourceCode.text.slice(leftToken.range[1], rightToken.range[0]).includes(" ") || + leftToken.loc.end.line < rightToken.loc.start.line + ) { + return; + } + + // Ignore comments that are the last token on their line if `ignoreEOLComments` is active. + if ( + ignoreEOLComments && + astUtils.isCommentToken(rightToken) && + ( + leftIndex === tokensAndComments.length - 2 || + rightToken.loc.end.line < tokensAndComments[leftIndex + 2].loc.start.line + ) + ) { + return; + } + + // Ignore tokens that are in a node in the "exceptions" object + if (hasExceptions) { + const parentNode = sourceCode.getNodeByRangeIndex(rightToken.range[0] - 1); + + if (parentNode && exceptions[parentNode.type]) { + return; + } + } + + let displayValue; + + if (rightToken.type === "Block") { + displayValue = `/*${formatReportedCommentValue(rightToken)}*/`; + } else if (rightToken.type === "Line") { + displayValue = `//${formatReportedCommentValue(rightToken)}`; + } else { + displayValue = rightToken.value; + } + + context.report({ + node: rightToken, + loc: rightToken.loc.start, + message: "Multiple spaces found before '{{displayValue}}'.", + data: { displayValue }, + fix: fixer => fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], " ") + }); + }); + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-multi-str.js b/tools/node_modules/eslint/lib/rules/no-multi-str.js new file mode 100644 index 0000000000..76f29cbb5a --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-multi-str.js @@ -0,0 +1,55 @@ +/** + * @fileoverview Rule to flag when using multiline strings + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow multiline strings", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + + /** + * Determines if a given node is part of JSX syntax. + * @param {ASTNode} node The node to check. + * @returns {boolean} True if the node is a JSX node, false if not. + * @private + */ + function isJSXElement(node) { + return node.type.indexOf("JSX") === 0; + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + + Literal(node) { + if (astUtils.LINEBREAK_MATCHER.test(node.raw) && !isJSXElement(node.parent)) { + context.report({ node, message: "Multiline support is limited to browsers supporting ES5 only." }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-multiple-empty-lines.js b/tools/node_modules/eslint/lib/rules/no-multiple-empty-lines.js new file mode 100644 index 0000000000..9d1067c205 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-multiple-empty-lines.js @@ -0,0 +1,136 @@ +/** + * @fileoverview Disallows multiple blank lines. + * implementation adapted from the no-trailing-spaces rule. + * @author Greg Cochard + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow multiple empty lines", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + type: "object", + properties: { + max: { + type: "integer", + minimum: 0 + }, + maxEOF: { + type: "integer", + minimum: 0 + }, + maxBOF: { + type: "integer", + minimum: 0 + } + }, + required: ["max"], + additionalProperties: false + } + ] + }, + + create(context) { + + // Use options.max or 2 as default + let max = 2, + maxEOF = max, + maxBOF = max; + + if (context.options.length) { + max = context.options[0].max; + maxEOF = typeof context.options[0].maxEOF !== "undefined" ? context.options[0].maxEOF : max; + maxBOF = typeof context.options[0].maxBOF !== "undefined" ? context.options[0].maxBOF : max; + } + + const sourceCode = context.getSourceCode(); + + // Swallow the final newline, as some editors add it automatically and we don't want it to cause an issue + const allLines = sourceCode.lines[sourceCode.lines.length - 1] === "" ? sourceCode.lines.slice(0, -1) : sourceCode.lines; + const templateLiteralLines = new Set(); + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + TemplateLiteral(node) { + node.quasis.forEach(literalPart => { + + // Empty lines have a semantic meaning if they're inside template literals. Don't count these as empty lines. + for (let ignoredLine = literalPart.loc.start.line; ignoredLine < literalPart.loc.end.line; ignoredLine++) { + templateLiteralLines.add(ignoredLine); + } + }); + }, + "Program:exit"(node) { + return allLines + + // Given a list of lines, first get a list of line numbers that are non-empty. + .reduce((nonEmptyLineNumbers, line, index) => { + if (line.trim() || templateLiteralLines.has(index + 1)) { + nonEmptyLineNumbers.push(index + 1); + } + return nonEmptyLineNumbers; + }, []) + + // Add a value at the end to allow trailing empty lines to be checked. + .concat(allLines.length + 1) + + // Given two line numbers of non-empty lines, report the lines between if the difference is too large. + .reduce((lastLineNumber, lineNumber) => { + let message, maxAllowed; + + if (lastLineNumber === 0) { + message = "Too many blank lines at the beginning of file. Max of {{max}} allowed."; + maxAllowed = maxBOF; + } else if (lineNumber === allLines.length + 1) { + message = "Too many blank lines at the end of file. Max of {{max}} allowed."; + maxAllowed = maxEOF; + } else { + message = "More than {{max}} blank {{pluralizedLines}} not allowed."; + maxAllowed = max; + } + + if (lineNumber - lastLineNumber - 1 > maxAllowed) { + context.report({ + node, + loc: { start: { line: lastLineNumber + 1, column: 0 }, end: { line: lineNumber, column: 0 } }, + message, + data: { max: maxAllowed, pluralizedLines: maxAllowed === 1 ? "line" : "lines" }, + fix(fixer) { + const rangeStart = sourceCode.getIndexFromLoc({ line: lastLineNumber + 1, column: 0 }); + + /* + * The end of the removal range is usually the start index of the next line. + * However, at the end of the file there is no next line, so the end of the + * range is just the length of the text. + */ + const lineNumberAfterRemovedLines = lineNumber - maxAllowed; + const rangeEnd = lineNumberAfterRemovedLines <= allLines.length + ? sourceCode.getIndexFromLoc({ line: lineNumberAfterRemovedLines, column: 0 }) + : sourceCode.text.length; + + return fixer.removeRange([rangeStart, rangeEnd]); + } + }); + } + + return lineNumber; + }, 0); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-native-reassign.js b/tools/node_modules/eslint/lib/rules/no-native-reassign.js new file mode 100644 index 0000000000..a60d4e499c --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-native-reassign.js @@ -0,0 +1,89 @@ +/** + * @fileoverview Rule to disallow assignments to native objects or read-only global variables + * @author Ilya Volodin + * @deprecated in ESLint v3.3.0 + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow assignments to native objects or read-only global variables", + category: "Best Practices", + recommended: false, + replacedBy: ["no-global-assign"] + }, + + deprecated: true, + + schema: [ + { + type: "object", + properties: { + exceptions: { + type: "array", + items: { type: "string" }, + uniqueItems: true + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const config = context.options[0]; + const exceptions = (config && config.exceptions) || []; + + /** + * Reports write references. + * @param {Reference} reference - A reference to check. + * @param {int} index - The index of the reference in the references. + * @param {Reference[]} references - The array that the reference belongs to. + * @returns {void} + */ + function checkReference(reference, index, references) { + const identifier = reference.identifier; + + if (reference.init === false && + reference.isWrite() && + + /* + * Destructuring assignments can have multiple default value, + * so possibly there are multiple writeable references for the same identifier. + */ + (index === 0 || references[index - 1].identifier !== identifier) + ) { + context.report({ + node: identifier, + message: "Read-only global '{{name}}' should not be modified.", + data: identifier + }); + } + } + + /** + * Reports write references if a given variable is read-only builtin. + * @param {Variable} variable - A variable to check. + * @returns {void} + */ + function checkVariable(variable) { + if (variable.writeable === false && exceptions.indexOf(variable.name) === -1) { + variable.references.forEach(checkReference); + } + } + + return { + Program() { + const globalScope = context.getScope(); + + globalScope.variables.forEach(checkVariable); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-negated-condition.js b/tools/node_modules/eslint/lib/rules/no-negated-condition.js new file mode 100644 index 0000000000..8ea8559ea1 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-negated-condition.js @@ -0,0 +1,82 @@ +/** + * @fileoverview Rule to disallow a negated condition + * @author Alberto RodrÃguez + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow negated conditions", + category: "Stylistic Issues", + recommended: false + }, + + schema: [] + }, + + create(context) { + + /** + * Determines if a given node is an if-else without a condition on the else + * @param {ASTNode} node The node to check. + * @returns {boolean} True if the node has an else without an if. + * @private + */ + function hasElseWithoutCondition(node) { + return node.alternate && node.alternate.type !== "IfStatement"; + } + + /** + * Determines if a given node is a negated unary expression + * @param {Object} test The test object to check. + * @returns {boolean} True if the node is a negated unary expression. + * @private + */ + function isNegatedUnaryExpression(test) { + return test.type === "UnaryExpression" && test.operator === "!"; + } + + /** + * Determines if a given node is a negated binary expression + * @param {Test} test The test to check. + * @returns {boolean} True if the node is a negated binary expression. + * @private + */ + function isNegatedBinaryExpression(test) { + return test.type === "BinaryExpression" && + (test.operator === "!=" || test.operator === "!=="); + } + + /** + * Determines if a given node has a negated if expression + * @param {ASTNode} node The node to check. + * @returns {boolean} True if the node has a negated if expression. + * @private + */ + function isNegatedIf(node) { + return isNegatedUnaryExpression(node.test) || isNegatedBinaryExpression(node.test); + } + + return { + IfStatement(node) { + if (!hasElseWithoutCondition(node)) { + return; + } + + if (isNegatedIf(node)) { + context.report({ node, message: "Unexpected negated condition." }); + } + }, + ConditionalExpression(node) { + if (isNegatedIf(node)) { + context.report({ node, message: "Unexpected negated condition." }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-negated-in-lhs.js b/tools/node_modules/eslint/lib/rules/no-negated-in-lhs.js new file mode 100644 index 0000000000..495cbc160e --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-negated-in-lhs.js @@ -0,0 +1,38 @@ +/** + * @fileoverview A rule to disallow negated left operands of the `in` operator + * @author Michael Ficarra + * @deprecated in ESLint v3.3.0 + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow negating the left operand in `in` expressions", + category: "Possible Errors", + recommended: false, + replacedBy: ["no-unsafe-negation"] + }, + deprecated: true, + + schema: [] + }, + + create(context) { + + return { + + BinaryExpression(node) { + if (node.operator === "in" && node.left.type === "UnaryExpression" && node.left.operator === "!") { + context.report({ node, message: "The 'in' expression's left operand is negated." }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-nested-ternary.js b/tools/node_modules/eslint/lib/rules/no-nested-ternary.js new file mode 100644 index 0000000000..4fe49fc9c0 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-nested-ternary.js @@ -0,0 +1,34 @@ +/** + * @fileoverview Rule to flag nested ternary expressions + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow nested ternary expressions", + category: "Stylistic Issues", + recommended: false + }, + + schema: [] + }, + + create(context) { + + return { + ConditionalExpression(node) { + if (node.alternate.type === "ConditionalExpression" || + node.consequent.type === "ConditionalExpression") { + context.report({ node, message: "Do not nest ternary expressions." }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-new-func.js b/tools/node_modules/eslint/lib/rules/no-new-func.js new file mode 100644 index 0000000000..6abbe8391d --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-new-func.js @@ -0,0 +1,45 @@ +/** + * @fileoverview Rule to flag when using new Function + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `new` operators with the `Function` object", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Reports a node. + * @param {ASTNode} node The node to report + * @returns {void} + * @private + */ + function report(node) { + context.report({ node, message: "The Function constructor is eval." }); + } + + return { + "NewExpression[callee.name = 'Function']": report, + "CallExpression[callee.name = 'Function']": report + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-new-object.js b/tools/node_modules/eslint/lib/rules/no-new-object.js new file mode 100644 index 0000000000..d4d77b5514 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-new-object.js @@ -0,0 +1,35 @@ +/** + * @fileoverview A rule to disallow calls to the Object constructor + * @author Matt DuVall <http://www.mattduvall.com/> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `Object` constructors", + category: "Stylistic Issues", + recommended: false + }, + + schema: [] + }, + + create(context) { + + return { + + NewExpression(node) { + if (node.callee.name === "Object") { + context.report({ node, message: "The object literal notation {} is preferrable." }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-new-require.js b/tools/node_modules/eslint/lib/rules/no-new-require.js new file mode 100644 index 0000000000..f9ea1f84bf --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-new-require.js @@ -0,0 +1,35 @@ +/** + * @fileoverview Rule to disallow use of new operator with the `require` function + * @author Wil Moore III + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `new` operators with calls to `require`", + category: "Node.js and CommonJS", + recommended: false + }, + + schema: [] + }, + + create(context) { + + return { + + NewExpression(node) { + if (node.callee.type === "Identifier" && node.callee.name === "require") { + context.report({ node, message: "Unexpected use of new with require." }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-new-symbol.js b/tools/node_modules/eslint/lib/rules/no-new-symbol.js new file mode 100644 index 0000000000..5743a4748a --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-new-symbol.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Rule to disallow use of the new operator with the `Symbol` object + * @author Alberto RodrÃguez + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `new` operators with the `Symbol` object", + category: "ECMAScript 6", + recommended: true + }, + + schema: [] + }, + + create(context) { + + return { + "Program:exit"() { + const globalScope = context.getScope(); + const variable = globalScope.set.get("Symbol"); + + if (variable && variable.defs.length === 0) { + variable.references.forEach(ref => { + const node = ref.identifier; + + if (node.parent && node.parent.type === "NewExpression") { + context.report({ node, message: "`Symbol` cannot be called as a constructor." }); + } + }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-new-wrappers.js b/tools/node_modules/eslint/lib/rules/no-new-wrappers.js new file mode 100644 index 0000000000..65bf79b87c --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-new-wrappers.js @@ -0,0 +1,37 @@ +/** + * @fileoverview Rule to flag when using constructor for wrapper objects + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `new` operators with the `String`, `Number`, and `Boolean` objects", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + + return { + + NewExpression(node) { + const wrapperObjects = ["String", "Number", "Boolean", "Math", "JSON"]; + + if (wrapperObjects.indexOf(node.callee.name) > -1) { + context.report({ node, message: "Do not use {{fn}} as a constructor.", data: { fn: node.callee.name } }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-new.js b/tools/node_modules/eslint/lib/rules/no-new.js new file mode 100644 index 0000000000..6e6025aac5 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-new.js @@ -0,0 +1,33 @@ +/** + * @fileoverview Rule to flag statements with function invocation preceded by + * "new" and not part of assignment + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `new` operators outside of assignments or comparisons", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + + return { + "ExpressionStatement > NewExpression"(node) { + context.report({ node: node.parent, message: "Do not use 'new' for side effects." }); + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-obj-calls.js b/tools/node_modules/eslint/lib/rules/no-obj-calls.js new file mode 100644 index 0000000000..0ca8a5effb --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-obj-calls.js @@ -0,0 +1,39 @@ +/** + * @fileoverview Rule to flag use of an object property of the global object (Math and JSON) as a function + * @author James Allardice + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow calling global object properties as functions", + category: "Possible Errors", + recommended: true + }, + + schema: [] + }, + + create(context) { + + return { + CallExpression(node) { + + if (node.callee.type === "Identifier") { + const name = node.callee.name; + + if (name === "Math" || name === "JSON" || name === "Reflect") { + context.report({ node, message: "'{{name}}' is not a function.", data: { name } }); + } + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-octal-escape.js b/tools/node_modules/eslint/lib/rules/no-octal-escape.js new file mode 100644 index 0000000000..04bfb6aae3 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-octal-escape.js @@ -0,0 +1,47 @@ +/** + * @fileoverview Rule to flag octal escape sequences in string literals. + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow octal escape sequences in string literals", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + + return { + + Literal(node) { + if (typeof node.value !== "string") { + return; + } + + const match = node.raw.match(/^([^\\]|\\[^0-7])*\\([0-3][0-7]{1,2}|[4-7][0-7]|[0-7])/); + + if (match) { + const octalDigit = match[2]; + + // \0 is actually not considered an octal + if (match[2] !== "0" || typeof match[3] !== "undefined") { + context.report({ node, message: "Don't use octal: '\\{{octalDigit}}'. Use '\\u....' instead.", data: { octalDigit } }); + } + } + } + + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-octal.js b/tools/node_modules/eslint/lib/rules/no-octal.js new file mode 100644 index 0000000000..58082d0d1c --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-octal.js @@ -0,0 +1,35 @@ +/** + * @fileoverview Rule to flag when initializing octal literal + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow octal literals", + category: "Best Practices", + recommended: true + }, + + schema: [] + }, + + create(context) { + + return { + + Literal(node) { + if (typeof node.value === "number" && /^0[0-7]/.test(node.raw)) { + context.report({ node, message: "Octal literals should not be used." }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-param-reassign.js b/tools/node_modules/eslint/lib/rules/no-param-reassign.js new file mode 100644 index 0000000000..f32e42ae2f --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-param-reassign.js @@ -0,0 +1,173 @@ +/** + * @fileoverview Disallow reassignment of function parameters. + * @author Nat Burns + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +const stopNodePattern = /(?:Statement|Declaration|Function(?:Expression)?|Program)$/; + +module.exports = { + meta: { + docs: { + description: "disallow reassigning `function` parameters", + category: "Best Practices", + recommended: false + }, + + schema: [ + { + oneOf: [ + { + type: "object", + properties: { + props: { + enum: [false] + } + }, + additionalProperties: false + }, + { + type: "object", + properties: { + props: { + enum: [true] + }, + ignorePropertyModificationsFor: { + type: "array", + items: { + type: "string" + }, + uniqueItems: true + } + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + const props = context.options[0] && Boolean(context.options[0].props); + const ignoredPropertyAssignmentsFor = context.options[0] && context.options[0].ignorePropertyModificationsFor || []; + + /** + * Checks whether or not the reference modifies properties of its variable. + * @param {Reference} reference - A reference to check. + * @returns {boolean} Whether or not the reference modifies properties of its variable. + */ + function isModifyingProp(reference) { + let node = reference.identifier; + let parent = node.parent; + + while (parent && !stopNodePattern.test(parent.type)) { + switch (parent.type) { + + // e.g. foo.a = 0; + case "AssignmentExpression": + return parent.left === node; + + // e.g. ++foo.a; + case "UpdateExpression": + return true; + + // e.g. delete foo.a; + case "UnaryExpression": + if (parent.operator === "delete") { + return true; + } + break; + + // EXCLUDES: e.g. cache.get(foo.a).b = 0; + case "CallExpression": + if (parent.callee !== node) { + return false; + } + break; + + // EXCLUDES: e.g. cache[foo.a] = 0; + case "MemberExpression": + if (parent.property === node) { + return false; + } + break; + + // EXCLUDES: e.g. ({ [foo]: a }) = bar; + case "Property": + if (parent.key === node) { + return false; + } + + break; + + // no default + } + + node = parent; + parent = node.parent; + } + + return false; + } + + /** + * Reports a reference if is non initializer and writable. + * @param {Reference} reference - A reference to check. + * @param {int} index - The index of the reference in the references. + * @param {Reference[]} references - The array that the reference belongs to. + * @returns {void} + */ + function checkReference(reference, index, references) { + const identifier = reference.identifier; + + if (identifier && + !reference.init && + + /* + * Destructuring assignments can have multiple default value, + * so possibly there are multiple writeable references for the same identifier. + */ + (index === 0 || references[index - 1].identifier !== identifier) + ) { + if (reference.isWrite()) { + context.report({ node: identifier, message: "Assignment to function parameter '{{name}}'.", data: { name: identifier.name } }); + } else if (props && isModifyingProp(reference) && ignoredPropertyAssignmentsFor.indexOf(identifier.name) === -1) { + context.report({ node: identifier, message: "Assignment to property of function parameter '{{name}}'.", data: { name: identifier.name } }); + } + } + } + + /** + * Finds and reports references that are non initializer and writable. + * @param {Variable} variable - A variable to check. + * @returns {void} + */ + function checkVariable(variable) { + if (variable.defs[0].type === "Parameter") { + variable.references.forEach(checkReference); + } + } + + /** + * Checks parameters of a given function node. + * @param {ASTNode} node - A function node to check. + * @returns {void} + */ + function checkForFunction(node) { + context.getDeclaredVariables(node).forEach(checkVariable); + } + + return { + + // `:exit` is needed for the `node.parent` property of identifier nodes. + "FunctionDeclaration:exit": checkForFunction, + "FunctionExpression:exit": checkForFunction, + "ArrowFunctionExpression:exit": checkForFunction + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-path-concat.js b/tools/node_modules/eslint/lib/rules/no-path-concat.js new file mode 100644 index 0000000000..1e153a43b6 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-path-concat.js @@ -0,0 +1,49 @@ +/** + * @fileoverview Disallow string concatenation when using __dirname and __filename + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow string concatenation with `__dirname` and `__filename`", + category: "Node.js and CommonJS", + recommended: false + }, + + schema: [] + }, + + create(context) { + + const MATCHER = /^__(?:dir|file)name$/; + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + BinaryExpression(node) { + + const left = node.left, + right = node.right; + + if (node.operator === "+" && + ((left.type === "Identifier" && MATCHER.test(left.name)) || + (right.type === "Identifier" && MATCHER.test(right.name))) + ) { + + context.report({ node, message: "Use path.join() or path.resolve() instead of + to create paths." }); + } + } + + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-plusplus.js b/tools/node_modules/eslint/lib/rules/no-plusplus.js new file mode 100644 index 0000000000..94f259ac9e --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-plusplus.js @@ -0,0 +1,61 @@ +/** + * @fileoverview Rule to flag use of unary increment and decrement operators. + * @author Ian Christian Myers + * @author Brody McKee (github.com/mrmckeb) + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow the unary operators `++` and `--`", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + allowForLoopAfterthoughts: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + const config = context.options[0]; + let allowInForAfterthought = false; + + if (typeof config === "object") { + allowInForAfterthought = config.allowForLoopAfterthoughts === true; + } + + return { + + UpdateExpression(node) { + if (allowInForAfterthought && node.parent.type === "ForStatement") { + return; + } + context.report({ + node, + message: "Unary operator '{{operator}}' used.", + data: { + operator: node.operator + } + }); + } + + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-process-env.js b/tools/node_modules/eslint/lib/rules/no-process-env.js new file mode 100644 index 0000000000..ef58b38e3c --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-process-env.js @@ -0,0 +1,39 @@ +/** + * @fileoverview Disallow the use of process.env() + * @author Vignesh Anand + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow the use of `process.env`", + category: "Node.js and CommonJS", + recommended: false + }, + + schema: [] + }, + + create(context) { + + return { + + MemberExpression(node) { + const objectName = node.object.name, + propertyName = node.property.name; + + if (objectName === "process" && !node.computed && propertyName && propertyName === "env") { + context.report({ node, message: "Unexpected use of process.env." }); + } + + } + + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-process-exit.js b/tools/node_modules/eslint/lib/rules/no-process-exit.js new file mode 100644 index 0000000000..04e423b88f --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-process-exit.js @@ -0,0 +1,35 @@ +/** + * @fileoverview Disallow the use of process.exit() + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow the use of `process.exit()`", + category: "Node.js and CommonJS", + recommended: false + }, + + schema: [] + }, + + create(context) { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + "CallExpression > MemberExpression.callee[object.name = 'process'][property.name = 'exit']"(node) { + context.report({ node: node.parent, message: "Don't use process.exit(); throw an error instead." }); + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-proto.js b/tools/node_modules/eslint/lib/rules/no-proto.js new file mode 100644 index 0000000000..933746f559 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-proto.js @@ -0,0 +1,38 @@ +/** + * @fileoverview Rule to flag usage of __proto__ property + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow the use of the `__proto__` property", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + + return { + + MemberExpression(node) { + + if (node.property && + (node.property.type === "Identifier" && node.property.name === "__proto__" && !node.computed) || + (node.property.type === "Literal" && node.property.value === "__proto__")) { + context.report({ node, message: "The '__proto__' property is deprecated." }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-prototype-builtins.js b/tools/node_modules/eslint/lib/rules/no-prototype-builtins.js new file mode 100644 index 0000000000..b9f040eaf6 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-prototype-builtins.js @@ -0,0 +1,54 @@ +/** + * @fileoverview Rule to disallow use of Object.prototype builtins on objects + * @author Andrew Levine + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow calling some `Object.prototype` methods directly on objects", + category: "Possible Errors", + recommended: false + }, + + schema: [] + }, + + create(context) { + const DISALLOWED_PROPS = [ + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable" + ]; + + /** + * Reports if a disallowed property is used in a CallExpression + * @param {ASTNode} node The CallExpression node. + * @returns {void} + */ + function disallowBuiltIns(node) { + if (node.callee.type !== "MemberExpression" || node.callee.computed) { + return; + } + const propName = node.callee.property.name; + + if (DISALLOWED_PROPS.indexOf(propName) > -1) { + context.report({ + message: "Do not access Object.prototype method '{{prop}}' from target object.", + loc: node.callee.property.loc.start, + data: { prop: propName }, + node + }); + } + } + + return { + CallExpression: disallowBuiltIns + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-redeclare.js b/tools/node_modules/eslint/lib/rules/no-redeclare.js new file mode 100644 index 0000000000..ccb57003ed --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-redeclare.js @@ -0,0 +1,101 @@ +/** + * @fileoverview Rule to flag when the same variable is declared more then once. + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow variable redeclaration", + category: "Best Practices", + recommended: true + }, + + schema: [ + { + type: "object", + properties: { + builtinGlobals: { type: "boolean" } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const options = { + builtinGlobals: Boolean(context.options[0] && context.options[0].builtinGlobals) + }; + + /** + * Find variables in a given scope and flag redeclared ones. + * @param {Scope} scope - An eslint-scope scope object. + * @returns {void} + * @private + */ + function findVariablesInScope(scope) { + scope.variables.forEach(variable => { + const hasBuiltin = options.builtinGlobals && "writeable" in variable; + const count = (hasBuiltin ? 1 : 0) + variable.identifiers.length; + + if (count >= 2) { + variable.identifiers.sort((a, b) => a.range[1] - b.range[1]); + + for (let i = (hasBuiltin ? 0 : 1), l = variable.identifiers.length; i < l; i++) { + context.report({ node: variable.identifiers[i], message: "'{{a}}' is already defined.", data: { a: variable.name } }); + } + } + }); + + } + + /** + * Find variables in the current scope. + * @param {ASTNode} node - The Program node. + * @returns {void} + * @private + */ + function checkForGlobal(node) { + const scope = context.getScope(), + parserOptions = context.parserOptions, + ecmaFeatures = parserOptions.ecmaFeatures || {}; + + // Nodejs env or modules has a special scope. + if (ecmaFeatures.globalReturn || node.sourceType === "module") { + findVariablesInScope(scope.childScopes[0]); + } else { + findVariablesInScope(scope); + } + } + + /** + * Find variables in the current scope. + * @returns {void} + * @private + */ + function checkForBlock() { + findVariablesInScope(context.getScope()); + } + + if (context.parserOptions.ecmaVersion >= 6) { + return { + Program: checkForGlobal, + BlockStatement: checkForBlock, + SwitchStatement: checkForBlock + }; + } + return { + Program: checkForGlobal, + FunctionDeclaration: checkForBlock, + FunctionExpression: checkForBlock, + ArrowFunctionExpression: checkForBlock + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-regex-spaces.js b/tools/node_modules/eslint/lib/rules/no-regex-spaces.js new file mode 100644 index 0000000000..9250437caa --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-regex-spaces.js @@ -0,0 +1,114 @@ +/** + * @fileoverview Rule to count multiple spaces in regular expressions + * @author Matt DuVall <http://www.mattduvall.com/> + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow multiple spaces in regular expressions", + category: "Possible Errors", + recommended: true + }, + + schema: [], + + fixable: "code" + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + /** + * Validate regular expressions + * @param {ASTNode} node node to validate + * @param {string} value regular expression to validate + * @param {number} valueStart The start location of the regex/string literal. It will always be the case that + * `sourceCode.getText().slice(valueStart, valueStart + value.length) === value` + * @returns {void} + * @private + */ + function checkRegex(node, value, valueStart) { + const multipleSpacesRegex = /( {2,})( [+*{?]|[^+*{?]|$)/, + regexResults = multipleSpacesRegex.exec(value); + + if (regexResults !== null) { + const count = regexResults[1].length; + + context.report({ + node, + message: "Spaces are hard to count. Use {{{count}}}.", + data: { count }, + fix(fixer) { + return fixer.replaceTextRange( + [valueStart + regexResults.index, valueStart + regexResults.index + count], + ` {${count}}` + ); + } + }); + + /* + * TODO: (platinumazure) Fix message to use rule message + * substitution when api.report is fixed in lib/eslint.js. + */ + } + } + + /** + * Validate regular expression literals + * @param {ASTNode} node node to validate + * @returns {void} + * @private + */ + function checkLiteral(node) { + const token = sourceCode.getFirstToken(node), + nodeType = token.type, + nodeValue = token.value; + + if (nodeType === "RegularExpression") { + checkRegex(node, nodeValue, token.range[0]); + } + } + + /** + * Check if node is a string + * @param {ASTNode} node node to evaluate + * @returns {boolean} True if its a string + * @private + */ + function isString(node) { + return node && node.type === "Literal" && typeof node.value === "string"; + } + + /** + * Validate strings passed to the RegExp constructor + * @param {ASTNode} node node to validate + * @returns {void} + * @private + */ + function checkFunction(node) { + const scope = context.getScope(); + const regExpVar = astUtils.getVariableByName(scope, "RegExp"); + const shadowed = regExpVar && regExpVar.defs.length > 0; + + if (node.callee.type === "Identifier" && node.callee.name === "RegExp" && isString(node.arguments[0]) && !shadowed) { + checkRegex(node, node.arguments[0].value, node.arguments[0].range[0] + 1); + } + } + + return { + Literal: checkLiteral, + CallExpression: checkFunction, + NewExpression: checkFunction + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-restricted-globals.js b/tools/node_modules/eslint/lib/rules/no-restricted-globals.js new file mode 100644 index 0000000000..75428fc174 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-restricted-globals.js @@ -0,0 +1,120 @@ +/** + * @fileoverview Restrict usage of specified globals. + * @author Benoît Zugmeyer + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const DEFAULT_MESSAGE_TEMPLATE = "Unexpected use of '{{name}}'.", + CUSTOM_MESSAGE_TEMPLATE = "Unexpected use of '{{name}}'. {{customMessage}}"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow specified global variables", + category: "Variables", + recommended: false + }, + + schema: { + type: "array", + items: { + oneOf: [ + { + type: "string" + }, + { + type: "object", + properties: { + name: { type: "string" }, + message: { type: "string" } + }, + required: ["name"], + additionalProperties: false + } + ] + }, + uniqueItems: true, + minItems: 0 + } + }, + + create(context) { + + // If no globals are restricted, we don't need to do anything + if (context.options.length === 0) { + return {}; + } + + const restrictedGlobalMessages = context.options.reduce((memo, option) => { + if (typeof option === "string") { + memo[option] = null; + } else { + memo[option.name] = option.message; + } + + return memo; + }, {}); + + /** + * Report a variable to be used as a restricted global. + * @param {Reference} reference the variable reference + * @returns {void} + * @private + */ + function reportReference(reference) { + const name = reference.identifier.name, + customMessage = restrictedGlobalMessages[name], + message = customMessage + ? CUSTOM_MESSAGE_TEMPLATE + : DEFAULT_MESSAGE_TEMPLATE; + + context.report({ + node: reference.identifier, + message, + data: { + name, + customMessage + } + }); + } + + /** + * Check if the given name is a restricted global name. + * @param {string} name name of a variable + * @returns {boolean} whether the variable is a restricted global or not + * @private + */ + function isRestricted(name) { + return restrictedGlobalMessages.hasOwnProperty(name); + } + + return { + Program() { + const scope = context.getScope(); + + // Report variables declared elsewhere (ex: variables defined as "global" by eslint) + scope.variables.forEach(variable => { + if (!variable.defs.length && isRestricted(variable.name)) { + variable.references.forEach(reportReference); + } + }); + + // Report variables not declared at all + scope.through.forEach(reference => { + if (isRestricted(reference.identifier.name)) { + reportReference(reference); + } + }); + + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-restricted-imports.js b/tools/node_modules/eslint/lib/rules/no-restricted-imports.js new file mode 100644 index 0000000000..eb477b4be6 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-restricted-imports.js @@ -0,0 +1,263 @@ +/** + * @fileoverview Restrict usage of specified node imports. + * @author Guy Ellis + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const DEFAULT_MESSAGE_TEMPLATE = "'{{importSource}}' import is restricted from being used."; +const CUSTOM_MESSAGE_TEMPLATE = "'{{importSource}}' import is restricted from being used. {{customMessage}}"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +const ignore = require("ignore"); + +const arrayOfStrings = { + type: "array", + items: { type: "string" }, + uniqueItems: true +}; + +const arrayOfStringsOrObjects = { + type: "array", + items: { + anyOf: [ + { type: "string" }, + { + type: "object", + properties: { + name: { type: "string" }, + message: { + type: "string", + minLength: 1 + }, + importNames: { + type: "array", + items: { + type: "string" + } + } + }, + additionalProperties: false, + required: ["name"] + } + ] + }, + uniqueItems: true +}; + +module.exports = { + meta: { + docs: { + description: "disallow specified modules when loaded by `import`", + category: "ECMAScript 6", + recommended: false + }, + + schema: { + anyOf: [ + arrayOfStringsOrObjects, + { + type: "array", + items: { + type: "object", + properties: { + paths: arrayOfStringsOrObjects, + patterns: arrayOfStrings + }, + additionalProperties: false + }, + additionalItems: false + } + ] + } + }, + + create(context) { + const options = Array.isArray(context.options) ? context.options : []; + const isPathAndPatternsObject = + typeof options[0] === "object" && + (options[0].hasOwnProperty("paths") || options[0].hasOwnProperty("patterns")); + + const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || []; + const restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || []; + + const restrictedPathMessages = restrictedPaths.reduce((memo, importSource) => { + if (typeof importSource === "string") { + memo[importSource] = { message: null }; + } else { + memo[importSource.name] = { + message: importSource.message, + importNames: importSource.importNames + }; + } + return memo; + }, {}); + + // if no imports are restricted we don"t need to check + if (Object.keys(restrictedPaths).length === 0 && restrictedPatterns.length === 0) { + return {}; + } + + const restrictedPatternsMatcher = ignore().add(restrictedPatterns); + + /** + * Checks to see if "*" is being used to import everything. + * @param {Set.<string>} importNames - Set of import names that are being imported + * @returns {boolean} whether everything is imported or not + */ + function isEverythingImported(importNames) { + return importNames.has("*"); + } + + /** + * Report a restricted path. + * @param {node} node representing the restricted path reference + * @returns {void} + * @private + */ + function reportPath(node) { + const importSource = node.source.value.trim(); + const customMessage = restrictedPathMessages[importSource] && restrictedPathMessages[importSource].message; + const message = customMessage + ? CUSTOM_MESSAGE_TEMPLATE + : DEFAULT_MESSAGE_TEMPLATE; + + context.report({ + node, + message, + data: { + importSource, + customMessage + } + }); + } + + /** + * Report a restricted path specifically for patterns. + * @param {node} node - representing the restricted path reference + * @returns {void} + * @private + */ + function reportPathForPatterns(node) { + const importSource = node.source.value.trim(); + + context.report({ + node, + message: "'{{importSource}}' import is restricted from being used by a pattern.", + data: { + importSource + } + }); + } + + /** + * Report a restricted path specifically when using the '*' import. + * @param {string} importSource - path of the import + * @param {node} node - representing the restricted path reference + * @returns {void} + * @private + */ + function reportPathForEverythingImported(importSource, node) { + const importNames = restrictedPathMessages[importSource].importNames; + + context.report({ + node, + message: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted.", + data: { + importSource, + importNames + } + }); + } + + /** + * Check if the given importSource is restricted because '*' is being imported. + * @param {string} importSource - path of the import + * @param {Set.<string>} importNames - Set of import names that are being imported + * @returns {boolean} whether the path is restricted + * @private + */ + function isRestrictedForEverythingImported(importSource, importNames) { + return Object.prototype.hasOwnProperty.call(restrictedPathMessages, importSource) && + restrictedPathMessages[importSource].importNames && + isEverythingImported(importNames); + } + + /** + * Check if the given importNames are restricted given a list of restrictedImportNames. + * @param {Set.<string>} importNames - Set of import names that are being imported + * @param {[string]} restrictedImportNames - array of import names that are restricted for this import + * @returns {boolean} whether the objectName is restricted + * @private + */ + function isRestrictedObject(importNames, restrictedImportNames) { + return restrictedImportNames.some(restrictedObjectName => ( + importNames.has(restrictedObjectName) + )); + } + + /** + * Check if the given importSource is a restricted path. + * @param {string} importSource - path of the import + * @param {Set.<string>} importNames - Set of import names that are being imported + * @returns {boolean} whether the variable is a restricted path or not + * @private + */ + function isRestrictedPath(importSource, importNames) { + let isRestricted = false; + + if (Object.prototype.hasOwnProperty.call(restrictedPathMessages, importSource)) { + if (restrictedPathMessages[importSource].importNames) { + isRestricted = isRestrictedObject(importNames, restrictedPathMessages[importSource].importNames); + } else { + isRestricted = true; + } + } + + return isRestricted; + } + + /** + * Check if the given importSource is restricted by a pattern. + * @param {string} importSource - path of the import + * @returns {boolean} whether the variable is a restricted pattern or not + * @private + */ + function isRestrictedPattern(importSource) { + return restrictedPatterns.length > 0 && restrictedPatternsMatcher.ignores(importSource); + } + + return { + ImportDeclaration(node) { + const importSource = node.source.value.trim(); + const importNames = node.specifiers.reduce((set, specifier) => { + if (specifier.type === "ImportDefaultSpecifier") { + set.add("default"); + } else if (specifier.type === "ImportNamespaceSpecifier") { + set.add("*"); + } else { + set.add(specifier.imported.name); + } + return set; + }, new Set()); + + if (isRestrictedForEverythingImported(importSource, importNames)) { + reportPathForEverythingImported(importSource, node); + } + + if (isRestrictedPath(importSource, importNames)) { + reportPath(node); + } + if (isRestrictedPattern(importSource)) { + reportPathForPatterns(node); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-restricted-modules.js b/tools/node_modules/eslint/lib/rules/no-restricted-modules.js new file mode 100644 index 0000000000..cd47975733 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-restricted-modules.js @@ -0,0 +1,177 @@ +/** + * @fileoverview Restrict usage of specified node modules. + * @author Christian Schulz + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const DEFAULT_MESSAGE_TEMPLATE = "'{{moduleName}}' module is restricted from being used."; +const CUSTOM_MESSAGE_TEMPLATE = "'{{moduleName}}' module is restricted from being used. {{customMessage}}"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +const ignore = require("ignore"); + +const arrayOfStrings = { + type: "array", + items: { type: "string" }, + uniqueItems: true +}; + +const arrayOfStringsOrObjects = { + type: "array", + items: { + anyOf: [ + { type: "string" }, + { + type: "object", + properties: { + name: { type: "string" }, + message: { + type: "string", + minLength: 1 + } + }, + additionalProperties: false, + required: ["name"] + } + ] + }, + uniqueItems: true +}; + +module.exports = { + meta: { + docs: { + description: "disallow specified modules when loaded by `require`", + category: "Node.js and CommonJS", + recommended: false + }, + + schema: { + anyOf: [ + arrayOfStringsOrObjects, + { + type: "array", + items: { + type: "object", + properties: { + paths: arrayOfStringsOrObjects, + patterns: arrayOfStrings + }, + additionalProperties: false + }, + additionalItems: false + } + ] + } + }, + + create(context) { + const options = Array.isArray(context.options) ? context.options : []; + const isPathAndPatternsObject = + typeof options[0] === "object" && + (options[0].hasOwnProperty("paths") || options[0].hasOwnProperty("patterns")); + + const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || []; + const restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || []; + + const restrictedPathMessages = restrictedPaths.reduce((memo, importName) => { + if (typeof importName === "string") { + memo[importName] = null; + } else { + memo[importName.name] = importName.message; + } + return memo; + }, {}); + + // if no imports are restricted we don"t need to check + if (Object.keys(restrictedPaths).length === 0 && restrictedPatterns.length === 0) { + return {}; + } + + const ig = ignore().add(restrictedPatterns); + + + /** + * Function to check if a node is a string literal. + * @param {ASTNode} node The node to check. + * @returns {boolean} If the node is a string literal. + */ + function isString(node) { + return node && node.type === "Literal" && typeof node.value === "string"; + } + + /** + * Function to check if a node is a require call. + * @param {ASTNode} node The node to check. + * @returns {boolean} If the node is a require call. + */ + function isRequireCall(node) { + return node.callee.type === "Identifier" && node.callee.name === "require"; + } + + /** + * Report a restricted path. + * @param {node} node representing the restricted path reference + * @returns {void} + * @private + */ + function reportPath(node) { + const moduleName = node.arguments[0].value.trim(); + const customMessage = restrictedPathMessages[moduleName]; + const message = customMessage + ? CUSTOM_MESSAGE_TEMPLATE + : DEFAULT_MESSAGE_TEMPLATE; + + context.report({ + node, + message, + data: { + moduleName, + customMessage + } + }); + } + + /** + * Check if the given name is a restricted path name + * @param {string} name name of a variable + * @returns {boolean} whether the variable is a restricted path or not + * @private + */ + function isRestrictedPath(name) { + return Object.prototype.hasOwnProperty.call(restrictedPathMessages, name); + } + + return { + CallExpression(node) { + if (isRequireCall(node)) { + + // node has arguments and first argument is string + if (node.arguments.length && isString(node.arguments[0])) { + const moduleName = node.arguments[0].value.trim(); + + // check if argument value is in restricted modules array + if (isRestrictedPath(moduleName)) { + reportPath(node); + } + + if (restrictedPatterns.length > 0 && ig.ignores(moduleName)) { + context.report({ + node, + message: "'{{moduleName}}' module is restricted from being used by a pattern.", + data: { moduleName } + }); + } + } + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-restricted-properties.js b/tools/node_modules/eslint/lib/rules/no-restricted-properties.js new file mode 100644 index 0000000000..cdc73f9e41 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-restricted-properties.js @@ -0,0 +1,173 @@ +/** + * @fileoverview Rule to disallow certain object properties + * @author Will Klein & Eli White + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow certain properties on certain objects", + category: "Best Practices", + recommended: false + }, + + schema: { + type: "array", + items: { + anyOf: [ // `object` and `property` are both optional, but at least one of them must be provided. + { + type: "object", + properties: { + object: { + type: "string" + }, + property: { + type: "string" + }, + message: { + type: "string" + } + }, + additionalProperties: false, + required: ["object"] + }, + { + type: "object", + properties: { + object: { + type: "string" + }, + property: { + type: "string" + }, + message: { + type: "string" + } + }, + additionalProperties: false, + required: ["property"] + } + ] + }, + uniqueItems: true + } + }, + + create(context) { + const restrictedCalls = context.options; + + if (restrictedCalls.length === 0) { + return {}; + } + + const restrictedProperties = new Map(); + const globallyRestrictedObjects = new Map(); + const globallyRestrictedProperties = new Map(); + + restrictedCalls.forEach(option => { + const objectName = option.object; + const propertyName = option.property; + + if (typeof objectName === "undefined") { + globallyRestrictedProperties.set(propertyName, { message: option.message }); + } else if (typeof propertyName === "undefined") { + globallyRestrictedObjects.set(objectName, { message: option.message }); + } else { + if (!restrictedProperties.has(objectName)) { + restrictedProperties.set(objectName, new Map()); + } + + restrictedProperties.get(objectName).set(propertyName, { + message: option.message + }); + } + }); + + /** + * Checks to see whether a property access is restricted, and reports it if so. + * @param {ASTNode} node The node to report + * @param {string} objectName The name of the object + * @param {string} propertyName The name of the property + * @returns {undefined} + */ + function checkPropertyAccess(node, objectName, propertyName) { + if (propertyName === null) { + return; + } + const matchedObject = restrictedProperties.get(objectName); + const matchedObjectProperty = matchedObject ? matchedObject.get(propertyName) : globallyRestrictedObjects.get(objectName); + const globalMatchedProperty = globallyRestrictedProperties.get(propertyName); + + if (matchedObjectProperty) { + const message = matchedObjectProperty.message ? ` ${matchedObjectProperty.message}` : ""; + + context.report({ + node, + // eslint-disable-next-line eslint-plugin/report-message-format + message: "'{{objectName}}.{{propertyName}}' is restricted from being used.{{message}}", + data: { + objectName, + propertyName, + message + } + }); + } else if (globalMatchedProperty) { + const message = globalMatchedProperty.message ? ` ${globalMatchedProperty.message}` : ""; + + context.report({ + node, + // eslint-disable-next-line eslint-plugin/report-message-format + message: "'{{propertyName}}' is restricted from being used.{{message}}", + data: { + propertyName, + message + } + }); + } + } + + /** + * Checks property accesses in a destructuring assignment expression, e.g. `var foo; ({foo} = bar);` + * @param {ASTNode} node An AssignmentExpression or AssignmentPattern node + * @returns {undefined} + */ + function checkDestructuringAssignment(node) { + if (node.right.type === "Identifier") { + const objectName = node.right.name; + + if (node.left.type === "ObjectPattern") { + node.left.properties.forEach(property => { + checkPropertyAccess(node.left, objectName, astUtils.getStaticPropertyName(property)); + }); + } + } + } + + return { + MemberExpression(node) { + checkPropertyAccess(node, node.object && node.object.name, astUtils.getStaticPropertyName(node)); + }, + VariableDeclarator(node) { + if (node.init && node.init.type === "Identifier") { + const objectName = node.init.name; + + if (node.id.type === "ObjectPattern") { + node.id.properties.forEach(property => { + checkPropertyAccess(node.id, objectName, astUtils.getStaticPropertyName(property)); + }); + } + } + }, + AssignmentExpression: checkDestructuringAssignment, + AssignmentPattern: checkDestructuringAssignment + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-restricted-syntax.js b/tools/node_modules/eslint/lib/rules/no-restricted-syntax.js new file mode 100644 index 0000000000..1798065ec0 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-restricted-syntax.js @@ -0,0 +1,62 @@ +/** + * @fileoverview Rule to flag use of certain node types + * @author Burak Yigit Kaya + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow specified syntax", + category: "Stylistic Issues", + recommended: false + }, + + schema: { + type: "array", + items: [{ + oneOf: [ + { + type: "string" + }, + { + type: "object", + properties: { + selector: { type: "string" }, + message: { type: "string" } + }, + required: ["selector"], + additionalProperties: false + } + ] + }], + uniqueItems: true, + minItems: 0 + } + }, + + create(context) { + return context.options.reduce((result, selectorOrObject) => { + const isStringFormat = (typeof selectorOrObject === "string"); + const hasCustomMessage = !isStringFormat && Boolean(selectorOrObject.message); + + const selector = isStringFormat ? selectorOrObject : selectorOrObject.selector; + const message = hasCustomMessage ? selectorOrObject.message : "Using '{{selector}}' is not allowed."; + + return Object.assign(result, { + [selector](node) { + context.report({ + node, + message, + data: hasCustomMessage ? {} : { selector } + }); + } + }); + }, {}); + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-return-assign.js b/tools/node_modules/eslint/lib/rules/no-return-assign.js new file mode 100644 index 0000000000..882f94b724 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-return-assign.js @@ -0,0 +1,71 @@ +/** + * @fileoverview Rule to flag when return statement contains assignment + * @author Ilya Volodin + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const SENTINEL_TYPE = /^(?:[a-zA-Z]+?Statement|ArrowFunctionExpression|FunctionExpression|ClassExpression)$/; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow assignment operators in `return` statements", + category: "Best Practices", + recommended: false + }, + + schema: [ + { + enum: ["except-parens", "always"] + } + ] + }, + + create(context) { + const always = (context.options[0] || "except-parens") !== "except-parens"; + const sourceCode = context.getSourceCode(); + + return { + AssignmentExpression(node) { + if (!always && astUtils.isParenthesised(sourceCode, node)) { + return; + } + + let parent = node.parent; + + // Find ReturnStatement or ArrowFunctionExpression in ancestors. + while (parent && !SENTINEL_TYPE.test(parent.type)) { + node = parent; + parent = parent.parent; + } + + // Reports. + if (parent && parent.type === "ReturnStatement") { + context.report({ + node: parent, + message: "Return statement should not contain assignment." + }); + } else if (parent && parent.type === "ArrowFunctionExpression" && parent.body === node) { + context.report({ + node: parent, + message: "Arrow function should not return assignment." + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-return-await.js b/tools/node_modules/eslint/lib/rules/no-return-await.js new file mode 100644 index 0000000000..2f06b61108 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-return-await.js @@ -0,0 +1,94 @@ +/** + * @fileoverview Disallows unnecessary `return await` + * @author Jordan Harband + */ +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +const message = "Redundant use of `await` on a return value."; + +module.exports = { + meta: { + docs: { + description: "disallow unnecessary `return await`", + category: "Best Practices", + recommended: false // TODO: set to true + }, + fixable: null, + schema: [ + ] + }, + + create(context) { + + /** + * Reports a found unnecessary `await` expression. + * @param {ASTNode} node The node representing the `await` expression to report + * @returns {void} + */ + function reportUnnecessaryAwait(node) { + context.report({ + node: context.getSourceCode().getFirstToken(node), + loc: node.loc, + message + }); + } + + /** + * Determines whether a thrown error from this node will be caught/handled within this function rather than immediately halting + * this function. For example, a statement in a `try` block will always have an error handler. A statement in + * a `catch` block will only have an error handler if there is also a `finally` block. + * @param {ASTNode} node A node representing a location where an could be thrown + * @returns {boolean} `true` if a thrown error will be caught/handled in this function + */ + function hasErrorHandler(node) { + let ancestor = node; + + while (!astUtils.isFunction(ancestor) && ancestor.type !== "Program") { + if (ancestor.parent.type === "TryStatement" && (ancestor === ancestor.parent.block || ancestor === ancestor.parent.handler && ancestor.parent.finalizer)) { + return true; + } + ancestor = ancestor.parent; + } + return false; + } + + /** + * Checks if a node is placed in tail call position. Once `return` arguments (or arrow function expressions) can be a complex expression, + * an `await` expression could or could not be unnecessary by the definition of this rule. So we're looking for `await` expressions that are in tail position. + * @param {ASTNode} node A node representing the `await` expression to check + * @returns {boolean} The checking result + */ + function isInTailCallPosition(node) { + if (node.parent.type === "ArrowFunctionExpression") { + return true; + } + if (node.parent.type === "ReturnStatement") { + return !hasErrorHandler(node.parent); + } + if (node.parent.type === "ConditionalExpression" && (node === node.parent.consequent || node === node.parent.alternate)) { + return isInTailCallPosition(node.parent); + } + if (node.parent.type === "LogicalExpression" && node === node.parent.right) { + return isInTailCallPosition(node.parent); + } + if (node.parent.type === "SequenceExpression" && node === node.parent.expressions[node.parent.expressions.length - 1]) { + return isInTailCallPosition(node.parent); + } + return false; + } + + return { + AwaitExpression(node) { + if (isInTailCallPosition(node) && !hasErrorHandler(node)) { + reportUnnecessaryAwait(node); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-script-url.js b/tools/node_modules/eslint/lib/rules/no-script-url.js new file mode 100644 index 0000000000..98f988ff1a --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-script-url.js @@ -0,0 +1,41 @@ +/** + * @fileoverview Rule to flag when using javascript: urls + * @author Ilya Volodin + */ +/* jshint scripturl: true */ +/* eslint no-script-url: 0 */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `javascript:` urls", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + + return { + + Literal(node) { + if (node.value && typeof node.value === "string") { + const value = node.value.toLowerCase(); + + if (value.indexOf("javascript:") === 0) { + context.report({ node, message: "Script URL is a form of eval." }); + } + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-self-assign.js b/tools/node_modules/eslint/lib/rules/no-self-assign.js new file mode 100644 index 0000000000..48b922d46b --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-self-assign.js @@ -0,0 +1,214 @@ +/** + * @fileoverview Rule to disallow assignments where both sides are exactly the same + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const SPACES = /\s+/g; + +/** + * Checks whether the property of 2 given member expression nodes are the same + * property or not. + * + * @param {ASTNode} left - A member expression node to check. + * @param {ASTNode} right - Another member expression node to check. + * @returns {boolean} `true` if the member expressions have the same property. + */ +function isSameProperty(left, right) { + if (left.property.type === "Identifier" && + left.property.type === right.property.type && + left.property.name === right.property.name && + left.computed === right.computed + ) { + return true; + } + + const lname = astUtils.getStaticPropertyName(left); + const rname = astUtils.getStaticPropertyName(right); + + return lname !== null && lname === rname; +} + +/** + * Checks whether 2 given member expression nodes are the reference to the same + * property or not. + * + * @param {ASTNode} left - A member expression node to check. + * @param {ASTNode} right - Another member expression node to check. + * @returns {boolean} `true` if the member expressions are the reference to the + * same property or not. + */ +function isSameMember(left, right) { + if (!isSameProperty(left, right)) { + return false; + } + + const lobj = left.object; + const robj = right.object; + + if (lobj.type !== robj.type) { + return false; + } + if (lobj.type === "MemberExpression") { + return isSameMember(lobj, robj); + } + return lobj.type === "Identifier" && lobj.name === robj.name; +} + +/** + * Traverses 2 Pattern nodes in parallel, then reports self-assignments. + * + * @param {ASTNode|null} left - A left node to traverse. This is a Pattern or + * a Property. + * @param {ASTNode|null} right - A right node to traverse. This is a Pattern or + * a Property. + * @param {boolean} props - The flag to check member expressions as well. + * @param {Function} report - A callback function to report. + * @returns {void} + */ +function eachSelfAssignment(left, right, props, report) { + if (!left || !right) { + + // do nothing + } else if ( + left.type === "Identifier" && + right.type === "Identifier" && + left.name === right.name + ) { + report(right); + } else if ( + left.type === "ArrayPattern" && + right.type === "ArrayExpression" + ) { + const end = Math.min(left.elements.length, right.elements.length); + + for (let i = 0; i < end; ++i) { + const rightElement = right.elements[i]; + + eachSelfAssignment(left.elements[i], rightElement, props, report); + + // After a spread element, those indices are unknown. + if (rightElement && rightElement.type === "SpreadElement") { + break; + } + } + } else if ( + left.type === "RestElement" && + right.type === "SpreadElement" + ) { + eachSelfAssignment(left.argument, right.argument, props, report); + } else if ( + left.type === "ObjectPattern" && + right.type === "ObjectExpression" && + right.properties.length >= 1 + ) { + + /* + * Gets the index of the last spread property. + * It's possible to overwrite properties followed by it. + */ + let startJ = 0; + + for (let i = right.properties.length - 1; i >= 0; --i) { + if (right.properties[i].type === "ExperimentalSpreadProperty") { + startJ = i + 1; + break; + } + } + + for (let i = 0; i < left.properties.length; ++i) { + for (let j = startJ; j < right.properties.length; ++j) { + eachSelfAssignment( + left.properties[i], + right.properties[j], + props, + report + ); + } + } + } else if ( + left.type === "Property" && + right.type === "Property" && + !left.computed && + !right.computed && + right.kind === "init" && + !right.method && + left.key.name === right.key.name + ) { + eachSelfAssignment(left.value, right.value, props, report); + } else if ( + props && + left.type === "MemberExpression" && + right.type === "MemberExpression" && + isSameMember(left, right) + ) { + report(right); + } +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow assignments where both sides are exactly the same", + category: "Best Practices", + recommended: true + }, + + schema: [ + { + type: "object", + properties: { + props: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + const options = context.options[0]; + const props = Boolean(options && options.props); + + /** + * Reports a given node as self assignments. + * + * @param {ASTNode} node - A node to report. This is an Identifier node. + * @returns {void} + */ + function report(node) { + context.report({ + node, + message: "'{{name}}' is assigned to itself.", + data: { + name: sourceCode.getText(node).replace(SPACES, "") + } + }); + } + + return { + AssignmentExpression(node) { + if (node.operator === "=") { + eachSelfAssignment(node.left, node.right, props, report); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-self-compare.js b/tools/node_modules/eslint/lib/rules/no-self-compare.js new file mode 100644 index 0000000000..5beaa181b9 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-self-compare.js @@ -0,0 +1,53 @@ +/** + * @fileoverview Rule to flag comparison where left part is the same as the right + * part. + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow comparisons where both sides are exactly the same", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + /** + * Determines whether two nodes are composed of the same tokens. + * @param {ASTNode} nodeA The first node + * @param {ASTNode} nodeB The second node + * @returns {boolean} true if the nodes have identical token representations + */ + function hasSameTokens(nodeA, nodeB) { + const tokensA = sourceCode.getTokens(nodeA); + const tokensB = sourceCode.getTokens(nodeB); + + return tokensA.length === tokensB.length && + tokensA.every((token, index) => token.type === tokensB[index].type && token.value === tokensB[index].value); + } + + return { + + BinaryExpression(node) { + const operators = new Set(["===", "==", "!==", "!=", ">", "<", ">=", "<="]); + + if (operators.has(node.operator) && hasSameTokens(node.left, node.right)) { + context.report({ node, message: "Comparing to itself is potentially pointless." }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-sequences.js b/tools/node_modules/eslint/lib/rules/no-sequences.js new file mode 100644 index 0000000000..5e746dfa88 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-sequences.js @@ -0,0 +1,112 @@ +/** + * @fileoverview Rule to flag use of comma operator + * @author Brandon Mills + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow comma operators", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + /** + * Parts of the grammar that are required to have parens. + */ + const parenthesized = { + DoWhileStatement: "test", + IfStatement: "test", + SwitchStatement: "discriminant", + WhileStatement: "test", + WithStatement: "object", + ArrowFunctionExpression: "body" + + /* + * Omitting CallExpression - commas are parsed as argument separators + * Omitting NewExpression - commas are parsed as argument separators + * Omitting ForInStatement - parts aren't individually parenthesised + * Omitting ForStatement - parts aren't individually parenthesised + */ + }; + + /** + * Determines whether a node is required by the grammar to be wrapped in + * parens, e.g. the test of an if statement. + * @param {ASTNode} node - The AST node + * @returns {boolean} True if parens around node belong to parent node. + */ + function requiresExtraParens(node) { + return node.parent && parenthesized[node.parent.type] && + node === node.parent[parenthesized[node.parent.type]]; + } + + /** + * Check if a node is wrapped in parens. + * @param {ASTNode} node - The AST node + * @returns {boolean} True if the node has a paren on each side. + */ + function isParenthesised(node) { + return astUtils.isParenthesised(sourceCode, node); + } + + /** + * Check if a node is wrapped in two levels of parens. + * @param {ASTNode} node - The AST node + * @returns {boolean} True if two parens surround the node on each side. + */ + function isParenthesisedTwice(node) { + const previousToken = sourceCode.getTokenBefore(node, 1), + nextToken = sourceCode.getTokenAfter(node, 1); + + return isParenthesised(node) && previousToken && nextToken && + astUtils.isOpeningParenToken(previousToken) && previousToken.range[1] <= node.range[0] && + astUtils.isClosingParenToken(nextToken) && nextToken.range[0] >= node.range[1]; + } + + return { + SequenceExpression(node) { + + // Always allow sequences in for statement update + if (node.parent.type === "ForStatement" && + (node === node.parent.init || node === node.parent.update)) { + return; + } + + // Wrapping a sequence in extra parens indicates intent + if (requiresExtraParens(node)) { + if (isParenthesisedTwice(node)) { + return; + } + } else { + if (isParenthesised(node)) { + return; + } + } + + const child = sourceCode.getTokenAfter(node.expressions[0]); + + context.report({ node, loc: child.loc.start, message: "Unexpected use of comma operator." }); + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-shadow-restricted-names.js b/tools/node_modules/eslint/lib/rules/no-shadow-restricted-names.js new file mode 100644 index 0000000000..6c60232b8b --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-shadow-restricted-names.js @@ -0,0 +1,69 @@ +/** + * @fileoverview Disallow shadowing of NaN, undefined, and Infinity (ES5 section 15.1.1) + * @author Michael Ficarra + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow identifiers from shadowing restricted names", + category: "Variables", + recommended: false + }, + + schema: [] + }, + + create(context) { + + const RESTRICTED = ["undefined", "NaN", "Infinity", "arguments", "eval"]; + + /** + * Check if the node name is present inside the restricted list + * @param {ASTNode} id id to evaluate + * @returns {void} + * @private + */ + function checkForViolation(id) { + if (RESTRICTED.indexOf(id.name) > -1) { + context.report({ + node: id, + message: "Shadowing of global property '{{idName}}'.", + data: { + idName: id.name + } + }); + } + } + + return { + VariableDeclarator(node) { + checkForViolation(node.id); + }, + ArrowFunctionExpression(node) { + [].map.call(node.params, checkForViolation); + }, + FunctionExpression(node) { + if (node.id) { + checkForViolation(node.id); + } + [].map.call(node.params, checkForViolation); + }, + FunctionDeclaration(node) { + if (node.id) { + checkForViolation(node.id); + [].map.call(node.params, checkForViolation); + } + }, + CatchClause(node) { + checkForViolation(node.param); + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-shadow.js b/tools/node_modules/eslint/lib/rules/no-shadow.js new file mode 100644 index 0000000000..e093d48c81 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-shadow.js @@ -0,0 +1,188 @@ +/** + * @fileoverview Rule to flag on declaring variables already declared in the outer scope + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow variable declarations from shadowing variables declared in the outer scope", + category: "Variables", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + builtinGlobals: { type: "boolean" }, + hoist: { enum: ["all", "functions", "never"] }, + allow: { + type: "array", + items: { + type: "string" + } + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + const options = { + builtinGlobals: Boolean(context.options[0] && context.options[0].builtinGlobals), + hoist: (context.options[0] && context.options[0].hoist) || "functions", + allow: (context.options[0] && context.options[0].allow) || [] + }; + + /** + * Check if variable name is allowed. + * + * @param {ASTNode} variable The variable to check. + * @returns {boolean} Whether or not the variable name is allowed. + */ + function isAllowed(variable) { + return options.allow.indexOf(variable.name) !== -1; + } + + /** + * Checks if a variable of the class name in the class scope of ClassDeclaration. + * + * ClassDeclaration creates two variables of its name into its outer scope and its class scope. + * So we should ignore the variable in the class scope. + * + * @param {Object} variable The variable to check. + * @returns {boolean} Whether or not the variable of the class name in the class scope of ClassDeclaration. + */ + function isDuplicatedClassNameVariable(variable) { + const block = variable.scope.block; + + return block.type === "ClassDeclaration" && block.id === variable.identifiers[0]; + } + + /** + * Checks if a variable is inside the initializer of scopeVar. + * + * To avoid reporting at declarations such as `var a = function a() {};`. + * But it should report `var a = function(a) {};` or `var a = function() { function a() {} };`. + * + * @param {Object} variable The variable to check. + * @param {Object} scopeVar The scope variable to look for. + * @returns {boolean} Whether or not the variable is inside initializer of scopeVar. + */ + function isOnInitializer(variable, scopeVar) { + const outerScope = scopeVar.scope; + const outerDef = scopeVar.defs[0]; + const outer = outerDef && outerDef.parent && outerDef.parent.range; + const innerScope = variable.scope; + const innerDef = variable.defs[0]; + const inner = innerDef && innerDef.name.range; + + return ( + outer && + inner && + outer[0] < inner[0] && + inner[1] < outer[1] && + ((innerDef.type === "FunctionName" && innerDef.node.type === "FunctionExpression") || innerDef.node.type === "ClassExpression") && + outerScope === innerScope.upper + ); + } + + /** + * Get a range of a variable's identifier node. + * @param {Object} variable The variable to get. + * @returns {Array|undefined} The range of the variable's identifier node. + */ + function getNameRange(variable) { + const def = variable.defs[0]; + + return def && def.name.range; + } + + /** + * Checks if a variable is in TDZ of scopeVar. + * @param {Object} variable The variable to check. + * @param {Object} scopeVar The variable of TDZ. + * @returns {boolean} Whether or not the variable is in TDZ of scopeVar. + */ + function isInTdz(variable, scopeVar) { + const outerDef = scopeVar.defs[0]; + const inner = getNameRange(variable); + const outer = getNameRange(scopeVar); + + return ( + inner && + outer && + inner[1] < outer[0] && + + // Excepts FunctionDeclaration if is {"hoist":"function"}. + (options.hoist !== "functions" || !outerDef || outerDef.node.type !== "FunctionDeclaration") + ); + } + + /** + * Checks the current context for shadowed variables. + * @param {Scope} scope - Fixme + * @returns {void} + */ + function checkForShadows(scope) { + const variables = scope.variables; + + for (let i = 0; i < variables.length; ++i) { + const variable = variables[i]; + + // Skips "arguments" or variables of a class name in the class scope of ClassDeclaration. + if (variable.identifiers.length === 0 || + isDuplicatedClassNameVariable(variable) || + isAllowed(variable) + ) { + continue; + } + + // Gets shadowed variable. + const shadowed = astUtils.getVariableByName(scope.upper, variable.name); + + if (shadowed && + (shadowed.identifiers.length > 0 || (options.builtinGlobals && "writeable" in shadowed)) && + !isOnInitializer(variable, shadowed) && + !(options.hoist !== "all" && isInTdz(variable, shadowed)) + ) { + context.report({ + node: variable.identifiers[0], + message: "'{{name}}' is already declared in the upper scope.", + data: variable + }); + } + } + } + + return { + "Program:exit"() { + const globalScope = context.getScope(); + const stack = globalScope.childScopes.slice(); + + while (stack.length) { + const scope = stack.pop(); + + stack.push.apply(stack, scope.childScopes); + checkForShadows(scope); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-spaced-func.js b/tools/node_modules/eslint/lib/rules/no-spaced-func.js new file mode 100644 index 0000000000..361c1e0cd7 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-spaced-func.js @@ -0,0 +1,75 @@ +/** + * @fileoverview Rule to check that spaced function application + * @author Matt DuVall <http://www.mattduvall.com> + * @deprecated in ESLint v3.3.0 + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow spacing between function identifiers and their applications (deprecated)", + category: "Stylistic Issues", + recommended: false, + replacedBy: ["func-call-spacing"] + }, + + deprecated: true, + + fixable: "whitespace", + schema: [] + }, + + create(context) { + + const sourceCode = context.getSourceCode(); + + /** + * Check if open space is present in a function name + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function detectOpenSpaces(node) { + const lastCalleeToken = sourceCode.getLastToken(node.callee); + let prevToken = lastCalleeToken, + parenToken = sourceCode.getTokenAfter(lastCalleeToken); + + // advances to an open parenthesis. + while ( + parenToken && + parenToken.range[1] < node.range[1] && + parenToken.value !== "(" + ) { + prevToken = parenToken; + parenToken = sourceCode.getTokenAfter(parenToken); + } + + // look for a space between the callee and the open paren + if (parenToken && + parenToken.range[1] < node.range[1] && + sourceCode.isSpaceBetweenTokens(prevToken, parenToken) + ) { + context.report({ + node, + loc: lastCalleeToken.loc.start, + message: "Unexpected space between function name and paren.", + fix(fixer) { + return fixer.removeRange([prevToken.range[1], parenToken.range[0]]); + } + }); + } + } + + return { + CallExpression: detectOpenSpaces, + NewExpression: detectOpenSpaces + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-sparse-arrays.js b/tools/node_modules/eslint/lib/rules/no-sparse-arrays.js new file mode 100644 index 0000000000..3044896c61 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-sparse-arrays.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Disallow sparse arrays + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow sparse arrays", + category: "Possible Errors", + recommended: true + }, + + schema: [] + }, + + create(context) { + + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + ArrayExpression(node) { + + const emptySpot = node.elements.indexOf(null) > -1; + + if (emptySpot) { + context.report({ node, message: "Unexpected comma in middle of array." }); + } + } + + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-sync.js b/tools/node_modules/eslint/lib/rules/no-sync.js new file mode 100644 index 0000000000..06305969a1 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-sync.js @@ -0,0 +1,53 @@ +/** + * @fileoverview Rule to check for properties whose identifier ends with the string Sync + * @author Matt DuVall<http://mattduvall.com/> + */ + +/* jshint node:true */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow synchronous methods", + category: "Node.js and CommonJS", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + allowAtRootLevel: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const selector = context.options[0] && context.options[0].allowAtRootLevel + ? ":function MemberExpression[property.name=/.*Sync$/]" + : "MemberExpression[property.name=/.*Sync$/]"; + + return { + [selector](node) { + context.report({ + node, + message: "Unexpected sync method: '{{propertyName}}'.", + data: { + propertyName: node.property.name + } + }); + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-tabs.js b/tools/node_modules/eslint/lib/rules/no-tabs.js new file mode 100644 index 0000000000..4bab96f387 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-tabs.js @@ -0,0 +1,47 @@ +/** + * @fileoverview Rule to check for tabs inside a file + * @author Gyandeep Singh + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ +const regex = /\t/; + +//------------------------------------------------------------------------------ +// Public Interface +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow all tabs", + category: "Stylistic Issues", + recommended: false + }, + schema: [] + }, + + create(context) { + return { + Program(node) { + context.getSourceCode().getLines().forEach((line, index) => { + const match = regex.exec(line); + + if (match) { + context.report({ + node, + loc: { + line: index + 1, + column: match.index + 1 + }, + message: "Unexpected tab character." + }); + } + }); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-template-curly-in-string.js b/tools/node_modules/eslint/lib/rules/no-template-curly-in-string.js new file mode 100644 index 0000000000..d8f6c31108 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-template-curly-in-string.js @@ -0,0 +1,37 @@ +/** + * @fileoverview Warn when using template string syntax in regular strings + * @author Jeroen Engels + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow template literal placeholder syntax in regular strings", + category: "Possible Errors", + recommended: false + }, + + schema: [] + }, + + create(context) { + const regex = /\$\{[^}]+\}/; + + return { + Literal(node) { + if (typeof node.value === "string" && regex.test(node.value)) { + context.report({ + node, + message: "Unexpected template string expression." + }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-ternary.js b/tools/node_modules/eslint/lib/rules/no-ternary.js new file mode 100644 index 0000000000..3e254f6812 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-ternary.js @@ -0,0 +1,34 @@ +/** + * @fileoverview Rule to flag use of ternary operators. + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow ternary operators", + category: "Stylistic Issues", + recommended: false + }, + + schema: [] + }, + + create(context) { + + return { + + ConditionalExpression(node) { + context.report({ node, message: "Ternary operator used." }); + } + + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-this-before-super.js b/tools/node_modules/eslint/lib/rules/no-this-before-super.js new file mode 100644 index 0000000000..2a686ac72e --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-this-before-super.js @@ -0,0 +1,299 @@ +/** + * @fileoverview A rule to disallow using `this`/`super` before `super()`. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether or not a given node is a constructor. + * @param {ASTNode} node - A node to check. This node type is one of + * `Program`, `FunctionDeclaration`, `FunctionExpression`, and + * `ArrowFunctionExpression`. + * @returns {boolean} `true` if the node is a constructor. + */ +function isConstructorFunction(node) { + return ( + node.type === "FunctionExpression" && + node.parent.type === "MethodDefinition" && + node.parent.kind === "constructor" + ); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `this`/`super` before calling `super()` in constructors", + category: "ECMAScript 6", + recommended: true + }, + + schema: [] + }, + + create(context) { + + /* + * Information for each constructor. + * - upper: Information of the upper constructor. + * - hasExtends: A flag which shows whether the owner class has a valid + * `extends` part. + * - scope: The scope of the owner class. + * - codePath: The code path of this constructor. + */ + let funcInfo = null; + + /* + * Information for each code path segment. + * Each key is the id of a code path segment. + * Each value is an object: + * - superCalled: The flag which shows `super()` called in all code paths. + * - invalidNodes: The array of invalid ThisExpression and Super nodes. + */ + let segInfoMap = Object.create(null); + + /** + * Gets whether or not `super()` is called in a given code path segment. + * @param {CodePathSegment} segment - A code path segment to get. + * @returns {boolean} `true` if `super()` is called. + */ + function isCalled(segment) { + return !segment.reachable || segInfoMap[segment.id].superCalled; + } + + /** + * Checks whether or not this is in a constructor. + * @returns {boolean} `true` if this is in a constructor. + */ + function isInConstructorOfDerivedClass() { + return Boolean(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends); + } + + /** + * Checks whether or not this is before `super()` is called. + * @returns {boolean} `true` if this is before `super()` is called. + */ + function isBeforeCallOfSuper() { + return ( + isInConstructorOfDerivedClass() && + !funcInfo.codePath.currentSegments.every(isCalled) + ); + } + + /** + * Sets a given node as invalid. + * @param {ASTNode} node - A node to set as invalid. This is one of + * a ThisExpression and a Super. + * @returns {void} + */ + function setInvalid(node) { + const segments = funcInfo.codePath.currentSegments; + + for (let i = 0; i < segments.length; ++i) { + const segment = segments[i]; + + if (segment.reachable) { + segInfoMap[segment.id].invalidNodes.push(node); + } + } + } + + /** + * Sets the current segment as `super` was called. + * @returns {void} + */ + function setSuperCalled() { + const segments = funcInfo.codePath.currentSegments; + + for (let i = 0; i < segments.length; ++i) { + const segment = segments[i]; + + if (segment.reachable) { + segInfoMap[segment.id].superCalled = true; + } + } + } + + return { + + /** + * Adds information of a constructor into the stack. + * @param {CodePath} codePath - A code path which was started. + * @param {ASTNode} node - The current node. + * @returns {void} + */ + onCodePathStart(codePath, node) { + if (isConstructorFunction(node)) { + + // Class > ClassBody > MethodDefinition > FunctionExpression + const classNode = node.parent.parent.parent; + + funcInfo = { + upper: funcInfo, + isConstructor: true, + hasExtends: Boolean( + classNode.superClass && + !astUtils.isNullOrUndefined(classNode.superClass) + ), + codePath + }; + } else { + funcInfo = { + upper: funcInfo, + isConstructor: false, + hasExtends: false, + codePath + }; + } + }, + + /** + * Removes the top of stack item. + * + * And this treverses all segments of this code path then reports every + * invalid node. + * + * @param {CodePath} codePath - A code path which was ended. + * @param {ASTNode} node - The current node. + * @returns {void} + */ + onCodePathEnd(codePath) { + const isDerivedClass = funcInfo.hasExtends; + + funcInfo = funcInfo.upper; + if (!isDerivedClass) { + return; + } + + codePath.traverseSegments((segment, controller) => { + const info = segInfoMap[segment.id]; + + for (let i = 0; i < info.invalidNodes.length; ++i) { + const invalidNode = info.invalidNodes[i]; + + context.report({ + message: "'{{kind}}' is not allowed before 'super()'.", + node: invalidNode, + data: { + kind: invalidNode.type === "Super" ? "super" : "this" + } + }); + } + + if (info.superCalled) { + controller.skip(); + } + }); + }, + + /** + * Initialize information of a given code path segment. + * @param {CodePathSegment} segment - A code path segment to initialize. + * @returns {void} + */ + onCodePathSegmentStart(segment) { + if (!isInConstructorOfDerivedClass()) { + return; + } + + // Initialize info. + segInfoMap[segment.id] = { + superCalled: ( + segment.prevSegments.length > 0 && + segment.prevSegments.every(isCalled) + ), + invalidNodes: [] + }; + }, + + /** + * Update information of the code path segment when a code path was + * looped. + * @param {CodePathSegment} fromSegment - The code path segment of the + * end of a loop. + * @param {CodePathSegment} toSegment - A code path segment of the head + * of a loop. + * @returns {void} + */ + onCodePathSegmentLoop(fromSegment, toSegment) { + if (!isInConstructorOfDerivedClass()) { + return; + } + + // Update information inside of the loop. + funcInfo.codePath.traverseSegments( + { first: toSegment, last: fromSegment }, + (segment, controller) => { + const info = segInfoMap[segment.id]; + + if (info.superCalled) { + info.invalidNodes = []; + controller.skip(); + } else if ( + segment.prevSegments.length > 0 && + segment.prevSegments.every(isCalled) + ) { + info.superCalled = true; + info.invalidNodes = []; + } + } + ); + }, + + /** + * Reports if this is before `super()`. + * @param {ASTNode} node - A target node. + * @returns {void} + */ + ThisExpression(node) { + if (isBeforeCallOfSuper()) { + setInvalid(node); + } + }, + + /** + * Reports if this is before `super()`. + * @param {ASTNode} node - A target node. + * @returns {void} + */ + Super(node) { + if (!astUtils.isCallee(node) && isBeforeCallOfSuper()) { + setInvalid(node); + } + }, + + /** + * Marks `super()` called. + * @param {ASTNode} node - A target node. + * @returns {void} + */ + "CallExpression:exit"(node) { + if (node.callee.type === "Super" && isBeforeCallOfSuper()) { + setSuperCalled(); + } + }, + + /** + * Resets state. + * @returns {void} + */ + "Program:exit"() { + segInfoMap = Object.create(null); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-throw-literal.js b/tools/node_modules/eslint/lib/rules/no-throw-literal.js new file mode 100644 index 0000000000..5e9054399a --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-throw-literal.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Rule to restrict what can be thrown as an exception. + * @author Dieter Oberkofler + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow throwing literals as exceptions", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + + return { + + ThrowStatement(node) { + if (!astUtils.couldBeError(node.argument)) { + context.report({ node, message: "Expected an object to be thrown." }); + } else if (node.argument.type === "Identifier") { + if (node.argument.name === "undefined") { + context.report({ node, message: "Do not throw undefined." }); + } + } + + } + + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-trailing-spaces.js b/tools/node_modules/eslint/lib/rules/no-trailing-spaces.js new file mode 100644 index 0000000000..fbbc640217 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-trailing-spaces.js @@ -0,0 +1,169 @@ +/** + * @fileoverview Disallow trailing spaces at the end of lines. + * @author Nodeca Team <https://github.com/nodeca> + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow trailing whitespace at the end of lines", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + type: "object", + properties: { + skipBlankLines: { + type: "boolean" + }, + ignoreComments: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + const BLANK_CLASS = "[ \t\u00a0\u2000-\u200b\u3000]", + SKIP_BLANK = `^${BLANK_CLASS}*$`, + NONBLANK = `${BLANK_CLASS}+$`; + + const options = context.options[0] || {}, + skipBlankLines = options.skipBlankLines || false, + ignoreComments = typeof options.ignoreComments === "boolean" && options.ignoreComments; + + /** + * Report the error message + * @param {ASTNode} node node to report + * @param {int[]} location range information + * @param {int[]} fixRange Range based on the whole program + * @returns {void} + */ + function report(node, location, fixRange) { + + /* + * Passing node is a bit dirty, because message data will contain big + * text in `source`. But... who cares :) ? + * One more kludge will not make worse the bloody wizardry of this + * plugin. + */ + context.report({ + node, + loc: location, + message: "Trailing spaces not allowed.", + fix(fixer) { + return fixer.removeRange(fixRange); + } + }); + } + + /** + * Given a list of comment nodes, return the line numbers for those comments. + * @param {Array} comments An array of comment nodes. + * @returns {number[]} An array of line numbers containing comments. + */ + function getCommentLineNumbers(comments) { + const lines = new Set(); + + comments.forEach(comment => { + for (let i = comment.loc.start.line; i <= comment.loc.end.line; i++) { + lines.add(i); + } + }); + + return lines; + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + Program: function checkTrailingSpaces(node) { + + /* + * Let's hack. Since Espree does not return whitespace nodes, + * fetch the source code and do matching via regexps. + */ + + const re = new RegExp(NONBLANK), + skipMatch = new RegExp(SKIP_BLANK), + lines = sourceCode.lines, + linebreaks = sourceCode.getText().match(astUtils.createGlobalLinebreakMatcher()), + comments = sourceCode.getAllComments(), + commentLineNumbers = getCommentLineNumbers(comments); + + let totalLength = 0, + fixRange = []; + + for (let i = 0, ii = lines.length; i < ii; i++) { + const matches = re.exec(lines[i]); + + /* + * Always add linebreak length to line length to accommodate for line break (\n or \r\n) + * Because during the fix time they also reserve one spot in the array. + * Usually linebreak length is 2 for \r\n (CRLF) and 1 for \n (LF) + */ + const linebreakLength = linebreaks && linebreaks[i] ? linebreaks[i].length : 1; + const lineLength = lines[i].length + linebreakLength; + + if (matches) { + const location = { + line: i + 1, + column: matches.index + }; + + const rangeStart = totalLength + location.column; + const rangeEnd = totalLength + lineLength - linebreakLength; + const containingNode = sourceCode.getNodeByRangeIndex(rangeStart); + + if (containingNode && containingNode.type === "TemplateElement" && + rangeStart > containingNode.parent.range[0] && + rangeEnd < containingNode.parent.range[1]) { + totalLength += lineLength; + continue; + } + + /* + * If the line has only whitespace, and skipBlankLines + * is true, don't report it + */ + if (skipBlankLines && skipMatch.test(lines[i])) { + totalLength += lineLength; + continue; + } + + fixRange = [rangeStart, rangeEnd]; + + if (!ignoreComments || !commentLineNumbers.has(location.line)) { + report(node, location, fixRange); + } + } + + totalLength += lineLength; + } + } + + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-undef-init.js b/tools/node_modules/eslint/lib/rules/no-undef-init.js new file mode 100644 index 0000000000..7e58f55a69 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-undef-init.js @@ -0,0 +1,63 @@ +/** + * @fileoverview Rule to flag when initializing to undefined + * @author Ilya Volodin + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow initializing variables to `undefined`", + category: "Variables", + recommended: false + }, + + schema: [], + + fixable: "code" + }, + + create(context) { + + const sourceCode = context.getSourceCode(); + + return { + + VariableDeclarator(node) { + const name = sourceCode.getText(node.id), + init = node.init && node.init.name, + scope = context.getScope(), + undefinedVar = astUtils.getVariableByName(scope, "undefined"), + shadowed = undefinedVar && undefinedVar.defs.length > 0; + + if (init === "undefined" && node.parent.kind !== "const" && !shadowed) { + context.report({ + node, + message: "It's not necessary to initialize '{{name}}' to undefined.", + data: { name }, + fix(fixer) { + if (node.parent.kind === "var") { + return null; + } + + if (node.id.type === "ArrayPattern" || node.id.type === "ObjectPattern") { + + // Don't fix destructuring assignment to `undefined`. + return null; + } + return fixer.removeRange([node.id.range[1], node.range[1]]); + } + }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-undef.js b/tools/node_modules/eslint/lib/rules/no-undef.js new file mode 100644 index 0000000000..74a33dd997 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-undef.js @@ -0,0 +1,71 @@ +/** + * @fileoverview Rule to flag references to undeclared variables. + * @author Mark Macdonald + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks if the given node is the argument of a typeof operator. + * @param {ASTNode} node The AST node being checked. + * @returns {boolean} Whether or not the node is the argument of a typeof operator. + */ +function hasTypeOfOperator(node) { + const parent = node.parent; + + return parent.type === "UnaryExpression" && parent.operator === "typeof"; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow the use of undeclared variables unless mentioned in `/*global */` comments", + category: "Variables", + recommended: true + }, + + schema: [ + { + type: "object", + properties: { + typeof: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const options = context.options[0]; + const considerTypeOf = options && options.typeof === true || false; + + return { + "Program:exit"(/* node */) { + const globalScope = context.getScope(); + + globalScope.through.forEach(ref => { + const identifier = ref.identifier; + + if (!considerTypeOf && hasTypeOfOperator(identifier)) { + return; + } + + context.report({ + node: identifier, + message: "'{{name}}' is not defined.", + data: identifier + }); + }); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-undefined.js b/tools/node_modules/eslint/lib/rules/no-undefined.js new file mode 100644 index 0000000000..7e9f96b921 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-undefined.js @@ -0,0 +1,77 @@ +/** + * @fileoverview Rule to flag references to the undefined variable. + * @author Michael Ficarra + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow the use of `undefined` as an identifier", + category: "Variables", + recommended: false + }, + + schema: [] + }, + + create(context) { + + /** + * Report an invalid "undefined" identifier node. + * @param {ASTNode} node The node to report. + * @returns {void} + */ + function report(node) { + context.report({ + node, + message: "Unexpected use of undefined." + }); + } + + /** + * Checks the given scope for references to `undefined` and reports + * all references found. + * @param {eslint-scope.Scope} scope The scope to check. + * @returns {void} + */ + function checkScope(scope) { + const undefinedVar = scope.set.get("undefined"); + + if (!undefinedVar) { + return; + } + + const references = undefinedVar.references; + + const defs = undefinedVar.defs; + + // Report non-initializing references (those are covered in defs below) + references + .filter(ref => !ref.init) + .forEach(ref => report(ref.identifier)); + + defs.forEach(def => report(def.name)); + } + + return { + "Program:exit"() { + const globalScope = context.getScope(); + + const stack = [globalScope]; + + while (stack.length) { + const scope = stack.pop(); + + stack.push.apply(stack, scope.childScopes); + checkScope(scope); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-underscore-dangle.js b/tools/node_modules/eslint/lib/rules/no-underscore-dangle.js new file mode 100644 index 0000000000..5964da41cd --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-underscore-dangle.js @@ -0,0 +1,203 @@ +/** + * @fileoverview Rule to flag trailing underscores in variable declarations. + * @author Matt DuVall <http://www.mattduvall.com> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow dangling underscores in identifiers", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + allow: { + type: "array", + items: { + type: "string" + } + }, + allowAfterThis: { + type: "boolean" + }, + allowAfterSuper: { + type: "boolean" + }, + enforceInMethodNames: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + const options = context.options[0] || {}; + const ALLOWED_VARIABLES = options.allow ? options.allow : []; + const allowAfterThis = typeof options.allowAfterThis !== "undefined" ? options.allowAfterThis : false; + const allowAfterSuper = typeof options.allowAfterSuper !== "undefined" ? options.allowAfterSuper : false; + const enforceInMethodNames = typeof options.enforceInMethodNames !== "undefined" ? options.enforceInMethodNames : false; + + //------------------------------------------------------------------------- + // Helpers + //------------------------------------------------------------------------- + + /** + * Check if identifier is present inside the allowed option + * @param {string} identifier name of the node + * @returns {boolean} true if its is present + * @private + */ + function isAllowed(identifier) { + return ALLOWED_VARIABLES.some(ident => ident === identifier); + } + + /** + * Check if identifier has a underscore at the end + * @param {ASTNode} identifier node to evaluate + * @returns {boolean} true if its is present + * @private + */ + function hasTrailingUnderscore(identifier) { + const len = identifier.length; + + return identifier !== "_" && (identifier[0] === "_" || identifier[len - 1] === "_"); + } + + /** + * Check if identifier is a special case member expression + * @param {ASTNode} identifier node to evaluate + * @returns {boolean} true if its is a special case + * @private + */ + function isSpecialCaseIdentifierForMemberExpression(identifier) { + return identifier === "__proto__"; + } + + /** + * Check if identifier is a special case variable expression + * @param {ASTNode} identifier node to evaluate + * @returns {boolean} true if its is a special case + * @private + */ + function isSpecialCaseIdentifierInVariableExpression(identifier) { + + // Checks for the underscore library usage here + return identifier === "_"; + } + + /** + * Check if function has a underscore at the end + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function checkForTrailingUnderscoreInFunctionDeclaration(node) { + if (node.id) { + const identifier = node.id.name; + + if (typeof identifier !== "undefined" && hasTrailingUnderscore(identifier) && !isAllowed(identifier)) { + context.report({ + node, + message: "Unexpected dangling '_' in '{{identifier}}'.", + data: { + identifier + } + }); + } + } + } + + /** + * Check if variable expression has a underscore at the end + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function checkForTrailingUnderscoreInVariableExpression(node) { + const identifier = node.id.name; + + if (typeof identifier !== "undefined" && hasTrailingUnderscore(identifier) && + !isSpecialCaseIdentifierInVariableExpression(identifier) && !isAllowed(identifier)) { + context.report({ + node, + message: "Unexpected dangling '_' in '{{identifier}}'.", + data: { + identifier + } + }); + } + } + + /** + * Check if member expression has a underscore at the end + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function checkForTrailingUnderscoreInMemberExpression(node) { + const identifier = node.property.name, + isMemberOfThis = node.object.type === "ThisExpression", + isMemberOfSuper = node.object.type === "Super"; + + if (typeof identifier !== "undefined" && hasTrailingUnderscore(identifier) && + !(isMemberOfThis && allowAfterThis) && + !(isMemberOfSuper && allowAfterSuper) && + !isSpecialCaseIdentifierForMemberExpression(identifier) && !isAllowed(identifier)) { + context.report({ + node, + message: "Unexpected dangling '_' in '{{identifier}}'.", + data: { + identifier + } + }); + } + } + + /** + * Check if method declaration or method property has a underscore at the end + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function checkForTrailingUnderscoreInMethod(node) { + const identifier = node.key.name; + const isMethod = node.type === "MethodDefinition" || node.type === "Property" && node.method; + + if (typeof identifier !== "undefined" && enforceInMethodNames && isMethod && hasTrailingUnderscore(identifier)) { + context.report({ + node, + message: "Unexpected dangling '_' in '{{identifier}}'.", + data: { + identifier + } + }); + } + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + FunctionDeclaration: checkForTrailingUnderscoreInFunctionDeclaration, + VariableDeclarator: checkForTrailingUnderscoreInVariableExpression, + MemberExpression: checkForTrailingUnderscoreInMemberExpression, + MethodDefinition: checkForTrailingUnderscoreInMethod, + Property: checkForTrailingUnderscoreInMethod + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-unexpected-multiline.js b/tools/node_modules/eslint/lib/rules/no-unexpected-multiline.js new file mode 100644 index 0000000000..9398b8a603 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-unexpected-multiline.js @@ -0,0 +1,98 @@ +/** + * @fileoverview Rule to spot scenarios where a newline looks like it is ending a statement, but is not. + * @author Glen Mailer + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow confusing multiline expressions", + category: "Possible Errors", + recommended: true + }, + + schema: [] + }, + + create(context) { + + const FUNCTION_MESSAGE = "Unexpected newline between function and ( of function call."; + const PROPERTY_MESSAGE = "Unexpected newline between object and [ of property access."; + const TAGGED_TEMPLATE_MESSAGE = "Unexpected newline between template tag and template literal."; + const DIVISION_MESSAGE = "Unexpected newline between numerator and division operator."; + + const REGEX_FLAG_MATCHER = /^[gimuy]+$/; + + const sourceCode = context.getSourceCode(); + + /** + * Check to see if there is a newline between the node and the following open bracket + * line's expression + * @param {ASTNode} node The node to check. + * @param {string} msg The error message to use. + * @returns {void} + * @private + */ + function checkForBreakAfter(node, msg) { + const openParen = sourceCode.getTokenAfter(node, astUtils.isNotClosingParenToken); + const nodeExpressionEnd = sourceCode.getTokenBefore(openParen); + + if (openParen.loc.start.line !== nodeExpressionEnd.loc.end.line) { + context.report({ node, loc: openParen.loc.start, message: msg, data: { char: openParen.value } }); + } + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + + MemberExpression(node) { + if (!node.computed) { + return; + } + checkForBreakAfter(node.object, PROPERTY_MESSAGE); + }, + + TaggedTemplateExpression(node) { + if (node.tag.loc.end.line === node.quasi.loc.start.line) { + return; + } + context.report({ node, loc: node.loc.start, message: TAGGED_TEMPLATE_MESSAGE }); + }, + + CallExpression(node) { + if (node.arguments.length === 0) { + return; + } + checkForBreakAfter(node.callee, FUNCTION_MESSAGE); + }, + + "BinaryExpression[operator='/'] > BinaryExpression[operator='/'].left"(node) { + const secondSlash = sourceCode.getTokenAfter(node, token => token.value === "/"); + const tokenAfterOperator = sourceCode.getTokenAfter(secondSlash); + + if ( + tokenAfterOperator.type === "Identifier" && + REGEX_FLAG_MATCHER.test(tokenAfterOperator.value) && + secondSlash.range[1] === tokenAfterOperator.range[0] + ) { + checkForBreakAfter(node.left, DIVISION_MESSAGE); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-unmodified-loop-condition.js b/tools/node_modules/eslint/lib/rules/no-unmodified-loop-condition.js new file mode 100644 index 0000000000..49dff0d0ce --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-unmodified-loop-condition.js @@ -0,0 +1,366 @@ +/** + * @fileoverview Rule to disallow use of unmodified expressions in loop conditions + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const Traverser = require("../util/traverser"), + astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const pushAll = Function.apply.bind(Array.prototype.push); +const SENTINEL_PATTERN = /(?:(?:Call|Class|Function|Member|New|Yield)Expression|Statement|Declaration)$/; +const LOOP_PATTERN = /^(?:DoWhile|For|While)Statement$/; // for-in/of statements don't have `test` property. +const GROUP_PATTERN = /^(?:BinaryExpression|ConditionalExpression)$/; +const SKIP_PATTERN = /^(?:ArrowFunction|Class|Function)Expression$/; +const DYNAMIC_PATTERN = /^(?:Call|Member|New|TaggedTemplate|Yield)Expression$/; + +/** + * @typedef {Object} LoopConditionInfo + * @property {eslint-scope.Reference} reference - The reference. + * @property {ASTNode} group - BinaryExpression or ConditionalExpression nodes + * that the reference is belonging to. + * @property {Function} isInLoop - The predicate which checks a given reference + * is in this loop. + * @property {boolean} modified - The flag that the reference is modified in + * this loop. + */ + +/** + * Checks whether or not a given reference is a write reference. + * + * @param {eslint-scope.Reference} reference - A reference to check. + * @returns {boolean} `true` if the reference is a write reference. + */ +function isWriteReference(reference) { + if (reference.init) { + const def = reference.resolved && reference.resolved.defs[0]; + + if (!def || def.type !== "Variable" || def.parent.kind !== "var") { + return false; + } + } + return reference.isWrite(); +} + +/** + * Checks whether or not a given loop condition info does not have the modified + * flag. + * + * @param {LoopConditionInfo} condition - A loop condition info to check. + * @returns {boolean} `true` if the loop condition info is "unmodified". + */ +function isUnmodified(condition) { + return !condition.modified; +} + +/** + * Checks whether or not a given loop condition info does not have the modified + * flag and does not have the group this condition belongs to. + * + * @param {LoopConditionInfo} condition - A loop condition info to check. + * @returns {boolean} `true` if the loop condition info is "unmodified". + */ +function isUnmodifiedAndNotBelongToGroup(condition) { + return !(condition.modified || condition.group); +} + +/** + * Checks whether or not a given reference is inside of a given node. + * + * @param {ASTNode} node - A node to check. + * @param {eslint-scope.Reference} reference - A reference to check. + * @returns {boolean} `true` if the reference is inside of the node. + */ +function isInRange(node, reference) { + const or = node.range; + const ir = reference.identifier.range; + + return or[0] <= ir[0] && ir[1] <= or[1]; +} + +/** + * Checks whether or not a given reference is inside of a loop node's condition. + * + * @param {ASTNode} node - A node to check. + * @param {eslint-scope.Reference} reference - A reference to check. + * @returns {boolean} `true` if the reference is inside of the loop node's + * condition. + */ +const isInLoop = { + WhileStatement: isInRange, + DoWhileStatement: isInRange, + ForStatement(node, reference) { + return ( + isInRange(node, reference) && + !(node.init && isInRange(node.init, reference)) + ); + } +}; + +/** + * Checks whether or not a given group node has any dynamic elements. + * + * @param {ASTNode} root - A node to check. + * This node is one of BinaryExpression or ConditionalExpression. + * @returns {boolean} `true` if the node is dynamic. + */ +function hasDynamicExpressions(root) { + let retv = false; + const traverser = new Traverser(); + + traverser.traverse(root, { + enter(node) { + if (DYNAMIC_PATTERN.test(node.type)) { + retv = true; + this.break(); + } else if (SKIP_PATTERN.test(node.type)) { + this.skip(); + } + } + }); + + return retv; +} + +/** + * Creates the loop condition information from a given reference. + * + * @param {eslint-scope.Reference} reference - A reference to create. + * @returns {LoopConditionInfo|null} Created loop condition info, or null. + */ +function toLoopCondition(reference) { + if (reference.init) { + return null; + } + + let group = null; + let child = reference.identifier; + let node = child.parent; + + while (node) { + if (SENTINEL_PATTERN.test(node.type)) { + if (LOOP_PATTERN.test(node.type) && node.test === child) { + + // This reference is inside of a loop condition. + return { + reference, + group, + isInLoop: isInLoop[node.type].bind(null, node), + modified: false + }; + } + + // This reference is outside of a loop condition. + break; + } + + /* + * If it's inside of a group, OK if either operand is modified. + * So stores the group this reference belongs to. + */ + if (GROUP_PATTERN.test(node.type)) { + + // If this expression is dynamic, no need to check. + if (hasDynamicExpressions(node)) { + break; + } else { + group = node; + } + } + + child = node; + node = node.parent; + } + + return null; +} + +/** + * Gets the function which encloses a given reference. + * This supports only FunctionDeclaration. + * + * @param {eslint-scope.Reference} reference - A reference to get. + * @returns {ASTNode|null} The function node or null. + */ +function getEncloseFunctionDeclaration(reference) { + let node = reference.identifier; + + while (node) { + if (node.type === "FunctionDeclaration") { + return node.id ? node : null; + } + + node = node.parent; + } + + return null; +} + +/** + * Updates the "modified" flags of given loop conditions with given modifiers. + * + * @param {LoopConditionInfo[]} conditions - The loop conditions to be updated. + * @param {eslint-scope.Reference[]} modifiers - The references to update. + * @returns {void} + */ +function updateModifiedFlag(conditions, modifiers) { + + for (let i = 0; i < conditions.length; ++i) { + const condition = conditions[i]; + + for (let j = 0; !condition.modified && j < modifiers.length; ++j) { + const modifier = modifiers[j]; + let funcNode, funcVar; + + /* + * Besides checking for the condition being in the loop, we want to + * check the function that this modifier is belonging to is called + * in the loop. + * FIXME: This should probably be extracted to a function. + */ + const inLoop = condition.isInLoop(modifier) || Boolean( + (funcNode = getEncloseFunctionDeclaration(modifier)) && + (funcVar = astUtils.getVariableByName(modifier.from.upper, funcNode.id.name)) && + funcVar.references.some(condition.isInLoop) + ); + + condition.modified = inLoop; + } + } +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow unmodified loop conditions", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + let groupMap = null; + + /** + * Reports a given condition info. + * + * @param {LoopConditionInfo} condition - A loop condition info to report. + * @returns {void} + */ + function report(condition) { + const node = condition.reference.identifier; + + context.report({ + node, + message: "'{{name}}' is not modified in this loop.", + data: node + }); + } + + /** + * Registers given conditions to the group the condition belongs to. + * + * @param {LoopConditionInfo[]} conditions - A loop condition info to + * register. + * @returns {void} + */ + function registerConditionsToGroup(conditions) { + for (let i = 0; i < conditions.length; ++i) { + const condition = conditions[i]; + + if (condition.group) { + let group = groupMap.get(condition.group); + + if (!group) { + group = []; + groupMap.set(condition.group, group); + } + group.push(condition); + } + } + } + + /** + * Reports references which are inside of unmodified groups. + * + * @param {LoopConditionInfo[]} conditions - A loop condition info to report. + * @returns {void} + */ + function checkConditionsInGroup(conditions) { + if (conditions.every(isUnmodified)) { + conditions.forEach(report); + } + } + + /** + * Finds unmodified references which are inside of a loop condition. + * Then reports the references which are outside of groups. + * + * @param {eslint-scope.Variable} variable - A variable to report. + * @returns {void} + */ + function checkReferences(variable) { + + // Gets references that exist in loop conditions. + const conditions = variable + .references + .map(toLoopCondition) + .filter(Boolean); + + if (conditions.length === 0) { + return; + } + + // Registers the conditions to belonging groups. + registerConditionsToGroup(conditions); + + // Check the conditions are modified. + const modifiers = variable.references.filter(isWriteReference); + + if (modifiers.length > 0) { + updateModifiedFlag(conditions, modifiers); + } + + /* + * Reports the conditions which are not belonging to groups. + * Others will be reported after all variables are done. + */ + conditions + .filter(isUnmodifiedAndNotBelongToGroup) + .forEach(report); + } + + return { + "Program:exit"() { + const queue = [context.getScope()]; + + groupMap = new Map(); + + let scope; + + while ((scope = queue.pop())) { + pushAll(queue, scope.childScopes); + scope.variables.forEach(checkReferences); + } + + groupMap.forEach(checkConditionsInGroup); + groupMap = null; + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-unneeded-ternary.js b/tools/node_modules/eslint/lib/rules/no-unneeded-ternary.js new file mode 100644 index 0000000000..5745537805 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-unneeded-ternary.js @@ -0,0 +1,155 @@ +/** + * @fileoverview Rule to flag no-unneeded-ternary + * @author Gyandeep Singh + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +// Operators that always result in a boolean value +const BOOLEAN_OPERATORS = new Set(["==", "===", "!=", "!==", ">", ">=", "<", "<=", "in", "instanceof"]); +const OPERATOR_INVERSES = { + "==": "!=", + "!=": "==", + "===": "!==", + "!==": "===" + + // Operators like < and >= are not true inverses, since both will return false with NaN. +}; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow ternary operators when simpler alternatives exist", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + defaultAssignment: { + type: "boolean" + } + }, + additionalProperties: false + } + ], + + fixable: "code" + }, + + create(context) { + const options = context.options[0] || {}; + const defaultAssignment = options.defaultAssignment !== false; + const sourceCode = context.getSourceCode(); + + /** + * Test if the node is a boolean literal + * @param {ASTNode} node - The node to report. + * @returns {boolean} True if the its a boolean literal + * @private + */ + function isBooleanLiteral(node) { + return node.type === "Literal" && typeof node.value === "boolean"; + } + + /** + * Creates an expression that represents the boolean inverse of the expression represented by the original node + * @param {ASTNode} node A node representing an expression + * @returns {string} A string representing an inverted expression + */ + function invertExpression(node) { + if (node.type === "BinaryExpression" && Object.prototype.hasOwnProperty.call(OPERATOR_INVERSES, node.operator)) { + const operatorToken = sourceCode.getFirstTokenBetween( + node.left, + node.right, + token => token.value === node.operator + ); + const text = sourceCode.getText(); + + return text.slice(node.range[0], + operatorToken.range[0]) + OPERATOR_INVERSES[node.operator] + text.slice(operatorToken.range[1], node.range[1]); + } + + if (astUtils.getPrecedence(node) < astUtils.getPrecedence({ type: "UnaryExpression" })) { + return `!(${astUtils.getParenthesisedText(sourceCode, node)})`; + } + return `!${astUtils.getParenthesisedText(sourceCode, node)}`; + } + + /** + * Tests if a given node always evaluates to a boolean value + * @param {ASTNode} node - An expression node + * @returns {boolean} True if it is determined that the node will always evaluate to a boolean value + */ + function isBooleanExpression(node) { + return node.type === "BinaryExpression" && BOOLEAN_OPERATORS.has(node.operator) || + node.type === "UnaryExpression" && node.operator === "!"; + } + + /** + * Test if the node matches the pattern id ? id : expression + * @param {ASTNode} node - The ConditionalExpression to check. + * @returns {boolean} True if the pattern is matched, and false otherwise + * @private + */ + function matchesDefaultAssignment(node) { + return node.test.type === "Identifier" && + node.consequent.type === "Identifier" && + node.test.name === node.consequent.name; + } + + return { + + ConditionalExpression(node) { + if (isBooleanLiteral(node.alternate) && isBooleanLiteral(node.consequent)) { + context.report({ + node, + loc: node.consequent.loc.start, + message: "Unnecessary use of boolean literals in conditional expression.", + fix(fixer) { + if (node.consequent.value === node.alternate.value) { + + // Replace `foo ? true : true` with just `true`, but don't replace `foo() ? true : true` + return node.test.type === "Identifier" ? fixer.replaceText(node, node.consequent.value.toString()) : null; + } + if (node.alternate.value) { + + // Replace `foo() ? false : true` with `!(foo())` + return fixer.replaceText(node, invertExpression(node.test)); + } + + // Replace `foo ? true : false` with `foo` if `foo` is guaranteed to be a boolean, or `!!foo` otherwise. + + return fixer.replaceText(node, isBooleanExpression(node.test) ? astUtils.getParenthesisedText(sourceCode, node.test) : `!${invertExpression(node.test)}`); + } + }); + } else if (!defaultAssignment && matchesDefaultAssignment(node)) { + context.report({ + node, + loc: node.consequent.loc.start, + message: "Unnecessary use of conditional expression for default assignment.", + fix: fixer => { + let nodeAlternate = astUtils.getParenthesisedText(sourceCode, node.alternate); + + if (node.alternate.type === "ConditionalExpression") { + const isAlternateParenthesised = astUtils.isParenthesised(sourceCode, node.alternate); + + nodeAlternate = isAlternateParenthesised ? nodeAlternate : `(${nodeAlternate})`; + } + + return fixer.replaceText(node, `${astUtils.getParenthesisedText(sourceCode, node.test)} || ${nodeAlternate}`); + } + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-unreachable.js b/tools/node_modules/eslint/lib/rules/no-unreachable.js new file mode 100644 index 0000000000..217a6a4299 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-unreachable.js @@ -0,0 +1,212 @@ +/** + * @fileoverview Checks for unreachable code due to return, throws, break, and continue. + * @author Joel Feenstra + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether or not a given variable declarator has the initializer. + * @param {ASTNode} node - A VariableDeclarator node to check. + * @returns {boolean} `true` if the node has the initializer. + */ +function isInitialized(node) { + return Boolean(node.init); +} + +/** + * Checks whether or not a given code path segment is unreachable. + * @param {CodePathSegment} segment - A CodePathSegment to check. + * @returns {boolean} `true` if the segment is unreachable. + */ +function isUnreachable(segment) { + return !segment.reachable; +} + +/** + * The class to distinguish consecutive unreachable statements. + */ +class ConsecutiveRange { + constructor(sourceCode) { + this.sourceCode = sourceCode; + this.startNode = null; + this.endNode = null; + } + + /** + * The location object of this range. + * @type {Object} + */ + get location() { + return { + start: this.startNode.loc.start, + end: this.endNode.loc.end + }; + } + + /** + * `true` if this range is empty. + * @type {boolean} + */ + get isEmpty() { + return !(this.startNode && this.endNode); + } + + /** + * Checks whether the given node is inside of this range. + * @param {ASTNode|Token} node - The node to check. + * @returns {boolean} `true` if the node is inside of this range. + */ + contains(node) { + return ( + node.range[0] >= this.startNode.range[0] && + node.range[1] <= this.endNode.range[1] + ); + } + + /** + * Checks whether the given node is consecutive to this range. + * @param {ASTNode} node - The node to check. + * @returns {boolean} `true` if the node is consecutive to this range. + */ + isConsecutive(node) { + return this.contains(this.sourceCode.getTokenBefore(node)); + } + + /** + * Merges the given node to this range. + * @param {ASTNode} node - The node to merge. + * @returns {void} + */ + merge(node) { + this.endNode = node; + } + + /** + * Resets this range by the given node or null. + * @param {ASTNode|null} node - The node to reset, or null. + * @returns {void} + */ + reset(node) { + this.startNode = this.endNode = node; + } +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow unreachable code after `return`, `throw`, `continue`, and `break` statements", + category: "Possible Errors", + recommended: true + }, + + schema: [] + }, + + create(context) { + let currentCodePath = null; + + const range = new ConsecutiveRange(context.getSourceCode()); + + /** + * Reports a given node if it's unreachable. + * @param {ASTNode} node - A statement node to report. + * @returns {void} + */ + function reportIfUnreachable(node) { + let nextNode = null; + + if (node && currentCodePath.currentSegments.every(isUnreachable)) { + + // Store this statement to distinguish consecutive statements. + if (range.isEmpty) { + range.reset(node); + return; + } + + // Skip if this statement is inside of the current range. + if (range.contains(node)) { + return; + } + + // Merge if this statement is consecutive to the current range. + if (range.isConsecutive(node)) { + range.merge(node); + return; + } + + nextNode = node; + } + + /* + * Report the current range since this statement is reachable or is + * not consecutive to the current range. + */ + if (!range.isEmpty) { + context.report({ + message: "Unreachable code.", + loc: range.location, + node: range.startNode + }); + } + + // Update the current range. + range.reset(nextNode); + } + + return { + + // Manages the current code path. + onCodePathStart(codePath) { + currentCodePath = codePath; + }, + + onCodePathEnd() { + currentCodePath = currentCodePath.upper; + }, + + // Registers for all statement nodes (excludes FunctionDeclaration). + BlockStatement: reportIfUnreachable, + BreakStatement: reportIfUnreachable, + ClassDeclaration: reportIfUnreachable, + ContinueStatement: reportIfUnreachable, + DebuggerStatement: reportIfUnreachable, + DoWhileStatement: reportIfUnreachable, + EmptyStatement: reportIfUnreachable, + ExpressionStatement: reportIfUnreachable, + ForInStatement: reportIfUnreachable, + ForOfStatement: reportIfUnreachable, + ForStatement: reportIfUnreachable, + IfStatement: reportIfUnreachable, + ImportDeclaration: reportIfUnreachable, + LabeledStatement: reportIfUnreachable, + ReturnStatement: reportIfUnreachable, + SwitchStatement: reportIfUnreachable, + ThrowStatement: reportIfUnreachable, + TryStatement: reportIfUnreachable, + + VariableDeclaration(node) { + if (node.kind !== "var" || node.declarations.some(isInitialized)) { + reportIfUnreachable(node); + } + }, + + WhileStatement: reportIfUnreachable, + WithStatement: reportIfUnreachable, + ExportNamedDeclaration: reportIfUnreachable, + ExportDefaultDeclaration: reportIfUnreachable, + ExportAllDeclaration: reportIfUnreachable, + + "Program:exit"() { + reportIfUnreachable(); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-unsafe-finally.js b/tools/node_modules/eslint/lib/rules/no-unsafe-finally.js new file mode 100644 index 0000000000..d25033e545 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-unsafe-finally.js @@ -0,0 +1,104 @@ +/** + * @fileoverview Rule to flag unsafe statements in finally block + * @author Onur Temizkan + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const SENTINEL_NODE_TYPE_RETURN_THROW = /^(?:Program|(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression)$/; +const SENTINEL_NODE_TYPE_BREAK = /^(?:Program|(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression|DoWhileStatement|WhileStatement|ForOfStatement|ForInStatement|ForStatement|SwitchStatement)$/; +const SENTINEL_NODE_TYPE_CONTINUE = /^(?:Program|(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression|DoWhileStatement|WhileStatement|ForOfStatement|ForInStatement|ForStatement)$/; + + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow control flow statements in `finally` blocks", + category: "Possible Errors", + recommended: true + }, + + schema: [] + }, + create(context) { + + /** + * Checks if the node is the finalizer of a TryStatement + * + * @param {ASTNode} node - node to check. + * @returns {boolean} - true if the node is the finalizer of a TryStatement + */ + function isFinallyBlock(node) { + return node.parent.type === "TryStatement" && node.parent.finalizer === node; + } + + /** + * Climbs up the tree if the node is not a sentinel node + * + * @param {ASTNode} node - node to check. + * @param {string} label - label of the break or continue statement + * @returns {boolean} - return whether the node is a finally block or a sentinel node + */ + function isInFinallyBlock(node, label) { + let labelInside = false; + let sentinelNodeType; + + if (node.type === "BreakStatement" && !node.label) { + sentinelNodeType = SENTINEL_NODE_TYPE_BREAK; + } else if (node.type === "ContinueStatement") { + sentinelNodeType = SENTINEL_NODE_TYPE_CONTINUE; + } else { + sentinelNodeType = SENTINEL_NODE_TYPE_RETURN_THROW; + } + + while (node && !sentinelNodeType.test(node.type)) { + if (node.parent.label && label && (node.parent.label.name === label.name)) { + labelInside = true; + } + if (isFinallyBlock(node)) { + if (label && labelInside) { + return false; + } + return true; + } + node = node.parent; + } + return false; + } + + /** + * Checks whether the possibly-unsafe statement is inside a finally block. + * + * @param {ASTNode} node - node to check. + * @returns {void} + */ + function check(node) { + if (isInFinallyBlock(node, node.label)) { + context.report({ + message: "Unsafe usage of {{nodeType}}.", + data: { + nodeType: node.type + }, + node, + line: node.loc.line, + column: node.loc.column + }); + } + } + + return { + ReturnStatement: check, + ThrowStatement: check, + BreakStatement: check, + ContinueStatement: check + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-unsafe-negation.js b/tools/node_modules/eslint/lib/rules/no-unsafe-negation.js new file mode 100644 index 0000000000..761dc03386 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-unsafe-negation.js @@ -0,0 +1,80 @@ +/** + * @fileoverview Rule to disallow negating the left operand of relational operators + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether the given operator is a relational operator or not. + * + * @param {string} op - The operator type to check. + * @returns {boolean} `true` if the operator is a relational operator. + */ +function isRelationalOperator(op) { + return op === "in" || op === "instanceof"; +} + +/** + * Checks whether the given node is a logical negation expression or not. + * + * @param {ASTNode} node - The node to check. + * @returns {boolean} `true` if the node is a logical negation expression. + */ +function isNegation(node) { + return node.type === "UnaryExpression" && node.operator === "!"; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow negating the left operand of relational operators", + category: "Possible Errors", + recommended: true + }, + schema: [], + fixable: "code" + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + return { + BinaryExpression(node) { + if (isRelationalOperator(node.operator) && + isNegation(node.left) && + !astUtils.isParenthesised(sourceCode, node.left) + ) { + context.report({ + node, + loc: node.left.loc, + message: "Unexpected negating the left operand of '{{operator}}' operator.", + data: node, + + fix(fixer) { + const negationToken = sourceCode.getFirstToken(node.left); + const fixRange = [negationToken.range[1], node.range[1]]; + const text = sourceCode.text.slice(fixRange[0], fixRange[1]); + + return fixer.replaceTextRange(fixRange, `(${text})`); + } + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-unused-expressions.js b/tools/node_modules/eslint/lib/rules/no-unused-expressions.js new file mode 100644 index 0000000000..b4e1074d54 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-unused-expressions.js @@ -0,0 +1,126 @@ +/** + * @fileoverview Flag expressions in statement position that do not side effect + * @author Michael Ficarra + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow unused expressions", + category: "Best Practices", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + allowShortCircuit: { + type: "boolean" + }, + allowTernary: { + type: "boolean" + }, + allowTaggedTemplates: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const config = context.options[0] || {}, + allowShortCircuit = config.allowShortCircuit || false, + allowTernary = config.allowTernary || false, + allowTaggedTemplates = config.allowTaggedTemplates || false; + + /** + * @param {ASTNode} node - any node + * @returns {boolean} whether the given node structurally represents a directive + */ + function looksLikeDirective(node) { + return node.type === "ExpressionStatement" && + node.expression.type === "Literal" && typeof node.expression.value === "string"; + } + + /** + * @param {Function} predicate - ([a] -> Boolean) the function used to make the determination + * @param {a[]} list - the input list + * @returns {a[]} the leading sequence of members in the given list that pass the given predicate + */ + function takeWhile(predicate, list) { + for (let i = 0; i < list.length; ++i) { + if (!predicate(list[i])) { + return list.slice(0, i); + } + } + return list.slice(); + } + + /** + * @param {ASTNode} node - a Program or BlockStatement node + * @returns {ASTNode[]} the leading sequence of directive nodes in the given node's body + */ + function directives(node) { + return takeWhile(looksLikeDirective, node.body); + } + + /** + * @param {ASTNode} node - any node + * @param {ASTNode[]} ancestors - the given node's ancestors + * @returns {boolean} whether the given node is considered a directive in its current position + */ + function isDirective(node, ancestors) { + const parent = ancestors[ancestors.length - 1], + grandparent = ancestors[ancestors.length - 2]; + + return (parent.type === "Program" || parent.type === "BlockStatement" && + (/Function/.test(grandparent.type))) && + directives(parent).indexOf(node) >= 0; + } + + /** + * Determines whether or not a given node is a valid expression. Recurses on short circuit eval and ternary nodes if enabled by flags. + * @param {ASTNode} node - any node + * @returns {boolean} whether the given node is a valid expression + */ + function isValidExpression(node) { + if (allowTernary) { + + // Recursive check for ternary and logical expressions + if (node.type === "ConditionalExpression") { + return isValidExpression(node.consequent) && isValidExpression(node.alternate); + } + } + + if (allowShortCircuit) { + if (node.type === "LogicalExpression") { + return isValidExpression(node.right); + } + } + + if (allowTaggedTemplates && node.type === "TaggedTemplateExpression") { + return true; + } + + return /^(?:Assignment|Call|New|Update|Yield|Await)Expression$/.test(node.type) || + (node.type === "UnaryExpression" && ["delete", "void"].indexOf(node.operator) >= 0); + } + + return { + ExpressionStatement(node) { + if (!isValidExpression(node.expression) && !isDirective(node, context.getAncestors())) { + context.report({ node, message: "Expected an assignment or function call and instead saw an expression." }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-unused-labels.js b/tools/node_modules/eslint/lib/rules/no-unused-labels.js new file mode 100644 index 0000000000..bcd3cfdc47 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-unused-labels.js @@ -0,0 +1,106 @@ +/** + * @fileoverview Rule to disallow unused labels. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow unused labels", + category: "Best Practices", + recommended: true + }, + + schema: [], + + fixable: "code" + }, + + create(context) { + const sourceCode = context.getSourceCode(); + let scopeInfo = null; + + /** + * Adds a scope info to the stack. + * + * @param {ASTNode} node - A node to add. This is a LabeledStatement. + * @returns {void} + */ + function enterLabeledScope(node) { + scopeInfo = { + label: node.label.name, + used: false, + upper: scopeInfo + }; + } + + /** + * Removes the top of the stack. + * At the same time, this reports the label if it's never used. + * + * @param {ASTNode} node - A node to report. This is a LabeledStatement. + * @returns {void} + */ + function exitLabeledScope(node) { + if (!scopeInfo.used) { + context.report({ + node: node.label, + message: "'{{name}}:' is defined but never used.", + data: node.label, + fix(fixer) { + + /* + * Only perform a fix if there are no comments between the label and the body. This will be the case + * when there is exactly one token/comment (the ":") between the label and the body. + */ + if (sourceCode.getTokenAfter(node.label, { includeComments: true }) === + sourceCode.getTokenBefore(node.body, { includeComments: true })) { + return fixer.removeRange([node.range[0], node.body.range[0]]); + } + + return null; + } + }); + } + + scopeInfo = scopeInfo.upper; + } + + /** + * Marks the label of a given node as used. + * + * @param {ASTNode} node - A node to mark. This is a BreakStatement or + * ContinueStatement. + * @returns {void} + */ + function markAsUsed(node) { + if (!node.label) { + return; + } + + const label = node.label.name; + let info = scopeInfo; + + while (info) { + if (info.label === label) { + info.used = true; + break; + } + info = info.upper; + } + } + + return { + LabeledStatement: enterLabeledScope, + "LabeledStatement:exit": exitLabeledScope, + BreakStatement: markAsUsed, + ContinueStatement: markAsUsed + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-unused-vars.js b/tools/node_modules/eslint/lib/rules/no-unused-vars.js new file mode 100644 index 0000000000..05940d5932 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-unused-vars.js @@ -0,0 +1,647 @@ +/** + * @fileoverview Rule to flag declared but unused variables + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const lodash = require("lodash"); +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow unused variables", + category: "Variables", + recommended: true + }, + + schema: [ + { + oneOf: [ + { + enum: ["all", "local"] + }, + { + type: "object", + properties: { + vars: { + enum: ["all", "local"] + }, + varsIgnorePattern: { + type: "string" + }, + args: { + enum: ["all", "after-used", "none"] + }, + ignoreRestSiblings: { + type: "boolean" + }, + argsIgnorePattern: { + type: "string" + }, + caughtErrors: { + enum: ["all", "none"] + }, + caughtErrorsIgnorePattern: { + type: "string" + } + } + } + ] + } + ] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + const REST_PROPERTY_TYPE = /^(?:Experimental)?RestProperty$/; + + const config = { + vars: "all", + args: "after-used", + ignoreRestSiblings: false, + caughtErrors: "none" + }; + + const firstOption = context.options[0]; + + if (firstOption) { + if (typeof firstOption === "string") { + config.vars = firstOption; + } else { + config.vars = firstOption.vars || config.vars; + config.args = firstOption.args || config.args; + config.ignoreRestSiblings = firstOption.ignoreRestSiblings || config.ignoreRestSiblings; + config.caughtErrors = firstOption.caughtErrors || config.caughtErrors; + + if (firstOption.varsIgnorePattern) { + config.varsIgnorePattern = new RegExp(firstOption.varsIgnorePattern); + } + + if (firstOption.argsIgnorePattern) { + config.argsIgnorePattern = new RegExp(firstOption.argsIgnorePattern); + } + + if (firstOption.caughtErrorsIgnorePattern) { + config.caughtErrorsIgnorePattern = new RegExp(firstOption.caughtErrorsIgnorePattern); + } + } + } + + /** + * Generate the warning message about the variable being + * defined and unused, including the ignore pattern if configured. + * @param {Variable} unusedVar - eslint-scope variable object. + * @returns {string} The warning message to be used with this unused variable. + */ + function getDefinedMessage(unusedVar) { + let type; + let pattern; + + if (config.varsIgnorePattern) { + type = "vars"; + pattern = config.varsIgnorePattern.toString(); + } + + if (unusedVar.defs && unusedVar.defs[0] && unusedVar.defs[0].type) { + const defType = unusedVar.defs[0].type; + + if (defType === "CatchClause" && config.caughtErrorsIgnorePattern) { + type = "args"; + pattern = config.caughtErrorsIgnorePattern.toString(); + } else if (defType === "Parameter" && config.argsIgnorePattern) { + type = "args"; + pattern = config.argsIgnorePattern.toString(); + } + } + + const additional = type ? ` Allowed unused ${type} must match ${pattern}.` : ""; + + return `'{{name}}' is defined but never used.${additional}`; + } + + /** + * Generate the warning message about the variable being + * assigned and unused, including the ignore pattern if configured. + * @returns {string} The warning message to be used with this unused variable. + */ + function getAssignedMessage() { + const additional = config.varsIgnorePattern ? ` Allowed unused vars must match ${config.varsIgnorePattern.toString()}.` : ""; + + return `'{{name}}' is assigned a value but never used.${additional}`; + } + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + const STATEMENT_TYPE = /(?:Statement|Declaration)$/; + + /** + * Determines if a given variable is being exported from a module. + * @param {Variable} variable - eslint-scope variable object. + * @returns {boolean} True if the variable is exported, false if not. + * @private + */ + function isExported(variable) { + + const definition = variable.defs[0]; + + if (definition) { + + let node = definition.node; + + if (node.type === "VariableDeclarator") { + node = node.parent; + } else if (definition.type === "Parameter") { + return false; + } + + return node.parent.type.indexOf("Export") === 0; + } + return false; + + } + + /** + * Determines if a variable has a sibling rest property + * @param {Variable} variable - eslint-scope variable object. + * @returns {boolean} True if the variable is exported, false if not. + * @private + */ + function hasRestSpreadSibling(variable) { + if (config.ignoreRestSiblings) { + return variable.defs.some(def => { + const propertyNode = def.name.parent; + const patternNode = propertyNode.parent; + + return ( + propertyNode.type === "Property" && + patternNode.type === "ObjectPattern" && + REST_PROPERTY_TYPE.test(patternNode.properties[patternNode.properties.length - 1].type) + ); + }); + } + + return false; + } + + /** + * Determines if a reference is a read operation. + * @param {Reference} ref - An eslint-scope Reference + * @returns {boolean} whether the given reference represents a read operation + * @private + */ + function isReadRef(ref) { + return ref.isRead(); + } + + /** + * Determine if an identifier is referencing an enclosing function name. + * @param {Reference} ref - The reference to check. + * @param {ASTNode[]} nodes - The candidate function nodes. + * @returns {boolean} True if it's a self-reference, false if not. + * @private + */ + function isSelfReference(ref, nodes) { + let scope = ref.from; + + while (scope) { + if (nodes.indexOf(scope.block) >= 0) { + return true; + } + + scope = scope.upper; + } + + return false; + } + + /** + * Checks the position of given nodes. + * + * @param {ASTNode} inner - A node which is expected as inside. + * @param {ASTNode} outer - A node which is expected as outside. + * @returns {boolean} `true` if the `inner` node exists in the `outer` node. + * @private + */ + function isInside(inner, outer) { + return ( + inner.range[0] >= outer.range[0] && + inner.range[1] <= outer.range[1] + ); + } + + /** + * If a given reference is left-hand side of an assignment, this gets + * the right-hand side node of the assignment. + * + * In the following cases, this returns null. + * + * - The reference is not the LHS of an assignment expression. + * - The reference is inside of a loop. + * - The reference is inside of a function scope which is different from + * the declaration. + * + * @param {eslint-scope.Reference} ref - A reference to check. + * @param {ASTNode} prevRhsNode - The previous RHS node. + * @returns {ASTNode|null} The RHS node or null. + * @private + */ + function getRhsNode(ref, prevRhsNode) { + const id = ref.identifier; + const parent = id.parent; + const granpa = parent.parent; + const refScope = ref.from.variableScope; + const varScope = ref.resolved.scope.variableScope; + const canBeUsedLater = refScope !== varScope || astUtils.isInLoop(id); + + /* + * Inherits the previous node if this reference is in the node. + * This is for `a = a + a`-like code. + */ + if (prevRhsNode && isInside(id, prevRhsNode)) { + return prevRhsNode; + } + + if (parent.type === "AssignmentExpression" && + granpa.type === "ExpressionStatement" && + id === parent.left && + !canBeUsedLater + ) { + return parent.right; + } + return null; + } + + /** + * Checks whether a given function node is stored to somewhere or not. + * If the function node is stored, the function can be used later. + * + * @param {ASTNode} funcNode - A function node to check. + * @param {ASTNode} rhsNode - The RHS node of the previous assignment. + * @returns {boolean} `true` if under the following conditions: + * - the funcNode is assigned to a variable. + * - the funcNode is bound as an argument of a function call. + * - the function is bound to a property and the object satisfies above conditions. + * @private + */ + function isStorableFunction(funcNode, rhsNode) { + let node = funcNode; + let parent = funcNode.parent; + + while (parent && isInside(parent, rhsNode)) { + switch (parent.type) { + case "SequenceExpression": + if (parent.expressions[parent.expressions.length - 1] !== node) { + return false; + } + break; + + case "CallExpression": + case "NewExpression": + return parent.callee !== node; + + case "AssignmentExpression": + case "TaggedTemplateExpression": + case "YieldExpression": + return true; + + default: + if (STATEMENT_TYPE.test(parent.type)) { + + /* + * If it encountered statements, this is a complex pattern. + * Since analyzeing complex patterns is hard, this returns `true` to avoid false positive. + */ + return true; + } + } + + node = parent; + parent = parent.parent; + } + + return false; + } + + /** + * Checks whether a given Identifier node exists inside of a function node which can be used later. + * + * "can be used later" means: + * - the function is assigned to a variable. + * - the function is bound to a property and the object can be used later. + * - the function is bound as an argument of a function call. + * + * If a reference exists in a function which can be used later, the reference is read when the function is called. + * + * @param {ASTNode} id - An Identifier node to check. + * @param {ASTNode} rhsNode - The RHS node of the previous assignment. + * @returns {boolean} `true` if the `id` node exists inside of a function node which can be used later. + * @private + */ + function isInsideOfStorableFunction(id, rhsNode) { + const funcNode = astUtils.getUpperFunction(id); + + return ( + funcNode && + isInside(funcNode, rhsNode) && + isStorableFunction(funcNode, rhsNode) + ); + } + + /** + * Checks whether a given reference is a read to update itself or not. + * + * @param {eslint-scope.Reference} ref - A reference to check. + * @param {ASTNode} rhsNode - The RHS node of the previous assignment. + * @returns {boolean} The reference is a read to update itself. + * @private + */ + function isReadForItself(ref, rhsNode) { + const id = ref.identifier; + const parent = id.parent; + const granpa = parent.parent; + + return ref.isRead() && ( + + // self update. e.g. `a += 1`, `a++` + ( + parent.type === "AssignmentExpression" && + granpa.type === "ExpressionStatement" && + parent.left === id + ) || + ( + parent.type === "UpdateExpression" && + granpa.type === "ExpressionStatement" + ) || + + // in RHS of an assignment for itself. e.g. `a = a + 1` + ( + rhsNode && + isInside(id, rhsNode) && + !isInsideOfStorableFunction(id, rhsNode) + ) + ); + } + + /** + * Determine if an identifier is used either in for-in loops. + * + * @param {Reference} ref - The reference to check. + * @returns {boolean} whether reference is used in the for-in loops + * @private + */ + function isForInRef(ref) { + let target = ref.identifier.parent; + + + // "for (var ...) { return; }" + if (target.type === "VariableDeclarator") { + target = target.parent.parent; + } + + if (target.type !== "ForInStatement") { + return false; + } + + // "for (...) { return; }" + if (target.body.type === "BlockStatement") { + target = target.body.body[0]; + + // "for (...) return;" + } else { + target = target.body; + } + + // For empty loop body + if (!target) { + return false; + } + + return target.type === "ReturnStatement"; + } + + /** + * Determines if the variable is used. + * @param {Variable} variable - The variable to check. + * @returns {boolean} True if the variable is used + * @private + */ + function isUsedVariable(variable) { + const functionNodes = variable.defs.filter(def => def.type === "FunctionName").map(def => def.node), + isFunctionDefinition = functionNodes.length > 0; + let rhsNode = null; + + return variable.references.some(ref => { + if (isForInRef(ref)) { + return true; + } + + const forItself = isReadForItself(ref, rhsNode); + + rhsNode = getRhsNode(ref, rhsNode); + + return ( + isReadRef(ref) && + !forItself && + !(isFunctionDefinition && isSelfReference(ref, functionNodes)) + ); + }); + } + + /** + * Checks whether the given variable is the last parameter in the non-ignored parameters. + * + * @param {eslint-scope.Variable} variable - The variable to check. + * @returns {boolean} `true` if the variable is the last. + */ + function isLastInNonIgnoredParameters(variable) { + const def = variable.defs[0]; + + // This is the last. + if (def.index === def.node.params.length - 1) { + return true; + } + + // if all parameters preceded by this variable are ignored and unused, this is the last. + if (config.argsIgnorePattern) { + const params = context.getDeclaredVariables(def.node); + const posteriorParams = params.slice(params.indexOf(variable) + 1); + + if (posteriorParams.every(v => v.references.length === 0 && config.argsIgnorePattern.test(v.name))) { + return true; + } + } + + return false; + } + + /** + * Gets an array of variables without read references. + * @param {Scope} scope - an eslint-scope Scope object. + * @param {Variable[]} unusedVars - an array that saving result. + * @returns {Variable[]} unused variables of the scope and descendant scopes. + * @private + */ + function collectUnusedVariables(scope, unusedVars) { + const variables = scope.variables; + const childScopes = scope.childScopes; + let i, l; + + if (scope.type !== "TDZ" && (scope.type !== "global" || config.vars === "all")) { + for (i = 0, l = variables.length; i < l; ++i) { + const variable = variables[i]; + + // skip a variable of class itself name in the class scope + if (scope.type === "class" && scope.block.id === variable.identifiers[0]) { + continue; + } + + // skip function expression names and variables marked with markVariableAsUsed() + if (scope.functionExpressionScope || variable.eslintUsed) { + continue; + } + + // skip implicit "arguments" variable + if (scope.type === "function" && variable.name === "arguments" && variable.identifiers.length === 0) { + continue; + } + + // explicit global variables don't have definitions. + const def = variable.defs[0]; + + if (def) { + const type = def.type; + + // skip catch variables + if (type === "CatchClause") { + if (config.caughtErrors === "none") { + continue; + } + + // skip ignored parameters + if (config.caughtErrorsIgnorePattern && config.caughtErrorsIgnorePattern.test(def.name.name)) { + continue; + } + } + + if (type === "Parameter") { + + // skip any setter argument + if ((def.node.parent.type === "Property" || def.node.parent.type === "MethodDefinition") && def.node.parent.kind === "set") { + continue; + } + + // if "args" option is "none", skip any parameter + if (config.args === "none") { + continue; + } + + // skip ignored parameters + if (config.argsIgnorePattern && config.argsIgnorePattern.test(def.name.name)) { + continue; + } + + // if "args" option is "after-used", skip all but the last parameter + if (config.args === "after-used" && astUtils.isFunction(def.name.parent) && !isLastInNonIgnoredParameters(variable)) { + continue; + } + } else { + + // skip ignored variables + if (config.varsIgnorePattern && config.varsIgnorePattern.test(def.name.name)) { + continue; + } + } + } + + if (!isUsedVariable(variable) && !isExported(variable) && !hasRestSpreadSibling(variable)) { + unusedVars.push(variable); + } + } + } + + for (i = 0, l = childScopes.length; i < l; ++i) { + collectUnusedVariables(childScopes[i], unusedVars); + } + + return unusedVars; + } + + /** + * Gets the index of a given variable name in a given comment. + * @param {eslint-scope.Variable} variable - A variable to get. + * @param {ASTNode} comment - A comment node which includes the variable name. + * @returns {number} The index of the variable name's location. + * @private + */ + function getColumnInComment(variable, comment) { + const namePattern = new RegExp(`[\\s,]${lodash.escapeRegExp(variable.name)}(?:$|[\\s,:])`, "g"); + + // To ignore the first text "global". + namePattern.lastIndex = comment.value.indexOf("global") + 6; + + // Search a given variable name. + const match = namePattern.exec(comment.value); + + return match ? match.index + 1 : 0; + } + + /** + * Creates the correct location of a given variables. + * The location is at its name string in a `/*global` comment. + * + * @param {eslint-scope.Variable} variable - A variable to get its location. + * @returns {{line: number, column: number}} The location object for the variable. + * @private + */ + function getLocation(variable) { + const comment = variable.eslintExplicitGlobalComment; + + return sourceCode.getLocFromIndex(comment.range[0] + 2 + getColumnInComment(variable, comment)); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + "Program:exit"(programNode) { + const unusedVars = collectUnusedVariables(context.getScope(), []); + + for (let i = 0, l = unusedVars.length; i < l; ++i) { + const unusedVar = unusedVars[i]; + + if (unusedVar.eslintExplicitGlobal) { + context.report({ + node: programNode, + loc: getLocation(unusedVar), + message: getDefinedMessage(unusedVar), + data: unusedVar + }); + } else if (unusedVar.defs.length > 0) { + context.report({ + node: unusedVar.identifiers[0], + message: unusedVar.references.some(ref => ref.isWrite()) + ? getAssignedMessage() + : getDefinedMessage(unusedVar), + data: unusedVar + }); + } + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-use-before-define.js b/tools/node_modules/eslint/lib/rules/no-use-before-define.js new file mode 100644 index 0000000000..ada01815d9 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-use-before-define.js @@ -0,0 +1,266 @@ +/** + * @fileoverview Rule to flag use of variables before they are defined + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const SENTINEL_TYPE = /^(?:(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression|CatchClause|ImportDeclaration|ExportNamedDeclaration)$/; +const FOR_IN_OF_TYPE = /^For(?:In|Of)Statement$/; + +/** + * Parses a given value as options. + * + * @param {any} options - A value to parse. + * @returns {Object} The parsed options. + */ +function parseOptions(options) { + let functions = true; + let classes = true; + let variables = true; + + if (typeof options === "string") { + functions = (options !== "nofunc"); + } else if (typeof options === "object" && options !== null) { + functions = options.functions !== false; + classes = options.classes !== false; + variables = options.variables !== false; + } + + return { functions, classes, variables }; +} + +/** + * Checks whether or not a given variable is a function declaration. + * + * @param {eslint-scope.Variable} variable - A variable to check. + * @returns {boolean} `true` if the variable is a function declaration. + */ +function isFunction(variable) { + return variable.defs[0].type === "FunctionName"; +} + +/** + * Checks whether or not a given variable is a class declaration in an upper function scope. + * + * @param {eslint-scope.Variable} variable - A variable to check. + * @param {eslint-scope.Reference} reference - A reference to check. + * @returns {boolean} `true` if the variable is a class declaration. + */ +function isOuterClass(variable, reference) { + return ( + variable.defs[0].type === "ClassName" && + variable.scope.variableScope !== reference.from.variableScope + ); +} + +/** + * Checks whether or not a given variable is a variable declaration in an upper function scope. + * @param {eslint-scope.Variable} variable - A variable to check. + * @param {eslint-scope.Reference} reference - A reference to check. + * @returns {boolean} `true` if the variable is a variable declaration. + */ +function isOuterVariable(variable, reference) { + return ( + variable.defs[0].type === "Variable" && + variable.scope.variableScope !== reference.from.variableScope + ); +} + +/** + * Checks whether or not a given location is inside of the range of a given node. + * + * @param {ASTNode} node - An node to check. + * @param {number} location - A location to check. + * @returns {boolean} `true` if the location is inside of the range of the node. + */ +function isInRange(node, location) { + return node && node.range[0] <= location && location <= node.range[1]; +} + +/** + * Checks whether or not a given reference is inside of the initializers of a given variable. + * + * This returns `true` in the following cases: + * + * var a = a + * var [a = a] = list + * var {a = a} = obj + * for (var a in a) {} + * for (var a of a) {} + * + * @param {Variable} variable - A variable to check. + * @param {Reference} reference - A reference to check. + * @returns {boolean} `true` if the reference is inside of the initializers. + */ +function isInInitializer(variable, reference) { + if (variable.scope !== reference.from) { + return false; + } + + let node = variable.identifiers[0].parent; + const location = reference.identifier.range[1]; + + while (node) { + if (node.type === "VariableDeclarator") { + if (isInRange(node.init, location)) { + return true; + } + if (FOR_IN_OF_TYPE.test(node.parent.parent.type) && + isInRange(node.parent.parent.right, location) + ) { + return true; + } + break; + } else if (node.type === "AssignmentPattern") { + if (isInRange(node.right, location)) { + return true; + } + } else if (SENTINEL_TYPE.test(node.type)) { + break; + } + + node = node.parent; + } + + return false; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow the use of variables before they are defined", + category: "Variables", + recommended: false + }, + + schema: [ + { + oneOf: [ + { + enum: ["nofunc"] + }, + { + type: "object", + properties: { + functions: { type: "boolean" }, + classes: { type: "boolean" }, + variables: { type: "boolean" } + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + const options = parseOptions(context.options[0]); + + /** + * Determines whether a given use-before-define case should be reported according to the options. + * @param {eslint-scope.Variable} variable The variable that gets used before being defined + * @param {eslint-scope.Reference} reference The reference to the variable + * @returns {boolean} `true` if the usage should be reported + */ + function isForbidden(variable, reference) { + if (isFunction(variable)) { + return options.functions; + } + if (isOuterClass(variable, reference)) { + return options.classes; + } + if (isOuterVariable(variable, reference)) { + return options.variables; + } + return true; + } + + /** + * Finds and validates all variables in a given scope. + * @param {Scope} scope The scope object. + * @returns {void} + * @private + */ + function findVariablesInScope(scope) { + scope.references.forEach(reference => { + const variable = reference.resolved; + + /* + * Skips when the reference is: + * - initialization's. + * - referring to an undefined variable. + * - referring to a global environment variable (there're no identifiers). + * - located preceded by the variable (except in initializers). + * - allowed by options. + */ + if (reference.init || + !variable || + variable.identifiers.length === 0 || + (variable.identifiers[0].range[1] < reference.identifier.range[1] && !isInInitializer(variable, reference)) || + !isForbidden(variable, reference) + ) { + return; + } + + // Reports. + context.report({ + node: reference.identifier, + message: "'{{name}}' was used before it was defined.", + data: reference.identifier + }); + }); + } + + /** + * Validates variables inside of a node's scope. + * @param {ASTNode} node The node to check. + * @returns {void} + * @private + */ + function findVariables() { + const scope = context.getScope(); + + findVariablesInScope(scope); + } + + const ruleDefinition = { + "Program:exit"(node) { + const scope = context.getScope(), + ecmaFeatures = context.parserOptions.ecmaFeatures || {}; + + findVariablesInScope(scope); + + // both Node.js and Modules have an extra scope + if (ecmaFeatures.globalReturn || node.sourceType === "module") { + findVariablesInScope(scope.childScopes[0]); + } + } + }; + + if (context.parserOptions.ecmaVersion >= 6) { + ruleDefinition["BlockStatement:exit"] = + ruleDefinition["SwitchStatement:exit"] = findVariables; + + ruleDefinition["ArrowFunctionExpression:exit"] = function(node) { + if (node.body.type !== "BlockStatement") { + findVariables(); + } + }; + } else { + ruleDefinition["FunctionExpression:exit"] = + ruleDefinition["FunctionDeclaration:exit"] = + ruleDefinition["ArrowFunctionExpression:exit"] = findVariables; + } + + return ruleDefinition; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-useless-call.js b/tools/node_modules/eslint/lib/rules/no-useless-call.js new file mode 100644 index 0000000000..e4820ac248 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-useless-call.js @@ -0,0 +1,80 @@ +/** + * @fileoverview A rule to disallow unnecessary `.call()` and `.apply()`. + * @author Toru Nagashima + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether or not a node is a `.call()`/`.apply()`. + * @param {ASTNode} node - A CallExpression node to check. + * @returns {boolean} Whether or not the node is a `.call()`/`.apply()`. + */ +function isCallOrNonVariadicApply(node) { + return ( + node.callee.type === "MemberExpression" && + node.callee.property.type === "Identifier" && + node.callee.computed === false && + ( + (node.callee.property.name === "call" && node.arguments.length >= 1) || + (node.callee.property.name === "apply" && node.arguments.length === 2 && node.arguments[1].type === "ArrayExpression") + ) + ); +} + + +/** + * Checks whether or not `thisArg` is not changed by `.call()`/`.apply()`. + * @param {ASTNode|null} expectedThis - The node that is the owner of the applied function. + * @param {ASTNode} thisArg - The node that is given to the first argument of the `.call()`/`.apply()`. + * @param {SourceCode} sourceCode - The ESLint source code object. + * @returns {boolean} Whether or not `thisArg` is not changed by `.call()`/`.apply()`. + */ +function isValidThisArg(expectedThis, thisArg, sourceCode) { + if (!expectedThis) { + return astUtils.isNullOrUndefined(thisArg); + } + return astUtils.equalTokens(expectedThis, thisArg, sourceCode); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow unnecessary calls to `.call()` and `.apply()`", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + return { + CallExpression(node) { + if (!isCallOrNonVariadicApply(node)) { + return; + } + + const applied = node.callee.object; + const expectedThis = (applied.type === "MemberExpression") ? applied.object : null; + const thisArg = node.arguments[0]; + + if (isValidThisArg(expectedThis, thisArg, sourceCode)) { + context.report({ node, message: "unnecessary '.{{name}}()'.", data: { name: node.callee.property.name } }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-useless-computed-key.js b/tools/node_modules/eslint/lib/rules/no-useless-computed-key.js new file mode 100644 index 0000000000..f8114ab754 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-useless-computed-key.js @@ -0,0 +1,75 @@ +/** + * @fileoverview Rule to disallow unnecessary computed property keys in object literals + * @author Burak Yigit Kaya + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +const MESSAGE_UNNECESSARY_COMPUTED = "Unnecessarily computed property [{{property}}] found."; + +module.exports = { + meta: { + docs: { + description: "disallow unnecessary computed property keys in object literals", + category: "ECMAScript 6", + recommended: false + }, + + schema: [], + + fixable: "code" + }, + create(context) { + const sourceCode = context.getSourceCode(); + + return { + Property(node) { + if (!node.computed) { + return; + } + + const key = node.key, + nodeType = typeof key.value; + + if (key.type === "Literal" && (nodeType === "string" || nodeType === "number") && key.value !== "__proto__") { + context.report({ + node, + message: MESSAGE_UNNECESSARY_COMPUTED, + data: { property: sourceCode.getText(key) }, + fix(fixer) { + const leftSquareBracket = sourceCode.getFirstToken(node, astUtils.isOpeningBracketToken); + const rightSquareBracket = sourceCode.getFirstTokenBetween(node.key, node.value, astUtils.isClosingBracketToken); + const tokensBetween = sourceCode.getTokensBetween(leftSquareBracket, rightSquareBracket, 1); + + if (tokensBetween.slice(0, -1).some((token, index) => + sourceCode.getText().slice(token.range[1], tokensBetween[index + 1].range[0]).trim())) { + + // If there are comments between the brackets and the property name, don't do a fix. + return null; + } + + const tokenBeforeLeftBracket = sourceCode.getTokenBefore(leftSquareBracket); + + // Insert a space before the key to avoid changing identifiers, e.g. ({ get[2]() {} }) to ({ get2() {} }) + const needsSpaceBeforeKey = tokenBeforeLeftBracket.range[1] === leftSquareBracket.range[0] && + !astUtils.canTokensBeAdjacent(tokenBeforeLeftBracket, sourceCode.getFirstToken(key)); + + const replacementKey = (needsSpaceBeforeKey ? " " : "") + key.raw; + + return fixer.replaceTextRange([leftSquareBracket.range[0], rightSquareBracket.range[1]], replacementKey); + } + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-useless-concat.js b/tools/node_modules/eslint/lib/rules/no-useless-concat.js new file mode 100644 index 0000000000..e42781fee7 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-useless-concat.js @@ -0,0 +1,108 @@ +/** + * @fileoverview disallow unncessary concatenation of template strings + * @author Henry Zhu + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether or not a given node is a concatenation. + * @param {ASTNode} node - A node to check. + * @returns {boolean} `true` if the node is a concatenation. + */ +function isConcatenation(node) { + return node.type === "BinaryExpression" && node.operator === "+"; +} + +/** + * Checks if the given token is a `+` token or not. + * @param {Token} token - The token to check. + * @returns {boolean} `true` if the token is a `+` token. + */ +function isConcatOperatorToken(token) { + return token.value === "+" && token.type === "Punctuator"; +} + +/** + * Get's the right most node on the left side of a BinaryExpression with + operator. + * @param {ASTNode} node - A BinaryExpression node to check. + * @returns {ASTNode} node + */ +function getLeft(node) { + let left = node.left; + + while (isConcatenation(left)) { + left = left.right; + } + return left; +} + +/** + * Get's the left most node on the right side of a BinaryExpression with + operator. + * @param {ASTNode} node - A BinaryExpression node to check. + * @returns {ASTNode} node + */ +function getRight(node) { + let right = node.right; + + while (isConcatenation(right)) { + right = right.left; + } + return right; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow unnecessary concatenation of literals or template literals", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + return { + BinaryExpression(node) { + + // check if not concatenation + if (node.operator !== "+") { + return; + } + + // account for the `foo + "a" + "b"` case + const left = getLeft(node); + const right = getRight(node); + + if (astUtils.isStringLiteral(left) && + astUtils.isStringLiteral(right) && + astUtils.isTokenOnSameLine(left, right) + ) { + const operatorToken = sourceCode.getFirstTokenBetween(left, right, isConcatOperatorToken); + + context.report({ + node, + loc: operatorToken.loc.start, + message: "Unexpected string concatenation of literals." + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-useless-constructor.js b/tools/node_modules/eslint/lib/rules/no-useless-constructor.js new file mode 100644 index 0000000000..f790c789f5 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-useless-constructor.js @@ -0,0 +1,182 @@ +/** + * @fileoverview Rule to flag the use of redundant constructors in classes. + * @author Alberto RodrÃguez + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether a given array of statements is a single call of `super`. + * + * @param {ASTNode[]} body - An array of statements to check. + * @returns {boolean} `true` if the body is a single call of `super`. + */ +function isSingleSuperCall(body) { + return ( + body.length === 1 && + body[0].type === "ExpressionStatement" && + body[0].expression.type === "CallExpression" && + body[0].expression.callee.type === "Super" + ); +} + +/** + * Checks whether a given node is a pattern which doesn't have any side effects. + * Default parameters and Destructuring parameters can have side effects. + * + * @param {ASTNode} node - A pattern node. + * @returns {boolean} `true` if the node doesn't have any side effects. + */ +function isSimple(node) { + return node.type === "Identifier" || node.type === "RestElement"; +} + +/** + * Checks whether a given array of expressions is `...arguments` or not. + * `super(...arguments)` passes all arguments through. + * + * @param {ASTNode[]} superArgs - An array of expressions to check. + * @returns {boolean} `true` if the superArgs is `...arguments`. + */ +function isSpreadArguments(superArgs) { + return ( + superArgs.length === 1 && + superArgs[0].type === "SpreadElement" && + superArgs[0].argument.type === "Identifier" && + superArgs[0].argument.name === "arguments" + ); +} + +/** + * Checks whether given 2 nodes are identifiers which have the same name or not. + * + * @param {ASTNode} ctorParam - A node to check. + * @param {ASTNode} superArg - A node to check. + * @returns {boolean} `true` if the nodes are identifiers which have the same + * name. + */ +function isValidIdentifierPair(ctorParam, superArg) { + return ( + ctorParam.type === "Identifier" && + superArg.type === "Identifier" && + ctorParam.name === superArg.name + ); +} + +/** + * Checks whether given 2 nodes are a rest/spread pair which has the same values. + * + * @param {ASTNode} ctorParam - A node to check. + * @param {ASTNode} superArg - A node to check. + * @returns {boolean} `true` if the nodes are a rest/spread pair which has the + * same values. + */ +function isValidRestSpreadPair(ctorParam, superArg) { + return ( + ctorParam.type === "RestElement" && + superArg.type === "SpreadElement" && + isValidIdentifierPair(ctorParam.argument, superArg.argument) + ); +} + +/** + * Checks whether given 2 nodes have the same value or not. + * + * @param {ASTNode} ctorParam - A node to check. + * @param {ASTNode} superArg - A node to check. + * @returns {boolean} `true` if the nodes have the same value or not. + */ +function isValidPair(ctorParam, superArg) { + return ( + isValidIdentifierPair(ctorParam, superArg) || + isValidRestSpreadPair(ctorParam, superArg) + ); +} + +/** + * Checks whether the parameters of a constructor and the arguments of `super()` + * have the same values or not. + * + * @param {ASTNode} ctorParams - The parameters of a constructor to check. + * @param {ASTNode} superArgs - The arguments of `super()` to check. + * @returns {boolean} `true` if those have the same values. + */ +function isPassingThrough(ctorParams, superArgs) { + if (ctorParams.length !== superArgs.length) { + return false; + } + + for (let i = 0; i < ctorParams.length; ++i) { + if (!isValidPair(ctorParams[i], superArgs[i])) { + return false; + } + } + + return true; +} + +/** + * Checks whether the constructor body is a redundant super call. + * + * @param {Array} body - constructor body content. + * @param {Array} ctorParams - The params to check against super call. + * @returns {boolean} true if the construtor body is redundant + */ +function isRedundantSuperCall(body, ctorParams) { + return ( + isSingleSuperCall(body) && + ctorParams.every(isSimple) && + ( + isSpreadArguments(body[0].expression.arguments) || + isPassingThrough(ctorParams, body[0].expression.arguments) + ) + ); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow unnecessary constructors", + category: "ECMAScript 6", + recommended: false + }, + + schema: [] + }, + + create(context) { + + /** + * Checks whether a node is a redundant constructor + * @param {ASTNode} node - node to check + * @returns {void} + */ + function checkForConstructor(node) { + if (node.kind !== "constructor") { + return; + } + + const body = node.value.body.body; + const ctorParams = node.value.params; + const superClass = node.parent.parent.superClass; + + if (superClass ? isRedundantSuperCall(body, ctorParams) : (body.length === 0)) { + context.report({ + node, + message: "Useless constructor." + }); + } + } + + return { + MethodDefinition: checkForConstructor + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-useless-escape.js b/tools/node_modules/eslint/lib/rules/no-useless-escape.js new file mode 100644 index 0000000000..cdc3e98df8 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-useless-escape.js @@ -0,0 +1,223 @@ +/** + * @fileoverview Look for useless escapes in strings and regexes + * @author Onur Temizkan + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +/** + * Returns the union of two sets. + * @param {Set} setA The first set + * @param {Set} setB The second set + * @returns {Set} The union of the two sets + */ +function union(setA, setB) { + return new Set(function *() { + yield* setA; + yield* setB; + }()); +} + +const VALID_STRING_ESCAPES = union(new Set("\\nrvtbfux"), astUtils.LINEBREAKS); +const REGEX_GENERAL_ESCAPES = new Set("\\bcdDfnrsStvwWxu0123456789]"); +const REGEX_NON_CHARCLASS_ESCAPES = union(REGEX_GENERAL_ESCAPES, new Set("^/.$*+?[{}|()B")); + +/** + * Parses a regular expression into a list of characters with character class info. + * @param {string} regExpText The raw text used to create the regular expression + * @returns {Object[]} A list of characters, each with info on escaping and whether they're in a character class. + * @example + * + * parseRegExp('a\\b[cd-]') + * + * returns: + * [ + * {text: 'a', index: 0, escaped: false, inCharClass: false, startsCharClass: false, endsCharClass: false}, + * {text: 'b', index: 2, escaped: true, inCharClass: false, startsCharClass: false, endsCharClass: false}, + * {text: 'c', index: 4, escaped: false, inCharClass: true, startsCharClass: true, endsCharClass: false}, + * {text: 'd', index: 5, escaped: false, inCharClass: true, startsCharClass: false, endsCharClass: false}, + * {text: '-', index: 6, escaped: false, inCharClass: true, startsCharClass: false, endsCharClass: false} + * ] + */ +function parseRegExp(regExpText) { + const charList = []; + + regExpText.split("").reduce((state, char, index) => { + if (!state.escapeNextChar) { + if (char === "\\") { + return Object.assign(state, { escapeNextChar: true }); + } + if (char === "[" && !state.inCharClass) { + return Object.assign(state, { inCharClass: true, startingCharClass: true }); + } + if (char === "]" && state.inCharClass) { + if (charList.length && charList[charList.length - 1].inCharClass) { + charList[charList.length - 1].endsCharClass = true; + } + return Object.assign(state, { inCharClass: false, startingCharClass: false }); + } + } + charList.push({ + text: char, + index, + escaped: state.escapeNextChar, + inCharClass: state.inCharClass, + startsCharClass: state.startingCharClass, + endsCharClass: false + }); + return Object.assign(state, { escapeNextChar: false, startingCharClass: false }); + }, { escapeNextChar: false, inCharClass: false, startingCharClass: false }); + + return charList; +} + +module.exports = { + meta: { + docs: { + description: "disallow unnecessary escape characters", + category: "Best Practices", + recommended: true + }, + + schema: [] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + /** + * Reports a node + * @param {ASTNode} node The node to report + * @param {number} startOffset The backslash's offset from the start of the node + * @param {string} character The uselessly escaped character (not including the backslash) + * @returns {void} + */ + function report(node, startOffset, character) { + context.report({ + node, + loc: sourceCode.getLocFromIndex(sourceCode.getIndexFromLoc(node.loc.start) + startOffset), + message: "Unnecessary escape character: \\{{character}}.", + data: { character } + }); + } + + /** + * Checks if the escape character in given string slice is unnecessary. + * + * @private + * @param {ASTNode} node - node to validate. + * @param {string} match - string slice to validate. + * @returns {void} + */ + function validateString(node, match) { + const isTemplateElement = node.type === "TemplateElement"; + const escapedChar = match[0][1]; + let isUnnecessaryEscape = !VALID_STRING_ESCAPES.has(escapedChar); + let isQuoteEscape; + + if (isTemplateElement) { + isQuoteEscape = escapedChar === "`"; + + if (escapedChar === "$") { + + // Warn if `\$` is not followed by `{` + isUnnecessaryEscape = match.input[match.index + 2] !== "{"; + } else if (escapedChar === "{") { + + /* + * Warn if `\{` is not preceded by `$`. If preceded by `$`, escaping + * is necessary and the rule should not warn. If preceded by `/$`, the rule + * will warn for the `/$` instead, as it is the first unnecessarily escaped character. + */ + isUnnecessaryEscape = match.input[match.index - 1] !== "$"; + } + } else { + isQuoteEscape = escapedChar === node.raw[0]; + } + + if (isUnnecessaryEscape && !isQuoteEscape) { + report(node, match.index + 1, match[0].slice(1)); + } + } + + /** + * Checks if a node has an escape. + * + * @param {ASTNode} node - node to check. + * @returns {void} + */ + function check(node) { + const isTemplateElement = node.type === "TemplateElement"; + + if ( + isTemplateElement && + node.parent && + node.parent.parent && + node.parent.parent.type === "TaggedTemplateExpression" && + node.parent === node.parent.parent.quasi + ) { + + // Don't report tagged template literals, because the backslash character is accessible to the tag function. + return; + } + + if (typeof node.value === "string" || isTemplateElement) { + + /* + * JSXAttribute doesn't have any escape sequence: https://facebook.github.io/jsx/. + * In addition, backticks are not supported by JSX yet: https://github.com/facebook/jsx/issues/25. + */ + if (node.parent.type === "JSXAttribute" || node.parent.type === "JSXElement") { + return; + } + + const value = isTemplateElement ? node.value.raw : node.raw.slice(1, -1); + const pattern = /\\[^\d]/g; + let match; + + while ((match = pattern.exec(value))) { + validateString(node, match); + } + } else if (node.regex) { + parseRegExp(node.regex.pattern) + + /* + * The '-' character is a special case, because it's only valid to escape it if it's in a character + * class, and is not at either edge of the character class. To account for this, don't consider '-' + * characters to be valid in general, and filter out '-' characters that appear in the middle of a + * character class. + */ + .filter(charInfo => !(charInfo.text === "-" && charInfo.inCharClass && !charInfo.startsCharClass && !charInfo.endsCharClass)) + + /* + * The '^' character is also a special case; it must always be escaped outside of character classes, but + * it only needs to be escaped in character classes if it's at the beginning of the character class. To + * account for this, consider it to be a valid escape character outside of character classes, and filter + * out '^' characters that appear at the start of a character class. + */ + .filter(charInfo => !(charInfo.text === "^" && charInfo.startsCharClass)) + + // Filter out characters that aren't escaped. + .filter(charInfo => charInfo.escaped) + + // Filter out characters that are valid to escape, based on their position in the regular expression. + .filter(charInfo => !(charInfo.inCharClass ? REGEX_GENERAL_ESCAPES : REGEX_NON_CHARCLASS_ESCAPES).has(charInfo.text)) + + // Report all the remaining characters. + .forEach(charInfo => report(node, charInfo.index, charInfo.text)); + } + + } + + return { + Literal: check, + TemplateElement: check + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-useless-rename.js b/tools/node_modules/eslint/lib/rules/no-useless-rename.js new file mode 100644 index 0000000000..a489a6e51b --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-useless-rename.js @@ -0,0 +1,147 @@ +/** + * @fileoverview Disallow renaming import, export, and destructured assignments to the same name. + * @author Kai Cataldo + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow renaming import, export, and destructured assignments to the same name", + category: "ECMAScript 6", + recommended: false + }, + fixable: "code", + schema: [ + { + type: "object", + properties: { + ignoreDestructuring: { type: "boolean" }, + ignoreImport: { type: "boolean" }, + ignoreExport: { type: "boolean" } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const options = context.options[0] || {}, + ignoreDestructuring = options.ignoreDestructuring === true, + ignoreImport = options.ignoreImport === true, + ignoreExport = options.ignoreExport === true; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Reports error for unnecessarily renamed assignments + * @param {ASTNode} node - node to report + * @param {ASTNode} initial - node with initial name value + * @param {ASTNode} result - node with new name value + * @param {string} type - the type of the offending node + * @returns {void} + */ + function reportError(node, initial, result, type) { + const name = initial.type === "Identifier" ? initial.name : initial.value; + + return context.report({ + node, + message: "{{type}} {{name}} unnecessarily renamed.", + data: { + name, + type + }, + fix(fixer) { + return fixer.replaceTextRange([ + initial.range[0], + result.range[1] + ], name); + } + }); + } + + /** + * Checks whether a destructured assignment is unnecessarily renamed + * @param {ASTNode} node - node to check + * @returns {void} + */ + function checkDestructured(node) { + if (ignoreDestructuring) { + return; + } + + const properties = node.properties; + + for (let i = 0; i < properties.length; i++) { + if (properties[i].shorthand) { + continue; + } + + /** + * If an ObjectPattern property is computed, we have no idea + * if a rename is useless or not. If an ObjectPattern property + * lacks a key, it is likely an ExperimentalRestProperty and + * so there is no "renaming" occurring here. + */ + if (properties[i].computed || !properties[i].key) { + continue; + } + + if (properties[i].key.type === "Identifier" && properties[i].key.name === properties[i].value.name || + properties[i].key.type === "Literal" && properties[i].key.value === properties[i].value.name) { + reportError(properties[i], properties[i].key, properties[i].value, "Destructuring assignment"); + } + } + } + + /** + * Checks whether an import is unnecessarily renamed + * @param {ASTNode} node - node to check + * @returns {void} + */ + function checkImport(node) { + if (ignoreImport) { + return; + } + + if (node.imported.name === node.local.name && + node.imported.range[0] !== node.local.range[0]) { + reportError(node, node.imported, node.local, "Import"); + } + } + + /** + * Checks whether an export is unnecessarily renamed + * @param {ASTNode} node - node to check + * @returns {void} + */ + function checkExport(node) { + if (ignoreExport) { + return; + } + + if (node.local.name === node.exported.name && + node.local.range[0] !== node.exported.range[0]) { + reportError(node, node.local, node.exported, "Export"); + } + + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + ObjectPattern: checkDestructured, + ImportSpecifier: checkImport, + ExportSpecifier: checkExport + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-useless-return.js b/tools/node_modules/eslint/lib/rules/no-useless-return.js new file mode 100644 index 0000000000..5415bf59b8 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-useless-return.js @@ -0,0 +1,304 @@ +/** + * @fileoverview Disallow redundant return statements + * @author Teddy Katz + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"), + FixTracker = require("../util/fix-tracker"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Adds all elements of 2nd argument into 1st argument. + * + * @param {Array} array - The destination array to add. + * @param {Array} elements - The source array to add. + * @returns {void} + */ +const pushAll = Function.apply.bind(Array.prototype.push); + +/** + * Removes the given element from the array. + * + * @param {Array} array - The source array to remove. + * @param {any} element - The target item to remove. + * @returns {void} + */ +function remove(array, element) { + const index = array.indexOf(element); + + if (index !== -1) { + array.splice(index, 1); + } +} + +/** + * Checks whether it can remove the given return statement or not. + * + * @param {ASTNode} node - The return statement node to check. + * @returns {boolean} `true` if the node is removeable. + */ +function isRemovable(node) { + return astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type); +} + +/** + * Checks whether the given return statement is in a `finally` block or not. + * + * @param {ASTNode} node - The return statement node to check. + * @returns {boolean} `true` if the node is in a `finally` block. + */ +function isInFinally(node) { + while (node && node.parent && !astUtils.isFunction(node)) { + if (node.parent.type === "TryStatement" && node.parent.finalizer === node) { + return true; + } + + node = node.parent; + } + + return false; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow redundant return statements", + category: "Best Practices", + recommended: false + }, + fixable: "code", + schema: [] + }, + + create(context) { + const segmentInfoMap = new WeakMap(); + const usedUnreachableSegments = new WeakSet(); + let scopeInfo = null; + + /** + * Checks whether the given segment is terminated by a return statement or not. + * + * @param {CodePathSegment} segment - The segment to check. + * @returns {boolean} `true` if the segment is terminated by a return statement, or if it's still a part of unreachable. + */ + function isReturned(segment) { + const info = segmentInfoMap.get(segment); + + return !info || info.returned; + } + + /** + * Collects useless return statements from the given previous segments. + * + * A previous segment may be an unreachable segment. + * In that case, the information object of the unreachable segment is not + * initialized because `onCodePathSegmentStart` event is not notified for + * unreachable segments. + * This goes to the previous segments of the unreachable segment recursively + * if the unreachable segment was generated by a return statement. Otherwise, + * this ignores the unreachable segment. + * + * This behavior would simulate code paths for the case that the return + * statement does not exist. + * + * @param {ASTNode[]} uselessReturns - The collected return statements. + * @param {CodePathSegment[]} prevSegments - The previous segments to traverse. + * @param {WeakSet<CodePathSegment>} [traversedSegments] A set of segments that have already been traversed in this call + * @returns {ASTNode[]} `uselessReturns`. + */ + function getUselessReturns(uselessReturns, prevSegments, traversedSegments) { + if (!traversedSegments) { + traversedSegments = new WeakSet(); + } + for (const segment of prevSegments) { + if (!segment.reachable) { + if (!traversedSegments.has(segment)) { + traversedSegments.add(segment); + getUselessReturns( + uselessReturns, + segment.allPrevSegments.filter(isReturned), + traversedSegments + ); + } + continue; + } + + pushAll(uselessReturns, segmentInfoMap.get(segment).uselessReturns); + } + + return uselessReturns; + } + + /** + * Removes the return statements on the given segment from the useless return + * statement list. + * + * This segment may be an unreachable segment. + * In that case, the information object of the unreachable segment is not + * initialized because `onCodePathSegmentStart` event is not notified for + * unreachable segments. + * This goes to the previous segments of the unreachable segment recursively + * if the unreachable segment was generated by a return statement. Otherwise, + * this ignores the unreachable segment. + * + * This behavior would simulate code paths for the case that the return + * statement does not exist. + * + * @param {CodePathSegment} segment - The segment to get return statements. + * @returns {void} + */ + function markReturnStatementsOnSegmentAsUsed(segment) { + if (!segment.reachable) { + usedUnreachableSegments.add(segment); + segment.allPrevSegments + .filter(isReturned) + .filter(prevSegment => !usedUnreachableSegments.has(prevSegment)) + .forEach(markReturnStatementsOnSegmentAsUsed); + return; + } + + const info = segmentInfoMap.get(segment); + + for (const node of info.uselessReturns) { + remove(scopeInfo.uselessReturns, node); + } + info.uselessReturns = []; + } + + /** + * Removes the return statements on the current segments from the useless + * return statement list. + * + * This function will be called at every statement except FunctionDeclaration, + * BlockStatement, and BreakStatement. + * + * - FunctionDeclarations are always executed whether it's returned or not. + * - BlockStatements do nothing. + * - BreakStatements go the next merely. + * + * @returns {void} + */ + function markReturnStatementsOnCurrentSegmentsAsUsed() { + scopeInfo + .codePath + .currentSegments + .forEach(markReturnStatementsOnSegmentAsUsed); + } + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + return { + + // Makes and pushs a new scope information. + onCodePathStart(codePath) { + scopeInfo = { + upper: scopeInfo, + uselessReturns: [], + codePath + }; + }, + + // Reports useless return statements if exist. + onCodePathEnd() { + for (const node of scopeInfo.uselessReturns) { + context.report({ + node, + loc: node.loc, + message: "Unnecessary return statement.", + fix(fixer) { + if (isRemovable(node)) { + + /* + * Extend the replacement range to include the + * entire function to avoid conflicting with + * no-else-return. + * https://github.com/eslint/eslint/issues/8026 + */ + return new FixTracker(fixer, context.getSourceCode()) + .retainEnclosingFunction(node) + .remove(node); + } + return null; + } + }); + } + + scopeInfo = scopeInfo.upper; + }, + + /* + * Initializes segments. + * NOTE: This event is notified for only reachable segments. + */ + onCodePathSegmentStart(segment) { + const info = { + uselessReturns: getUselessReturns([], segment.allPrevSegments), + returned: false + }; + + // Stores the info. + segmentInfoMap.set(segment, info); + }, + + // Adds ReturnStatement node to check whether it's useless or not. + ReturnStatement(node) { + if (node.argument) { + markReturnStatementsOnCurrentSegmentsAsUsed(); + } + if (node.argument || astUtils.isInLoop(node) || isInFinally(node)) { + return; + } + + for (const segment of scopeInfo.codePath.currentSegments) { + const info = segmentInfoMap.get(segment); + + if (info) { + info.uselessReturns.push(node); + info.returned = true; + } + } + scopeInfo.uselessReturns.push(node); + }, + + /* + * Registers for all statement nodes except FunctionDeclaration, BlockStatement, BreakStatement. + * Removes return statements of the current segments from the useless return statement list. + */ + ClassDeclaration: markReturnStatementsOnCurrentSegmentsAsUsed, + ContinueStatement: markReturnStatementsOnCurrentSegmentsAsUsed, + DebuggerStatement: markReturnStatementsOnCurrentSegmentsAsUsed, + DoWhileStatement: markReturnStatementsOnCurrentSegmentsAsUsed, + EmptyStatement: markReturnStatementsOnCurrentSegmentsAsUsed, + ExpressionStatement: markReturnStatementsOnCurrentSegmentsAsUsed, + ForInStatement: markReturnStatementsOnCurrentSegmentsAsUsed, + ForOfStatement: markReturnStatementsOnCurrentSegmentsAsUsed, + ForStatement: markReturnStatementsOnCurrentSegmentsAsUsed, + IfStatement: markReturnStatementsOnCurrentSegmentsAsUsed, + ImportDeclaration: markReturnStatementsOnCurrentSegmentsAsUsed, + LabeledStatement: markReturnStatementsOnCurrentSegmentsAsUsed, + SwitchStatement: markReturnStatementsOnCurrentSegmentsAsUsed, + ThrowStatement: markReturnStatementsOnCurrentSegmentsAsUsed, + TryStatement: markReturnStatementsOnCurrentSegmentsAsUsed, + VariableDeclaration: markReturnStatementsOnCurrentSegmentsAsUsed, + WhileStatement: markReturnStatementsOnCurrentSegmentsAsUsed, + WithStatement: markReturnStatementsOnCurrentSegmentsAsUsed, + ExportNamedDeclaration: markReturnStatementsOnCurrentSegmentsAsUsed, + ExportDefaultDeclaration: markReturnStatementsOnCurrentSegmentsAsUsed, + ExportAllDeclaration: markReturnStatementsOnCurrentSegmentsAsUsed + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-var.js b/tools/node_modules/eslint/lib/rules/no-var.js new file mode 100644 index 0000000000..d3c163e557 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-var.js @@ -0,0 +1,328 @@ +/** + * @fileoverview Rule to check for the usage of var. + * @author Jamund Ferguson + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Check whether a given variable is a global variable or not. + * @param {eslint-scope.Variable} variable The variable to check. + * @returns {boolean} `true` if the variable is a global variable. + */ +function isGlobal(variable) { + return Boolean(variable.scope) && variable.scope.type === "global"; +} + +/** + * Finds the nearest function scope or global scope walking up the scope + * hierarchy. + * + * @param {eslint-scope.Scope} scope - The scope to traverse. + * @returns {eslint-scope.Scope} a function scope or global scope containing the given + * scope. + */ +function getEnclosingFunctionScope(scope) { + while (scope.type !== "function" && scope.type !== "global") { + scope = scope.upper; + } + return scope; +} + +/** + * Checks whether the given variable has any references from a more specific + * function expression (i.e. a closure). + * + * @param {eslint-scope.Variable} variable - A variable to check. + * @returns {boolean} `true` if the variable is used from a closure. + */ +function isReferencedInClosure(variable) { + const enclosingFunctionScope = getEnclosingFunctionScope(variable.scope); + + return variable.references.some(reference => + getEnclosingFunctionScope(reference.from) !== enclosingFunctionScope); +} + +/** + * Checks whether the given node is the assignee of a loop. + * + * @param {ASTNode} node - A VariableDeclaration node to check. + * @returns {boolean} `true` if the declaration is assigned as part of loop + * iteration. + */ +function isLoopAssignee(node) { + return (node.parent.type === "ForOfStatement" || node.parent.type === "ForInStatement") && + node === node.parent.left; +} + +/** + * Checks whether the given variable declaration is immediately initialized. + * + * @param {ASTNode} node - A VariableDeclaration node to check. + * @returns {boolean} `true` if the declaration has an initializer. + */ +function isDeclarationInitialized(node) { + return node.declarations.every(declarator => declarator.init !== null); +} + +const SCOPE_NODE_TYPE = /^(?:Program|BlockStatement|SwitchStatement|ForStatement|ForInStatement|ForOfStatement)$/; + +/** + * Gets the scope node which directly contains a given node. + * + * @param {ASTNode} node - A node to get. This is a `VariableDeclaration` or + * an `Identifier`. + * @returns {ASTNode} A scope node. This is one of `Program`, `BlockStatement`, + * `SwitchStatement`, `ForStatement`, `ForInStatement`, and + * `ForOfStatement`. + */ +function getScopeNode(node) { + while (node) { + if (SCOPE_NODE_TYPE.test(node.type)) { + return node; + } + + node = node.parent; + } + + /* istanbul ignore next : unreachable */ + return null; +} + +/** + * Checks whether a given variable is redeclared or not. + * + * @param {eslint-scope.Variable} variable - A variable to check. + * @returns {boolean} `true` if the variable is redeclared. + */ +function isRedeclared(variable) { + return variable.defs.length >= 2; +} + +/** + * Checks whether a given variable is used from outside of the specified scope. + * + * @param {ASTNode} scopeNode - A scope node to check. + * @returns {Function} The predicate function which checks whether a given + * variable is used from outside of the specified scope. + */ +function isUsedFromOutsideOf(scopeNode) { + + /** + * Checks whether a given reference is inside of the specified scope or not. + * + * @param {eslint-scope.Reference} reference - A reference to check. + * @returns {boolean} `true` if the reference is inside of the specified + * scope. + */ + function isOutsideOfScope(reference) { + const scope = scopeNode.range; + const id = reference.identifier.range; + + return id[0] < scope[0] || id[1] > scope[1]; + } + + return function(variable) { + return variable.references.some(isOutsideOfScope); + }; +} + +/** + * Creates the predicate function which checks whether a variable has their references in TDZ. + * + * The predicate function would return `true`: + * + * - if a reference is before the declarator. E.g. (var a = b, b = 1;)(var {a = b, b} = {};) + * - if a reference is in the expression of their default value. E.g. (var {a = a} = {};) + * - if a reference is in the expression of their initializer. E.g. (var a = a;) + * + * @param {ASTNode} node - The initializer node of VariableDeclarator. + * @returns {Function} The predicate function. + * @private + */ +function hasReferenceInTDZ(node) { + const initStart = node.range[0]; + const initEnd = node.range[1]; + + return variable => { + const id = variable.defs[0].name; + const idStart = id.range[0]; + const defaultValue = (id.parent.type === "AssignmentPattern" ? id.parent.right : null); + const defaultStart = defaultValue && defaultValue.range[0]; + const defaultEnd = defaultValue && defaultValue.range[1]; + + return variable.references.some(reference => { + const start = reference.identifier.range[0]; + const end = reference.identifier.range[1]; + + return !reference.init && ( + start < idStart || + (defaultValue !== null && start >= defaultStart && end <= defaultEnd) || + (start >= initStart && end <= initEnd) + ); + }); + }; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require `let` or `const` instead of `var`", + category: "ECMAScript 6", + recommended: false + }, + + schema: [], + fixable: "code" + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + /** + * Checks whether the variables which are defined by the given declarator node have their references in TDZ. + * + * @param {ASTNode} declarator - The VariableDeclarator node to check. + * @returns {boolean} `true` if one of the variables which are defined by the given declarator node have their references in TDZ. + */ + function hasSelfReferenceInTDZ(declarator) { + if (!declarator.init) { + return false; + } + const variables = context.getDeclaredVariables(declarator); + + return variables.some(hasReferenceInTDZ(declarator.init)); + } + + /** + * Checks whether it can fix a given variable declaration or not. + * It cannot fix if the following cases: + * + * - A variable is a global variable. + * - A variable is declared on a SwitchCase node. + * - A variable is redeclared. + * - A variable is used from outside the scope. + * - A variable is used from a closure within a loop. + * - A variable might be used before it is assigned within a loop. + * - A variable might be used in TDZ. + * - A variable is declared in statement position (e.g. a single-line `IfStatement`) + * + * ## A variable is declared on a SwitchCase node. + * + * If this rule modifies 'var' declarations on a SwitchCase node, it + * would generate the warnings of 'no-case-declarations' rule. And the + * 'eslint:recommended' preset includes 'no-case-declarations' rule, so + * this rule doesn't modify those declarations. + * + * ## A variable is redeclared. + * + * The language spec disallows redeclarations of `let` declarations. + * Those variables would cause syntax errors. + * + * ## A variable is used from outside the scope. + * + * The language spec disallows accesses from outside of the scope for + * `let` declarations. Those variables would cause reference errors. + * + * ## A variable is used from a closure within a loop. + * + * A `var` declaration within a loop shares the same variable instance + * across all loop iterations, while a `let` declaration creates a new + * instance for each iteration. This means if a variable in a loop is + * referenced by any closure, changing it from `var` to `let` would + * change the behavior in a way that is generally unsafe. + * + * ## A variable might be used before it is assigned within a loop. + * + * Within a loop, a `let` declaration without an initializer will be + * initialized to null, while a `var` declaration will retain its value + * from the previous iteration, so it is only safe to change `var` to + * `let` if we can statically determine that the variable is always + * assigned a value before its first access in the loop body. To keep + * the implementation simple, we only convert `var` to `let` within + * loops when the variable is a loop assignee or the declaration has an + * initializer. + * + * @param {ASTNode} node - A variable declaration node to check. + * @returns {boolean} `true` if it can fix the node. + */ + function canFix(node) { + const variables = context.getDeclaredVariables(node); + const scopeNode = getScopeNode(node); + + if (node.parent.type === "SwitchCase" || + node.declarations.some(hasSelfReferenceInTDZ) || + variables.some(isGlobal) || + variables.some(isRedeclared) || + variables.some(isUsedFromOutsideOf(scopeNode)) + ) { + return false; + } + + if (astUtils.isInLoop(node)) { + if (variables.some(isReferencedInClosure)) { + return false; + } + if (!isLoopAssignee(node) && !isDeclarationInitialized(node)) { + return false; + } + } + + if ( + !isLoopAssignee(node) && + !(node.parent.type === "ForStatement" && node.parent.init === node) && + !astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type) + ) { + + // If the declaration is not in a block, e.g. `if (foo) var bar = 1;`, then it can't be fixed. + return false; + } + + return true; + } + + /** + * Reports a given variable declaration node. + * + * @param {ASTNode} node - A variable declaration node to report. + * @returns {void} + */ + function report(node) { + const varToken = sourceCode.getFirstToken(node); + + context.report({ + node, + message: "Unexpected var, use let or const instead.", + + fix(fixer) { + if (canFix(node)) { + return fixer.replaceText(varToken, "let"); + } + return null; + } + }); + } + + return { + "VariableDeclaration:exit"(node) { + if (node.kind === "var") { + report(node); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-void.js b/tools/node_modules/eslint/lib/rules/no-void.js new file mode 100644 index 0000000000..5202fa49a8 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-void.js @@ -0,0 +1,37 @@ +/** + * @fileoverview Rule to disallow use of void operator. + * @author Mike Sidorov + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `void` operators", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + UnaryExpression(node) { + if (node.operator === "void") { + context.report({ node, message: "Expected 'undefined' and instead saw 'void'." }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-warning-comments.js b/tools/node_modules/eslint/lib/rules/no-warning-comments.js new file mode 100644 index 0000000000..c0ecaca9e7 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-warning-comments.js @@ -0,0 +1,139 @@ +/** + * @fileoverview Rule that warns about used warning comments + * @author Alexander Schmidt <https://github.com/lxanders> + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow specified warning terms in comments", + category: "Best Practices", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + terms: { + type: "array", + items: { + type: "string" + } + }, + location: { + enum: ["start", "anywhere"] + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + const sourceCode = context.getSourceCode(), + configuration = context.options[0] || {}, + warningTerms = configuration.terms || ["todo", "fixme", "xxx"], + location = configuration.location || "start", + selfConfigRegEx = /\bno-warning-comments\b/; + + /** + * Convert a warning term into a RegExp which will match a comment containing that whole word in the specified + * location ("start" or "anywhere"). If the term starts or ends with non word characters, then the match will not + * require word boundaries on that side. + * + * @param {string} term A term to convert to a RegExp + * @returns {RegExp} The term converted to a RegExp + */ + function convertToRegExp(term) { + const escaped = term.replace(/[-/\\$^*+?.()|[\]{}]/g, "\\$&"); + let prefix; + + /* + * If the term ends in a word character (a-z0-9_), ensure a word + * boundary at the end, so that substrings do not get falsely + * matched. eg "todo" in a string such as "mastodon". + * If the term ends in a non-word character, then \b won't match on + * the boundary to the next non-word character, which would likely + * be a space. For example `/\bFIX!\b/.test('FIX! blah') === false`. + * In these cases, use no bounding match. Same applies for the + * prefix, handled below. + */ + const suffix = /\w$/.test(term) ? "\\b" : ""; + + if (location === "start") { + + /* + * When matching at the start, ignore leading whitespace, and + * there's no need to worry about word boundaries. + */ + prefix = "^\\s*"; + } else if (/^\w/.test(term)) { + prefix = "\\b"; + } else { + prefix = ""; + } + + return new RegExp(prefix + escaped + suffix, "i"); + } + + const warningRegExps = warningTerms.map(convertToRegExp); + + /** + * Checks the specified comment for matches of the configured warning terms and returns the matches. + * @param {string} comment The comment which is checked. + * @returns {Array} All matched warning terms for this comment. + */ + function commentContainsWarningTerm(comment) { + const matches = []; + + warningRegExps.forEach((regex, index) => { + if (regex.test(comment)) { + matches.push(warningTerms[index]); + } + }); + + return matches; + } + + /** + * Checks the specified node for matching warning comments and reports them. + * @param {ASTNode} node The AST node being checked. + * @returns {void} undefined. + */ + function checkComment(node) { + if (astUtils.isDirectiveComment(node) && selfConfigRegEx.test(node.value)) { + return; + } + + const matches = commentContainsWarningTerm(node.value); + + matches.forEach(matchedTerm => { + context.report({ + node, + message: "Unexpected '{{matchedTerm}}' comment.", + data: { + matchedTerm + } + }); + }); + } + + return { + Program() { + const comments = sourceCode.getAllComments(); + + comments.filter(token => token.type !== "Shebang").forEach(checkComment); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-whitespace-before-property.js b/tools/node_modules/eslint/lib/rules/no-whitespace-before-property.js new file mode 100644 index 0000000000..2d476b66c0 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-whitespace-before-property.js @@ -0,0 +1,94 @@ +/** + * @fileoverview Rule to disallow whitespace before properties + * @author Kai Cataldo + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow whitespace before properties", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + schema: [] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Reports whitespace before property token + * @param {ASTNode} node - the node to report in the event of an error + * @param {Token} leftToken - the left token + * @param {Token} rightToken - the right token + * @returns {void} + * @private + */ + function reportError(node, leftToken, rightToken) { + const replacementText = node.computed ? "" : "."; + + context.report({ + node, + message: "Unexpected whitespace before property {{propName}}.", + data: { + propName: sourceCode.getText(node.property) + }, + fix(fixer) { + if (!node.computed && astUtils.isDecimalInteger(node.object)) { + + /* + * If the object is a number literal, fixing it to something like 5.toString() would cause a SyntaxError. + * Don't fix this case. + */ + return null; + } + return fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], replacementText); + } + }); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + MemberExpression(node) { + let rightToken; + let leftToken; + + if (!astUtils.isTokenOnSameLine(node.object, node.property)) { + return; + } + + if (node.computed) { + rightToken = sourceCode.getTokenBefore(node.property, astUtils.isOpeningBracketToken); + leftToken = sourceCode.getTokenBefore(rightToken); + } else { + rightToken = sourceCode.getFirstToken(node.property); + leftToken = sourceCode.getTokenBefore(rightToken, 1); + } + + if (sourceCode.isSpaceBetweenTokens(leftToken, rightToken)) { + reportError(node, leftToken, rightToken); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/no-with.js b/tools/node_modules/eslint/lib/rules/no-with.js new file mode 100644 index 0000000000..be9e346360 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/no-with.js @@ -0,0 +1,32 @@ +/** + * @fileoverview Rule to flag use of with statement + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `with` statements", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + + return { + WithStatement(node) { + context.report({ node, message: "Unexpected use of 'with' statement." }); + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/nonblock-statement-body-position.js b/tools/node_modules/eslint/lib/rules/nonblock-statement-body-position.js new file mode 100644 index 0000000000..212e36a57c --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/nonblock-statement-body-position.js @@ -0,0 +1,114 @@ +/** + * @fileoverview enforce the location of single-line statements + * @author Teddy Katz + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +const POSITION_SCHEMA = { enum: ["beside", "below", "any"] }; + +module.exports = { + meta: { + docs: { + description: "enforce the location of single-line statements", + category: "Stylistic Issues", + recommended: false + }, + fixable: "whitespace", + schema: [ + POSITION_SCHEMA, + { + properties: { + overrides: { + properties: { + if: POSITION_SCHEMA, + else: POSITION_SCHEMA, + while: POSITION_SCHEMA, + do: POSITION_SCHEMA, + for: POSITION_SCHEMA + }, + additionalProperties: false + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + //---------------------------------------------------------------------- + // Helpers + //---------------------------------------------------------------------- + + /** + * Gets the applicable preference for a particular keyword + * @param {string} keywordName The name of a keyword, e.g. 'if' + * @returns {string} The applicable option for the keyword, e.g. 'beside' + */ + function getOption(keywordName) { + return context.options[1] && context.options[1].overrides && context.options[1].overrides[keywordName] || + context.options[0] || + "beside"; + } + + /** + * Validates the location of a single-line statement + * @param {ASTNode} node The single-line statement + * @param {string} keywordName The applicable keyword name for the single-line statement + * @returns {void} + */ + function validateStatement(node, keywordName) { + const option = getOption(keywordName); + + if (node.type === "BlockStatement" || option === "any") { + return; + } + + const tokenBefore = sourceCode.getTokenBefore(node); + + if (tokenBefore.loc.end.line === node.loc.start.line && option === "below") { + context.report({ + node, + message: "Expected a linebreak before this statement.", + fix: fixer => fixer.insertTextBefore(node, "\n") + }); + } else if (tokenBefore.loc.end.line !== node.loc.start.line && option === "beside") { + context.report({ + node, + message: "Expected no linebreak before this statement.", + fix(fixer) { + if (sourceCode.getText().slice(tokenBefore.range[1], node.range[0]).trim()) { + return null; + } + return fixer.replaceTextRange([tokenBefore.range[1], node.range[0]], " "); + } + }); + } + } + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + return { + IfStatement(node) { + validateStatement(node.consequent, "if"); + + // Check the `else` node, but don't check 'else if' statements. + if (node.alternate && node.alternate.type !== "IfStatement") { + validateStatement(node.alternate, "else"); + } + }, + WhileStatement: node => validateStatement(node.body, "while"), + DoWhileStatement: node => validateStatement(node.body, "do"), + ForStatement: node => validateStatement(node.body, "for"), + ForInStatement: node => validateStatement(node.body, "for"), + ForOfStatement: node => validateStatement(node.body, "for") + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/object-curly-newline.js b/tools/node_modules/eslint/lib/rules/object-curly-newline.js new file mode 100644 index 0000000000..ebad69de2e --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/object-curly-newline.js @@ -0,0 +1,249 @@ +/** + * @fileoverview Rule to require or disallow line breaks inside braces. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +// Schema objects. +const OPTION_VALUE = { + oneOf: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + multiline: { + type: "boolean" + }, + minProperties: { + type: "integer", + minimum: 0 + }, + consistent: { + type: "boolean" + } + }, + additionalProperties: false, + minProperties: 1 + } + ] +}; + +/** + * Normalizes a given option value. + * + * @param {string|Object|undefined} value - An option value to parse. + * @returns {{multiline: boolean, minProperties: number, consistent: boolean}} Normalized option object. + */ +function normalizeOptionValue(value) { + let multiline = false; + let minProperties = Number.POSITIVE_INFINITY; + let consistent = false; + + if (value) { + if (value === "always") { + minProperties = 0; + } else if (value === "never") { + minProperties = Number.POSITIVE_INFINITY; + } else { + multiline = Boolean(value.multiline); + minProperties = value.minProperties || Number.POSITIVE_INFINITY; + consistent = Boolean(value.consistent); + } + } else { + multiline = true; + } + + return { multiline, minProperties, consistent }; +} + +/** + * Normalizes a given option value. + * + * @param {string|Object|undefined} options - An option value to parse. + * @returns {{ObjectExpression: {multiline: boolean, minProperties: number}, ObjectPattern: {multiline: boolean, minProperties: number}}} Normalized option object. + */ +function normalizeOptions(options) { + if (options && (options.ObjectExpression || options.ObjectPattern)) { + return { + ObjectExpression: normalizeOptionValue(options.ObjectExpression), + ObjectPattern: normalizeOptionValue(options.ObjectPattern) + }; + } + + const value = normalizeOptionValue(options); + + return { ObjectExpression: value, ObjectPattern: value }; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent line breaks inside braces", + category: "Stylistic Issues", + recommended: false + }, + fixable: "whitespace", + schema: [ + { + oneOf: [ + OPTION_VALUE, + { + type: "object", + properties: { + ObjectExpression: OPTION_VALUE, + ObjectPattern: OPTION_VALUE + }, + additionalProperties: false, + minProperties: 1 + } + ] + } + ] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + const normalizedOptions = normalizeOptions(context.options[0]); + + /** + * Reports a given node if it violated this rule. + * + * @param {ASTNode} node - A node to check. This is an ObjectExpression node or an ObjectPattern node. + * @param {{multiline: boolean, minProperties: number}} options - An option object. + * @returns {void} + */ + function check(node) { + const options = normalizedOptions[node.type]; + const openBrace = sourceCode.getFirstToken(node, token => token.value === "{"); + let closeBrace; + + if (node.typeAnnotation) { + closeBrace = sourceCode.getTokenBefore(node.typeAnnotation); + } else { + closeBrace = sourceCode.getLastToken(node); + } + + let first = sourceCode.getTokenAfter(openBrace, { includeComments: true }); + let last = sourceCode.getTokenBefore(closeBrace, { includeComments: true }); + const needsLinebreaks = ( + node.properties.length >= options.minProperties || + ( + options.multiline && + node.properties.length > 0 && + first.loc.start.line !== last.loc.end.line + ) + ); + const hasCommentsFirstToken = astUtils.isCommentToken(first); + const hasCommentsLastToken = astUtils.isCommentToken(last); + + /* + * Use tokens or comments to check multiline or not. + * But use only tokens to check whether line breaks are needed. + * This allows: + * var obj = { // eslint-disable-line foo + * a: 1 + * } + */ + first = sourceCode.getTokenAfter(openBrace); + last = sourceCode.getTokenBefore(closeBrace); + + if (needsLinebreaks) { + if (astUtils.isTokenOnSameLine(openBrace, first)) { + context.report({ + message: "Expected a line break after this opening brace.", + node, + loc: openBrace.loc.start, + fix(fixer) { + if (hasCommentsFirstToken) { + return null; + } + + return fixer.insertTextAfter(openBrace, "\n"); + } + }); + } + if (astUtils.isTokenOnSameLine(last, closeBrace)) { + context.report({ + message: "Expected a line break before this closing brace.", + node, + loc: closeBrace.loc.start, + fix(fixer) { + if (hasCommentsLastToken) { + return null; + } + + return fixer.insertTextBefore(closeBrace, "\n"); + } + }); + } + } else { + const consistent = options.consistent; + const hasLineBreakBetweenOpenBraceAndFirst = !astUtils.isTokenOnSameLine(openBrace, first); + const hasLineBreakBetweenCloseBraceAndLast = !astUtils.isTokenOnSameLine(last, closeBrace); + + if ( + (!consistent && hasLineBreakBetweenOpenBraceAndFirst) || + (consistent && hasLineBreakBetweenOpenBraceAndFirst && !hasLineBreakBetweenCloseBraceAndLast) + ) { + context.report({ + message: "Unexpected line break after this opening brace.", + node, + loc: openBrace.loc.start, + fix(fixer) { + if (hasCommentsFirstToken) { + return null; + } + + return fixer.removeRange([ + openBrace.range[1], + first.range[0] + ]); + } + }); + } + if ( + (!consistent && hasLineBreakBetweenCloseBraceAndLast) || + (consistent && !hasLineBreakBetweenOpenBraceAndFirst && hasLineBreakBetweenCloseBraceAndLast) + ) { + context.report({ + message: "Unexpected line break before this closing brace.", + node, + loc: closeBrace.loc.start, + fix(fixer) { + if (hasCommentsLastToken) { + return null; + } + + return fixer.removeRange([ + last.range[1], + closeBrace.range[0] + ]); + } + }); + } + } + } + + return { + ObjectExpression: check, + ObjectPattern: check + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/object-curly-spacing.js b/tools/node_modules/eslint/lib/rules/object-curly-spacing.js new file mode 100644 index 0000000000..3341e915f2 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/object-curly-spacing.js @@ -0,0 +1,299 @@ +/** + * @fileoverview Disallows or enforces spaces inside of object literals. + * @author Jamund Ferguson + */ +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent spacing inside braces", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + arraysInObjects: { + type: "boolean" + }, + objectsInObjects: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const spaced = context.options[0] === "always", + sourceCode = context.getSourceCode(); + + /** + * Determines whether an option is set, relative to the spacing option. + * If spaced is "always", then check whether option is set to false. + * If spaced is "never", then check whether option is set to true. + * @param {Object} option - The option to exclude. + * @returns {boolean} Whether or not the property is excluded. + */ + function isOptionSet(option) { + return context.options[1] ? context.options[1][option] === !spaced : false; + } + + const options = { + spaced, + arraysInObjectsException: isOptionSet("arraysInObjects"), + objectsInObjectsException: isOptionSet("objectsInObjects") + }; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Reports that there shouldn't be a space after the first token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportNoBeginningSpace(node, token) { + context.report({ + node, + loc: token.loc.start, + message: "There should be no space after '{{token}}'.", + data: { + token: token.value + }, + fix(fixer) { + const nextToken = context.getSourceCode().getTokenAfter(token); + + return fixer.removeRange([token.range[1], nextToken.range[0]]); + } + }); + } + + /** + * Reports that there shouldn't be a space before the last token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportNoEndingSpace(node, token) { + context.report({ + node, + loc: token.loc.start, + message: "There should be no space before '{{token}}'.", + data: { + token: token.value + }, + fix(fixer) { + const previousToken = context.getSourceCode().getTokenBefore(token); + + return fixer.removeRange([previousToken.range[1], token.range[0]]); + } + }); + } + + /** + * Reports that there should be a space after the first token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportRequiredBeginningSpace(node, token) { + context.report({ + node, + loc: token.loc.start, + message: "A space is required after '{{token}}'.", + data: { + token: token.value + }, + fix(fixer) { + return fixer.insertTextAfter(token, " "); + } + }); + } + + /** + * Reports that there should be a space before the last token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportRequiredEndingSpace(node, token) { + context.report({ + node, + loc: token.loc.start, + message: "A space is required before '{{token}}'.", + data: { + token: token.value + }, + fix(fixer) { + return fixer.insertTextBefore(token, " "); + } + }); + } + + /** + * Determines if spacing in curly braces is valid. + * @param {ASTNode} node The AST node to check. + * @param {Token} first The first token to check (should be the opening brace) + * @param {Token} second The second token to check (should be first after the opening brace) + * @param {Token} penultimate The penultimate token to check (should be last before closing brace) + * @param {Token} last The last token to check (should be closing brace) + * @returns {void} + */ + function validateBraceSpacing(node, first, second, penultimate, last) { + if (astUtils.isTokenOnSameLine(first, second)) { + const firstSpaced = sourceCode.isSpaceBetweenTokens(first, second); + + if (options.spaced && !firstSpaced) { + reportRequiredBeginningSpace(node, first); + } + if (!options.spaced && firstSpaced) { + reportNoBeginningSpace(node, first); + } + } + + if (astUtils.isTokenOnSameLine(penultimate, last)) { + const shouldCheckPenultimate = ( + options.arraysInObjectsException && astUtils.isClosingBracketToken(penultimate) || + options.objectsInObjectsException && astUtils.isClosingBraceToken(penultimate) + ); + const penultimateType = shouldCheckPenultimate && sourceCode.getNodeByRangeIndex(penultimate.range[0]).type; + + const closingCurlyBraceMustBeSpaced = ( + options.arraysInObjectsException && penultimateType === "ArrayExpression" || + options.objectsInObjectsException && (penultimateType === "ObjectExpression" || penultimateType === "ObjectPattern") + ) ? !options.spaced : options.spaced; + + const lastSpaced = sourceCode.isSpaceBetweenTokens(penultimate, last); + + if (closingCurlyBraceMustBeSpaced && !lastSpaced) { + reportRequiredEndingSpace(node, last); + } + if (!closingCurlyBraceMustBeSpaced && lastSpaced) { + reportNoEndingSpace(node, last); + } + } + } + + /** + * Gets '}' token of an object node. + * + * Because the last token of object patterns might be a type annotation, + * this traverses tokens preceded by the last property, then returns the + * first '}' token. + * + * @param {ASTNode} node - The node to get. This node is an + * ObjectExpression or an ObjectPattern. And this node has one or + * more properties. + * @returns {Token} '}' token. + */ + function getClosingBraceOfObject(node) { + const lastProperty = node.properties[node.properties.length - 1]; + + return sourceCode.getTokenAfter(lastProperty, astUtils.isClosingBraceToken); + } + + /** + * Reports a given object node if spacing in curly braces is invalid. + * @param {ASTNode} node - An ObjectExpression or ObjectPattern node to check. + * @returns {void} + */ + function checkForObject(node) { + if (node.properties.length === 0) { + return; + } + + const first = sourceCode.getFirstToken(node), + last = getClosingBraceOfObject(node), + second = sourceCode.getTokenAfter(first), + penultimate = sourceCode.getTokenBefore(last); + + validateBraceSpacing(node, first, second, penultimate, last); + } + + /** + * Reports a given import node if spacing in curly braces is invalid. + * @param {ASTNode} node - An ImportDeclaration node to check. + * @returns {void} + */ + function checkForImport(node) { + if (node.specifiers.length === 0) { + return; + } + + let firstSpecifier = node.specifiers[0]; + const lastSpecifier = node.specifiers[node.specifiers.length - 1]; + + if (lastSpecifier.type !== "ImportSpecifier") { + return; + } + if (firstSpecifier.type !== "ImportSpecifier") { + firstSpecifier = node.specifiers[1]; + } + + const first = sourceCode.getTokenBefore(firstSpecifier), + last = sourceCode.getTokenAfter(lastSpecifier, astUtils.isNotCommaToken), + second = sourceCode.getTokenAfter(first), + penultimate = sourceCode.getTokenBefore(last); + + validateBraceSpacing(node, first, second, penultimate, last); + } + + /** + * Reports a given export node if spacing in curly braces is invalid. + * @param {ASTNode} node - An ExportNamedDeclaration node to check. + * @returns {void} + */ + function checkForExport(node) { + if (node.specifiers.length === 0) { + return; + } + + const firstSpecifier = node.specifiers[0], + lastSpecifier = node.specifiers[node.specifiers.length - 1], + first = sourceCode.getTokenBefore(firstSpecifier), + last = sourceCode.getTokenAfter(lastSpecifier, astUtils.isNotCommaToken), + second = sourceCode.getTokenAfter(first), + penultimate = sourceCode.getTokenBefore(last); + + validateBraceSpacing(node, first, second, penultimate, last); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + // var {x} = y; + ObjectPattern: checkForObject, + + // var y = {x: 'y'} + ObjectExpression: checkForObject, + + // import {y} from 'x'; + ImportDeclaration: checkForImport, + + // export {name} from 'yo'; + ExportNamedDeclaration: checkForExport + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/object-property-newline.js b/tools/node_modules/eslint/lib/rules/object-property-newline.js new file mode 100644 index 0000000000..0463e389ab --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/object-property-newline.js @@ -0,0 +1,84 @@ +/** + * @fileoverview Rule to enforce placing object properties on separate lines. + * @author Vitor Balocco + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce placing object properties on separate lines", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + allowMultiplePropertiesPerLine: { + type: "boolean" + } + }, + additionalProperties: false + } + ], + + fixable: "whitespace" + }, + + create(context) { + const allowSameLine = context.options[0] && Boolean(context.options[0].allowMultiplePropertiesPerLine); + const errorMessage = allowSameLine + ? "Object properties must go on a new line if they aren't all on the same line." + : "Object properties must go on a new line."; + + const sourceCode = context.getSourceCode(); + + return { + ObjectExpression(node) { + if (allowSameLine) { + if (node.properties.length > 1) { + const firstTokenOfFirstProperty = sourceCode.getFirstToken(node.properties[0]); + const lastTokenOfLastProperty = sourceCode.getLastToken(node.properties[node.properties.length - 1]); + + if (firstTokenOfFirstProperty.loc.end.line === lastTokenOfLastProperty.loc.start.line) { + + // All keys and values are on the same line + return; + } + } + } + + for (let i = 1; i < node.properties.length; i++) { + const lastTokenOfPreviousProperty = sourceCode.getLastToken(node.properties[i - 1]); + const firstTokenOfCurrentProperty = sourceCode.getFirstToken(node.properties[i]); + + if (lastTokenOfPreviousProperty.loc.end.line === firstTokenOfCurrentProperty.loc.start.line) { + context.report({ + node, + loc: firstTokenOfCurrentProperty.loc.start, + message: errorMessage, + fix(fixer) { + const comma = sourceCode.getTokenBefore(firstTokenOfCurrentProperty); + const rangeAfterComma = [comma.range[1], firstTokenOfCurrentProperty.range[0]]; + + // Don't perform a fix if there are any comments between the comma and the next property. + if (sourceCode.text.slice(rangeAfterComma[0], rangeAfterComma[1]).trim()) { + return null; + } + + return fixer.replaceTextRange(rangeAfterComma, "\n"); + } + }); + } + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/object-shorthand.js b/tools/node_modules/eslint/lib/rules/object-shorthand.js new file mode 100644 index 0000000000..980d0fc35a --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/object-shorthand.js @@ -0,0 +1,454 @@ +/** + * @fileoverview Rule to enforce concise object methods and properties. + * @author Jamund Ferguson + */ + +"use strict"; + +const OPTIONS = { + always: "always", + never: "never", + methods: "methods", + properties: "properties", + consistent: "consistent", + consistentAsNeeded: "consistent-as-needed" +}; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ +module.exports = { + meta: { + docs: { + description: "require or disallow method and property shorthand syntax for object literals", + category: "ECMAScript 6", + recommended: false + }, + + fixable: "code", + + schema: { + anyOf: [ + { + type: "array", + items: [ + { + enum: ["always", "methods", "properties", "never", "consistent", "consistent-as-needed"] + } + ], + minItems: 0, + maxItems: 1 + }, + { + type: "array", + items: [ + { + enum: ["always", "methods", "properties"] + }, + { + type: "object", + properties: { + avoidQuotes: { + type: "boolean" + } + }, + additionalProperties: false + } + ], + minItems: 0, + maxItems: 2 + }, + { + type: "array", + items: [ + { + enum: ["always", "methods"] + }, + { + type: "object", + properties: { + ignoreConstructors: { + type: "boolean" + }, + avoidQuotes: { + type: "boolean" + }, + avoidExplicitReturnArrows: { + type: "boolean" + } + }, + additionalProperties: false + } + ], + minItems: 0, + maxItems: 2 + } + ] + } + }, + + create(context) { + const APPLY = context.options[0] || OPTIONS.always; + const APPLY_TO_METHODS = APPLY === OPTIONS.methods || APPLY === OPTIONS.always; + const APPLY_TO_PROPS = APPLY === OPTIONS.properties || APPLY === OPTIONS.always; + const APPLY_NEVER = APPLY === OPTIONS.never; + const APPLY_CONSISTENT = APPLY === OPTIONS.consistent; + const APPLY_CONSISTENT_AS_NEEDED = APPLY === OPTIONS.consistentAsNeeded; + + const PARAMS = context.options[1] || {}; + const IGNORE_CONSTRUCTORS = PARAMS.ignoreConstructors; + const AVOID_QUOTES = PARAMS.avoidQuotes; + const AVOID_EXPLICIT_RETURN_ARROWS = !!PARAMS.avoidExplicitReturnArrows; + const sourceCode = context.getSourceCode(); + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Determines if the first character of the name is a capital letter. + * @param {string} name The name of the node to evaluate. + * @returns {boolean} True if the first character of the property name is a capital letter, false if not. + * @private + */ + function isConstructor(name) { + const firstChar = name.charAt(0); + + return firstChar === firstChar.toUpperCase(); + } + + /** + * Determines if the property can have a shorthand form. + * @param {ASTNode} property Property AST node + * @returns {boolean} True if the property can have a shorthand form + * @private + * + */ + function canHaveShorthand(property) { + return (property.kind !== "set" && property.kind !== "get" && property.type !== "SpreadProperty" && property.type !== "ExperimentalSpreadProperty"); + } + + /** + * Checks whether a node is a string literal. + * @param {ASTNode} node - Any AST node. + * @returns {boolean} `true` if it is a string literal. + */ + function isStringLiteral(node) { + return node.type === "Literal" && typeof node.value === "string"; + } + + /** + * Determines if the property is a shorthand or not. + * @param {ASTNode} property Property AST node + * @returns {boolean} True if the property is considered shorthand, false if not. + * @private + * + */ + function isShorthand(property) { + + // property.method is true when `{a(){}}`. + return (property.shorthand || property.method); + } + + /** + * Determines if the property's key and method or value are named equally. + * @param {ASTNode} property Property AST node + * @returns {boolean} True if the key and value are named equally, false if not. + * @private + * + */ + function isRedundant(property) { + const value = property.value; + + if (value.type === "FunctionExpression") { + return !value.id; // Only anonymous should be shorthand method. + } + if (value.type === "Identifier") { + return astUtils.getStaticPropertyName(property) === value.name; + } + + return false; + } + + /** + * Ensures that an object's properties are consistently shorthand, or not shorthand at all. + * @param {ASTNode} node Property AST node + * @param {boolean} checkRedundancy Whether to check longform redundancy + * @returns {void} + * + */ + function checkConsistency(node, checkRedundancy) { + + // We are excluding getters/setters and spread properties as they are considered neither longform nor shorthand. + const properties = node.properties.filter(canHaveShorthand); + + // Do we still have properties left after filtering the getters and setters? + if (properties.length > 0) { + const shorthandProperties = properties.filter(isShorthand); + + /* + * If we do not have an equal number of longform properties as + * shorthand properties, we are using the annotations inconsistently + */ + if (shorthandProperties.length !== properties.length) { + + // We have at least 1 shorthand property + if (shorthandProperties.length > 0) { + context.report({ node, message: "Unexpected mix of shorthand and non-shorthand properties." }); + } else if (checkRedundancy) { + + /* + * If all properties of the object contain a method or value with a name matching it's key, + * all the keys are redundant. + */ + const canAlwaysUseShorthand = properties.every(isRedundant); + + if (canAlwaysUseShorthand) { + context.report({ node, message: "Expected shorthand for all properties." }); + } + } + } + } + } + + /** + * Fixes a FunctionExpression node by making it into a shorthand property. + * @param {SourceCodeFixer} fixer The fixer object + * @param {ASTNode} node A `Property` node that has a `FunctionExpression` or `ArrowFunctionExpression` as its value + * @returns {Object} A fix for this node + */ + function makeFunctionShorthand(fixer, node) { + const firstKeyToken = node.computed + ? sourceCode.getFirstToken(node, astUtils.isOpeningBracketToken) + : sourceCode.getFirstToken(node.key); + const lastKeyToken = node.computed + ? sourceCode.getFirstTokenBetween(node.key, node.value, astUtils.isClosingBracketToken) + : sourceCode.getLastToken(node.key); + const keyText = sourceCode.text.slice(firstKeyToken.range[0], lastKeyToken.range[1]); + let keyPrefix = ""; + + if (node.value.generator) { + keyPrefix = "*"; + } else if (node.value.async) { + keyPrefix = "async "; + } + + if (node.value.type === "FunctionExpression") { + const functionToken = sourceCode.getTokens(node.value).find(token => token.type === "Keyword" && token.value === "function"); + const tokenBeforeParams = node.value.generator ? sourceCode.getTokenAfter(functionToken) : functionToken; + + return fixer.replaceTextRange( + [firstKeyToken.range[0], node.range[1]], + keyPrefix + keyText + sourceCode.text.slice(tokenBeforeParams.range[1], node.value.range[1]) + ); + } + const arrowToken = sourceCode.getTokens(node.value).find(token => token.value === "=>"); + const tokenBeforeArrow = sourceCode.getTokenBefore(arrowToken); + const hasParensAroundParameters = tokenBeforeArrow.type === "Punctuator" && tokenBeforeArrow.value === ")"; + const oldParamText = sourceCode.text.slice(sourceCode.getFirstToken(node.value, node.value.async ? 1 : 0).range[0], tokenBeforeArrow.range[1]); + const newParamText = hasParensAroundParameters ? oldParamText : `(${oldParamText})`; + + return fixer.replaceTextRange( + [firstKeyToken.range[0], node.range[1]], + keyPrefix + keyText + newParamText + sourceCode.text.slice(arrowToken.range[1], node.value.range[1]) + ); + + } + + /** + * Fixes a FunctionExpression node by making it into a longform property. + * @param {SourceCodeFixer} fixer The fixer object + * @param {ASTNode} node A `Property` node that has a `FunctionExpression` as its value + * @returns {Object} A fix for this node + */ + function makeFunctionLongform(fixer, node) { + const firstKeyToken = node.computed ? sourceCode.getTokens(node).find(token => token.value === "[") : sourceCode.getFirstToken(node.key); + const lastKeyToken = node.computed ? sourceCode.getTokensBetween(node.key, node.value).find(token => token.value === "]") : sourceCode.getLastToken(node.key); + const keyText = sourceCode.text.slice(firstKeyToken.range[0], lastKeyToken.range[1]); + let functionHeader = "function"; + + if (node.value.generator) { + functionHeader = "function*"; + } else if (node.value.async) { + functionHeader = "async function"; + } + + return fixer.replaceTextRange([node.range[0], lastKeyToken.range[1]], `${keyText}: ${functionHeader}`); + } + + /* + * To determine whether a given arrow function has a lexical identifier (`this`, `arguments`, `super`, or `new.target`), + * create a stack of functions that define these identifiers (i.e. all functions except arrow functions) as the AST is + * traversed. Whenever a new function is encountered, create a new entry on the stack (corresponding to a different lexical + * scope of `this`), and whenever a function is exited, pop that entry off the stack. When an arrow function is entered, + * keep a reference to it on the current stack entry, and remove that reference when the arrow function is exited. + * When a lexical identifier is encountered, mark all the arrow functions on the current stack entry by adding them + * to an `arrowsWithLexicalIdentifiers` set. Any arrow function in that set will not be reported by this rule, + * because converting it into a method would change the value of one of the lexical identifiers. + */ + const lexicalScopeStack = []; + const arrowsWithLexicalIdentifiers = new WeakSet(); + const argumentsIdentifiers = new WeakSet(); + + /** + * Enters a function. This creates a new lexical identifier scope, so a new Set of arrow functions is pushed onto the stack. + * Also, this marks all `arguments` identifiers so that they can be detected later. + * @returns {void} + */ + function enterFunction() { + lexicalScopeStack.unshift(new Set()); + context.getScope().variables.filter(variable => variable.name === "arguments").forEach(variable => { + variable.references.map(ref => ref.identifier).forEach(identifier => argumentsIdentifiers.add(identifier)); + }); + } + + /** + * Exits a function. This pops the current set of arrow functions off the lexical scope stack. + * @returns {void} + */ + function exitFunction() { + lexicalScopeStack.shift(); + } + + /** + * Marks the current function as having a lexical keyword. This implies that all arrow functions + * in the current lexical scope contain a reference to this lexical keyword. + * @returns {void} + */ + function reportLexicalIdentifier() { + lexicalScopeStack[0].forEach(arrowFunction => arrowsWithLexicalIdentifiers.add(arrowFunction)); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + Program: enterFunction, + FunctionDeclaration: enterFunction, + FunctionExpression: enterFunction, + "Program:exit": exitFunction, + "FunctionDeclaration:exit": exitFunction, + "FunctionExpression:exit": exitFunction, + + ArrowFunctionExpression(node) { + lexicalScopeStack[0].add(node); + }, + "ArrowFunctionExpression:exit"(node) { + lexicalScopeStack[0].delete(node); + }, + + ThisExpression: reportLexicalIdentifier, + Super: reportLexicalIdentifier, + MetaProperty(node) { + if (node.meta.name === "new" && node.property.name === "target") { + reportLexicalIdentifier(); + } + }, + Identifier(node) { + if (argumentsIdentifiers.has(node)) { + reportLexicalIdentifier(); + } + }, + + ObjectExpression(node) { + if (APPLY_CONSISTENT) { + checkConsistency(node, false); + } else if (APPLY_CONSISTENT_AS_NEEDED) { + checkConsistency(node, true); + } + }, + + "Property:exit"(node) { + const isConciseProperty = node.method || node.shorthand; + + // Ignore destructuring assignment + if (node.parent.type === "ObjectPattern") { + return; + } + + // getters and setters are ignored + if (node.kind === "get" || node.kind === "set") { + return; + } + + // only computed methods can fail the following checks + if (node.computed && node.value.type !== "FunctionExpression" && node.value.type !== "ArrowFunctionExpression") { + return; + } + + //-------------------------------------------------------------- + // Checks for property/method shorthand. + if (isConciseProperty) { + if (node.method && (APPLY_NEVER || AVOID_QUOTES && isStringLiteral(node.key))) { + const message = APPLY_NEVER ? "Expected longform method syntax." : "Expected longform method syntax for string literal keys."; + + // { x() {} } should be written as { x: function() {} } + context.report({ + node, + message, + fix: fixer => makeFunctionLongform(fixer, node) + }); + } else if (APPLY_NEVER) { + + // { x } should be written as { x: x } + context.report({ + node, + message: "Expected longform property syntax.", + fix: fixer => fixer.insertTextAfter(node.key, `: ${node.key.name}`) + }); + } + } else if (APPLY_TO_METHODS && !node.value.id && (node.value.type === "FunctionExpression" || node.value.type === "ArrowFunctionExpression")) { + if (IGNORE_CONSTRUCTORS && node.key.type === "Identifier" && isConstructor(node.key.name)) { + return; + } + if (AVOID_QUOTES && isStringLiteral(node.key)) { + return; + } + + // {[x]: function(){}} should be written as {[x]() {}} + if (node.value.type === "FunctionExpression" || + node.value.type === "ArrowFunctionExpression" && + node.value.body.type === "BlockStatement" && + AVOID_EXPLICIT_RETURN_ARROWS && + !arrowsWithLexicalIdentifiers.has(node.value) + ) { + context.report({ + node, + message: "Expected method shorthand.", + fix: fixer => makeFunctionShorthand(fixer, node) + }); + } + } else if (node.value.type === "Identifier" && node.key.name === node.value.name && APPLY_TO_PROPS) { + + // {x: x} should be written as {x} + context.report({ + node, + message: "Expected property shorthand.", + fix(fixer) { + return fixer.replaceText(node, node.value.name); + } + }); + } else if (node.value.type === "Identifier" && node.key.type === "Literal" && node.key.value === node.value.name && APPLY_TO_PROPS) { + if (AVOID_QUOTES) { + return; + } + + // {"x": x} should be written as {x} + context.report({ + node, + message: "Expected property shorthand.", + fix(fixer) { + return fixer.replaceText(node, node.value.name); + } + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/one-var-declaration-per-line.js b/tools/node_modules/eslint/lib/rules/one-var-declaration-per-line.js new file mode 100644 index 0000000000..61b505c82d --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/one-var-declaration-per-line.js @@ -0,0 +1,86 @@ +/** + * @fileoverview Rule to check multiple var declarations per line + * @author Alberto RodrÃguez + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require or disallow newlines around variable declarations", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + enum: ["always", "initializations"] + } + ], + + fixable: "whitespace" + }, + + create(context) { + + const ERROR_MESSAGE = "Expected variable declaration to be on a new line."; + const always = context.options[0] === "always"; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + + /** + * Determine if provided keyword is a variant of for specifiers + * @private + * @param {string} keyword - keyword to test + * @returns {boolean} True if `keyword` is a variant of for specifier + */ + function isForTypeSpecifier(keyword) { + return keyword === "ForStatement" || keyword === "ForInStatement" || keyword === "ForOfStatement"; + } + + /** + * Checks newlines around variable declarations. + * @private + * @param {ASTNode} node - `VariableDeclaration` node to test + * @returns {void} + */ + function checkForNewLine(node) { + if (isForTypeSpecifier(node.parent.type)) { + return; + } + + const declarations = node.declarations; + let prev; + + declarations.forEach(current => { + if (prev && prev.loc.end.line === current.loc.start.line) { + if (always || prev.init || current.init) { + context.report({ + node, + message: ERROR_MESSAGE, + loc: current.loc.start, + fix: fixer => fixer.insertTextBefore(current, "\n") + }); + } + } + prev = current; + }); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + VariableDeclaration: checkForNewLine + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/one-var.js b/tools/node_modules/eslint/lib/rules/one-var.js new file mode 100644 index 0000000000..9e40d4ea6f --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/one-var.js @@ -0,0 +1,367 @@ +/** + * @fileoverview A rule to control the use of single variable declarations. + * @author Ian Christian Myers + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce variables to be declared either together or separately in functions", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + oneOf: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + var: { + enum: ["always", "never"] + }, + let: { + enum: ["always", "never"] + }, + const: { + enum: ["always", "never"] + } + }, + additionalProperties: false + }, + { + type: "object", + properties: { + initialized: { + enum: ["always", "never"] + }, + uninitialized: { + enum: ["always", "never"] + } + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + + const MODE_ALWAYS = "always", + MODE_NEVER = "never"; + + const mode = context.options[0] || MODE_ALWAYS; + + const options = { + }; + + if (typeof mode === "string") { // simple options configuration with just a string + options.var = { uninitialized: mode, initialized: mode }; + options.let = { uninitialized: mode, initialized: mode }; + options.const = { uninitialized: mode, initialized: mode }; + } else if (typeof mode === "object") { // options configuration is an object + if (mode.hasOwnProperty("var") && typeof mode.var === "string") { + options.var = { uninitialized: mode.var, initialized: mode.var }; + } + if (mode.hasOwnProperty("let") && typeof mode.let === "string") { + options.let = { uninitialized: mode.let, initialized: mode.let }; + } + if (mode.hasOwnProperty("const") && typeof mode.const === "string") { + options.const = { uninitialized: mode.const, initialized: mode.const }; + } + if (mode.hasOwnProperty("uninitialized")) { + if (!options.var) { + options.var = {}; + } + if (!options.let) { + options.let = {}; + } + if (!options.const) { + options.const = {}; + } + options.var.uninitialized = mode.uninitialized; + options.let.uninitialized = mode.uninitialized; + options.const.uninitialized = mode.uninitialized; + } + if (mode.hasOwnProperty("initialized")) { + if (!options.var) { + options.var = {}; + } + if (!options.let) { + options.let = {}; + } + if (!options.const) { + options.const = {}; + } + options.var.initialized = mode.initialized; + options.let.initialized = mode.initialized; + options.const.initialized = mode.initialized; + } + } + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + const functionStack = []; + const blockStack = []; + + /** + * Increments the blockStack counter. + * @returns {void} + * @private + */ + function startBlock() { + blockStack.push({ + let: { initialized: false, uninitialized: false }, + const: { initialized: false, uninitialized: false } + }); + } + + /** + * Increments the functionStack counter. + * @returns {void} + * @private + */ + function startFunction() { + functionStack.push({ initialized: false, uninitialized: false }); + startBlock(); + } + + /** + * Decrements the blockStack counter. + * @returns {void} + * @private + */ + function endBlock() { + blockStack.pop(); + } + + /** + * Decrements the functionStack counter. + * @returns {void} + * @private + */ + function endFunction() { + functionStack.pop(); + endBlock(); + } + + /** + * Records whether initialized or uninitialized variables are defined in current scope. + * @param {string} statementType node.kind, one of: "var", "let", or "const" + * @param {ASTNode[]} declarations List of declarations + * @param {Object} currentScope The scope being investigated + * @returns {void} + * @private + */ + function recordTypes(statementType, declarations, currentScope) { + for (let i = 0; i < declarations.length; i++) { + if (declarations[i].init === null) { + if (options[statementType] && options[statementType].uninitialized === MODE_ALWAYS) { + currentScope.uninitialized = true; + } + } else { + if (options[statementType] && options[statementType].initialized === MODE_ALWAYS) { + currentScope.initialized = true; + } + } + } + } + + /** + * Determines the current scope (function or block) + * @param {string} statementType node.kind, one of: "var", "let", or "const" + * @returns {Object} The scope associated with statementType + */ + function getCurrentScope(statementType) { + let currentScope; + + if (statementType === "var") { + currentScope = functionStack[functionStack.length - 1]; + } else if (statementType === "let") { + currentScope = blockStack[blockStack.length - 1].let; + } else if (statementType === "const") { + currentScope = blockStack[blockStack.length - 1].const; + } + return currentScope; + } + + /** + * Counts the number of initialized and uninitialized declarations in a list of declarations + * @param {ASTNode[]} declarations List of declarations + * @returns {Object} Counts of 'uninitialized' and 'initialized' declarations + * @private + */ + function countDeclarations(declarations) { + const counts = { uninitialized: 0, initialized: 0 }; + + for (let i = 0; i < declarations.length; i++) { + if (declarations[i].init === null) { + counts.uninitialized++; + } else { + counts.initialized++; + } + } + return counts; + } + + /** + * Determines if there is more than one var statement in the current scope. + * @param {string} statementType node.kind, one of: "var", "let", or "const" + * @param {ASTNode[]} declarations List of declarations + * @returns {boolean} Returns true if it is the first var declaration, false if not. + * @private + */ + function hasOnlyOneStatement(statementType, declarations) { + + const declarationCounts = countDeclarations(declarations); + const currentOptions = options[statementType] || {}; + const currentScope = getCurrentScope(statementType); + + if (currentOptions.uninitialized === MODE_ALWAYS && currentOptions.initialized === MODE_ALWAYS) { + if (currentScope.uninitialized || currentScope.initialized) { + return false; + } + } + + if (declarationCounts.uninitialized > 0) { + if (currentOptions.uninitialized === MODE_ALWAYS && currentScope.uninitialized) { + return false; + } + } + if (declarationCounts.initialized > 0) { + if (currentOptions.initialized === MODE_ALWAYS && currentScope.initialized) { + return false; + } + } + recordTypes(statementType, declarations, currentScope); + return true; + } + + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + Program: startFunction, + FunctionDeclaration: startFunction, + FunctionExpression: startFunction, + ArrowFunctionExpression: startFunction, + BlockStatement: startBlock, + ForStatement: startBlock, + ForInStatement: startBlock, + ForOfStatement: startBlock, + SwitchStatement: startBlock, + + VariableDeclaration(node) { + const parent = node.parent; + const type = node.kind; + + if (!options[type]) { + return; + } + + const declarations = node.declarations; + const declarationCounts = countDeclarations(declarations); + + // always + if (!hasOnlyOneStatement(type, declarations)) { + if (options[type].initialized === MODE_ALWAYS && options[type].uninitialized === MODE_ALWAYS) { + context.report({ + node, + message: "Combine this with the previous '{{type}}' statement.", + data: { + type + } + }); + } else { + if (options[type].initialized === MODE_ALWAYS) { + context.report({ + node, + message: "Combine this with the previous '{{type}}' statement with initialized variables.", + data: { + type + } + }); + } + if (options[type].uninitialized === MODE_ALWAYS) { + if (node.parent.left === node && (node.parent.type === "ForInStatement" || node.parent.type === "ForOfStatement")) { + return; + } + context.report({ + node, + message: "Combine this with the previous '{{type}}' statement with uninitialized variables.", + data: { + type + } + }); + } + } + } + + // never + if (parent.type !== "ForStatement" || parent.init !== node) { + const totalDeclarations = declarationCounts.uninitialized + declarationCounts.initialized; + + if (totalDeclarations > 1) { + + if (options[type].initialized === MODE_NEVER && options[type].uninitialized === MODE_NEVER) { + + // both initialized and uninitialized + context.report({ + node, + message: "Split '{{type}}' declarations into multiple statements.", + data: { + type + } + }); + } else if (options[type].initialized === MODE_NEVER && declarationCounts.initialized > 0) { + + // initialized + context.report({ + node, + message: "Split initialized '{{type}}' declarations into multiple statements.", + data: { + type + } + }); + } else if (options[type].uninitialized === MODE_NEVER && declarationCounts.uninitialized > 0) { + + // uninitialized + context.report({ + node, + message: "Split uninitialized '{{type}}' declarations into multiple statements.", + data: { + type + } + }); + } + } + } + }, + + "ForStatement:exit": endBlock, + "ForOfStatement:exit": endBlock, + "ForInStatement:exit": endBlock, + "SwitchStatement:exit": endBlock, + "BlockStatement:exit": endBlock, + "Program:exit": endFunction, + "FunctionDeclaration:exit": endFunction, + "FunctionExpression:exit": endFunction, + "ArrowFunctionExpression:exit": endFunction + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/operator-assignment.js b/tools/node_modules/eslint/lib/rules/operator-assignment.js new file mode 100644 index 0000000000..f776609f5e --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/operator-assignment.js @@ -0,0 +1,206 @@ +/** + * @fileoverview Rule to replace assignment expressions with operator assignment + * @author Brandon Mills + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether an operator is commutative and has an operator assignment + * shorthand form. + * @param {string} operator Operator to check. + * @returns {boolean} True if the operator is commutative and has a + * shorthand form. + */ +function isCommutativeOperatorWithShorthand(operator) { + return ["*", "&", "^", "|"].indexOf(operator) >= 0; +} + +/** + * Checks whether an operator is not commuatative and has an operator assignment + * shorthand form. + * @param {string} operator Operator to check. + * @returns {boolean} True if the operator is not commuatative and has + * a shorthand form. + */ +function isNonCommutativeOperatorWithShorthand(operator) { + return ["+", "-", "/", "%", "<<", ">>", ">>>", "**"].indexOf(operator) >= 0; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +/** + * Checks whether two expressions reference the same value. For example: + * a = a + * a.b = a.b + * a[0] = a[0] + * a['b'] = a['b'] + * @param {ASTNode} a Left side of the comparison. + * @param {ASTNode} b Right side of the comparison. + * @returns {boolean} True if both sides match and reference the same value. + */ +function same(a, b) { + if (a.type !== b.type) { + return false; + } + + switch (a.type) { + case "Identifier": + return a.name === b.name; + + case "Literal": + return a.value === b.value; + + case "MemberExpression": + + /* + * x[0] = x[0] + * x[y] = x[y] + * x.y = x.y + */ + return same(a.object, b.object) && same(a.property, b.property); + + default: + return false; + } +} + +/** + * Determines if the left side of a node can be safely fixed (i.e. if it activates the same getters/setters and) + * toString calls regardless of whether assignment shorthand is used) + * @param {ASTNode} node The node on the left side of the expression + * @returns {boolean} `true` if the node can be fixed + */ +function canBeFixed(node) { + return node.type === "Identifier" || + node.type === "MemberExpression" && node.object.type === "Identifier" && (!node.computed || node.property.type === "Literal"); +} + +module.exports = { + meta: { + docs: { + description: "require or disallow assignment operator shorthand where possible", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + enum: ["always", "never"] + } + ], + + fixable: "code" + }, + + create(context) { + + const sourceCode = context.getSourceCode(); + + /** + * Returns the operator token of an AssignmentExpression or BinaryExpression + * @param {ASTNode} node An AssignmentExpression or BinaryExpression node + * @returns {Token} The operator token in the node + */ + function getOperatorToken(node) { + return sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator); + } + + /** + * Ensures that an assignment uses the shorthand form where possible. + * @param {ASTNode} node An AssignmentExpression node. + * @returns {void} + */ + function verify(node) { + if (node.operator !== "=" || node.right.type !== "BinaryExpression") { + return; + } + + const left = node.left; + const expr = node.right; + const operator = expr.operator; + + if (isCommutativeOperatorWithShorthand(operator) || isNonCommutativeOperatorWithShorthand(operator)) { + if (same(left, expr.left)) { + context.report({ + node, + message: "Assignment can be replaced with operator assignment.", + fix(fixer) { + if (canBeFixed(left)) { + const equalsToken = getOperatorToken(node); + const operatorToken = getOperatorToken(expr); + const leftText = sourceCode.getText().slice(node.range[0], equalsToken.range[0]); + const rightText = sourceCode.getText().slice(operatorToken.range[1], node.right.range[1]); + + return fixer.replaceText(node, `${leftText}${expr.operator}=${rightText}`); + } + return null; + } + }); + } else if (same(left, expr.right) && isCommutativeOperatorWithShorthand(operator)) { + + /* + * This case can't be fixed safely. + * If `a` and `b` both have custom valueOf() behavior, then fixing `a = b * a` to `a *= b` would + * change the execution order of the valueOf() functions. + */ + context.report({ + node, + message: "Assignment can be replaced with operator assignment." + }); + } + } + } + + /** + * Warns if an assignment expression uses operator assignment shorthand. + * @param {ASTNode} node An AssignmentExpression node. + * @returns {void} + */ + function prohibit(node) { + if (node.operator !== "=") { + context.report({ + node, + message: "Unexpected operator assignment shorthand.", + fix(fixer) { + if (canBeFixed(node.left)) { + const operatorToken = getOperatorToken(node); + const leftText = sourceCode.getText().slice(node.range[0], operatorToken.range[0]); + const newOperator = node.operator.slice(0, -1); + let rightText; + + // If this change would modify precedence (e.g. `foo *= bar + 1` => `foo = foo * (bar + 1)`), parenthesize the right side. + if ( + astUtils.getPrecedence(node.right) <= astUtils.getPrecedence({ type: "BinaryExpression", operator: newOperator }) && + !astUtils.isParenthesised(sourceCode, node.right) + ) { + rightText = `${sourceCode.text.slice(operatorToken.range[1], node.right.range[0])}(${sourceCode.getText(node.right)})`; + } else { + rightText = sourceCode.text.slice(operatorToken.range[1], node.range[1]); + } + + return fixer.replaceText(node, `${leftText}= ${leftText}${newOperator}${rightText}`); + } + return null; + } + }); + } + } + + return { + AssignmentExpression: context.options[0] !== "never" ? verify : prohibit + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/operator-linebreak.js b/tools/node_modules/eslint/lib/rules/operator-linebreak.js new file mode 100644 index 0000000000..271cbb35c1 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/operator-linebreak.js @@ -0,0 +1,252 @@ +/** + * @fileoverview Operator linebreak - enforces operator linebreak style of two types: after and before + * @author Benoît Zugmeyer + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent linebreak style for operators", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + enum: ["after", "before", "none", null] + }, + { + type: "object", + properties: { + overrides: { + type: "object", + properties: { + anyOf: { + type: "string", + enum: ["after", "before", "none", "ignore"] + } + } + } + }, + additionalProperties: false + } + ], + + fixable: "code" + }, + + create(context) { + + const usedDefaultGlobal = !context.options[0]; + const globalStyle = context.options[0] || "after"; + const options = context.options[1] || {}; + const styleOverrides = options.overrides ? Object.assign({}, options.overrides) : {}; + + if (usedDefaultGlobal && !styleOverrides["?"]) { + styleOverrides["?"] = "before"; + } + + if (usedDefaultGlobal && !styleOverrides[":"]) { + styleOverrides[":"] = "before"; + } + + const sourceCode = context.getSourceCode(); + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Gets a fixer function to fix rule issues + * @param {Token} operatorToken The operator token of an expression + * @param {string} desiredStyle The style for the rule. One of 'before', 'after', 'none' + * @returns {Function} A fixer function + */ + function getFixer(operatorToken, desiredStyle) { + return fixer => { + const tokenBefore = sourceCode.getTokenBefore(operatorToken); + const tokenAfter = sourceCode.getTokenAfter(operatorToken); + const textBefore = sourceCode.text.slice(tokenBefore.range[1], operatorToken.range[0]); + const textAfter = sourceCode.text.slice(operatorToken.range[1], tokenAfter.range[0]); + const hasLinebreakBefore = !astUtils.isTokenOnSameLine(tokenBefore, operatorToken); + const hasLinebreakAfter = !astUtils.isTokenOnSameLine(operatorToken, tokenAfter); + let newTextBefore, newTextAfter; + + if (hasLinebreakBefore !== hasLinebreakAfter && desiredStyle !== "none") { + + // If there is a comment before and after the operator, don't do a fix. + if (sourceCode.getTokenBefore(operatorToken, { includeComments: true }) !== tokenBefore && + sourceCode.getTokenAfter(operatorToken, { includeComments: true }) !== tokenAfter) { + + return null; + } + + /* + * If there is only one linebreak and it's on the wrong side of the operator, swap the text before and after the operator. + * foo && + * bar + * would get fixed to + * foo + * && bar + */ + newTextBefore = textAfter; + newTextAfter = textBefore; + } else { + const LINEBREAK_REGEX = astUtils.createGlobalLinebreakMatcher(); + + // Otherwise, if no linebreak is desired and no comments interfere, replace the linebreaks with empty strings. + newTextBefore = desiredStyle === "before" || textBefore.trim() ? textBefore : textBefore.replace(LINEBREAK_REGEX, ""); + newTextAfter = desiredStyle === "after" || textAfter.trim() ? textAfter : textAfter.replace(LINEBREAK_REGEX, ""); + + // If there was no change (due to interfering comments), don't output a fix. + if (newTextBefore === textBefore && newTextAfter === textAfter) { + return null; + } + } + + if (newTextAfter === "" && tokenAfter.type === "Punctuator" && "+-".includes(operatorToken.value) && tokenAfter.value === operatorToken.value) { + + // To avoid accidentally creating a ++ or -- operator, insert a space if the operator is a +/- and the following token is a unary +/-. + newTextAfter += " "; + } + + return fixer.replaceTextRange([tokenBefore.range[1], tokenAfter.range[0]], newTextBefore + operatorToken.value + newTextAfter); + }; + } + + /** + * Checks the operator placement + * @param {ASTNode} node The node to check + * @param {ASTNode} leftSide The node that comes before the operator in `node` + * @private + * @returns {void} + */ + function validateNode(node, leftSide) { + + /* + * When the left part of a binary expression is a single expression wrapped in + * parentheses (ex: `(a) + b`), leftToken will be the last token of the expression + * and operatorToken will be the closing parenthesis. + * The leftToken should be the last closing parenthesis, and the operatorToken + * should be the token right after that. + */ + const operatorToken = sourceCode.getTokenAfter(leftSide, astUtils.isNotClosingParenToken); + const leftToken = sourceCode.getTokenBefore(operatorToken); + const rightToken = sourceCode.getTokenAfter(operatorToken); + const operator = operatorToken.value; + const operatorStyleOverride = styleOverrides[operator]; + const style = operatorStyleOverride || globalStyle; + const fix = getFixer(operatorToken, style); + + // if single line + if (astUtils.isTokenOnSameLine(leftToken, operatorToken) && + astUtils.isTokenOnSameLine(operatorToken, rightToken)) { + + // do nothing. + + } else if (operatorStyleOverride !== "ignore" && !astUtils.isTokenOnSameLine(leftToken, operatorToken) && + !astUtils.isTokenOnSameLine(operatorToken, rightToken)) { + + // lone operator + context.report({ + node, + loc: { + line: operatorToken.loc.end.line, + column: operatorToken.loc.end.column + }, + message: "Bad line breaking before and after '{{operator}}'.", + data: { + operator + }, + fix + }); + + } else if (style === "before" && astUtils.isTokenOnSameLine(leftToken, operatorToken)) { + + context.report({ + node, + loc: { + line: operatorToken.loc.end.line, + column: operatorToken.loc.end.column + }, + message: "'{{operator}}' should be placed at the beginning of the line.", + data: { + operator + }, + fix + }); + + } else if (style === "after" && astUtils.isTokenOnSameLine(operatorToken, rightToken)) { + + context.report({ + node, + loc: { + line: operatorToken.loc.end.line, + column: operatorToken.loc.end.column + }, + message: "'{{operator}}' should be placed at the end of the line.", + data: { + operator + }, + fix + }); + + } else if (style === "none") { + + context.report({ + node, + loc: { + line: operatorToken.loc.end.line, + column: operatorToken.loc.end.column + }, + message: "There should be no line break before or after '{{operator}}'.", + data: { + operator + }, + fix + }); + + } + } + + /** + * Validates a binary expression using `validateNode` + * @param {BinaryExpression|LogicalExpression|AssignmentExpression} node node to be validated + * @returns {void} + */ + function validateBinaryExpression(node) { + validateNode(node, node.left); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + BinaryExpression: validateBinaryExpression, + LogicalExpression: validateBinaryExpression, + AssignmentExpression: validateBinaryExpression, + VariableDeclarator(node) { + if (node.init) { + validateNode(node, node.id); + } + }, + ConditionalExpression(node) { + validateNode(node, node.test); + validateNode(node, node.consequent); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/padded-blocks.js b/tools/node_modules/eslint/lib/rules/padded-blocks.js new file mode 100644 index 0000000000..ad65882ac6 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/padded-blocks.js @@ -0,0 +1,256 @@ +/** + * @fileoverview A rule to ensure blank lines within blocks. + * @author Mathias Schreck <https://github.com/lo1tuma> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require or disallow padding within blocks", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + oneOf: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + blocks: { + enum: ["always", "never"] + }, + switches: { + enum: ["always", "never"] + }, + classes: { + enum: ["always", "never"] + } + }, + additionalProperties: false, + minProperties: 1 + } + ] + } + ] + }, + + create(context) { + const options = {}; + const config = context.options[0] || "always"; + + if (typeof config === "string") { + const shouldHavePadding = config === "always"; + + options.blocks = shouldHavePadding; + options.switches = shouldHavePadding; + options.classes = shouldHavePadding; + } else { + if (config.hasOwnProperty("blocks")) { + options.blocks = config.blocks === "always"; + } + if (config.hasOwnProperty("switches")) { + options.switches = config.switches === "always"; + } + if (config.hasOwnProperty("classes")) { + options.classes = config.classes === "always"; + } + } + + const ALWAYS_MESSAGE = "Block must be padded by blank lines.", + NEVER_MESSAGE = "Block must not be padded by blank lines."; + + const sourceCode = context.getSourceCode(); + + /** + * Gets the open brace token from a given node. + * @param {ASTNode} node - A BlockStatement or SwitchStatement node from which to get the open brace. + * @returns {Token} The token of the open brace. + */ + function getOpenBrace(node) { + if (node.type === "SwitchStatement") { + return sourceCode.getTokenBefore(node.cases[0]); + } + return sourceCode.getFirstToken(node); + } + + /** + * Checks if the given parameter is a comment node + * @param {ASTNode|Token} node An AST node or token + * @returns {boolean} True if node is a comment + */ + function isComment(node) { + return node.type === "Line" || node.type === "Block"; + } + + /** + * Checks if there is padding between two tokens + * @param {Token} first The first token + * @param {Token} second The second token + * @returns {boolean} True if there is at least a line between the tokens + */ + function isPaddingBetweenTokens(first, second) { + return second.loc.start.line - first.loc.end.line >= 2; + } + + + /** + * Checks if the given token has a blank line after it. + * @param {Token} token The token to check. + * @returns {boolean} Whether or not the token is followed by a blank line. + */ + function getFirstBlockToken(token) { + let prev, + first = token; + + do { + prev = first; + first = sourceCode.getTokenAfter(first, { includeComments: true }); + } while (isComment(first) && first.loc.start.line === prev.loc.end.line); + + return first; + } + + /** + * Checks if the given token is preceeded by a blank line. + * @param {Token} token The token to check + * @returns {boolean} Whether or not the token is preceeded by a blank line + */ + function getLastBlockToken(token) { + let last = token, + next; + + do { + next = last; + last = sourceCode.getTokenBefore(last, { includeComments: true }); + } while (isComment(last) && last.loc.end.line === next.loc.start.line); + + return last; + } + + /** + * Checks if a node should be padded, according to the rule config. + * @param {ASTNode} node The AST node to check. + * @returns {boolean} True if the node should be padded, false otherwise. + */ + function requirePaddingFor(node) { + switch (node.type) { + case "BlockStatement": + return options.blocks; + case "SwitchStatement": + return options.switches; + case "ClassBody": + return options.classes; + + /* istanbul ignore next */ + default: + throw new Error("unreachable"); + } + } + + /** + * Checks the given BlockStatement node to be padded if the block is not empty. + * @param {ASTNode} node The AST node of a BlockStatement. + * @returns {void} undefined. + */ + function checkPadding(node) { + const openBrace = getOpenBrace(node), + firstBlockToken = getFirstBlockToken(openBrace), + tokenBeforeFirst = sourceCode.getTokenBefore(firstBlockToken, { includeComments: true }), + closeBrace = sourceCode.getLastToken(node), + lastBlockToken = getLastBlockToken(closeBrace), + tokenAfterLast = sourceCode.getTokenAfter(lastBlockToken, { includeComments: true }), + blockHasTopPadding = isPaddingBetweenTokens(tokenBeforeFirst, firstBlockToken), + blockHasBottomPadding = isPaddingBetweenTokens(lastBlockToken, tokenAfterLast); + + if (requirePaddingFor(node)) { + if (!blockHasTopPadding) { + context.report({ + node, + loc: { line: tokenBeforeFirst.loc.start.line, column: tokenBeforeFirst.loc.start.column }, + fix(fixer) { + return fixer.insertTextAfter(tokenBeforeFirst, "\n"); + }, + message: ALWAYS_MESSAGE + }); + } + if (!blockHasBottomPadding) { + context.report({ + node, + loc: { line: tokenAfterLast.loc.end.line, column: tokenAfterLast.loc.end.column - 1 }, + fix(fixer) { + return fixer.insertTextBefore(tokenAfterLast, "\n"); + }, + message: ALWAYS_MESSAGE + }); + } + } else { + if (blockHasTopPadding) { + + context.report({ + node, + loc: { line: tokenBeforeFirst.loc.start.line, column: tokenBeforeFirst.loc.start.column }, + fix(fixer) { + return fixer.replaceTextRange([tokenBeforeFirst.range[1], firstBlockToken.range[0] - firstBlockToken.loc.start.column], "\n"); + }, + message: NEVER_MESSAGE + }); + } + + if (blockHasBottomPadding) { + + context.report({ + node, + loc: { line: tokenAfterLast.loc.end.line, column: tokenAfterLast.loc.end.column - 1 }, + message: NEVER_MESSAGE, + fix(fixer) { + return fixer.replaceTextRange([lastBlockToken.range[1], tokenAfterLast.range[0] - tokenAfterLast.loc.start.column], "\n"); + } + }); + } + } + } + + const rule = {}; + + if (options.hasOwnProperty("switches")) { + rule.SwitchStatement = function(node) { + if (node.cases.length === 0) { + return; + } + checkPadding(node); + }; + } + + if (options.hasOwnProperty("blocks")) { + rule.BlockStatement = function(node) { + if (node.body.length === 0) { + return; + } + checkPadding(node); + }; + } + + if (options.hasOwnProperty("classes")) { + rule.ClassBody = function(node) { + if (node.body.length === 0) { + return; + } + checkPadding(node); + }; + } + + return rule; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/padding-line-between-statements.js b/tools/node_modules/eslint/lib/rules/padding-line-between-statements.js new file mode 100644 index 0000000000..a89c49decf --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/padding-line-between-statements.js @@ -0,0 +1,589 @@ +/** + * @fileoverview Rule to require or disallow newlines between statements + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const LT = `[${Array.from(astUtils.LINEBREAKS).join("")}]`; +const PADDING_LINE_SEQUENCE = new RegExp( + String.raw`^(\s*?${LT})\s*${LT}(\s*;?)$` +); +const CJS_EXPORT = /^(?:module\s*\.\s*)?exports(?:\s*\.|\s*\[|$)/; +const CJS_IMPORT = /^require\(/; + +/** + * Creates tester which check if a node starts with specific keyword. + * + * @param {string} keyword The keyword to test. + * @returns {Object} the created tester. + * @private + */ +function newKeywordTester(keyword) { + return { + test: (node, sourceCode) => + sourceCode.getFirstToken(node).value === keyword + }; +} + +/** + * Creates tester which check if a node is specific type. + * + * @param {string} type The node type to test. + * @returns {Object} the created tester. + * @private + */ +function newNodeTypeTester(type) { + return { + test: node => + node.type === type + }; +} + +/** + * Checks the given node is an expression statement of IIFE. + * + * @param {ASTNode} node The node to check. + * @returns {boolean} `true` if the node is an expression statement of IIFE. + * @private + */ +function isIIFEStatement(node) { + if (node.type === "ExpressionStatement") { + let call = node.expression; + + if (call.type === "UnaryExpression") { + call = call.argument; + } + return call.type === "CallExpression" && astUtils.isFunction(call.callee); + } + return false; +} + +/** + * Checks whether the given node is a block-like statement. + * This checks the last token of the node is the closing brace of a block. + * + * @param {SourceCode} sourceCode The source code to get tokens. + * @param {ASTNode} node The node to check. + * @returns {boolean} `true` if the node is a block-like statement. + * @private + */ +function isBlockLikeStatement(sourceCode, node) { + + // do-while with a block is a block-like statement. + if (node.type === "DoWhileStatement" && node.body.type === "BlockStatement") { + return true; + } + + /* + * IIFE is a block-like statement specially from + * JSCS#disallowPaddingNewLinesAfterBlocks. + */ + if (isIIFEStatement(node)) { + return true; + } + + // Checks the last token is a closing brace of blocks. + const lastToken = sourceCode.getLastToken(node, astUtils.isNotSemicolonToken); + const belongingNode = lastToken && astUtils.isClosingBraceToken(lastToken) + ? sourceCode.getNodeByRangeIndex(lastToken.range[0]) + : null; + + return Boolean(belongingNode) && ( + belongingNode.type === "BlockStatement" || + belongingNode.type === "SwitchStatement" + ); +} + +/** + * Check whether the given node is a directive or not. + * @param {ASTNode} node The node to check. + * @param {SourceCode} sourceCode The source code object to get tokens. + * @returns {boolean} `true` if the node is a directive. + */ +function isDirective(node, sourceCode) { + return ( + node.type === "ExpressionStatement" && + ( + node.parent.type === "Program" || + ( + node.parent.type === "BlockStatement" && + astUtils.isFunction(node.parent.parent) + ) + ) && + node.expression.type === "Literal" && + typeof node.expression.value === "string" && + !astUtils.isParenthesised(sourceCode, node.expression) + ); +} + +/** + * Check whether the given node is a part of directive prologue or not. + * @param {ASTNode} node The node to check. + * @param {SourceCode} sourceCode The source code object to get tokens. + * @returns {boolean} `true` if the node is a part of directive prologue. + */ +function isDirectivePrologue(node, sourceCode) { + if (isDirective(node, sourceCode)) { + for (const sibling of node.parent.body) { + if (sibling === node) { + break; + } + if (!isDirective(sibling, sourceCode)) { + return false; + } + } + return true; + } + return false; +} + +/** + * Gets the actual last token. + * + * If a semicolon is semicolon-less style's semicolon, this ignores it. + * For example: + * + * foo() + * ;[1, 2, 3].forEach(bar) + * + * @param {SourceCode} sourceCode The source code to get tokens. + * @param {ASTNode} node The node to get. + * @returns {Token} The actual last token. + * @private + */ +function getActualLastToken(sourceCode, node) { + const semiToken = sourceCode.getLastToken(node); + const prevToken = sourceCode.getTokenBefore(semiToken); + const nextToken = sourceCode.getTokenAfter(semiToken); + const isSemicolonLessStyle = Boolean( + prevToken && + nextToken && + prevToken.range[0] >= node.range[0] && + astUtils.isSemicolonToken(semiToken) && + semiToken.loc.start.line !== prevToken.loc.end.line && + semiToken.loc.end.line === nextToken.loc.start.line + ); + + return isSemicolonLessStyle ? prevToken : semiToken; +} + +/** + * This returns the concatenation of the first 2 captured strings. + * @param {string} _ Unused. Whole matched string. + * @param {string} trailingSpaces The trailing spaces of the first line. + * @param {string} indentSpaces The indentation spaces of the last line. + * @returns {string} The concatenation of trailingSpaces and indentSpaces. + * @private + */ +function replacerToRemovePaddingLines(_, trailingSpaces, indentSpaces) { + return trailingSpaces + indentSpaces; +} + +/** + * Check and report statements for `any` configuration. + * It does nothing. + * + * @returns {void} + * @private + */ +function verifyForAny() { +} + +/** + * Check and report statements for `never` configuration. + * This autofix removes blank lines between the given 2 statements. + * However, if comments exist between 2 blank lines, it does not remove those + * blank lines automatically. + * + * @param {RuleContext} context The rule context to report. + * @param {ASTNode} _ Unused. The previous node to check. + * @param {ASTNode} nextNode The next node to check. + * @param {Array<Token[]>} paddingLines The array of token pairs that blank + * lines exist between the pair. + * @returns {void} + * @private + */ +function verifyForNever(context, _, nextNode, paddingLines) { + if (paddingLines.length === 0) { + return; + } + + context.report({ + node: nextNode, + message: "Unexpected blank line before this statement.", + fix(fixer) { + if (paddingLines.length >= 2) { + return null; + } + + const prevToken = paddingLines[0][0]; + const nextToken = paddingLines[0][1]; + const start = prevToken.range[1]; + const end = nextToken.range[0]; + const text = context.getSourceCode().text + .slice(start, end) + .replace(PADDING_LINE_SEQUENCE, replacerToRemovePaddingLines); + + return fixer.replaceTextRange([start, end], text); + } + }); +} + +/** + * Check and report statements for `always` configuration. + * This autofix inserts a blank line between the given 2 statements. + * If the `prevNode` has trailing comments, it inserts a blank line after the + * trailing comments. + * + * @param {RuleContext} context The rule context to report. + * @param {ASTNode} prevNode The previous node to check. + * @param {ASTNode} nextNode The next node to check. + * @param {Array<Token[]>} paddingLines The array of token pairs that blank + * lines exist between the pair. + * @returns {void} + * @private + */ +function verifyForAlways(context, prevNode, nextNode, paddingLines) { + if (paddingLines.length > 0) { + return; + } + + context.report({ + node: nextNode, + message: "Expected blank line before this statement.", + fix(fixer) { + const sourceCode = context.getSourceCode(); + let prevToken = getActualLastToken(sourceCode, prevNode); + const nextToken = sourceCode.getFirstTokenBetween( + prevToken, + nextNode, + { + includeComments: true, + + /** + * Skip the trailing comments of the previous node. + * This inserts a blank line after the last trailing comment. + * + * For example: + * + * foo(); // trailing comment. + * // comment. + * bar(); + * + * Get fixed to: + * + * foo(); // trailing comment. + * + * // comment. + * bar(); + * + * @param {Token} token The token to check. + * @returns {boolean} `true` if the token is not a trailing comment. + * @private + */ + filter(token) { + if (astUtils.isTokenOnSameLine(prevToken, token)) { + prevToken = token; + return false; + } + return true; + } + } + ) || nextNode; + const insertText = astUtils.isTokenOnSameLine(prevToken, nextToken) + ? "\n\n" + : "\n"; + + return fixer.insertTextAfter(prevToken, insertText); + } + }); +} + +/** + * Types of blank lines. + * `any`, `never`, and `always` are defined. + * Those have `verify` method to check and report statements. + * @private + */ +const PaddingTypes = { + any: { verify: verifyForAny }, + never: { verify: verifyForNever }, + always: { verify: verifyForAlways } +}; + +/** + * Types of statements. + * Those have `test` method to check it matches to the given statement. + * @private + */ +const StatementTypes = { + "*": { test: () => true }, + "block-like": { + test: (node, sourceCode) => isBlockLikeStatement(sourceCode, node) + }, + "cjs-export": { + test: (node, sourceCode) => + node.type === "ExpressionStatement" && + node.expression.type === "AssignmentExpression" && + CJS_EXPORT.test(sourceCode.getText(node.expression.left)) + }, + "cjs-import": { + test: (node, sourceCode) => + node.type === "VariableDeclaration" && + node.declarations.length > 0 && + Boolean(node.declarations[0].init) && + CJS_IMPORT.test(sourceCode.getText(node.declarations[0].init)) + }, + directive: { + test: isDirectivePrologue + }, + expression: { + test: (node, sourceCode) => + node.type === "ExpressionStatement" && + !isDirectivePrologue(node, sourceCode) + }, + "multiline-block-like": { + test: (node, sourceCode) => + node.loc.start.line !== node.loc.end.line && + isBlockLikeStatement(sourceCode, node) + }, + + block: newNodeTypeTester("BlockStatement"), + empty: newNodeTypeTester("EmptyStatement"), + + break: newKeywordTester("break"), + case: newKeywordTester("case"), + class: newKeywordTester("class"), + const: newKeywordTester("const"), + continue: newKeywordTester("continue"), + debugger: newKeywordTester("debugger"), + default: newKeywordTester("default"), + do: newKeywordTester("do"), + export: newKeywordTester("export"), + for: newKeywordTester("for"), + function: newKeywordTester("function"), + if: newKeywordTester("if"), + import: newKeywordTester("import"), + let: newKeywordTester("let"), + return: newKeywordTester("return"), + switch: newKeywordTester("switch"), + throw: newKeywordTester("throw"), + try: newKeywordTester("try"), + var: newKeywordTester("var"), + while: newKeywordTester("while"), + with: newKeywordTester("with") +}; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require or disallow padding lines between statements", + category: "Stylistic Issues", + recommended: false + }, + fixable: "whitespace", + schema: { + definitions: { + paddingType: { + enum: Object.keys(PaddingTypes) + }, + statementType: { + anyOf: [ + { enum: Object.keys(StatementTypes) }, + { + type: "array", + items: { enum: Object.keys(StatementTypes) }, + minItems: 1, + uniqueItems: true, + additionalItems: false + } + ] + } + }, + type: "array", + items: { + type: "object", + properties: { + blankLine: { $ref: "#/definitions/paddingType" }, + prev: { $ref: "#/definitions/statementType" }, + next: { $ref: "#/definitions/statementType" } + }, + additionalProperties: false, + required: ["blankLine", "prev", "next"] + }, + additionalItems: false + } + }, + + create(context) { + const sourceCode = context.getSourceCode(); + const configureList = context.options || []; + let scopeInfo = null; + + /** + * Processes to enter to new scope. + * This manages the current previous statement. + * @returns {void} + * @private + */ + function enterScope() { + scopeInfo = { + upper: scopeInfo, + prevNode: null + }; + } + + /** + * Processes to exit from the current scope. + * @returns {void} + * @private + */ + function exitScope() { + scopeInfo = scopeInfo.upper; + } + + /** + * Checks whether the given node matches the given type. + * + * @param {ASTNode} node The statement node to check. + * @param {string|string[]} type The statement type to check. + * @returns {boolean} `true` if the statement node matched the type. + * @private + */ + function match(node, type) { + while (node.type === "LabeledStatement") { + node = node.body; + } + if (Array.isArray(type)) { + return type.some(match.bind(null, node)); + } + return StatementTypes[type].test(node, sourceCode); + } + + /** + * Finds the last matched configure from configureList. + * + * @param {ASTNode} prevNode The previous statement to match. + * @param {ASTNode} nextNode The current statement to match. + * @returns {Object} The tester of the last matched configure. + * @private + */ + function getPaddingType(prevNode, nextNode) { + for (let i = configureList.length - 1; i >= 0; --i) { + const configure = configureList[i]; + const matched = + match(prevNode, configure.prev) && + match(nextNode, configure.next); + + if (matched) { + return PaddingTypes[configure.blankLine]; + } + } + return PaddingTypes.any; + } + + /** + * Gets padding line sequences between the given 2 statements. + * Comments are separators of the padding line sequences. + * + * @param {ASTNode} prevNode The previous statement to count. + * @param {ASTNode} nextNode The current statement to count. + * @returns {Array<Token[]>} The array of token pairs. + * @private + */ + function getPaddingLineSequences(prevNode, nextNode) { + const pairs = []; + let prevToken = getActualLastToken(sourceCode, prevNode); + + if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) { + do { + const token = sourceCode.getTokenAfter( + prevToken, + { includeComments: true } + ); + + if (token.loc.start.line - prevToken.loc.end.line >= 2) { + pairs.push([prevToken, token]); + } + prevToken = token; + + } while (prevToken.range[0] < nextNode.range[0]); + } + + return pairs; + } + + /** + * Verify padding lines between the given node and the previous node. + * + * @param {ASTNode} node The node to verify. + * @returns {void} + * @private + */ + function verify(node) { + const parentType = node.parent.type; + const validParent = + astUtils.STATEMENT_LIST_PARENTS.has(parentType) || + parentType === "SwitchStatement"; + + if (!validParent) { + return; + } + + // Save this node as the current previous statement. + const prevNode = scopeInfo.prevNode; + + // Verify. + if (prevNode) { + const type = getPaddingType(prevNode, node); + const paddingLines = getPaddingLineSequences(prevNode, node); + + type.verify(context, prevNode, node, paddingLines); + } + + scopeInfo.prevNode = node; + } + + /** + * Verify padding lines between the given node and the previous node. + * Then process to enter to new scope. + * + * @param {ASTNode} node The node to verify. + * @returns {void} + * @private + */ + function verifyThenEnterScope(node) { + verify(node); + enterScope(); + } + + return { + Program: enterScope, + BlockStatement: enterScope, + SwitchStatement: enterScope, + "Program:exit": exitScope, + "BlockStatement:exit": exitScope, + "SwitchStatement:exit": exitScope, + + ":statement": verify, + + SwitchCase: verifyThenEnterScope, + "SwitchCase:exit": exitScope + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/prefer-arrow-callback.js b/tools/node_modules/eslint/lib/rules/prefer-arrow-callback.js new file mode 100644 index 0000000000..31ae2859fe --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/prefer-arrow-callback.js @@ -0,0 +1,304 @@ +/** + * @fileoverview A rule to suggest using arrow functions as callbacks. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether or not a given variable is a function name. + * @param {eslint-scope.Variable} variable - A variable to check. + * @returns {boolean} `true` if the variable is a function name. + */ +function isFunctionName(variable) { + return variable && variable.defs[0].type === "FunctionName"; +} + +/** + * Checks whether or not a given MetaProperty node equals to a given value. + * @param {ASTNode} node - A MetaProperty node to check. + * @param {string} metaName - The name of `MetaProperty.meta`. + * @param {string} propertyName - The name of `MetaProperty.property`. + * @returns {boolean} `true` if the node is the specific value. + */ +function checkMetaProperty(node, metaName, propertyName) { + return node.meta.name === metaName && node.property.name === propertyName; +} + +/** + * Gets the variable object of `arguments` which is defined implicitly. + * @param {eslint-scope.Scope} scope - A scope to get. + * @returns {eslint-scope.Variable} The found variable object. + */ +function getVariableOfArguments(scope) { + const variables = scope.variables; + + for (let i = 0; i < variables.length; ++i) { + const variable = variables[i]; + + if (variable.name === "arguments") { + + /* + * If there was a parameter which is named "arguments", the + * implicit "arguments" is not defined. + * So does fast return with null. + */ + return (variable.identifiers.length === 0) ? variable : null; + } + } + + /* istanbul ignore next */ + return null; +} + +/** + * Checkes whether or not a given node is a callback. + * @param {ASTNode} node - A node to check. + * @returns {Object} + * {boolean} retv.isCallback - `true` if the node is a callback. + * {boolean} retv.isLexicalThis - `true` if the node is with `.bind(this)`. + */ +function getCallbackInfo(node) { + const retv = { isCallback: false, isLexicalThis: false }; + let parent = node.parent; + + while (node) { + switch (parent.type) { + + // Checks parents recursively. + + case "LogicalExpression": + case "ConditionalExpression": + break; + + // Checks whether the parent node is `.bind(this)` call. + case "MemberExpression": + if (parent.object === node && + !parent.property.computed && + parent.property.type === "Identifier" && + parent.property.name === "bind" && + parent.parent.type === "CallExpression" && + parent.parent.callee === parent + ) { + retv.isLexicalThis = ( + parent.parent.arguments.length === 1 && + parent.parent.arguments[0].type === "ThisExpression" + ); + parent = parent.parent; + } else { + return retv; + } + break; + + // Checks whether the node is a callback. + case "CallExpression": + case "NewExpression": + if (parent.callee !== node) { + retv.isCallback = true; + } + return retv; + + default: + return retv; + } + + node = parent; + parent = parent.parent; + } + + /* istanbul ignore next */ + throw new Error("unreachable"); +} + +/** + * Checks whether a simple list of parameters contains any duplicates. This does not handle complex + * parameter lists (e.g. with destructuring), since complex parameter lists are a SyntaxError with duplicate + * parameter names anyway. Instead, it always returns `false` for complex parameter lists. + * @param {ASTNode[]} paramsList The list of parameters for a function + * @returns {boolean} `true` if the list of parameters contains any duplicates + */ +function hasDuplicateParams(paramsList) { + return paramsList.every(param => param.type === "Identifier") && paramsList.length !== new Set(paramsList.map(param => param.name)).size; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require using arrow functions for callbacks", + category: "ECMAScript 6", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + allowNamedFunctions: { + type: "boolean" + }, + allowUnboundThis: { + type: "boolean" + } + }, + additionalProperties: false + } + ], + + fixable: "code" + }, + + create(context) { + const options = context.options[0] || {}; + + const allowUnboundThis = options.allowUnboundThis !== false; // default to true + const allowNamedFunctions = options.allowNamedFunctions; + const sourceCode = context.getSourceCode(); + + /* + * {Array<{this: boolean, super: boolean, meta: boolean}>} + * - this - A flag which shows there are one or more ThisExpression. + * - super - A flag which shows there are one or more Super. + * - meta - A flag which shows there are one or more MethProperty. + */ + let stack = []; + + /** + * Pushes new function scope with all `false` flags. + * @returns {void} + */ + function enterScope() { + stack.push({ this: false, super: false, meta: false }); + } + + /** + * Pops a function scope from the stack. + * @returns {{this: boolean, super: boolean, meta: boolean}} The information of the last scope. + */ + function exitScope() { + return stack.pop(); + } + + return { + + // Reset internal state. + Program() { + stack = []; + }, + + // If there are below, it cannot replace with arrow functions merely. + ThisExpression() { + const info = stack[stack.length - 1]; + + if (info) { + info.this = true; + } + }, + + Super() { + const info = stack[stack.length - 1]; + + if (info) { + info.super = true; + } + }, + + MetaProperty(node) { + const info = stack[stack.length - 1]; + + if (info && checkMetaProperty(node, "new", "target")) { + info.meta = true; + } + }, + + // To skip nested scopes. + FunctionDeclaration: enterScope, + "FunctionDeclaration:exit": exitScope, + + // Main. + FunctionExpression: enterScope, + "FunctionExpression:exit"(node) { + const scopeInfo = exitScope(); + + // Skip named function expressions + if (allowNamedFunctions && node.id && node.id.name) { + return; + } + + // Skip generators. + if (node.generator) { + return; + } + + // Skip recursive functions. + const nameVar = context.getDeclaredVariables(node)[0]; + + if (isFunctionName(nameVar) && nameVar.references.length > 0) { + return; + } + + // Skip if it's using arguments. + const variable = getVariableOfArguments(context.getScope()); + + if (variable && variable.references.length > 0) { + return; + } + + // Reports if it's a callback which can replace with arrows. + const callbackInfo = getCallbackInfo(node); + + if (callbackInfo.isCallback && + (!allowUnboundThis || !scopeInfo.this || callbackInfo.isLexicalThis) && + !scopeInfo.super && + !scopeInfo.meta + ) { + context.report({ + node, + message: "Unexpected function expression.", + fix(fixer) { + if ((!callbackInfo.isLexicalThis && scopeInfo.this) || hasDuplicateParams(node.params)) { + + /* + * If the callback function does not have .bind(this) and contains a reference to `this`, there + * is no way to determine what `this` should be, so don't perform any fixes. + * If the callback function has duplicates in its list of parameters (possible in sloppy mode), + * don't replace it with an arrow function, because this is a SyntaxError with arrow functions. + */ + return null; + } + + const paramsLeftParen = node.params.length ? sourceCode.getTokenBefore(node.params[0]) : sourceCode.getTokenBefore(node.body, 1); + const paramsRightParen = sourceCode.getTokenBefore(node.body); + const asyncKeyword = node.async ? "async " : ""; + const paramsFullText = sourceCode.text.slice(paramsLeftParen.range[0], paramsRightParen.range[1]); + const arrowFunctionText = `${asyncKeyword}${paramsFullText} => ${sourceCode.getText(node.body)}`; + + /* + * If the callback function has `.bind(this)`, replace it with an arrow function and remove the binding. + * Otherwise, just replace the arrow function itself. + */ + const replacedNode = callbackInfo.isLexicalThis ? node.parent.parent : node; + + /* + * If the replaced node is part of a BinaryExpression, LogicalExpression, or MemberExpression, then + * the arrow function needs to be parenthesized, because `foo || () => {}` is invalid syntax even + * though `foo || function() {}` is valid. + */ + const needsParens = replacedNode.parent.type !== "CallExpression" && replacedNode.parent.type !== "ConditionalExpression"; + const replacementText = needsParens ? `(${arrowFunctionText})` : arrowFunctionText; + + return fixer.replaceText(replacedNode, replacementText); + } + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/prefer-const.js b/tools/node_modules/eslint/lib/rules/prefer-const.js new file mode 100644 index 0000000000..a8cf3b7ef6 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/prefer-const.js @@ -0,0 +1,321 @@ +/** + * @fileoverview A rule to suggest using of const declaration for variables that are never reassigned after declared. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const PATTERN_TYPE = /^(?:.+?Pattern|RestElement|SpreadProperty|ExperimentalRestProperty|Property)$/; +const DECLARATION_HOST_TYPE = /^(?:Program|BlockStatement|SwitchCase)$/; +const DESTRUCTURING_HOST_TYPE = /^(?:VariableDeclarator|AssignmentExpression)$/; + +/** + * Adds multiple items to the tail of an array. + * + * @param {any[]} array - A destination to add. + * @param {any[]} values - Items to be added. + * @returns {void} + */ +const pushAll = Function.apply.bind(Array.prototype.push); + +/** + * Checks whether a given node is located at `ForStatement.init` or not. + * + * @param {ASTNode} node - A node to check. + * @returns {boolean} `true` if the node is located at `ForStatement.init`. + */ +function isInitOfForStatement(node) { + return node.parent.type === "ForStatement" && node.parent.init === node; +} + +/** + * Checks whether a given Identifier node becomes a VariableDeclaration or not. + * + * @param {ASTNode} identifier - An Identifier node to check. + * @returns {boolean} `true` if the node can become a VariableDeclaration. + */ +function canBecomeVariableDeclaration(identifier) { + let node = identifier.parent; + + while (PATTERN_TYPE.test(node.type)) { + node = node.parent; + } + + return ( + node.type === "VariableDeclarator" || + ( + node.type === "AssignmentExpression" && + node.parent.type === "ExpressionStatement" && + DECLARATION_HOST_TYPE.test(node.parent.parent.type) + ) + ); +} + +/** + * Gets an identifier node of a given variable. + * + * If the initialization exists or one or more reading references exist before + * the first assignment, the identifier node is the node of the declaration. + * Otherwise, the identifier node is the node of the first assignment. + * + * If the variable should not change to const, this function returns null. + * - If the variable is reassigned. + * - If the variable is never initialized nor assigned. + * - If the variable is initialized in a different scope from the declaration. + * - If the unique assignment of the variable cannot change to a declaration. + * e.g. `if (a) b = 1` / `return (b = 1)` + * - If the variable is declared in the global scope and `eslintUsed` is `true`. + * `/*exported foo` directive comment makes such variables. This rule does not + * warn such variables because this rule cannot distinguish whether the + * exported variables are reassigned or not. + * + * @param {eslint-scope.Variable} variable - A variable to get. + * @param {boolean} ignoreReadBeforeAssign - + * The value of `ignoreReadBeforeAssign` option. + * @returns {ASTNode|null} + * An Identifier node if the variable should change to const. + * Otherwise, null. + */ +function getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign) { + if (variable.eslintUsed && variable.scope.type === "global") { + return null; + } + + // Finds the unique WriteReference. + let writer = null; + let isReadBeforeInit = false; + const references = variable.references; + + for (let i = 0; i < references.length; ++i) { + const reference = references[i]; + + if (reference.isWrite()) { + const isReassigned = ( + writer !== null && + writer.identifier !== reference.identifier + ); + + if (isReassigned) { + return null; + } + writer = reference; + + } else if (reference.isRead() && writer === null) { + if (ignoreReadBeforeAssign) { + return null; + } + isReadBeforeInit = true; + } + } + + /* + * If the assignment is from a different scope, ignore it. + * If the assignment cannot change to a declaration, ignore it. + */ + const shouldBeConst = ( + writer !== null && + writer.from === variable.scope && + canBecomeVariableDeclaration(writer.identifier) + ); + + if (!shouldBeConst) { + return null; + } + if (isReadBeforeInit) { + return variable.defs[0].name; + } + return writer.identifier; +} + +/** + * Gets the VariableDeclarator/AssignmentExpression node that a given reference + * belongs to. + * This is used to detect a mix of reassigned and never reassigned in a + * destructuring. + * + * @param {eslint-scope.Reference} reference - A reference to get. + * @returns {ASTNode|null} A VariableDeclarator/AssignmentExpression node or + * null. + */ +function getDestructuringHost(reference) { + if (!reference.isWrite()) { + return null; + } + let node = reference.identifier.parent; + + while (PATTERN_TYPE.test(node.type)) { + node = node.parent; + } + + if (!DESTRUCTURING_HOST_TYPE.test(node.type)) { + return null; + } + return node; +} + +/** + * Groups by the VariableDeclarator/AssignmentExpression node that each + * reference of given variables belongs to. + * This is used to detect a mix of reassigned and never reassigned in a + * destructuring. + * + * @param {eslint-scope.Variable[]} variables - Variables to group by destructuring. + * @param {boolean} ignoreReadBeforeAssign - + * The value of `ignoreReadBeforeAssign` option. + * @returns {Map<ASTNode, ASTNode[]>} Grouped identifier nodes. + */ +function groupByDestructuring(variables, ignoreReadBeforeAssign) { + const identifierMap = new Map(); + + for (let i = 0; i < variables.length; ++i) { + const variable = variables[i]; + const references = variable.references; + const identifier = getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign); + let prevId = null; + + for (let j = 0; j < references.length; ++j) { + const reference = references[j]; + const id = reference.identifier; + + /* + * Avoid counting a reference twice or more for default values of + * destructuring. + */ + if (id === prevId) { + continue; + } + prevId = id; + + // Add the identifier node into the destructuring group. + const group = getDestructuringHost(reference); + + if (group) { + if (identifierMap.has(group)) { + identifierMap.get(group).push(identifier); + } else { + identifierMap.set(group, [identifier]); + } + } + } + } + + return identifierMap; +} + +/** + * Finds the nearest parent of node with a given type. + * + * @param {ASTNode} node – The node to search from. + * @param {string} type – The type field of the parent node. + * @param {Function} shouldStop – a predicate that returns true if the traversal should stop, and false otherwise. + * @returns {ASTNode} The closest ancestor with the specified type; null if no such ancestor exists. + */ +function findUp(node, type, shouldStop) { + if (!node || shouldStop(node)) { + return null; + } + if (node.type === type) { + return node; + } + return findUp(node.parent, type, shouldStop); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require `const` declarations for variables that are never reassigned after declared", + category: "ECMAScript 6", + recommended: false + }, + + fixable: "code", + + schema: [ + { + type: "object", + properties: { + destructuring: { enum: ["any", "all"] }, + ignoreReadBeforeAssign: { type: "boolean" } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const options = context.options[0] || {}; + const sourceCode = context.getSourceCode(); + const checkingMixedDestructuring = options.destructuring !== "all"; + const ignoreReadBeforeAssign = options.ignoreReadBeforeAssign === true; + const variables = []; + + /** + * Reports given identifier nodes if all of the nodes should be declared + * as const. + * + * The argument 'nodes' is an array of Identifier nodes. + * This node is the result of 'getIdentifierIfShouldBeConst()', so it's + * nullable. In simple declaration or assignment cases, the length of + * the array is 1. In destructuring cases, the length of the array can + * be 2 or more. + * + * @param {(eslint-scope.Reference|null)[]} nodes - + * References which are grouped by destructuring to report. + * @returns {void} + */ + function checkGroup(nodes) { + const nodesToReport = nodes.filter(Boolean); + + if (nodes.length && (checkingMixedDestructuring || nodesToReport.length === nodes.length)) { + const varDeclParent = findUp(nodes[0], "VariableDeclaration", parentNode => parentNode.type.endsWith("Statement")); + const shouldFix = varDeclParent && + + /* + * If there are multiple variable declarations, like {let a = 1, b = 2}, then + * do not attempt to fix if one of the declarations should be `const`. It's + * too hard to know how the developer would want to automatically resolve the issue. + */ + varDeclParent.declarations.length === 1 && + + // Don't do a fix unless the variable is initialized (or it's in a for-in or for-of loop) + (varDeclParent.parent.type === "ForInStatement" || varDeclParent.parent.type === "ForOfStatement" || varDeclParent.declarations[0].init) && + + /* + * If options.destucturing is "all", then this warning will not occur unless + * every assignment in the destructuring should be const. In that case, it's safe + * to apply the fix. + */ + nodesToReport.length === nodes.length; + + nodesToReport.forEach(node => { + context.report({ + node, + message: "'{{name}}' is never reassigned. Use 'const' instead.", + data: node, + fix: shouldFix ? fixer => fixer.replaceText(sourceCode.getFirstToken(varDeclParent), "const") : null + }); + }); + } + } + + return { + "Program:exit"() { + groupByDestructuring(variables, ignoreReadBeforeAssign).forEach(checkGroup); + }, + + VariableDeclaration(node) { + if (node.kind === "let" && !isInitOfForStatement(node)) { + pushAll(variables, context.getDeclaredVariables(node)); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/prefer-destructuring.js b/tools/node_modules/eslint/lib/rules/prefer-destructuring.js new file mode 100644 index 0000000000..56c348a478 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/prefer-destructuring.js @@ -0,0 +1,217 @@ +/** + * @fileoverview Prefer destructuring from arrays and objects + * @author Alex LaFroscia + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require destructuring from arrays and/or objects", + category: "ECMAScript 6", + recommended: false + }, + schema: [ + { + + /* + * old support {array: Boolean, object: Boolean} + * new support {VariableDeclarator: {}, AssignmentExpression: {}} + */ + oneOf: [ + { + type: "object", + properties: { + VariableDeclarator: { + type: "object", + properties: { + array: { + type: "boolean" + }, + object: { + type: "boolean" + } + }, + additionalProperties: false + }, + AssignmentExpression: { + type: "object", + properties: { + array: { + type: "boolean" + }, + object: { + type: "boolean" + } + }, + additionalProperties: false + } + }, + additionalProperties: false + }, + { + type: "object", + properties: { + array: { + type: "boolean" + }, + object: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + { + type: "object", + properties: { + enforceForRenamedProperties: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + create(context) { + + const enabledTypes = context.options[0]; + const enforceForRenamedProperties = context.options[1] && context.options[1].enforceForRenamedProperties; + let normalizedOptions = { + VariableDeclarator: { array: true, object: true }, + AssignmentExpression: { array: true, object: true } + }; + + if (enabledTypes) { + normalizedOptions = typeof enabledTypes.array !== "undefined" || typeof enabledTypes.object !== "undefined" + ? { VariableDeclarator: enabledTypes, AssignmentExpression: enabledTypes } + : enabledTypes; + } + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * @param {string} nodeType "AssignmentExpression" or "VariableDeclarator" + * @param {string} destructuringType "array" or "object" + * @returns {boolean} `true` if the destructuring type should be checked for the given node + */ + function shouldCheck(nodeType, destructuringType) { + return normalizedOptions && + normalizedOptions[nodeType] && + normalizedOptions[nodeType][destructuringType]; + } + + /** + * Determines if the given node is accessing an array index + * + * This is used to differentiate array index access from object property + * access. + * + * @param {ASTNode} node the node to evaluate + * @returns {boolean} whether or not the node is an integer + */ + function isArrayIndexAccess(node) { + return Number.isInteger(node.property.value); + } + + /** + * Report that the given node should use destructuring + * + * @param {ASTNode} reportNode the node to report + * @param {string} type the type of destructuring that should have been done + * @returns {void} + */ + function report(reportNode, type) { + context.report({ node: reportNode, message: "Use {{type}} destructuring.", data: { type } }); + } + + /** + * Check that the `prefer-destructuring` rules are followed based on the + * given left- and right-hand side of the assignment. + * + * Pulled out into a separate method so that VariableDeclarators and + * AssignmentExpressions can share the same verification logic. + * + * @param {ASTNode} leftNode the left-hand side of the assignment + * @param {ASTNode} rightNode the right-hand side of the assignment + * @param {ASTNode} reportNode the node to report the error on + * @returns {void} + */ + function performCheck(leftNode, rightNode, reportNode) { + if (rightNode.type !== "MemberExpression" || rightNode.object.type === "Super") { + return; + } + + if (isArrayIndexAccess(rightNode)) { + if (shouldCheck(reportNode.type, "array")) { + report(reportNode, "array"); + } + return; + } + + if (shouldCheck(reportNode.type, "object") && enforceForRenamedProperties) { + report(reportNode, "object"); + return; + } + + if (shouldCheck(reportNode.type, "object")) { + const property = rightNode.property; + + if ((property.type === "Literal" && leftNode.name === property.value) || (property.type === "Identifier" && + leftNode.name === property.name)) { + report(reportNode, "object"); + } + } + } + + /** + * Check if a given variable declarator is coming from an property access + * that should be using destructuring instead + * + * @param {ASTNode} node the variable declarator to check + * @returns {void} + */ + function checkVariableDeclarator(node) { + + // Skip if variable is declared without assignment + if (!node.init) { + return; + } + + // We only care about member expressions past this point + if (node.init.type !== "MemberExpression") { + return; + } + + performCheck(node.id, node.init, node); + } + + /** + * Run the `prefer-destructuring` check on an AssignmentExpression + * + * @param {ASTNode} node the AssignmentExpression node + * @returns {void} + */ + function checkAssigmentExpression(node) { + if (node.operator === "=") { + performCheck(node.left, node.right, node); + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + VariableDeclarator: checkVariableDeclarator, + AssignmentExpression: checkAssigmentExpression + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/prefer-numeric-literals.js b/tools/node_modules/eslint/lib/rules/prefer-numeric-literals.js new file mode 100644 index 0000000000..929e660c66 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/prefer-numeric-literals.js @@ -0,0 +1,112 @@ +/** + * @fileoverview Rule to disallow `parseInt()` in favor of binary, octal, and hexadecimal literals + * @author Annie Zhang, Henry Zhu + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks to see if a CallExpression's callee node is `parseInt` or + * `Number.parseInt`. + * @param {ASTNode} calleeNode The callee node to evaluate. + * @returns {boolean} True if the callee is `parseInt` or `Number.parseInt`, + * false otherwise. + */ +function isParseInt(calleeNode) { + switch (calleeNode.type) { + case "Identifier": + return calleeNode.name === "parseInt"; + case "MemberExpression": + return calleeNode.object.type === "Identifier" && + calleeNode.object.name === "Number" && + calleeNode.property.type === "Identifier" && + calleeNode.property.name === "parseInt"; + + // no default + } + + return false; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow `parseInt()` and `Number.parseInt()` in favor of binary, octal, and hexadecimal literals", + category: "ECMAScript 6", + recommended: false + }, + + schema: [], + + fixable: "code" + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + const radixMap = { + 2: "binary", + 8: "octal", + 16: "hexadecimal" + }; + + const prefixMap = { + 2: "0b", + 8: "0o", + 16: "0x" + }; + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + return { + + CallExpression(node) { + + // doesn't check parseInt() if it doesn't have a radix argument + if (node.arguments.length !== 2) { + return; + } + + // only error if the radix is 2, 8, or 16 + const radixName = radixMap[node.arguments[1].value]; + + if (isParseInt(node.callee) && + radixName && + node.arguments[0].type === "Literal" + ) { + context.report({ + node, + message: "Use {{radixName}} literals instead of {{functionName}}().", + data: { + radixName, + functionName: sourceCode.getText(node.callee) + }, + fix(fixer) { + const newPrefix = prefixMap[node.arguments[1].value]; + + if (+(newPrefix + node.arguments[0].value) !== parseInt(node.arguments[0].value, node.arguments[1].value)) { + + /* + * If the newly-produced literal would be invalid, (e.g. 0b1234), + * or it would yield an incorrect parseInt result for some other reason, don't make a fix. + */ + return null; + } + return fixer.replaceText(node, prefixMap[node.arguments[1].value] + node.arguments[0].value); + } + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/prefer-promise-reject-errors.js b/tools/node_modules/eslint/lib/rules/prefer-promise-reject-errors.js new file mode 100644 index 0000000000..d2a6b5df10 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/prefer-promise-reject-errors.js @@ -0,0 +1,124 @@ +/** + * @fileoverview restrict values that can be used as Promise rejection reasons + * @author Teddy Katz + */ +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require using Error objects as Promise rejection reasons", + category: "Best Practices", + recommended: false + }, + fixable: null, + schema: [ + { + type: "object", + properties: { + allowEmptyReject: { type: "boolean" } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + const ALLOW_EMPTY_REJECT = context.options.length && context.options[0].allowEmptyReject; + + //---------------------------------------------------------------------- + // Helpers + //---------------------------------------------------------------------- + + /** + * Checks the argument of a reject() or Promise.reject() CallExpression, and reports it if it can't be an Error + * @param {ASTNode} callExpression A CallExpression node which is used to reject a Promise + * @returns {void} + */ + function checkRejectCall(callExpression) { + if (!callExpression.arguments.length && ALLOW_EMPTY_REJECT) { + return; + } + if ( + !callExpression.arguments.length || + !astUtils.couldBeError(callExpression.arguments[0]) || + callExpression.arguments[0].type === "Identifier" && callExpression.arguments[0].name === "undefined" + ) { + context.report({ + node: callExpression, + message: "Expected the Promise rejection reason to be an Error." + }); + } + } + + /** + * Determines whether a function call is a Promise.reject() call + * @param {ASTNode} node A CallExpression node + * @returns {boolean} `true` if the call is a Promise.reject() call + */ + function isPromiseRejectCall(node) { + return node.callee.type === "MemberExpression" && + node.callee.object.type === "Identifier" && node.callee.object.name === "Promise" && + node.callee.property.type === "Identifier" && node.callee.property.name === "reject"; + } + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + return { + + // Check `Promise.reject(value)` calls. + CallExpression(node) { + if (isPromiseRejectCall(node)) { + checkRejectCall(node); + } + }, + + /* + * Check for `new Promise((resolve, reject) => {})`, and check for reject() calls. + * This function is run on "NewExpression:exit" instead of "NewExpression" to ensure that + * the nodes in the expression already have the `parent` property. + */ + "NewExpression:exit"(node) { + if ( + node.callee.type === "Identifier" && node.callee.name === "Promise" && + node.arguments.length && astUtils.isFunction(node.arguments[0]) && + node.arguments[0].params.length > 1 && node.arguments[0].params[1].type === "Identifier" + ) { + context.getDeclaredVariables(node.arguments[0]) + + /* + * Find the first variable that matches the second parameter's name. + * If the first parameter has the same name as the second parameter, then the variable will actually + * be "declared" when the first parameter is evaluated, but then it will be immediately overwritten + * by the second parameter. It's not possible for an expression with the variable to be evaluated before + * the variable is overwritten, because functions with duplicate parameters cannot have destructuring or + * default assignments in their parameter lists. Therefore, it's not necessary to explicitly account for + * this case. + */ + .find(variable => variable.name === node.arguments[0].params[1].name) + + // Get the references to that variable. + .references + + // Only check the references that read the parameter's value. + .filter(ref => ref.isRead()) + + // Only check the references that are used as the callee in a function call, e.g. `reject(foo)`. + .filter(ref => ref.identifier.parent.type === "CallExpression" && ref.identifier === ref.identifier.parent.callee) + + // Check the argument of the function call to determine whether it's an Error. + .forEach(ref => checkRejectCall(ref.identifier.parent)); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/prefer-reflect.js b/tools/node_modules/eslint/lib/rules/prefer-reflect.js new file mode 100644 index 0000000000..a47e66c5f5 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/prefer-reflect.js @@ -0,0 +1,119 @@ +/** + * @fileoverview Rule to suggest using "Reflect" api over Function/Object methods + * @author Keith Cirkel <http://keithcirkel.co.uk> + * @deprecated in ESLint v3.9.0 + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require `Reflect` methods where applicable", + category: "ECMAScript 6", + recommended: false, + replacedBy: [] + }, + + deprecated: true, + + schema: [ + { + type: "object", + properties: { + exceptions: { + type: "array", + items: { + enum: [ + "apply", + "call", + "delete", + "defineProperty", + "getOwnPropertyDescriptor", + "getPrototypeOf", + "setPrototypeOf", + "isExtensible", + "getOwnPropertyNames", + "preventExtensions" + ] + }, + uniqueItems: true + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const existingNames = { + apply: "Function.prototype.apply", + call: "Function.prototype.call", + defineProperty: "Object.defineProperty", + getOwnPropertyDescriptor: "Object.getOwnPropertyDescriptor", + getPrototypeOf: "Object.getPrototypeOf", + setPrototypeOf: "Object.setPrototypeOf", + isExtensible: "Object.isExtensible", + getOwnPropertyNames: "Object.getOwnPropertyNames", + preventExtensions: "Object.preventExtensions" + }; + + const reflectSubsitutes = { + apply: "Reflect.apply", + call: "Reflect.apply", + defineProperty: "Reflect.defineProperty", + getOwnPropertyDescriptor: "Reflect.getOwnPropertyDescriptor", + getPrototypeOf: "Reflect.getPrototypeOf", + setPrototypeOf: "Reflect.setPrototypeOf", + isExtensible: "Reflect.isExtensible", + getOwnPropertyNames: "Reflect.getOwnPropertyNames", + preventExtensions: "Reflect.preventExtensions" + }; + + const exceptions = (context.options[0] || {}).exceptions || []; + + /** + * Reports the Reflect violation based on the `existing` and `substitute` + * @param {Object} node The node that violates the rule. + * @param {string} existing The existing method name that has been used. + * @param {string} substitute The Reflect substitute that should be used. + * @returns {void} + */ + function report(node, existing, substitute) { + context.report({ + node, + message: "Avoid using {{existing}}, instead use {{substitute}}.", + data: { + existing, + substitute + } + }); + } + + return { + CallExpression(node) { + const methodName = (node.callee.property || {}).name; + const isReflectCall = (node.callee.object || {}).name === "Reflect"; + const hasReflectSubsitute = reflectSubsitutes.hasOwnProperty(methodName); + const userConfiguredException = exceptions.indexOf(methodName) !== -1; + + if (hasReflectSubsitute && !isReflectCall && !userConfiguredException) { + report(node, existingNames[methodName], reflectSubsitutes[methodName]); + } + }, + UnaryExpression(node) { + const isDeleteOperator = node.operator === "delete"; + const targetsIdentifier = node.argument.type === "Identifier"; + const userConfiguredException = exceptions.indexOf("delete") !== -1; + + if (isDeleteOperator && !targetsIdentifier && !userConfiguredException) { + report(node, "the delete keyword", "Reflect.deleteProperty"); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/prefer-rest-params.js b/tools/node_modules/eslint/lib/rules/prefer-rest-params.js new file mode 100644 index 0000000000..03342371b2 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/prefer-rest-params.js @@ -0,0 +1,111 @@ +/** + * @fileoverview Rule to + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Gets the variable object of `arguments` which is defined implicitly. + * @param {eslint-scope.Scope} scope - A scope to get. + * @returns {eslint-scope.Variable} The found variable object. + */ +function getVariableOfArguments(scope) { + const variables = scope.variables; + + for (let i = 0; i < variables.length; ++i) { + const variable = variables[i]; + + if (variable.name === "arguments") { + + /* + * If there was a parameter which is named "arguments", the implicit "arguments" is not defined. + * So does fast return with null. + */ + return (variable.identifiers.length === 0) ? variable : null; + } + } + + /* istanbul ignore next : unreachable */ + return null; +} + +/** + * Checks if the given reference is not normal member access. + * + * - arguments .... true // not member access + * - arguments[i] .... true // computed member access + * - arguments[0] .... true // computed member access + * - arguments.length .... false // normal member access + * + * @param {eslint-scope.Reference} reference - The reference to check. + * @returns {boolean} `true` if the reference is not normal member access. + */ +function isNotNormalMemberAccess(reference) { + const id = reference.identifier; + const parent = id.parent; + + return !( + parent.type === "MemberExpression" && + parent.object === id && + !parent.computed + ); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require rest parameters instead of `arguments`", + category: "ECMAScript 6", + recommended: false + }, + + schema: [] + }, + + create(context) { + + /** + * Reports a given reference. + * + * @param {eslint-scope.Reference} reference - A reference to report. + * @returns {void} + */ + function report(reference) { + context.report({ + node: reference.identifier, + loc: reference.identifier.loc, + message: "Use the rest parameters instead of 'arguments'." + }); + } + + /** + * Reports references of the implicit `arguments` variable if exist. + * + * @returns {void} + */ + function checkForArguments() { + const argumentsVar = getVariableOfArguments(context.getScope()); + + if (argumentsVar) { + argumentsVar + .references + .filter(isNotNormalMemberAccess) + .forEach(report); + } + } + + return { + "FunctionDeclaration:exit": checkForArguments, + "FunctionExpression:exit": checkForArguments + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/prefer-spread.js b/tools/node_modules/eslint/lib/rules/prefer-spread.js new file mode 100644 index 0000000000..c111d5f98e --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/prefer-spread.js @@ -0,0 +1,96 @@ +/** + * @fileoverview A rule to suggest using of the spread operator instead of `.apply()`. + * @author Toru Nagashima + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether or not a node is a `.apply()` for variadic. + * @param {ASTNode} node - A CallExpression node to check. + * @returns {boolean} Whether or not the node is a `.apply()` for variadic. + */ +function isVariadicApplyCalling(node) { + return ( + node.callee.type === "MemberExpression" && + node.callee.property.type === "Identifier" && + node.callee.property.name === "apply" && + node.callee.computed === false && + node.arguments.length === 2 && + node.arguments[1].type !== "ArrayExpression" && + node.arguments[1].type !== "SpreadElement" + ); +} + + +/** + * Checks whether or not `thisArg` is not changed by `.apply()`. + * @param {ASTNode|null} expectedThis - The node that is the owner of the applied function. + * @param {ASTNode} thisArg - The node that is given to the first argument of the `.apply()`. + * @param {RuleContext} context - The ESLint rule context object. + * @returns {boolean} Whether or not `thisArg` is not changed by `.apply()`. + */ +function isValidThisArg(expectedThis, thisArg, context) { + if (!expectedThis) { + return astUtils.isNullOrUndefined(thisArg); + } + return astUtils.equalTokens(expectedThis, thisArg, context); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require spread operators instead of `.apply()`", + category: "ECMAScript 6", + recommended: false + }, + + schema: [], + + fixable: "code" + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + return { + CallExpression(node) { + if (!isVariadicApplyCalling(node)) { + return; + } + + const applied = node.callee.object; + const expectedThis = (applied.type === "MemberExpression") ? applied.object : null; + const thisArg = node.arguments[0]; + + if (isValidThisArg(expectedThis, thisArg, sourceCode)) { + context.report({ + node, + message: "Use the spread operator instead of '.apply()'.", + fix(fixer) { + if (expectedThis && expectedThis.type !== "Identifier") { + + // Don't fix cases where the `this` value could be a computed expression. + return null; + } + + const propertyDot = sourceCode.getFirstTokenBetween(applied, node.callee.property, token => token.value === "."); + + return fixer.replaceTextRange([propertyDot.range[0], node.range[1]], `(...${sourceCode.getText(node.arguments[1])})`); + } + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/prefer-template.js b/tools/node_modules/eslint/lib/rules/prefer-template.js new file mode 100644 index 0000000000..076ce6a3ea --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/prefer-template.js @@ -0,0 +1,232 @@ +/** + * @fileoverview A rule to suggest using template literals instead of string concatenation. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether or not a given node is a concatenation. + * @param {ASTNode} node - A node to check. + * @returns {boolean} `true` if the node is a concatenation. + */ +function isConcatenation(node) { + return node.type === "BinaryExpression" && node.operator === "+"; +} + +/** + * Gets the top binary expression node for concatenation in parents of a given node. + * @param {ASTNode} node - A node to get. + * @returns {ASTNode} the top binary expression node in parents of a given node. + */ +function getTopConcatBinaryExpression(node) { + while (isConcatenation(node.parent)) { + node = node.parent; + } + return node; +} + +/** + * Checks whether or not a given binary expression has string literals. + * @param {ASTNode} node - A node to check. + * @returns {boolean} `true` if the node has string literals. + */ +function hasStringLiteral(node) { + if (isConcatenation(node)) { + + // `left` is deeper than `right` normally. + return hasStringLiteral(node.right) || hasStringLiteral(node.left); + } + return astUtils.isStringLiteral(node); +} + +/** + * Checks whether or not a given binary expression has non string literals. + * @param {ASTNode} node - A node to check. + * @returns {boolean} `true` if the node has non string literals. + */ +function hasNonStringLiteral(node) { + if (isConcatenation(node)) { + + // `left` is deeper than `right` normally. + return hasNonStringLiteral(node.right) || hasNonStringLiteral(node.left); + } + return !astUtils.isStringLiteral(node); +} + +/** + * Determines whether a given node will start with a template curly expression (`${}`) when being converted to a template literal. + * @param {ASTNode} node The node that will be fixed to a template literal + * @returns {boolean} `true` if the node will start with a template curly. + */ +function startsWithTemplateCurly(node) { + if (node.type === "BinaryExpression") { + return startsWithTemplateCurly(node.left); + } + if (node.type === "TemplateLiteral") { + return node.expressions.length && node.quasis.length && node.quasis[0].range[0] === node.quasis[0].range[1]; + } + return node.type !== "Literal" || typeof node.value !== "string"; +} + +/** + * Determines whether a given node end with a template curly expression (`${}`) when being converted to a template literal. + * @param {ASTNode} node The node that will be fixed to a template literal + * @returns {boolean} `true` if the node will end with a template curly. + */ +function endsWithTemplateCurly(node) { + if (node.type === "BinaryExpression") { + return startsWithTemplateCurly(node.right); + } + if (node.type === "TemplateLiteral") { + return node.expressions.length && node.quasis.length && node.quasis[node.quasis.length - 1].range[0] === node.quasis[node.quasis.length - 1].range[1]; + } + return node.type !== "Literal" || typeof node.value !== "string"; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require template literals instead of string concatenation", + category: "ECMAScript 6", + recommended: false + }, + + schema: [], + + fixable: "code" + }, + + create(context) { + const sourceCode = context.getSourceCode(); + let done = Object.create(null); + + /** + * Gets the non-token text between two nodes, ignoring any other tokens that appear between the two tokens. + * @param {ASTNode} node1 The first node + * @param {ASTNode} node2 The second node + * @returns {string} The text between the nodes, excluding other tokens + */ + function getTextBetween(node1, node2) { + const allTokens = [node1].concat(sourceCode.getTokensBetween(node1, node2)).concat(node2); + const sourceText = sourceCode.getText(); + + return allTokens.slice(0, -1).reduce((accumulator, token, index) => accumulator + sourceText.slice(token.range[1], allTokens[index + 1].range[0]), ""); + } + + /** + * Returns a template literal form of the given node. + * @param {ASTNode} currentNode A node that should be converted to a template literal + * @param {string} textBeforeNode Text that should appear before the node + * @param {string} textAfterNode Text that should appear after the node + * @returns {string} A string form of this node, represented as a template literal + */ + function getTemplateLiteral(currentNode, textBeforeNode, textAfterNode) { + if (currentNode.type === "Literal" && typeof currentNode.value === "string") { + + /* + * If the current node is a string literal, escape any instances of ${ or ` to prevent them from being interpreted + * as a template placeholder. However, if the code already contains a backslash before the ${ or ` + * for some reason, don't add another backslash, because that would change the meaning of the code (it would cause + * an actual backslash character to appear before the dollar sign). + */ + return `\`${currentNode.raw.slice(1, -1).replace(/\\*(\${|`)/g, matched => { + if (matched.lastIndexOf("\\") % 2) { + return `\\${matched}`; + } + return matched; + + // Unescape any quotes that appear in the original Literal that no longer need to be escaped. + }).replace(new RegExp(`\\\\${currentNode.raw[0]}`, "g"), currentNode.raw[0])}\``; + } + + if (currentNode.type === "TemplateLiteral") { + return sourceCode.getText(currentNode); + } + + if (isConcatenation(currentNode) && hasStringLiteral(currentNode) && hasNonStringLiteral(currentNode)) { + const plusSign = sourceCode.getFirstTokenBetween(currentNode.left, currentNode.right, token => token.value === "+"); + const textBeforePlus = getTextBetween(currentNode.left, plusSign); + const textAfterPlus = getTextBetween(plusSign, currentNode.right); + const leftEndsWithCurly = endsWithTemplateCurly(currentNode.left); + const rightStartsWithCurly = startsWithTemplateCurly(currentNode.right); + + if (leftEndsWithCurly) { + + // If the left side of the expression ends with a template curly, add the extra text to the end of the curly bracket. + // `foo${bar}` /* comment */ + 'baz' --> `foo${bar /* comment */ }${baz}` + return getTemplateLiteral(currentNode.left, textBeforeNode, textBeforePlus + textAfterPlus).slice(0, -1) + + getTemplateLiteral(currentNode.right, null, textAfterNode).slice(1); + } + if (rightStartsWithCurly) { + + // Otherwise, if the right side of the expression starts with a template curly, add the text there. + // 'foo' /* comment */ + `${bar}baz` --> `foo${ /* comment */ bar}baz` + return getTemplateLiteral(currentNode.left, textBeforeNode, null).slice(0, -1) + + getTemplateLiteral(currentNode.right, textBeforePlus + textAfterPlus, textAfterNode).slice(1); + } + + /* + * Otherwise, these nodes should not be combined into a template curly, since there is nowhere to put + * the text between them. + */ + return `${getTemplateLiteral(currentNode.left, textBeforeNode, null)}${textBeforePlus}+${textAfterPlus}${getTemplateLiteral(currentNode.right, textAfterNode, null)}`; + } + + return `\`\${${textBeforeNode || ""}${sourceCode.getText(currentNode)}${textAfterNode || ""}}\``; + } + + /** + * 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; + } + + const 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({ + node: topBinaryExpr, + message: "Unexpected string concatenation.", + fix(fixer) { + return fixer.replaceText(topBinaryExpr, getTemplateLiteral(topBinaryExpr, null, null)); + } + }); + } + } + + return { + Program() { + done = Object.create(null); + }, + + Literal: checkForStringConcat, + TemplateLiteral: checkForStringConcat + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/quote-props.js b/tools/node_modules/eslint/lib/rules/quote-props.js new file mode 100644 index 0000000000..6ac1f3c138 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/quote-props.js @@ -0,0 +1,298 @@ +/** + * @fileoverview Rule to flag non-quoted property names in object literals. + * @author Mathias Bynens <http://mathiasbynens.be/> + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const espree = require("espree"), + keywords = require("../util/keywords"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require quotes around object literal property names", + category: "Stylistic Issues", + recommended: false + }, + + 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 + } + ] + }, + + fixable: "code" + }, + + create(context) { + + const 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.", + sourceCode = context.getSourceCode(); + + + /** + * 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 && String(+tokens[0].value) === tokens[0].value)); + } + + /** + * Returns a string representation of a property node with quotes removed + * @param {ASTNode} key Key AST Node, which may or may not be quoted + * @returns {string} A replacement string for this property + */ + function getUnquotedKey(key) { + return key.type === "Identifier" ? key.name : key.value; + } + + /** + * Returns a string representation of a property node with quotes added + * @param {ASTNode} key Key AST Node, which may or may not be quoted + * @returns {string} A replacement string for this property + */ + function getQuotedKey(key) { + if (key.type === "Literal" && typeof key.value === "string") { + + // If the key is already a string literal, don't replace the quotes with double quotes. + return sourceCode.getText(key); + } + + // Otherwise, the key is either an identifier or a number literal. + return `"${key.type === "Identifier" ? key.name : key.value}"`; + } + + /** + * Ensures that a property's key is quoted only when necessary + * @param {ASTNode} node Property AST node + * @returns {void} + */ + function checkUnnecessaryQuotes(node) { + const key = node.key; + + if (node.method || node.computed || node.shorthand) { + return; + } + + if (key.type === "Literal" && typeof key.value === "string") { + let tokens; + + try { + tokens = espree.tokenize(key.value); + } catch (e) { + return; + } + + if (tokens.length !== 1) { + return; + } + + const isKeywordToken = isKeyword(tokens[0].value); + + if (isKeywordToken && KEYWORDS) { + return; + } + + if (CHECK_UNNECESSARY && areQuotesRedundant(key.value, tokens, NUMBERS)) { + context.report({ + node, + message: MESSAGE_UNNECESSARY, + data: { property: key.value }, + fix: fixer => fixer.replaceText(key, getUnquotedKey(key)) + }); + } + } else if (KEYWORDS && key.type === "Identifier" && isKeyword(key.name)) { + context.report({ + node, + message: MESSAGE_RESERVED, + data: { property: key.name }, + fix: fixer => fixer.replaceText(key, getQuotedKey(key)) + }); + } else if (NUMBERS && key.type === "Literal" && typeof key.value === "number") { + context.report({ + node, + message: MESSAGE_NUMERIC, + data: { property: key.value }, + fix: fixer => fixer.replaceText(key, getQuotedKey(key)) + }); + } + } + + /** + * Ensures that a property's key is quoted + * @param {ASTNode} node Property AST node + * @returns {void} + */ + function checkOmittedQuotes(node) { + const key = node.key; + + if (!node.method && !node.computed && !node.shorthand && !(key.type === "Literal" && typeof key.value === "string")) { + context.report({ + node, + message: MESSAGE_UNQUOTED, + data: { property: key.name || key.value }, + fix: fixer => fixer.replaceText(key, getQuotedKey(key)) + }); + } + } + + /** + * 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) { + const quotedProps = [], + unquotedProps = []; + let keywordKeyName = null, + necessaryQuotes = false; + + node.properties.forEach(property => { + const key = property.key; + + if (!key || property.method || property.computed || property.shorthand) { + return; + } + + if (key.type === "Literal" && typeof key.value === "string") { + + quotedProps.push(property); + + if (checkQuotesRedundancy) { + let tokens; + + try { + tokens = espree.tokenize(key.value); + } catch (e) { + necessaryQuotes = true; + return; + } + + necessaryQuotes = necessaryQuotes || !areQuotesRedundant(key.value, tokens) || KEYWORDS && isKeyword(tokens[0].value); + } + } else if (KEYWORDS && checkQuotesRedundancy && key.type === "Identifier" && isKeyword(key.name)) { + unquotedProps.push(property); + necessaryQuotes = true; + keywordKeyName = key.name; + } else { + unquotedProps.push(property); + } + }); + + if (checkQuotesRedundancy && quotedProps.length && !necessaryQuotes) { + quotedProps.forEach(property => { + context.report({ + node: property, + message: "Properties shouldn't be quoted as all quotes are redundant.", + fix: fixer => fixer.replaceText(property.key, getUnquotedKey(property.key)) + }); + }); + } else if (unquotedProps.length && keywordKeyName) { + unquotedProps.forEach(property => { + context.report({ + node: property, + message: "Properties should be quoted as '{{property}}' is a reserved word.", + data: { property: keywordKeyName }, + fix: fixer => fixer.replaceText(property.key, getQuotedKey(property.key)) + }); + }); + } else if (quotedProps.length && unquotedProps.length) { + unquotedProps.forEach(property => { + context.report({ + node: property, + message: "Inconsistently quoted property '{{key}}' found.", + data: { key: property.key.name || property.key.value }, + fix: fixer => fixer.replaceText(property.key, getQuotedKey(property.key)) + }); + }); + } + } + + return { + Property(node) { + if (MODE === "always" || !MODE) { + checkOmittedQuotes(node); + } + if (MODE === "as-needed") { + checkUnnecessaryQuotes(node); + } + }, + ObjectExpression(node) { + if (MODE === "consistent") { + checkConsistency(node, false); + } + if (MODE === "consistent-as-needed") { + checkConsistency(node, true); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/quotes.js b/tools/node_modules/eslint/lib/rules/quotes.js new file mode 100644 index 0000000000..914762727b --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/quotes.js @@ -0,0 +1,296 @@ +/** + * @fileoverview A rule to choose between single and double quote marks + * @author Matt DuVall <http://www.mattduvall.com/>, Brandon Payton + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Constants +//------------------------------------------------------------------------------ + +const QUOTE_SETTINGS = { + double: { + quote: "\"", + alternateQuote: "'", + description: "doublequote" + }, + single: { + quote: "'", + alternateQuote: "\"", + description: "singlequote" + }, + backtick: { + quote: "`", + alternateQuote: "\"", + description: "backtick" + } +}; + +// An unescaped newline is a newline preceded by an even number of backslashes. +const UNESCAPED_LINEBREAK_PATTERN = new RegExp(String.raw`(^|[^\\])(\\\\)*[${Array.from(astUtils.LINEBREAKS).join("")}]`); + +/** + * Switches quoting of javascript string between ' " and ` + * escaping and unescaping as necessary. + * Only escaping of the minimal set of characters is changed. + * Note: escaping of newlines when switching from backtick to other quotes is not handled. + * @param {string} str - A string to convert. + * @returns {string} The string with changed quotes. + * @private + */ +QUOTE_SETTINGS.double.convert = +QUOTE_SETTINGS.single.convert = +QUOTE_SETTINGS.backtick.convert = function(str) { + const newQuote = this.quote; + const oldQuote = str[0]; + + if (newQuote === oldQuote) { + return str; + } + return newQuote + str.slice(1, -1).replace(/\\(\${|\r\n?|\n|.)|["'`]|\${|(\r\n?|\n)/g, (match, escaped, newline) => { + if (escaped === oldQuote || oldQuote === "`" && escaped === "${") { + return escaped; // unescape + } + if (match === newQuote || newQuote === "`" && match === "${") { + return `\\${match}`; // escape + } + if (newline && oldQuote === "`") { + return "\\n"; // escape newlines + } + return match; + }) + newQuote; +}; + +const AVOID_ESCAPE = "avoid-escape"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce the consistent use of either backticks, double, or single quotes", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "code", + + schema: [ + { + enum: ["single", "double", "backtick"] + }, + { + anyOf: [ + { + enum: ["avoid-escape"] + }, + { + type: "object", + properties: { + avoidEscape: { + type: "boolean" + }, + allowTemplateLiterals: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + + const quoteOption = context.options[0], + settings = QUOTE_SETTINGS[quoteOption || "double"], + options = context.options[1], + allowTemplateLiterals = options && options.allowTemplateLiterals === true, + sourceCode = context.getSourceCode(); + let avoidEscape = options && options.avoidEscape === true; + + // deprecated + if (options === AVOID_ESCAPE) { + avoidEscape = true; + } + + /** + * Determines if a given node is part of JSX syntax. + * + * This function returns `true` in the following cases: + * + * - `<div className="foo"></div>` ... If the literal is an attribute value, the parent of the literal is `JSXAttribute`. + * - `<div>foo</div>` ... If the literal is a text content, the parent of the literal is `JSXElement`. + * + * In particular, this function returns `false` in the following cases: + * + * - `<div className={"foo"}></div>` + * - `<div>{"foo"}</div>` + * + * In both cases, inside of the braces is handled as normal JavaScript. + * The braces are `JSXExpressionContainer` nodes. + * + * @param {ASTNode} node The Literal node to check. + * @returns {boolean} True if the node is a part of JSX, false if not. + * @private + */ + function isJSXLiteral(node) { + return node.parent.type === "JSXAttribute" || node.parent.type === "JSXElement"; + } + + /** + * 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" + ); + } + + /** + * 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) { + const block = node.parent.parent; + + if (block.type !== "Program" && (block.type !== "BlockStatement" || !astUtils.isFunction(block.parent))) { + return false; + } + + // Check the node is at a prologue. + for (let i = 0; i < block.body.length; ++i) { + const statement = block.body[i]; + + if (statement === node.parent) { + return true; + } + if (!isDirective(statement)) { + break; + } + } + + 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) { + const parent = node.parent; + + switch (parent.type) { + + // Directive Prologues. + case "ExpressionStatement": + return isPartOfDirectivePrologue(node); + + // LiteralPropertyName. + case "Property": + case "MethodDefinition": + 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 { + + Literal(node) { + const val = node.value, + rawVal = node.raw; + + if (settings && typeof val === "string") { + let isValid = (quoteOption === "backtick" && isAllowedAsNonBacktick(node)) || + isJSXLiteral(node) || + astUtils.isSurroundedBy(rawVal, settings.quote); + + if (!isValid && avoidEscape) { + isValid = astUtils.isSurroundedBy(rawVal, settings.alternateQuote) && rawVal.indexOf(settings.quote) >= 0; + } + + if (!isValid) { + context.report({ + node, + message: "Strings must use {{description}}.", + data: { + description: settings.description + }, + fix(fixer) { + return fixer.replaceText(node, settings.convert(node.raw)); + } + }); + } + } + }, + + TemplateLiteral(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" && node === node.parent.quasi + ) { + return; + } + + // A warning should be produced if the template literal only has one TemplateElement, and has no unescaped newlines. + const shouldWarn = node.quasis.length === 1 && !UNESCAPED_LINEBREAK_PATTERN.test(node.quasis[0].value.raw); + + if (shouldWarn) { + context.report({ + node, + message: "Strings must use {{description}}.", + data: { + description: settings.description + }, + fix(fixer) { + if (isPartOfDirectivePrologue(node)) { + + /* + * TemplateLiterals in a directive prologue aren't actually directives, but if they're + * in the directive prologue, then fixing them might turn them into directives and change + * the behavior of the code. + */ + return null; + } + return fixer.replaceText(node, settings.convert(sourceCode.getText(node))); + } + }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/radix.js b/tools/node_modules/eslint/lib/rules/radix.js new file mode 100644 index 0000000000..0484c3bfb3 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/radix.js @@ -0,0 +1,171 @@ +/** + * @fileoverview Rule to flag use of parseInt without a radix argument + * @author James Allardice + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const MODE_ALWAYS = "always", + MODE_AS_NEEDED = "as-needed"; + +/** + * Checks whether a given variable is shadowed or not. + * + * @param {eslint-scope.Variable} variable - A variable to check. + * @returns {boolean} `true` if the variable is shadowed. + */ +function isShadowed(variable) { + return variable.defs.length >= 1; +} + +/** + * Checks whether a given node is a MemberExpression of `parseInt` method or not. + * + * @param {ASTNode} node - A node to check. + * @returns {boolean} `true` if the node is a MemberExpression of `parseInt` + * method. + */ +function isParseIntMethod(node) { + return ( + node.type === "MemberExpression" && + !node.computed && + node.property.type === "Identifier" && + node.property.name === "parseInt" + ); +} + +/** + * Checks whether a given node is a valid value of radix or not. + * + * The following values are invalid. + * + * - A literal except numbers. + * - undefined. + * + * @param {ASTNode} radix - A node of radix to check. + * @returns {boolean} `true` if the node is valid. + */ +function isValidRadix(radix) { + return !( + (radix.type === "Literal" && typeof radix.value !== "number") || + (radix.type === "Identifier" && radix.name === "undefined") + ); +} + +/** + * Checks whether a given node is a default value of radix or not. + * + * @param {ASTNode} radix - A node of radix to check. + * @returns {boolean} `true` if the node is the literal node of `10`. + */ +function isDefaultRadix(radix) { + return radix.type === "Literal" && radix.value === 10; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +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(context) { + const 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) { + const args = node.arguments; + + switch (args.length) { + case 0: + context.report({ + node, + message: "Missing parameters." + }); + break; + + case 1: + if (mode === MODE_ALWAYS) { + context.report({ + node, + message: "Missing radix parameter." + }); + } + break; + + default: + if (mode === MODE_AS_NEEDED && isDefaultRadix(args[1])) { + context.report({ + node, + message: "Redundant radix parameter." + }); + } else if (!isValidRadix(args[1])) { + context.report({ + node, + message: "Invalid radix parameter." + }); + } + break; + } + } + + return { + "Program:exit"() { + const scope = context.getScope(); + let variable; + + // Check `parseInt()` + variable = astUtils.getVariableByName(scope, "parseInt"); + if (!isShadowed(variable)) { + variable.references.forEach(reference => { + const node = reference.identifier; + + if (astUtils.isCallee(node)) { + checkArguments(node.parent); + } + }); + } + + // Check `Number.parseInt()` + variable = astUtils.getVariableByName(scope, "Number"); + if (!isShadowed(variable)) { + variable.references.forEach(reference => { + const node = reference.identifier.parent; + + if (isParseIntMethod(node) && astUtils.isCallee(node)) { + checkArguments(node.parent); + } + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/require-await.js b/tools/node_modules/eslint/lib/rules/require-await.js new file mode 100644 index 0000000000..a5698ae058 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/require-await.js @@ -0,0 +1,95 @@ +/** + * @fileoverview Rule to disallow async functions which have no `await` expression. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Capitalize the 1st letter of the given text. + * + * @param {string} text - The text to capitalize. + * @returns {string} The text that the 1st letter was capitalized. + */ +function capitalizeFirstLetter(text) { + return text[0].toUpperCase() + text.slice(1); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow async functions which have no `await` expression", + category: "Best Practices", + recommended: false + }, + schema: [] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + let scopeInfo = null; + + /** + * Push the scope info object to the stack. + * + * @returns {void} + */ + function enterFunction() { + scopeInfo = { + upper: scopeInfo, + hasAwait: false + }; + } + + /** + * Pop the top scope info object from the stack. + * Also, it reports the function if needed. + * + * @param {ASTNode} node - The node to report. + * @returns {void} + */ + function exitFunction(node) { + if (node.async && !scopeInfo.hasAwait && !astUtils.isEmptyFunction(node)) { + context.report({ + node, + loc: astUtils.getFunctionHeadLoc(node, sourceCode), + message: "{{name}} has no 'await' expression.", + data: { + name: capitalizeFirstLetter( + astUtils.getFunctionNameWithKind(node) + ) + } + }); + } + + scopeInfo = scopeInfo.upper; + } + + return { + FunctionDeclaration: enterFunction, + FunctionExpression: enterFunction, + ArrowFunctionExpression: enterFunction, + "FunctionDeclaration:exit": exitFunction, + "FunctionExpression:exit": exitFunction, + "ArrowFunctionExpression:exit": exitFunction, + + AwaitExpression() { + scopeInfo.hasAwait = true; + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/require-jsdoc.js b/tools/node_modules/eslint/lib/rules/require-jsdoc.js new file mode 100644 index 0000000000..a02ee3659c --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/require-jsdoc.js @@ -0,0 +1,105 @@ +/** + * @fileoverview Rule to check for jsdoc presence. + * @author Gyandeep Singh + */ +"use strict"; + +module.exports = { + meta: { + docs: { + description: "require JSDoc comments", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + require: { + type: "object", + properties: { + ClassDeclaration: { + type: "boolean" + }, + MethodDefinition: { + type: "boolean" + }, + FunctionDeclaration: { + type: "boolean" + }, + ArrowFunctionExpression: { + type: "boolean" + }, + FunctionExpression: { + type: "boolean" + } + }, + additionalProperties: false + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const source = context.getSourceCode(); + const DEFAULT_OPTIONS = { + FunctionDeclaration: true, + MethodDefinition: false, + ClassDeclaration: false, + ArrowFunctionExpression: false, + FunctionExpression: false + }; + const options = Object.assign(DEFAULT_OPTIONS, context.options[0] && context.options[0].require || {}); + + /** + * Report the error message + * @param {ASTNode} node node to report + * @returns {void} + */ + function report(node) { + context.report({ node, message: "Missing JSDoc comment." }); + } + + /** + * Check if the jsdoc comment is present or not. + * @param {ASTNode} node node to examine + * @returns {void} + */ + function checkJsDoc(node) { + const jsdocComment = source.getJSDocComment(node); + + if (!jsdocComment) { + report(node); + } + } + + return { + FunctionDeclaration(node) { + if (options.FunctionDeclaration) { + checkJsDoc(node); + } + }, + FunctionExpression(node) { + if ( + (options.MethodDefinition && node.parent.type === "MethodDefinition") || + (options.FunctionExpression && (node.parent.type === "VariableDeclarator" || (node.parent.type === "Property" && node === node.parent.value))) + ) { + checkJsDoc(node); + } + }, + ClassDeclaration(node) { + if (options.ClassDeclaration) { + checkJsDoc(node); + } + }, + ArrowFunctionExpression(node) { + if (options.ArrowFunctionExpression && node.parent.type === "VariableDeclarator") { + checkJsDoc(node); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/require-yield.js b/tools/node_modules/eslint/lib/rules/require-yield.js new file mode 100644 index 0000000000..5cc2944bc6 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/require-yield.js @@ -0,0 +1,71 @@ +/** + * @fileoverview Rule to flag the generator functions that does not have yield. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require generator functions to contain `yield`", + category: "ECMAScript 6", + recommended: true + }, + + schema: [] + }, + + create(context) { + const 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); + } + } + + /** + * 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; + } + + const countYield = stack.pop(); + + if (countYield === 0 && node.body.body.length > 0) { + context.report({ node, message: "This generator function does not have 'yield'." }); + } + } + + return { + FunctionDeclaration: beginChecking, + "FunctionDeclaration:exit": endChecking, + FunctionExpression: beginChecking, + "FunctionExpression:exit": endChecking, + + // Increases the count of `yield` keyword. + YieldExpression() { + + /* istanbul ignore else */ + if (stack.length > 0) { + stack[stack.length - 1] += 1; + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/rest-spread-spacing.js b/tools/node_modules/eslint/lib/rules/rest-spread-spacing.js new file mode 100644 index 0000000000..91770eca74 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/rest-spread-spacing.js @@ -0,0 +1,107 @@ +/** + * @fileoverview Enforce spacing between rest and spread operators and their expressions. + * @author Kai Cataldo + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce spacing between rest and spread operators and their expressions", + category: "ECMAScript 6", + recommended: false + }, + fixable: "whitespace", + schema: [ + { + enum: ["always", "never"] + } + ] + }, + + create(context) { + const sourceCode = context.getSourceCode(), + alwaysSpace = context.options[0] === "always"; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Checks whitespace between rest/spread operators and their expressions + * @param {ASTNode} node - The node to check + * @returns {void} + */ + function checkWhiteSpace(node) { + const operator = sourceCode.getFirstToken(node), + nextToken = sourceCode.getTokenAfter(operator), + hasWhitespace = sourceCode.isSpaceBetweenTokens(operator, nextToken); + let type; + + switch (node.type) { + case "SpreadElement": + type = "spread"; + break; + case "RestElement": + type = "rest"; + break; + case "ExperimentalSpreadProperty": + type = "spread property"; + break; + case "ExperimentalRestProperty": + type = "rest property"; + break; + default: + return; + } + + if (alwaysSpace && !hasWhitespace) { + context.report({ + node, + loc: { + line: operator.loc.end.line, + column: operator.loc.end.column + }, + message: "Expected whitespace after {{type}} operator.", + data: { + type + }, + fix(fixer) { + return fixer.replaceTextRange([operator.range[1], nextToken.range[0]], " "); + } + }); + } else if (!alwaysSpace && hasWhitespace) { + context.report({ + node, + loc: { + line: operator.loc.end.line, + column: operator.loc.end.column + }, + message: "Unexpected whitespace after {{type}} operator.", + data: { + type + }, + fix(fixer) { + return fixer.removeRange([operator.range[1], nextToken.range[0]]); + } + }); + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + SpreadElement: checkWhiteSpace, + RestElement: checkWhiteSpace, + ExperimentalSpreadProperty: checkWhiteSpace, + ExperimentalRestProperty: checkWhiteSpace + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/semi-spacing.js b/tools/node_modules/eslint/lib/rules/semi-spacing.js new file mode 100644 index 0000000000..fd300e4a37 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/semi-spacing.js @@ -0,0 +1,211 @@ +/** + * @fileoverview Validates spacing before and after semicolon + * @author Mathias Schreck + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +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 + } + ] + }, + + create(context) { + + const config = context.options[0], + sourceCode = context.getSourceCode(); + let requireSpaceBefore = false, + requireSpaceAfter = true; + + 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) { + const tokenBefore = sourceCode.getTokenBefore(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) { + const tokenAfter = sourceCode.getTokenAfter(token); + + 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) { + const tokenAfter = sourceCode.getTokenAfter(token); + + 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) { + const tokenBefore = sourceCode.getTokenBefore(token); + + 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) { + const nextToken = sourceCode.getTokenAfter(token); + + return (nextToken && astUtils.isClosingBraceToken(nextToken) || astUtils.isClosingParenToken(nextToken)); + } + + /** + * 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) { + if (astUtils.isSemicolonToken(token)) { + const location = token.loc.start; + + if (hasLeadingSpace(token)) { + if (!requireSpaceBefore) { + context.report({ + node, + loc: location, + message: "Unexpected whitespace before semicolon.", + fix(fixer) { + const tokenBefore = sourceCode.getTokenBefore(token); + + return fixer.removeRange([tokenBefore.range[1], token.range[0]]); + } + }); + } + } else { + if (requireSpaceBefore) { + context.report({ + node, + loc: location, + message: "Missing whitespace before semicolon.", + fix(fixer) { + return fixer.insertTextBefore(token, " "); + } + }); + } + } + + if (!isFirstTokenInCurrentLine(token) && !isLastTokenInCurrentLine(token) && !isBeforeClosingParen(token)) { + if (hasTrailingSpace(token)) { + if (!requireSpaceAfter) { + context.report({ + node, + loc: location, + message: "Unexpected whitespace after semicolon.", + fix(fixer) { + const tokenAfter = sourceCode.getTokenAfter(token); + + return fixer.removeRange([token.range[1], tokenAfter.range[0]]); + } + }); + } + } else { + if (requireSpaceAfter) { + context.report({ + node, + loc: location, + message: "Missing whitespace after semicolon.", + fix(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) { + const token = sourceCode.getLastToken(node); + + checkSemicolonSpacing(token, node); + } + + return { + VariableDeclaration: checkNode, + ExpressionStatement: checkNode, + BreakStatement: checkNode, + ContinueStatement: checkNode, + DebuggerStatement: checkNode, + ReturnStatement: checkNode, + ThrowStatement: checkNode, + ImportDeclaration: checkNode, + ExportNamedDeclaration: checkNode, + ExportAllDeclaration: checkNode, + ExportDefaultDeclaration: checkNode, + ForStatement(node) { + if (node.init) { + checkSemicolonSpacing(sourceCode.getTokenAfter(node.init), node); + } + + if (node.test) { + checkSemicolonSpacing(sourceCode.getTokenAfter(node.test), node); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/semi-style.js b/tools/node_modules/eslint/lib/rules/semi-style.js new file mode 100644 index 0000000000..41fb39246c --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/semi-style.js @@ -0,0 +1,143 @@ +/** + * @fileoverview Rule to enforce location of semicolons. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +const SELECTOR = `:matches(${ + [ + "BreakStatement", "ContinueStatement", "DebuggerStatement", + "DoWhileStatement", "ExportAllDeclaration", + "ExportDefaultDeclaration", "ExportNamedDeclaration", + "ExpressionStatement", "ImportDeclaration", "ReturnStatement", + "ThrowStatement", "VariableDeclaration" + ].join(",") +})`; + +/** + * Get the child node list of a given node. + * This returns `Program#body`, `BlockStatement#body`, or `SwitchCase#consequent`. + * This is used to check whether a node is the first/last child. + * @param {Node} node A node to get child node list. + * @returns {Node[]|null} The child node list. + */ +function getChildren(node) { + const t = node.type; + + if (t === "BlockStatement" || t === "Program") { + return node.body; + } + if (t === "SwitchCase") { + return node.consequent; + } + return null; +} + +/** + * Check whether a given node is the last statement in the parent block. + * @param {Node} node A node to check. + * @returns {boolean} `true` if the node is the last statement in the parent block. + */ +function isLastChild(node) { + const t = node.parent.type; + + if (t === "IfStatement" && node.parent.consequent === node && node.parent.alternate) { // before `else` keyword. + return true; + } + if (t === "DoWhileStatement") { // before `while` keyword. + return true; + } + const nodeList = getChildren(node.parent); + + return nodeList !== null && nodeList[nodeList.length - 1] === node; // before `}` or etc. +} + +module.exports = { + meta: { + docs: { + description: "enforce location of semicolons", + category: "Stylistic Issues", + recommended: false + }, + schema: [{ enum: ["last", "first"] }], + fixable: "whitespace" + }, + + create(context) { + const sourceCode = context.getSourceCode(); + const option = context.options[0] || "last"; + + /** + * Check the given semicolon token. + * @param {Token} semiToken The semicolon token to check. + * @param {"first"|"last"} expected The expected location to check. + * @returns {void} + */ + function check(semiToken, expected) { + const prevToken = sourceCode.getTokenBefore(semiToken); + const nextToken = sourceCode.getTokenAfter(semiToken); + const prevIsSameLine = !prevToken || astUtils.isTokenOnSameLine(prevToken, semiToken); + const nextIsSameLine = !nextToken || astUtils.isTokenOnSameLine(semiToken, nextToken); + + if ((expected === "last" && !prevIsSameLine) || (expected === "first" && !nextIsSameLine)) { + context.report({ + loc: semiToken.loc, + message: "Expected this semicolon to be at {{pos}}.", + data: { + pos: (expected === "last") + ? "the end of the previous line" + : "the beginning of the next line" + }, + fix(fixer) { + if (prevToken && nextToken && sourceCode.commentsExistBetween(prevToken, nextToken)) { + return null; + } + + const start = prevToken ? prevToken.range[1] : semiToken.range[0]; + const end = nextToken ? nextToken.range[0] : semiToken.range[1]; + const text = (expected === "last") ? ";\n" : "\n;"; + + return fixer.replaceTextRange([start, end], text); + } + }); + } + } + + return { + [SELECTOR](node) { + if (option === "first" && isLastChild(node)) { + return; + } + + const lastToken = sourceCode.getLastToken(node); + + if (astUtils.isSemicolonToken(lastToken)) { + check(lastToken, option); + } + }, + + ForStatement(node) { + const firstSemi = node.init && sourceCode.getTokenAfter(node.init, astUtils.isSemicolonToken); + const secondSemi = node.test && sourceCode.getTokenAfter(node.test, astUtils.isSemicolonToken); + + if (firstSemi) { + check(firstSemi, "last"); + } + if (secondSemi) { + check(secondSemi, "last"); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/semi.js b/tools/node_modules/eslint/lib/rules/semi.js new file mode 100644 index 0000000000..78b6966dea --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/semi.js @@ -0,0 +1,325 @@ +/** + * @fileoverview Rule to flag missing semicolons. + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const FixTracker = require("../util/fix-tracker"); +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require or disallow semicolons instead of ASI", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "code", + + schema: { + anyOf: [ + { + type: "array", + items: [ + { + enum: ["never"] + }, + { + type: "object", + properties: { + beforeStatementContinuationChars: { + enum: ["always", "any", "never"] + } + }, + additionalProperties: false + } + ], + minItems: 0, + maxItems: 2 + }, + { + type: "array", + items: [ + { + enum: ["always"] + }, + { + type: "object", + properties: { + omitLastInOneLineBlock: { type: "boolean" } + }, + additionalProperties: false + } + ], + minItems: 0, + maxItems: 2 + } + ] + } + }, + + create(context) { + + const OPT_OUT_PATTERN = /^[-[(/+`]/; // One of [(/+-` + const options = context.options[1]; + const never = context.options[0] === "never"; + const exceptOneLine = Boolean(options && options.omitLastInOneLineBlock); + const beforeStatementContinuationChars = (options && options.beforeStatementContinuationChars) || "any"; + const 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) { + const lastToken = sourceCode.getLastToken(node); + let message, + fix, + 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) { + + /* + * Expand the replacement range to include the surrounding + * tokens to avoid conflicting with no-extra-semi. + * https://github.com/eslint/eslint/issues/7928 + */ + return new FixTracker(fixer, sourceCode) + .retainSurroundingTokens(lastToken) + .remove(lastToken); + }; + } + + context.report({ + node, + loc, + message, + fix + }); + + } + + /** + * Check whether a given semicolon token is redandant. + * @param {Token} semiToken A semicolon token to check. + * @returns {boolean} `true` if the next token is `;` or `}`. + */ + function isRedundantSemi(semiToken) { + const nextToken = sourceCode.getTokenAfter(semiToken); + + return ( + !nextToken || + astUtils.isClosingBraceToken(nextToken) || + astUtils.isSemicolonToken(nextToken) + ); + } + + /** + * Check whether a given token is the closing brace of an arrow function. + * @param {Token} lastToken A token to check. + * @returns {boolean} `true` if the token is the closing brace of an arrow function. + */ + function isEndOfArrowBlock(lastToken) { + if (!astUtils.isClosingBraceToken(lastToken)) { + return false; + } + const node = sourceCode.getNodeByRangeIndex(lastToken.range[0]); + + return ( + node.type === "BlockStatement" && + node.parent.type === "ArrowFunctionExpression" + ); + } + + /** + * Check whether a given node is on the same line with the next token. + * @param {Node} node A statement node to check. + * @returns {boolean} `true` if the node is on the same line with the next token. + */ + function isOnSameLineWithNextToken(node) { + const prevToken = sourceCode.getLastToken(node, 1); + const nextToken = sourceCode.getTokenAfter(node); + + return !!nextToken && astUtils.isTokenOnSameLine(prevToken, nextToken); + } + + /** + * Check whether a given node can connect the next line if the next line is unreliable. + * @param {Node} node A statement node to check. + * @returns {boolean} `true` if the node can connect the next line. + */ + function maybeAsiHazardAfter(node) { + const t = node.type; + + if (t === "DoWhileStatement" || + t === "BreakStatement" || + t === "ContinueStatement" || + t === "DebuggerStatement" || + t === "ImportDeclaration" || + t === "ExportAllDeclaration" + ) { + return false; + } + if (t === "ReturnStatement") { + return Boolean(node.argument); + } + if (t === "ExportNamedDeclaration") { + return Boolean(node.declaration); + } + if (isEndOfArrowBlock(sourceCode.getLastToken(node, 1))) { + return false; + } + + return true; + } + + /** + * Check whether a given token can connect the previous statement. + * @param {Token} token A token to check. + * @returns {boolean} `true` if the token is one of `[`, `(`, `/`, `+`, `-`, ```, `++`, and `--`. + */ + function maybeAsiHazardBefore(token) { + return ( + Boolean(token) && + OPT_OUT_PATTERN.test(token.value) && + token.value !== "++" && + token.value !== "--" + ); + } + + /** + * Check if the semicolon of a given node is unnecessary, only true if: + * - next token is a valid statement divider (`;` or `}`). + * - next token is on a new line and the node is not connectable to the new line. + * @param {Node} node A statement node to check. + * @returns {boolean} whether the semicolon is unnecessary. + */ + function canRemoveSemicolon(node) { + if (isRedundantSemi(sourceCode.getLastToken(node))) { + return true; // `;;` or `;}` + } + if (isOnSameLineWithNextToken(node)) { + return false; // One liner. + } + if (beforeStatementContinuationChars === "never" && !maybeAsiHazardAfter(node)) { + return true; // ASI works. This statement doesn't connect to the next. + } + if (!maybeAsiHazardBefore(sourceCode.getTokenAfter(node))) { + return true; // ASI works. The next token doesn't connect to this statement. + } + + 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) { + const parent = node.parent; + const nextToken = sourceCode.getTokenAfter(node); + + if (!nextToken || nextToken.value !== "}") { + return false; + } + 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) { + const isSemi = astUtils.isSemicolonToken(sourceCode.getLastToken(node)); + + if (never) { + if (isSemi && canRemoveSemicolon(node)) { + report(node, true); + } else if (!isSemi && beforeStatementContinuationChars === "always" && maybeAsiHazardBefore(sourceCode.getTokenAfter(node))) { + report(node); + } + } else { + const oneLinerBlock = (exceptOneLine && isOneLinerBlock(node)); + + if (isSemi && oneLinerBlock) { + report(node, true); + } else if (!isSemi && !oneLinerBlock) { + report(node); + } + } + } + + /** + * Checks to see if there's a semicolon after a variable declaration. + * @param {ASTNode} node The node to check. + * @returns {void} + */ + function checkForSemicolonForVariableDeclaration(node) { + const parent = node.parent; + + 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(node) { + if (!node.declaration) { + checkForSemicolon(node); + } + }, + ExportDefaultDeclaration(node) { + if (!/(?:Class|Function)Declaration/.test(node.declaration.type)) { + checkForSemicolon(node); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/sort-imports.js b/tools/node_modules/eslint/lib/rules/sort-imports.js new file mode 100644 index 0000000000..2bd415e974 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/sort-imports.js @@ -0,0 +1,196 @@ +/** + * @fileoverview Rule to require sorting of import declarations + * @author Christian Schuller + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +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 + } + ], + + fixable: "code" + }, + + create(context) { + + const configuration = context.options[0] || {}, + ignoreCase = configuration.ignoreCase || false, + ignoreMemberSort = configuration.ignoreMemberSort || false, + memberSyntaxSortOrder = configuration.memberSyntaxSortOrder || ["none", "all", "multiple", "single"], + sourceCode = context.getSourceCode(); + let 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"; + } + if (node.specifiers[0].type === "ImportNamespaceSpecifier") { + return "all"; + } + if (node.specifiers.length === 1) { + return "single"; + } + 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 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; + } + return null; + + } + + return { + ImportDeclaration(node) { + if (previousDeclaration) { + const currentMemberSyntaxGroupIndex = getMemberParameterGroupIndex(node), + previousMemberSyntaxGroupIndex = getMemberParameterGroupIndex(previousDeclaration); + let currentLocalMemberName = getFirstLocalMemberName(node), + previousLocalMemberName = getFirstLocalMemberName(previousDeclaration); + + if (ignoreCase) { + previousLocalMemberName = previousLocalMemberName && previousLocalMemberName.toLowerCase(); + currentLocalMemberName = currentLocalMemberName && currentLocalMemberName.toLowerCase(); + } + + /* + * 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, + message: "Expected '{{syntaxA}}' syntax before '{{syntaxB}}' syntax.", + data: { + syntaxA: memberSyntaxSortOrder[currentMemberSyntaxGroupIndex], + syntaxB: memberSyntaxSortOrder[previousMemberSyntaxGroupIndex] + } + }); + } + } else { + if (previousLocalMemberName && + currentLocalMemberName && + currentLocalMemberName < previousLocalMemberName + ) { + context.report({ + node, + message: "Imports should be sorted alphabetically." + }); + } + } + } + + if (!ignoreMemberSort) { + const importSpecifiers = node.specifiers.filter(specifier => specifier.type === "ImportSpecifier"); + const getSortableName = ignoreCase ? specifier => specifier.local.name.toLowerCase() : specifier => specifier.local.name; + const firstUnsortedIndex = importSpecifiers.map(getSortableName).findIndex((name, index, array) => array[index - 1] > name); + + if (firstUnsortedIndex !== -1) { + context.report({ + node: importSpecifiers[firstUnsortedIndex], + message: "Member '{{memberName}}' of the import declaration should be sorted alphabetically.", + data: { memberName: importSpecifiers[firstUnsortedIndex].local.name }, + fix(fixer) { + if (importSpecifiers.some(specifier => + sourceCode.getCommentsBefore(specifier).length || sourceCode.getCommentsAfter(specifier).length)) { + + // If there are comments in the ImportSpecifier list, don't rearrange the specifiers. + return null; + } + + return fixer.replaceTextRange( + [importSpecifiers[0].range[0], importSpecifiers[importSpecifiers.length - 1].range[1]], + importSpecifiers + + // Clone the importSpecifiers array to avoid mutating it + .slice() + + // Sort the array into the desired order + .sort((specifierA, specifierB) => { + const aName = getSortableName(specifierA); + const bName = getSortableName(specifierB); + + return aName > bName ? 1 : -1; + }) + + // Build a string out of the sorted list of import specifiers and the text between the originals + .reduce((sourceText, specifier, index) => { + const textAfterSpecifier = index === importSpecifiers.length - 1 + ? "" + : sourceCode.getText().slice(importSpecifiers[index].range[1], importSpecifiers[index + 1].range[0]); + + return sourceText + sourceCode.getText(specifier) + textAfterSpecifier; + }, "") + ); + } + }); + } + } + + previousDeclaration = node; + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/sort-keys.js b/tools/node_modules/eslint/lib/rules/sort-keys.js new file mode 100644 index 0000000000..8821f62943 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/sort-keys.js @@ -0,0 +1,157 @@ +/** + * @fileoverview Rule to require object keys to be sorted + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"), + naturalCompare = require("natural-compare"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Gets the property name of the given `Property` node. + * + * - If the property's key is an `Identifier` node, this returns the key's name + * whether it's a computed property or not. + * - If the property has a static name, this returns the static name. + * - Otherwise, this returns null. + * + * @param {ASTNode} node - The `Property` node to get. + * @returns {string|null} The property name or null. + * @private + */ +function getPropertyName(node) { + return astUtils.getStaticPropertyName(node) || node.key.name || null; +} + +/** + * Functions which check that the given 2 names are in specific order. + * + * Postfix `I` is meant insensitive. + * Postfix `N` is meant natual. + * + * @private + */ +const isValidOrders = { + asc(a, b) { + return a <= b; + }, + ascI(a, b) { + return a.toLowerCase() <= b.toLowerCase(); + }, + ascN(a, b) { + return naturalCompare(a, b) <= 0; + }, + ascIN(a, b) { + return naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0; + }, + desc(a, b) { + return isValidOrders.asc(b, a); + }, + descI(a, b) { + return isValidOrders.ascI(b, a); + }, + descN(a, b) { + return isValidOrders.ascN(b, a); + }, + descIN(a, b) { + return isValidOrders.ascIN(b, a); + } +}; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require object keys to be sorted", + category: "Stylistic Issues", + recommended: false + }, + schema: [ + { + enum: ["asc", "desc"] + }, + { + type: "object", + properties: { + caseSensitive: { + type: "boolean" + }, + natural: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + // Parse options. + const order = context.options[0] || "asc"; + const options = context.options[1]; + const insensitive = (options && options.caseSensitive) === false; + const natual = Boolean(options && options.natural); + const isValidOrder = isValidOrders[ + order + (insensitive ? "I" : "") + (natual ? "N" : "") + ]; + + // The stack to save the previous property's name for each object literals. + let stack = null; + + return { + ObjectExpression() { + stack = { + upper: stack, + prevName: null + }; + }, + + "ObjectExpression:exit"() { + stack = stack.upper; + }, + + Property(node) { + if (node.parent.type === "ObjectPattern") { + return; + } + + const prevName = stack.prevName; + const thisName = getPropertyName(node); + + stack.prevName = thisName || prevName; + + if (!prevName || !thisName) { + return; + } + + if (!isValidOrder(prevName, thisName)) { + context.report({ + node, + loc: node.key.loc, + message: "Expected object keys to be in {{natual}}{{insensitive}}{{order}}ending order. '{{thisName}}' should be before '{{prevName}}'.", + data: { + thisName, + prevName, + order, + insensitive: insensitive ? "insensitive " : "", + natual: natual ? "natural " : "" + } + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/sort-vars.js b/tools/node_modules/eslint/lib/rules/sort-vars.js new file mode 100644 index 0000000000..c77cdf8620 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/sort-vars.js @@ -0,0 +1,96 @@ +/** + * @fileoverview Rule to require sorting of variables within a single Variable Declaration block + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require variables within the same declaration block to be sorted", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + ignoreCase: { + type: "boolean" + } + }, + additionalProperties: false + } + ], + + fixable: "code" + }, + + create(context) { + + const configuration = context.options[0] || {}, + ignoreCase = configuration.ignoreCase || false, + sourceCode = context.getSourceCode(); + + return { + VariableDeclaration(node) { + const idDeclarations = node.declarations.filter(decl => decl.id.type === "Identifier"); + const getSortableName = ignoreCase ? decl => decl.id.name.toLowerCase() : decl => decl.id.name; + const unfixable = idDeclarations.some(decl => decl.init !== null && decl.init.type !== "Literal"); + let fixed = false; + + idDeclarations.slice(1).reduce((memo, decl) => { + const lastVariableName = getSortableName(memo), + currentVariableName = getSortableName(decl); + + if (currentVariableName < lastVariableName) { + context.report({ + node: decl, + message: "Variables within the same declaration block should be sorted alphabetically.", + fix(fixer) { + if (unfixable || fixed) { + return null; + } + return fixer.replaceTextRange( + [idDeclarations[0].range[0], idDeclarations[idDeclarations.length - 1].range[1]], + idDeclarations + + // Clone the idDeclarations array to avoid mutating it + .slice() + + // Sort the array into the desired order + .sort((declA, declB) => { + const aName = getSortableName(declA); + const bName = getSortableName(declB); + + return aName > bName ? 1 : -1; + }) + + // Build a string out of the sorted list of identifier declarations and the text between the originals + .reduce((sourceText, identifier, index) => { + const textAfterIdentifier = index === idDeclarations.length - 1 + ? "" + : sourceCode.getText().slice(idDeclarations[index].range[1], idDeclarations[index + 1].range[0]); + + return sourceText + sourceCode.getText(identifier) + textAfterIdentifier; + }, "") + + ); + } + }); + fixed = true; + return memo; + } + return decl; + + }, idDeclarations[0]); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/space-before-blocks.js b/tools/node_modules/eslint/lib/rules/space-before-blocks.js new file mode 100644 index 0000000000..f50298c9c4 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/space-before-blocks.js @@ -0,0 +1,148 @@ +/** + * @fileoverview A rule to ensure whitespace before blocks. + * @author Mathias Schreck <https://github.com/lo1tuma> + */ + +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent spacing before blocks", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + oneOf: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + keywords: { + enum: ["always", "never"] + }, + functions: { + enum: ["always", "never"] + }, + classes: { + enum: ["always", "never"] + } + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + const config = context.options[0], + sourceCode = context.getSourceCode(); + let 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; + } + + /** + * 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) { + const precedingToken = sourceCode.getTokenBefore(node); + + if (precedingToken && !isConflicted(precedingToken) && astUtils.isTokenOnSameLine(precedingToken, node)) { + const hasSpace = sourceCode.isSpaceBetweenTokens(precedingToken, node); + const parent = context.getAncestors().pop(); + let requireSpace; + + if (parent.type === "FunctionExpression" || parent.type === "FunctionDeclaration") { + requireSpace = checkFunctions; + } else if (node.type === "ClassBody") { + requireSpace = checkClasses; + } else { + requireSpace = checkKeywords; + } + + if (requireSpace) { + if (!hasSpace) { + context.report({ + node, + message: "Missing space before opening brace.", + fix(fixer) { + return fixer.insertTextBefore(node, " "); + } + }); + } + } else { + if (hasSpace) { + context.report({ + node, + message: "Unexpected space before opening brace.", + fix(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) { + const cases = node.cases; + let openingBrace; + + if (cases.length > 0) { + openingBrace = sourceCode.getTokenBefore(cases[0]); + } else { + openingBrace = sourceCode.getLastToken(node, 1); + } + + checkPrecedingSpace(openingBrace); + } + + return { + BlockStatement: checkPrecedingSpace, + ClassBody: checkPrecedingSpace, + SwitchStatement: checkSpaceBeforeCaseBlock + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/space-before-function-paren.js b/tools/node_modules/eslint/lib/rules/space-before-function-paren.js new file mode 100644 index 0000000000..8851c4587a --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/space-before-function-paren.js @@ -0,0 +1,142 @@ +/** + * @fileoverview Rule to validate spacing before function paren. + * @author Mathias Schreck <https://github.com/lo1tuma> + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent spacing before `function` definition opening parenthesis", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + oneOf: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + anonymous: { + enum: ["always", "never", "ignore"] + }, + named: { + enum: ["always", "never", "ignore"] + }, + asyncArrow: { + enum: ["always", "never", "ignore"] + } + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + const baseConfig = typeof context.options[0] === "string" ? context.options[0] : "always"; + const overrideConfig = typeof context.options[0] === "object" ? context.options[0] : {}; + + /** + * Determines whether a function has a name. + * @param {ASTNode} node The function node. + * @returns {boolean} Whether the function has a name. + */ + function isNamedFunction(node) { + if (node.id) { + return true; + } + + const parent = node.parent; + + return parent.type === "MethodDefinition" || + (parent.type === "Property" && + ( + parent.kind === "get" || + parent.kind === "set" || + parent.method + ) + ); + } + + /** + * Gets the config for a given function + * @param {ASTNode} node The function node + * @returns {string} "always", "never", or "ignore" + */ + function getConfigForFunction(node) { + if (node.type === "ArrowFunctionExpression") { + + // Always ignore non-async functions and arrow functions without parens, e.g. async foo => bar + if (node.async && astUtils.isOpeningParenToken(sourceCode.getFirstToken(node, { skip: 1 }))) { + return overrideConfig.asyncArrow || baseConfig; + } + } else if (isNamedFunction(node)) { + return overrideConfig.named || baseConfig; + + // `generator-star-spacing` should warn anonymous generators. E.g. `function* () {}` + } else if (!node.generator) { + return overrideConfig.anonymous || baseConfig; + } + + return "ignore"; + } + + /** + * Checks the parens of a function node + * @param {ASTNode} node A function node + * @returns {void} + */ + function checkFunction(node) { + const functionConfig = getConfigForFunction(node); + + if (functionConfig === "ignore") { + return; + } + + const rightToken = sourceCode.getFirstToken(node, astUtils.isOpeningParenToken); + const leftToken = sourceCode.getTokenBefore(rightToken); + const hasSpacing = sourceCode.isSpaceBetweenTokens(leftToken, rightToken); + + if (hasSpacing && functionConfig === "never") { + context.report({ + node, + loc: leftToken.loc.end, + message: "Unexpected space before function parentheses.", + fix: fixer => fixer.removeRange([leftToken.range[1], rightToken.range[0]]) + }); + } else if (!hasSpacing && functionConfig === "always") { + context.report({ + node, + loc: leftToken.loc.end, + message: "Missing space before function parentheses.", + fix: fixer => fixer.insertTextAfter(leftToken, " ") + }); + } + } + + return { + ArrowFunctionExpression: checkFunction, + FunctionDeclaration: checkFunction, + FunctionExpression: checkFunction + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/space-in-parens.js b/tools/node_modules/eslint/lib/rules/space-in-parens.js new file mode 100644 index 0000000000..67ec5847cd --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/space-in-parens.js @@ -0,0 +1,274 @@ +/** + * @fileoverview Disallows or enforces spaces inside of parentheses. + * @author Jonathan Rajavuori + */ +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent spacing inside parentheses", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + exceptions: { + type: "array", + items: { + enum: ["{}", "[]", "()", "empty"] + }, + uniqueItems: true + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + const 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[1] && context.options[1].exceptions) || [], + options = {}; + let exceptions; + + if (exceptionsArrayOptions.length) { + options.braceException = exceptionsArrayOptions.indexOf("{}") !== -1; + options.bracketException = exceptionsArrayOptions.indexOf("[]") !== -1; + options.parenException = exceptionsArrayOptions.indexOf("()") !== -1; + options.empty = exceptionsArrayOptions.indexOf("empty") !== -1; + } + + /** + * 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() { + const openers = [], + closers = []; + + if (options.braceException) { + openers.push("{"); + closers.push("}"); + } + + if (options.bracketException) { + openers.push("["); + closers.push("]"); + } + + if (options.parenException) { + openers.push("("); + closers.push(")"); + } + + if (options.empty) { + openers.push(")"); + closers.push("("); + } + + return { + openers, + closers + }; + } + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + const 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; + } + + if (ALWAYS) { + if (astUtils.isClosingParenToken(right)) { + return false; + } + return !isOpenerException(right); + } + 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 (astUtils.isOpeningParenToken(left)) { + return false; + } + + if (sourceCode.isSpaceBetweenTokens(left, right)) { + return false; + } + + if (ALWAYS) { + return !isCloserException(left); + } + 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; + } + + if (!astUtils.isTokenOnSameLine(left, right)) { + return false; + } + + if (!sourceCode.isSpaceBetweenTokens(left, right)) { + return false; + } + + if (ALWAYS) { + return isOpenerException(right); + } + 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 (astUtils.isOpeningParenToken(left)) { + return false; + } + + if (!astUtils.isTokenOnSameLine(left, right)) { + return false; + } + + if (!sourceCode.isSpaceBetweenTokens(left, right)) { + return false; + } + + if (ALWAYS) { + return isCloserException(left); + } + return !isCloserException(left); + + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + Program: function checkParenSpaces(node) { + exceptions = getExceptions(); + const tokens = sourceCode.tokensAndComments; + + tokens.forEach((token, i) => { + const prevToken = tokens[i - 1]; + const nextToken = tokens[i + 1]; + + if (!astUtils.isOpeningParenToken(token) && !astUtils.isClosingParenToken(token)) { + return; + } + + if (token.value === "(" && shouldOpenerHaveSpace(token, nextToken)) { + context.report({ + node, + loc: token.loc.start, + message: MISSING_SPACE_MESSAGE, + fix(fixer) { + return fixer.insertTextAfter(token, " "); + } + }); + } else if (token.value === "(" && shouldOpenerRejectSpace(token, nextToken)) { + context.report({ + node, + loc: token.loc.start, + message: REJECTED_SPACE_MESSAGE, + fix(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, + loc: token.loc.start, + message: MISSING_SPACE_MESSAGE, + fix(fixer) { + return fixer.insertTextBefore(token, " "); + } + }); + } else if (token.value === ")" && shouldCloserRejectSpace(prevToken, token)) { + context.report({ + node, + loc: token.loc.start, + message: REJECTED_SPACE_MESSAGE, + fix(fixer) { + return fixer.removeRange([prevToken.range[1], token.range[0]]); + } + }); + } + }); + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/space-infix-ops.js b/tools/node_modules/eslint/lib/rules/space-infix-ops.js new file mode 100644 index 0000000000..b92c889c7f --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/space-infix-ops.js @@ -0,0 +1,167 @@ +/** + * @fileoverview Require spaces around infix operators + * @author Michael Ficarra + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require spacing around infix operators", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + type: "object", + properties: { + int32Hint: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const int32Hint = context.options[0] ? context.options[0].int32Hint === true : false; + + const OPERATORS = [ + "*", "/", "%", "+", "-", "<<", ">>", ">>>", "<", "<=", ">", ">=", "in", + "instanceof", "==", "!=", "===", "!==", "&", "^", "|", "&&", "||", "=", + "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "&=", "^=", "|=", + "?", ":", ",", "**" + ]; + + const sourceCode = context.getSourceCode(); + + /** + * 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) { + const tokens = sourceCode.getTokensBetween(left, right, 1); + + for (let i = 1, l = tokens.length - 1; i < l; ++i) { + const op = tokens[i]; + + if ( + (op.type === "Punctuator" || op.type === "Keyword") && + OPERATORS.indexOf(op.value) >= 0 && + (tokens[i - 1].range[1] >= op.range[0] || op.range[1] >= tokens[i + 1].range[0]) + ) { + return op; + } + } + return null; + } + + /** + * Reports an AST node as a rule violation + * @param {ASTNode} mainNode - The node to report + * @param {Object} culpritToken - The token which has a problem + * @returns {void} + * @private + */ + function report(mainNode, culpritToken) { + context.report({ + node: mainNode, + loc: culpritToken.loc.start, + message: "Infix operators must be spaced.", + fix(fixer) { + const previousToken = sourceCode.getTokenBefore(culpritToken); + const afterToken = sourceCode.getTokenAfter(culpritToken); + let 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); + } + }); + } + + /** + * Check if the node is binary then report + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function checkBinary(node) { + const leftNode = (node.left.typeAnnotation) ? node.left.typeAnnotation : node.left; + const rightNode = node.right; + + const nonSpacedNode = getFirstNonSpacedToken(leftNode, rightNode); + + if (nonSpacedNode) { + if (!(int32Hint && sourceCode.getText(node).endsWith("|0"))) { + report(node, nonSpacedNode); + } + } + } + + /** + * Check if the node is conditional + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function checkConditional(node) { + const nonSpacedConsequesntNode = getFirstNonSpacedToken(node.test, node.consequent); + const 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) { + const leftNode = (node.id.typeAnnotation) ? node.id.typeAnnotation : node.id; + const rightNode = node.init; + + if (rightNode) { + const nonSpacedNode = getFirstNonSpacedToken(leftNode, rightNode); + + if (nonSpacedNode) { + report(node, nonSpacedNode); + } + } + } + + return { + AssignmentExpression: checkBinary, + AssignmentPattern: checkBinary, + BinaryExpression: checkBinary, + LogicalExpression: checkBinary, + ConditionalExpression: checkConditional, + VariableDeclarator: checkVar + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/space-unary-ops.js b/tools/node_modules/eslint/lib/rules/space-unary-ops.js new file mode 100644 index 0000000000..06a70fb6ad --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/space-unary-ops.js @@ -0,0 +1,319 @@ +/** + * @fileoverview This rule shoud require or disallow spaces before or after unary operations. + * @author Marcin Kumorek + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent spacing before or after unary operators", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + type: "object", + properties: { + words: { + type: "boolean" + }, + nonwords: { + type: "boolean" + }, + overrides: { + type: "object", + additionalProperties: { + type: "boolean" + } + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const options = context.options && Array.isArray(context.options) && context.options[0] || { words: true, nonwords: false }; + + const sourceCode = context.getSourceCode(); + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * 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 === "!"; + } + + /** + * 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 {string} operator Operator + * @returns {boolean} Whether or not an override has been provided for the operator + */ + function overrideExistsForOperator(operator) { + return options.overrides && options.overrides.hasOwnProperty(operator); + } + + /** + * Gets the value that the override was set to for this operator + * @param {string} operator Operator + * @returns {boolean} Whether or not an override enforces a space with this operator + */ + function overrideEnforcesSpaces(operator) { + return options.overrides[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, + message: "Unary word operator '{{word}}' must be followed by whitespace.", + data: { + word + }, + fix(fixer) { + return fixer.insertTextAfter(firstToken, " "); + } + }); + } + } + + /** + * 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, + message: "Unexpected space after unary word operator '{{word}}'.", + data: { + word + }, + fix(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(word)) { + if (overrideEnforcesSpaces(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); + } + } + + /** + * Verifies YieldExpressions satisfy spacing requirements + * @param {ASTnode} node AST node + * @returns {void} + */ + function checkForSpacesAfterYield(node) { + const tokens = sourceCode.getFirstTokens(node, 3), + word = "yield"; + + if (!node.argument || node.delegate) { + return; + } + + checkUnaryWordOperatorForSpaces(node, tokens[0], tokens[1], word); + } + + /** + * Verifies AwaitExpressions satisfy spacing requirements + * @param {ASTNode} node AwaitExpression AST node + * @returns {void} + */ + function checkForSpacesAfterAwait(node) { + const tokens = sourceCode.getFirstTokens(node, 3); + + checkUnaryWordOperatorForSpaces(node, tokens[0], tokens[1], "await"); + } + + /** + * 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, + message: "Unary operator '{{operator}}' must be followed by whitespace.", + data: { + operator: firstToken.value + }, + fix(fixer) { + return fixer.insertTextAfter(firstToken, " "); + } + }); + } + } else { + if (firstToken.range[1] === secondToken.range[0]) { + context.report({ + node, + message: "Space is required before unary expressions '{{token}}'.", + data: { + token: secondToken.value + }, + fix(fixer) { + return fixer.insertTextBefore(secondToken, " "); + } + }); + } + } + } + + /** + * 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, + message: "Unexpected space after unary operator '{{operator}}'.", + data: { + operator: firstToken.value + }, + fix(fixer) { + if (astUtils.canTokensBeAdjacent(firstToken, secondToken)) { + return fixer.removeRange([firstToken.range[1], secondToken.range[0]]); + } + return null; + } + }); + } + } else { + if (secondToken.range[0] > firstToken.range[1]) { + context.report({ + node, + message: "Unexpected space before unary operator '{{operator}}'.", + data: { + operator: secondToken.value + }, + fix(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) { + const tokens = node.type === "UpdateExpression" && !node.prefix + ? sourceCode.getLastTokens(node, 2) + : sourceCode.getFirstTokens(node, 2); + const firstToken = tokens[0]; + const secondToken = tokens[1]; + + if ((node.type === "NewExpression" || node.prefix) && firstToken.type === "Keyword") { + checkUnaryWordOperatorForSpaces(node, firstToken, secondToken); + return; + } + + const operator = node.prefix ? tokens[0].value : tokens[1].value; + + if (overrideExistsForOperator(operator)) { + if (overrideEnforcesSpaces(operator)) { + verifyNonWordsHaveSpaces(node, firstToken, secondToken); + } else { + verifyNonWordsDontHaveSpaces(node, firstToken, secondToken); + } + } else if (options.nonwords) { + verifyNonWordsHaveSpaces(node, firstToken, secondToken); + } else { + verifyNonWordsDontHaveSpaces(node, firstToken, secondToken); + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + UnaryExpression: checkForSpaces, + UpdateExpression: checkForSpaces, + NewExpression: checkForSpaces, + YieldExpression: checkForSpacesAfterYield, + AwaitExpression: checkForSpacesAfterAwait + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/spaced-comment.js b/tools/node_modules/eslint/lib/rules/spaced-comment.js new file mode 100644 index 0000000000..8cdd05514c --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/spaced-comment.js @@ -0,0 +1,375 @@ +/** + * @fileoverview Source code for spaced-comments rule + * @author Gyandeep Singh + */ +"use strict"; + +const lodash = require("lodash"); +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Escapes the control characters of a given string. + * @param {string} s - A string to escape. + * @returns {string} An escaped string. + */ +function escape(s) { + const isOneChar = s.length === 1; + + s = lodash.escapeRegExp(s); + return isOneChar ? s : `(?:${s})`; +} + +/** + * Escapes the control characters of a given string. + * And adds a repeat flag. + * @param {string} s - A string to escape. + * @returns {string} An escaped string. + */ +function escapeAndRepeat(s) { + return `${escape(s)}+`; +} + +/** + * Parses `markers` option. + * If markers don't include `"*"`, this adds `"*"` to allow JSDoc comments. + * @param {string[]} [markers] - A marker list. + * @returns {string[]} A marker list. + */ +function parseMarkersOption(markers) { + markers = markers ? markers.slice(0) : []; + + // `*` is a marker for JSDoc comments. + if (markers.indexOf("*") === -1) { + markers.push("*"); + } + + return markers; +} + +/** + * Creates string pattern for exceptions. + * Generated pattern: + * + * 1. A space or an exception pattern sequence. + * + * @param {string[]} exceptions - An exception pattern list. + * @returns {string} A regular expression string for exceptions. + */ +function createExceptionsPattern(exceptions) { + let pattern = ""; + + /* + * A space or an exception pattern sequence. + * [] ==> "\s" + * ["-"] ==> "(?:\s|\-+$)" + * ["-", "="] ==> "(?:\s|(?:\-+|=+)$)" + * ["-", "=", "--=="] ==> "(?:\s|(?:\-+|=+|(?:\-\-==)+)$)" ==> https://jex.im/regulex/#!embed=false&flags=&re=(%3F%3A%5Cs%7C(%3F%3A%5C-%2B%7C%3D%2B%7C(%3F%3A%5C-%5C-%3D%3D)%2B)%24) + */ + if (exceptions.length === 0) { + + // a space. + pattern += "\\s"; + } else { + + // a space or... + pattern += "(?:\\s|"; + + if (exceptions.length === 1) { + + // a sequence of the exception pattern. + pattern += escapeAndRepeat(exceptions[0]); + } else { + + // a sequence of one of the exception patterns. + pattern += "(?:"; + pattern += exceptions.map(escapeAndRepeat).join("|"); + pattern += ")"; + } + pattern += `(?:$|[${Array.from(astUtils.LINEBREAKS).join("")}]))`; + } + + return pattern; +} + +/** + * Creates RegExp object for `always` mode. + * Generated pattern for beginning of comment: + * + * 1. First, a marker or nothing. + * 2. Next, a space or an exception pattern sequence. + * + * @param {string[]} markers - A marker list. + * @param {string[]} exceptions - An exception pattern list. + * @returns {RegExp} A RegExp object for the beginning of a comment in `always` mode. + */ +function createAlwaysStylePattern(markers, exceptions) { + let pattern = "^"; + + /* + * A marker or nothing. + * ["*"] ==> "\*?" + * ["*", "!"] ==> "(?:\*|!)?" + * ["*", "/", "!<"] ==> "(?:\*|\/|(?:!<))?" ==> https://jex.im/regulex/#!embed=false&flags=&re=(%3F%3A%5C*%7C%5C%2F%7C(%3F%3A!%3C))%3F + */ + if (markers.length === 1) { + + // the marker. + pattern += escape(markers[0]); + } else { + + // one of markers. + pattern += "(?:"; + pattern += markers.map(escape).join("|"); + pattern += ")"; + } + + pattern += "?"; // or nothing. + pattern += createExceptionsPattern(exceptions); + + return new RegExp(pattern); +} + +/** + * Creates RegExp object for `never` mode. + * Generated pattern for beginning of comment: + * + * 1. First, a marker or nothing (captured). + * 2. Next, a space or a tab. + * + * @param {string[]} markers - A marker list. + * @returns {RegExp} A RegExp object for `never` mode. + */ +function createNeverStylePattern(markers) { + const pattern = `^(${markers.map(escape).join("|")})?[ \t]+`; + + return new RegExp(pattern); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent spacing after the `//` or `/*` in a comment", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + exceptions: { + type: "array", + items: { + type: "string" + } + }, + markers: { + type: "array", + 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" + } + }, + balanced: { + type: "boolean" + } + }, + additionalProperties: false + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + const sourceCode = context.getSourceCode(); + + // Unless the first option is never, require a space + const requireSpace = context.options[0] !== "never"; + + /* + * Parse the second options. + * If markers don't include `"*"`, it's added automatically for JSDoc + * comments. + */ + const config = context.options[1] || {}; + const balanced = config.block && config.block.balanced; + + const styleRules = ["block", "line"].reduce((rule, type) => { + const markers = parseMarkersOption(config[type] && config[type].markers || config.markers); + const exceptions = config[type] && config[type].exceptions || config.exceptions || []; + const endNeverPattern = "[ \t]+$"; + + // Create RegExp object for valid patterns. + rule[type] = { + beginRegex: requireSpace ? createAlwaysStylePattern(markers, exceptions) : createNeverStylePattern(markers), + endRegex: balanced && requireSpace ? new RegExp(`${createExceptionsPattern(exceptions)}$`) : new RegExp(endNeverPattern), + hasExceptions: exceptions.length > 0, + markers: new RegExp(`^(${markers.map(escape).join("|")})`) + }; + + return rule; + }, {}); + + /** + * Reports a beginning 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. + * @param {string} refChar - Character used for reference in the error message. + * @returns {void} + */ + function reportBegin(node, message, match, refChar) { + const type = node.type.toLowerCase(), + commentIdentifier = type === "block" ? "/*" : "//"; + + context.report({ + node, + fix(fixer) { + const start = node.range[0]; + let end = start + 2; + + if (requireSpace) { + if (match) { + end += match[0].length; + } + return fixer.insertTextAfterRange([start, end], " "); + } + end += match[0].length; + return fixer.replaceTextRange([start, end], commentIdentifier + (match[1] ? match[1] : "")); + + }, + message, + data: { refChar } + }); + } + + /** + * Reports an ending spacing error with an appropriate message. + * @param {ASTNode} node - A comment node to check. + * @param {string} message - An error message to report. + * @param {string} match - An array of the matched whitespace characters. + * @returns {void} + */ + function reportEnd(node, message, match) { + context.report({ + node, + fix(fixer) { + if (requireSpace) { + return fixer.insertTextAfterRange([node.range[0], node.range[1] - 2], " "); + } + const end = node.range[1] - 2, + start = end - match[0].length; + + return fixer.replaceTextRange([start, end], ""); + + }, + message + }); + } + + /** + * Reports a given comment if it's invalid. + * @param {ASTNode} node - a comment node to check. + * @returns {void} + */ + function checkCommentForSpace(node) { + const type = node.type.toLowerCase(), + rule = styleRules[type], + commentIdentifier = type === "block" ? "/*" : "//"; + + // Ignores empty comments. + if (node.value.length === 0) { + return; + } + + const beginMatch = rule.beginRegex.exec(node.value); + const endMatch = rule.endRegex.exec(node.value); + + // Checks. + if (requireSpace) { + if (!beginMatch) { + const hasMarker = rule.markers.exec(node.value); + const marker = hasMarker ? commentIdentifier + hasMarker[0] : commentIdentifier; + + if (rule.hasExceptions) { + reportBegin(node, "Expected exception block, space or tab after '{{refChar}}' in comment.", hasMarker, marker); + } else { + reportBegin(node, "Expected space or tab after '{{refChar}}' in comment.", hasMarker, marker); + } + } + + if (balanced && type === "block" && !endMatch) { + reportEnd(node, "Expected space or tab before '*/' in comment."); + } + } else { + if (beginMatch) { + if (!beginMatch[1]) { + reportBegin(node, "Unexpected space or tab after '{{refChar}}' in comment.", beginMatch, commentIdentifier); + } else { + reportBegin(node, "Unexpected space or tab after marker ({{refChar}}) in comment.", beginMatch, beginMatch[1]); + } + } + + if (balanced && type === "block" && endMatch) { + reportEnd(node, "Unexpected space or tab before '*/' in comment.", endMatch); + } + } + } + + return { + Program() { + const comments = sourceCode.getAllComments(); + + comments.filter(token => token.type !== "Shebang").forEach(checkCommentForSpace); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/strict.js b/tools/node_modules/eslint/lib/rules/strict.js new file mode 100644 index 0000000000..433b471367 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/strict.js @@ -0,0 +1,277 @@ +/** + * @fileoverview Rule to control usage of strict mode directives. + * @author Brandon Mills + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const messages = { + function: "Use the function form of 'use strict'.", + global: "Use the global form of 'use strict'.", + multiple: "Multiple 'use strict' directives.", + never: "Strict mode is not permitted.", + unnecessary: "Unnecessary 'use strict' directive.", + module: "'use strict' is unnecessary inside of modules.", + implied: "'use strict' is unnecessary when implied strict mode is enabled.", + unnecessaryInClasses: "'use strict' is unnecessary inside of classes.", + nonSimpleParameterList: "'use strict' directive inside a function with non-simple parameter list throws a syntax error since ES2016.", + wrap: "Wrap {{name}} in a function with 'use strict' directive." +}; + +/** + * Gets all of the Use Strict Directives in the Directive Prologue of a group of + * statements. + * @param {ASTNode[]} statements Statements in the program or function body. + * @returns {ASTNode[]} All of the Use Strict Directives. + */ +function getUseStrictDirectives(statements) { + const directives = []; + + for (let i = 0; i < statements.length; i++) { + const statement = statements[i]; + + if ( + statement.type === "ExpressionStatement" && + statement.expression.type === "Literal" && + statement.expression.value === "use strict" + ) { + directives[i] = statement; + } else { + break; + } + } + + return directives; +} + +/** + * Checks whether a given parameter is a simple parameter. + * + * @param {ASTNode} node - A pattern node to check. + * @returns {boolean} `true` if the node is an Identifier node. + */ +function isSimpleParameter(node) { + return node.type === "Identifier"; +} + +/** + * Checks whether a given parameter list is a simple parameter list. + * + * @param {ASTNode[]} params - A parameter list to check. + * @returns {boolean} `true` if the every parameter is an Identifier node. + */ +function isSimpleParameterList(params) { + return params.every(isSimpleParameter); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require or disallow strict mode directives", + category: "Strict Mode", + recommended: false + }, + + schema: [ + { + enum: ["never", "global", "function", "safe"] + } + ], + + fixable: "code" + }, + + create(context) { + + const ecmaFeatures = context.parserOptions.ecmaFeatures || {}, + scopes = [], + classScopes = []; + let mode = context.options[0] || "safe"; + + if (ecmaFeatures.impliedStrict) { + mode = "implied"; + } else if (mode === "safe") { + mode = ecmaFeatures.globalReturn ? "global" : "function"; + } + + /** + * Determines whether a reported error should be fixed, depending on the error type. + * @param {string} errorType The type of error + * @returns {boolean} `true` if the reported error should be fixed + */ + function shouldFix(errorType) { + return errorType === "multiple" || errorType === "unnecessary" || errorType === "module" || errorType === "implied" || errorType === "unnecessaryInClasses"; + } + + /** + * Gets a fixer function to remove a given 'use strict' directive. + * @param {ASTNode} node The directive that should be removed + * @returns {Function} A fixer function + */ + function getFixFunction(node) { + return fixer => fixer.remove(node); + } + + /** + * 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. + * @param {boolean} fix `true` if the directive should be fixed (i.e. removed) + * @returns {void} + */ + function reportSlice(nodes, start, end, message, fix) { + nodes.slice(start, end).forEach(node => { + context.report({ node, message, fix: fix ? getFixFunction(node) : null }); + }); + } + + /** + * Report all nodes in an array with a given message. + * @param {ASTNode[]} nodes Nodes. + * @param {string} message Message to display. + * @param {boolean} fix `true` if the directive should be fixed (i.e. removed) + * @returns {void} + */ + function reportAll(nodes, message, fix) { + reportSlice(nodes, 0, nodes.length, message, fix); + } + + /** + * Report all nodes in an array, except the first, with a given message. + * @param {ASTNode[]} nodes Nodes. + * @param {string} message Message to display. + * @param {boolean} fix `true` if the directive should be fixed (i.e. removed) + * @returns {void} + */ + function reportAllExceptFirst(nodes, message, fix) { + reportSlice(nodes, 1, nodes.length, message, fix); + } + + /** + * 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) { + const 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 (!isSimpleParameterList(node.params)) { + context.report({ node: useStrictDirectives[0], message: messages.nonSimpleParameterList }); + } else if (isParentStrict) { + context.report({ node: useStrictDirectives[0], message: messages.unnecessary, fix: getFixFunction(useStrictDirectives[0]) }); + } else if (isInClass) { + context.report({ node: useStrictDirectives[0], message: messages.unnecessaryInClasses, fix: getFixFunction(useStrictDirectives[0]) }); + } + + reportAllExceptFirst(useStrictDirectives, messages.multiple, true); + } else if (isParentGlobal) { + if (isSimpleParameterList(node.params)) { + context.report({ node, message: messages.function }); + } else { + context.report({ + node, + message: messages.wrap, + data: { name: astUtils.getFunctionNameWithKind(node) } + }); + } + } + + scopes.push(isParentStrict || isStrict); + } + + /** + * Exiting a function in 'function' mode pops its scope off the stack. + * @returns {void} + */ + function exitFunctionInFunctionMode() { + scopes.pop(); + } + + /** + * 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) { + const isBlock = node.body.type === "BlockStatement", + useStrictDirectives = isBlock + ? getUseStrictDirectives(node.body.body) : []; + + if (mode === "function") { + enterFunctionInFunctionMode(node, useStrictDirectives); + } else if (useStrictDirectives.length > 0) { + if (isSimpleParameterList(node.params)) { + reportAll(useStrictDirectives, messages[mode], shouldFix(mode)); + } else { + context.report({ node: useStrictDirectives[0], message: messages.nonSimpleParameterList }); + reportAllExceptFirst(useStrictDirectives, messages.multiple, true); + } + } + } + + const rule = { + Program(node) { + const useStrictDirectives = getUseStrictDirectives(node.body); + + if (node.sourceType === "module") { + mode = "module"; + } + + if (mode === "global") { + if (node.body.length > 0 && useStrictDirectives.length === 0) { + context.report({ node, message: messages.global }); + } + reportAllExceptFirst(useStrictDirectives, messages.multiple, true); + } else { + reportAll(useStrictDirectives, messages[mode], shouldFix(mode)); + } + }, + FunctionDeclaration: enterFunction, + FunctionExpression: enterFunction, + ArrowFunctionExpression: enterFunction + }; + + if (mode === "function") { + Object.assign(rule, { + + // Inside of class bodies are always strict mode. + ClassBody() { + classScopes.push(true); + }, + "ClassBody:exit"() { + classScopes.pop(); + }, + + "FunctionDeclaration:exit": exitFunctionInFunctionMode, + "FunctionExpression:exit": exitFunctionInFunctionMode, + "ArrowFunctionExpression:exit": exitFunctionInFunctionMode + }); + } + + return rule; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/switch-colon-spacing.js b/tools/node_modules/eslint/lib/rules/switch-colon-spacing.js new file mode 100644 index 0000000000..400e850997 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/switch-colon-spacing.js @@ -0,0 +1,133 @@ +/** + * @fileoverview Rule to enforce spacing around colons of switch statements. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce spacing around colons of switch statements", + category: "Stylistic Issues", + recommended: false + }, + schema: [ + { + type: "object", + properties: { + before: { type: "boolean" }, + after: { type: "boolean" } + }, + additionalProperties: false + } + ], + fixable: "whitespace" + }, + + create(context) { + const sourceCode = context.getSourceCode(); + const options = context.options[0] || {}; + const beforeSpacing = options.before === true; // false by default + const afterSpacing = options.after !== false; // true by default + + /** + * Get the colon token of the given SwitchCase node. + * @param {ASTNode} node The SwitchCase node to get. + * @returns {Token} The colon token of the node. + */ + function getColonToken(node) { + if (node.test) { + return sourceCode.getTokenAfter(node.test, astUtils.isColonToken); + } + return sourceCode.getFirstToken(node, 1); + } + + /** + * Check whether the spacing between the given 2 tokens is valid or not. + * @param {Token} left The left token to check. + * @param {Token} right The right token to check. + * @param {boolean} expected The expected spacing to check. `true` if there should be a space. + * @returns {boolean} `true` if the spacing between the tokens is valid. + */ + function isValidSpacing(left, right, expected) { + return ( + astUtils.isClosingBraceToken(right) || + !astUtils.isTokenOnSameLine(left, right) || + sourceCode.isSpaceBetweenTokens(left, right) === expected + ); + } + + /** + * Check whether comments exist between the given 2 tokens. + * @param {Token} left The left token to check. + * @param {Token} right The right token to check. + * @returns {boolean} `true` if comments exist between the given 2 tokens. + */ + function commentsExistBetween(left, right) { + return sourceCode.getFirstTokenBetween( + left, + right, + { + includeComments: true, + filter: astUtils.isCommentToken + } + ) !== null; + } + + /** + * Fix the spacing between the given 2 tokens. + * @param {RuleFixer} fixer The fixer to fix. + * @param {Token} left The left token of fix range. + * @param {Token} right The right token of fix range. + * @param {boolean} spacing The spacing style. `true` if there should be a space. + * @returns {Fix|null} The fix object. + */ + function fix(fixer, left, right, spacing) { + if (commentsExistBetween(left, right)) { + return null; + } + if (spacing) { + return fixer.insertTextAfter(left, " "); + } + return fixer.removeRange([left.range[1], right.range[0]]); + } + + return { + SwitchCase(node) { + const colonToken = getColonToken(node); + const beforeToken = sourceCode.getTokenBefore(colonToken); + const afterToken = sourceCode.getTokenAfter(colonToken); + + if (!isValidSpacing(beforeToken, colonToken, beforeSpacing)) { + context.report({ + node, + loc: colonToken.loc, + message: "{{verb}} space(s) before this colon.", + data: { verb: beforeSpacing ? "Expected" : "Unexpected" }, + fix: fixer => fix(fixer, beforeToken, colonToken, beforeSpacing) + }); + } + if (!isValidSpacing(colonToken, afterToken, afterSpacing)) { + context.report({ + node, + loc: colonToken.loc, + message: "{{verb}} space(s) after this colon.", + data: { verb: afterSpacing ? "Expected" : "Unexpected" }, + fix: fixer => fix(fixer, colonToken, afterToken, afterSpacing) + }); + } + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/symbol-description.js b/tools/node_modules/eslint/lib/rules/symbol-description.js new file mode 100644 index 0000000000..3f5ffd7463 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/symbol-description.js @@ -0,0 +1,66 @@ +/** + * @fileoverview Rule to enforce description with the `Symbol` object + * @author Jarek Rencz + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + + +module.exports = { + meta: { + docs: { + description: "require symbol descriptions", + category: "ECMAScript 6", + recommended: false + }, + + schema: [] + }, + + create(context) { + + /** + * Reports if node does not conform the rule in case rule is set to + * report missing description + * + * @param {ASTNode} node - A CallExpression node to check. + * @returns {void} + */ + function checkArgument(node) { + if (node.arguments.length === 0) { + context.report({ + node, + message: "Expected Symbol to have a description." + }); + } + } + + return { + "Program:exit"() { + const scope = context.getScope(); + const variable = astUtils.getVariableByName(scope, "Symbol"); + + if (variable && variable.defs.length === 0) { + variable.references.forEach(reference => { + const node = reference.identifier; + + if (astUtils.isCallee(node)) { + checkArgument(node.parent); + } + }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/template-curly-spacing.js b/tools/node_modules/eslint/lib/rules/template-curly-spacing.js new file mode 100644 index 0000000000..1d491a24c9 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/template-curly-spacing.js @@ -0,0 +1,121 @@ +/** + * @fileoverview Rule to enforce spacing around embedded expressions of template strings + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const OPEN_PAREN = /\$\{$/; +const CLOSE_PAREN = /^\}/; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +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(context) { + const sourceCode = context.getSourceCode(); + const always = context.options[0] === "always"; + const 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) { + const 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 '}'.", + data: { + prefix + }, + fix(fixer) { + if (always) { + return fixer.insertTextBefore(token, " "); + } + 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) { + const 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 '${'.", + data: { + prefix + }, + fix(fixer) { + if (always) { + return fixer.insertTextAfter(token, " "); + } + return fixer.removeRange([ + token.range[1], + nextToken.range[0] + ]); + } + }); + } + } + + return { + TemplateElement(node) { + const token = sourceCode.getFirstToken(node); + + checkSpacingBefore(token); + checkSpacingAfter(token); + } + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/template-tag-spacing.js b/tools/node_modules/eslint/lib/rules/template-tag-spacing.js new file mode 100755 index 0000000000..907c537ff3 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/template-tag-spacing.js @@ -0,0 +1,77 @@ +/** + * @fileoverview Rule to check spacing between template tags and their literals + * @author Jonathan Wilsson + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require or disallow spacing between template tags and their literals", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { enum: ["always", "never"] } + ] + }, + + create(context) { + const never = context.options[0] !== "always"; + const sourceCode = context.getSourceCode(); + + /** + * Check if a space is present between a template tag and its literal + * @param {ASTNode} node node to evaluate + * @returns {void} + * @private + */ + function checkSpacing(node) { + const tagToken = sourceCode.getTokenBefore(node.quasi); + const literalToken = sourceCode.getFirstToken(node.quasi); + const hasWhitespace = sourceCode.isSpaceBetweenTokens(tagToken, literalToken); + + if (never && hasWhitespace) { + context.report({ + node, + loc: tagToken.loc.start, + message: "Unexpected space between template tag and template literal.", + fix(fixer) { + const comments = sourceCode.getCommentsBefore(node.quasi); + + // Don't fix anything if there's a single line comment after the template tag + if (comments.some(comment => comment.type === "Line")) { + return null; + } + + return fixer.replaceTextRange( + [tagToken.range[1], literalToken.range[0]], + comments.reduce((text, comment) => text + sourceCode.getText(comment), "") + ); + } + }); + } else if (!never && !hasWhitespace) { + context.report({ + node, + loc: tagToken.loc.start, + message: "Missing space between template tag and template literal.", + fix(fixer) { + return fixer.insertTextAfter(tagToken, " "); + } + }); + } + } + + return { + TaggedTemplateExpression: checkSpacing + }; + } +}; diff --git a/tools/node_modules/eslint/lib/rules/unicode-bom.js b/tools/node_modules/eslint/lib/rules/unicode-bom.js new file mode 100644 index 0000000000..7109a49edb --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/unicode-bom.js @@ -0,0 +1,66 @@ +/** + * @fileoverview Require or disallow Unicode BOM + * @author Andrew Johnston <https://github.com/ehjay> + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require or disallow Unicode byte order mark (BOM)", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + enum: ["always", "never"] + } + ] + }, + + create(context) { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + Program: function checkUnicodeBOM(node) { + + const sourceCode = context.getSourceCode(), + location = { column: 0, line: 1 }, + requireBOM = context.options[0] || "never"; + + if (!sourceCode.hasBOM && (requireBOM === "always")) { + context.report({ + node, + loc: location, + message: "Expected Unicode BOM (Byte Order Mark).", + fix(fixer) { + return fixer.insertTextBeforeRange([0, 1], "\uFEFF"); + } + }); + } else if (sourceCode.hasBOM && (requireBOM === "never")) { + context.report({ + node, + loc: location, + message: "Unexpected Unicode BOM (Byte Order Mark).", + fix(fixer) { + return fixer.removeRange([-1, 0]); + } + }); + } + } + + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/use-isnan.js b/tools/node_modules/eslint/lib/rules/use-isnan.js new file mode 100644 index 0000000000..5ec48a0386 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/use-isnan.js @@ -0,0 +1,34 @@ +/** + * @fileoverview Rule to flag comparisons to the value NaN + * @author James Allardice + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require calls to `isNaN()` when checking for `NaN`", + category: "Possible Errors", + recommended: true + }, + + schema: [] + }, + + create(context) { + + return { + BinaryExpression(node) { + if (/^(?:[<>]|[!=]=)=?$/.test(node.operator) && (node.left.name === "NaN" || node.right.name === "NaN")) { + context.report({ node, message: "Use the isNaN function to compare with NaN." }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/valid-jsdoc.js b/tools/node_modules/eslint/lib/rules/valid-jsdoc.js new file mode 100644 index 0000000000..ea60b115ce --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/valid-jsdoc.js @@ -0,0 +1,423 @@ +/** + * @fileoverview Validates JSDoc comments are syntactically correct + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const doctrine = require("doctrine"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce valid JSDoc comments", + category: "Possible Errors", + recommended: false + }, + + 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(context) { + + const 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) + const 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) + }); + } + + /** + * Indicate that return has been found in the current function. + * @param {ASTNode} node The return node. + * @returns {void} + * @private + */ + function addReturn(node) { + const functionState = fns[fns.length - 1]; + + if (functionState && node.argument !== null) { + functionState.returnPresent = true; + } + } + + /** + * 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) { + let currentType; + + if (type.name) { + currentType = type.name; + } else if (type.expression) { + currentType = type.expression.name; + } + + const expectedType = currentType && preferType[currentType]; + + return { + currentType, + expectedType + }; + } + + /** + * Validate type for a given JSDoc node + * @param {Object} jsdocNode JSDoc node + * @param {Object} type JSDoc tag + * @returns {void} + * @private + */ + function validateType(jsdocNode, type) { + if (!type || !canTypeBeValidated(type.type)) { + return; + } + + const typesToCheck = []; + let elements = []; + + switch (type.type) { + case "TypeApplication": // {Array.<String>} + 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}> + if (type.value) { + typesToCheck.push(getCurrentExpectedTypes(type.value)); + } + break; + default: + typesToCheck.push(getCurrentExpectedTypes(type)); + } + + elements.forEach(validateType.bind(null, jsdocNode)); + + typesToCheck.forEach(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 + } + }); + } + }); + } + + /** + * 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) { + const jsdocNode = sourceCode.getJSDocComment(node), + functionData = fns.pop(), + params = Object.create(null), + paramsTags = []; + let hasReturns = false, + returnsTag, + hasConstructor = false, + isInterface = false, + isOverride = false, + isAbstract = false; + + // make sure only to validate JSDoc comments + if (jsdocNode) { + let jsdoc; + + try { + jsdoc = doctrine.parse(jsdocNode.value, { + strict: true, + unwrap: true, + sloppy: true + }); + } catch (ex) { + + if (/braces/i.test(ex.message)) { + context.report({ node: jsdocNode, message: "JSDoc type missing brace." }); + } else { + context.report({ node: jsdocNode, message: "JSDoc syntax error." }); + } + + return; + } + + jsdoc.tags.forEach(tag => { + + switch (tag.title.toLowerCase()) { + + case "param": + case "arg": + case "argument": + paramsTags.push(tag); + break; + + case "return": + case "returns": + hasReturns = true; + returnsTag = tag; + break; + + case "constructor": + case "class": + hasConstructor = true; + break; + + case "override": + case "inheritdoc": + isOverride = true; + break; + + case "abstract": + case "virtual": + isAbstract = true; + break; + + case "interface": + isInterface = true; + break; + + // no default + } + + // check tag preferences + if (prefer.hasOwnProperty(tag.title) && tag.title !== prefer[tag.title]) { + context.report({ node: jsdocNode, message: "Use @{{name}} instead.", data: { name: prefer[tag.title] } }); + } + + // validate the types + if (checkPreferType && tag.type) { + validateType(jsdocNode, tag.type); + } + }); + + paramsTags.forEach(param => { + if (!param.type) { + context.report({ node: jsdocNode, message: "Missing JSDoc parameter type for '{{name}}'.", data: { name: param.name } }); + } + if (!param.description && requireParamDescription) { + context.report({ node: jsdocNode, message: "Missing JSDoc parameter description for '{{name}}'.", data: { name: param.name } }); + } + if (params[param.name]) { + context.report({ node: jsdocNode, message: "Duplicate JSDoc parameter '{{name}}'.", data: { name: param.name } }); + } else if (param.name.indexOf(".") === -1) { + params[param.name] = 1; + } + }); + + if (hasReturns) { + if (!requireReturn && !functionData.returnPresent && (returnsTag.type === null || !isValidReturnType(returnsTag)) && !isAbstract) { + context.report({ + node: jsdocNode, + message: "Unexpected @{{title}} tag; function has no return statement.", + data: { + title: returnsTag.title + } + }); + } else { + if (requireReturnType && !returnsTag.type) { + context.report({ node: jsdocNode, message: "Missing JSDoc return type." }); + } + + if (!isValidReturnType(returnsTag) && !returnsTag.description && requireReturnDescription) { + context.report({ node: jsdocNode, message: "Missing JSDoc return description." }); + } + } + } + + // 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({ + node: jsdocNode, + message: "Missing JSDoc @{{returns}} for function.", + data: { + returns: prefer.returns || "returns" + } + }); + } + } + + // check the parameters + const jsdocParams = Object.keys(params); + + if (node.params) { + node.params.forEach((param, i) => { + if (param.type === "AssignmentPattern") { + param = param.left; + } + + const name = param.name; + + // TODO(nzakas): Figure out logical things to do with destructured, default, rest params + if (param.type === "Identifier") { + if (jsdocParams[i] && (name !== jsdocParams[i])) { + context.report({ + node: jsdocNode, + message: "Expected JSDoc for '{{name}}' but found '{{jsdocName}}'.", + data: { + name, + jsdocName: jsdocParams[i] + } + }); + } else if (!params[name] && !isOverride) { + context.report({ + node: jsdocNode, + message: "Missing JSDoc for parameter '{{name}}'.", + data: { + name + } + }); + } + } + }); + } + + if (options.matchDescription) { + const regex = new RegExp(options.matchDescription); + + if (!regex.test(jsdoc.description)) { + context.report({ node: jsdocNode, message: "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 + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/valid-typeof.js b/tools/node_modules/eslint/lib/rules/valid-typeof.js new file mode 100644 index 0000000000..fba4cc5712 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/valid-typeof.js @@ -0,0 +1,77 @@ +/** + * @fileoverview Ensures that the results of typeof are compared against a valid string + * @author Ian Christian Myers + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce comparing `typeof` expressions against valid strings", + category: "Possible Errors", + recommended: true + }, + + schema: [ + { + type: "object", + properties: { + requireStringLiterals: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + const VALID_TYPES = ["symbol", "undefined", "object", "boolean", "number", "string", "function"], + OPERATORS = ["==", "===", "!=", "!=="]; + + const requireStringLiterals = context.options[0] && context.options[0].requireStringLiterals; + + /** + * Determines whether a node is a typeof expression. + * @param {ASTNode} node The node + * @returns {boolean} `true` if the node is a typeof expression + */ + function isTypeofExpression(node) { + return node.type === "UnaryExpression" && node.operator === "typeof"; + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + UnaryExpression(node) { + if (isTypeofExpression(node)) { + const parent = context.getAncestors().pop(); + + if (parent.type === "BinaryExpression" && OPERATORS.indexOf(parent.operator) !== -1) { + const sibling = parent.left === node ? parent.right : parent.left; + + if (sibling.type === "Literal" || sibling.type === "TemplateLiteral" && !sibling.expressions.length) { + const value = sibling.type === "Literal" ? sibling.value : sibling.quasis[0].value.cooked; + + if (VALID_TYPES.indexOf(value) === -1) { + context.report({ node: sibling, message: "Invalid typeof comparison value." }); + } + } else if (requireStringLiterals && !isTypeofExpression(sibling)) { + context.report({ node: sibling, message: "Typeof comparisons should be to string literals." }); + } + } + } + } + + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/vars-on-top.js b/tools/node_modules/eslint/lib/rules/vars-on-top.js new file mode 100644 index 0000000000..f74db905b1 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/vars-on-top.js @@ -0,0 +1,149 @@ +/** + * @fileoverview Rule to enforce var declarations are only at the top of a function. + * @author Danny Fritz + * @author Gyandeep Singh + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require `var` declarations be placed at the top of their containing scope", + category: "Best Practices", + recommended: false + }, + + schema: [] + }, + + create(context) { + const errorMessage = "All 'var' declarations must be at the top of the function scope."; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * @param {ASTNode} node - any node + * @returns {boolean} whether the given node structurally represents a directive + */ + function looksLikeDirective(node) { + return node.type === "ExpressionStatement" && + node.expression.type === "Literal" && typeof node.expression.value === "string"; + } + + /** + * Check to see if its a ES6 import declaration + * @param {ASTNode} node - any node + * @returns {boolean} whether the given node represents a import declaration + */ + function looksLikeImport(node) { + return node.type === "ImportDeclaration" || node.type === "ImportSpecifier" || + node.type === "ImportDefaultSpecifier" || node.type === "ImportNamespaceSpecifier"; + } + + /** + * Checks whether a given node is a variable declaration or not. + * + * @param {ASTNode} node - any node + * @returns {boolean} `true` if the node is a variable declaration. + */ + function isVariableDeclaration(node) { + return ( + node.type === "VariableDeclaration" || + ( + node.type === "ExportNamedDeclaration" && + node.declaration && + node.declaration.type === "VariableDeclaration" + ) + ); + } + + /** + * 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) { + const l = statements.length; + let i = 0; + + // skip over directives + for (; i < l; ++i) { + if (!looksLikeDirective(statements[i]) && !looksLikeImport(statements[i])) { + break; + } + } + + for (; i < l; ++i) { + if (!isVariableDeclaration(statements[i])) { + return false; + } + if (statements[i] === node) { + return true; + } + } + + return false; + } + + /** + * 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, message: errorMessage }); + } + } + + /** + * Checks whether variable is on top at functional block scope level + * @param {ASTNode} node - The node to check + * @param {ASTNode} parent - Parent of the node + * @param {ASTNode} grandParent - Parent of the node's parent + * @returns {void} + */ + function blockScopeVarCheck(node, parent, grandParent) { + if (!(/Function/.test(grandParent.type) && + parent.type === "BlockStatement" && + isVarOnTop(node, parent.body))) { + context.report({ node, message: errorMessage }); + } + } + + //-------------------------------------------------------------------------- + // Public API + //-------------------------------------------------------------------------- + + return { + VariableDeclaration(node) { + const ancestors = context.getAncestors(); + let parent = ancestors.pop(); + let 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); + } + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/wrap-iife.js b/tools/node_modules/eslint/lib/rules/wrap-iife.js new file mode 100644 index 0000000000..c4e6a9e0c7 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/wrap-iife.js @@ -0,0 +1,151 @@ +/** + * @fileoverview Rule to flag when IIFE is not wrapped in parens + * @author Ilya Volodin + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require parentheses around immediate `function` invocations", + category: "Best Practices", + recommended: false + }, + + schema: [ + { + enum: ["outside", "inside", "any"] + }, + { + type: "object", + properties: { + functionPrototypeMethods: { + type: "boolean" + } + }, + additionalProperties: false + } + ], + + fixable: "code" + }, + + create(context) { + + const style = context.options[0] || "outside"; + const includeFunctionPrototypeMethods = (context.options[1] && context.options[1].functionPrototypeMethods) || false; + + const sourceCode = context.getSourceCode(); + + /** + * Check if the node is wrapped in () + * @param {ASTNode} node node to evaluate + * @returns {boolean} True if it is wrapped + * @private + */ + function wrapped(node) { + return astUtils.isParenthesised(sourceCode, node); + } + + /** + * Get the function node from an IIFE + * @param {ASTNode} node node to evaluate + * @returns {ASTNode} node that is the function expression of the given IIFE, or null if none exist + */ + function getFunctionNodeFromIIFE(node) { + const callee = node.callee; + + if (callee.type === "FunctionExpression") { + return callee; + } + + if (includeFunctionPrototypeMethods && + callee.type === "MemberExpression" && + callee.object.type === "FunctionExpression" && + (astUtils.getStaticPropertyName(callee) === "call" || astUtils.getStaticPropertyName(callee) === "apply") + ) { + return callee.object; + } + + return null; + } + + + return { + CallExpression(node) { + const innerNode = getFunctionNodeFromIIFE(node); + + if (!innerNode) { + return; + } + + const callExpressionWrapped = wrapped(node), + functionExpressionWrapped = wrapped(innerNode); + + if (!callExpressionWrapped && !functionExpressionWrapped) { + context.report({ + node, + message: "Wrap an immediate function invocation in parentheses.", + fix(fixer) { + const nodeToSurround = style === "inside" ? innerNode : node; + + return fixer.replaceText(nodeToSurround, `(${sourceCode.getText(nodeToSurround)})`); + } + }); + } else if (style === "inside" && !functionExpressionWrapped) { + context.report({ + node, + message: "Wrap only the function expression in parens.", + fix(fixer) { + + /* + * The outer call expression will always be wrapped at this point. + * Replace the range between the end of the function expression and the end of the call expression. + * for example, in `(function(foo) {}(bar))`, the range `(bar))` should get replaced with `)(bar)`. + * Replace the parens from the outer expression, and parenthesize the function expression. + */ + const parenAfter = sourceCode.getTokenAfter(node); + + return fixer.replaceTextRange( + [innerNode.range[1], parenAfter.range[1]], + `)${sourceCode.getText().slice(innerNode.range[1], parenAfter.range[0])}` + ); + } + }); + } else if (style === "outside" && !callExpressionWrapped) { + context.report({ + node, + message: "Move the invocation into the parens that contain the function.", + fix(fixer) { + + /* + * The inner function expression will always be wrapped at this point. + * It's only necessary to replace the range between the end of the function expression + * and the call expression. For example, in `(function(foo) {})(bar)`, the range `)(bar)` + * should get replaced with `(bar))`. + */ + const parenAfter = sourceCode.getTokenAfter(innerNode); + + return fixer.replaceTextRange( + [parenAfter.range[0], node.range[1]], + `${sourceCode.getText().slice(parenAfter.range[1], node.range[1])})` + ); + } + }); + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/wrap-regex.js b/tools/node_modules/eslint/lib/rules/wrap-regex.js new file mode 100644 index 0000000000..79f3d30515 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/wrap-regex.js @@ -0,0 +1,52 @@ +/** + * @fileoverview Rule to flag when regex literals are not wrapped in parens + * @author Matt DuVall <http://www.mattduvall.com> + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require parenthesis around regex literals", + category: "Stylistic Issues", + recommended: false + }, + + schema: [], + + fixable: "code" + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + return { + + Literal(node) { + const token = sourceCode.getFirstToken(node), + nodeType = token.type; + + if (nodeType === "RegularExpression") { + const source = sourceCode.getTokenBefore(node); + const ancestors = context.getAncestors(); + const grandparent = ancestors[ancestors.length - 1]; + + if (grandparent.type === "MemberExpression" && grandparent.object === node && + (!source || source.value !== "(")) { + context.report({ + node, + message: "Wrap the regexp literal in parens to disambiguate the slash.", + fix: fixer => fixer.replaceText(node, `(${sourceCode.getText(node)})`) + }); + } + } + } + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/yield-star-spacing.js b/tools/node_modules/eslint/lib/rules/yield-star-spacing.js new file mode 100644 index 0000000000..eb20fc01b0 --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/yield-star-spacing.js @@ -0,0 +1,117 @@ +/** + * @fileoverview Rule to check the spacing around the * in yield* expressions. + * @author Bryan Smith + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require or disallow spacing around the `*` in `yield*` expressions", + category: "ECMAScript 6", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + oneOf: [ + { + enum: ["before", "after", "both", "neither"] + }, + { + type: "object", + properties: { + before: { type: "boolean" }, + after: { type: "boolean" } + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + const 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]) { + const after = leftToken.value === "*"; + const spaceRequired = mode[side]; + const node = after ? leftToken : rightToken; + const type = spaceRequired ? "Missing" : "Unexpected"; + const message = "{{type}} space {{side}} *."; + + context.report({ + node, + message, + data: { + type, + side + }, + fix(fixer) { + if (spaceRequired) { + if (after) { + return fixer.insertTextAfter(node, " "); + } + return fixer.insertTextBefore(node, " "); + } + 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; + } + + const tokens = sourceCode.getFirstTokens(node, 3); + const yieldToken = tokens[0]; + const starToken = tokens[1]; + const nextToken = tokens[2]; + + checkSpacing("before", yieldToken, starToken); + checkSpacing("after", starToken, nextToken); + } + + return { + YieldExpression: checkExpression + }; + + } +}; diff --git a/tools/node_modules/eslint/lib/rules/yoda.js b/tools/node_modules/eslint/lib/rules/yoda.js new file mode 100644 index 0000000000..2ccb61f73a --- /dev/null +++ b/tools/node_modules/eslint/lib/rules/yoda.js @@ -0,0 +1,310 @@ +/** + * @fileoverview Rule to require or disallow yoda comparisons + * @author Nicholas C. Zakas + */ +"use strict"; + +//-------------------------------------------------------------------------- +// Requirements +//-------------------------------------------------------------------------- + +const astUtils = require("../ast-utils"); + +//-------------------------------------------------------------------------- +// Helpers +//-------------------------------------------------------------------------- + +/** + * Determines whether an operator is a comparison operator. + * @param {string} operator The operator to check. + * @returns {boolean} Whether or not it is a comparison operator. + */ +function isComparisonOperator(operator) { + return (/^(==|===|!=|!==|<|>|<=|>=)$/).test(operator); +} + +/** + * Determines whether an operator is an equality operator. + * @param {string} operator The operator to check. + * @returns {boolean} Whether or not it is an equality operator. + */ +function isEqualityOperator(operator) { + return (/^(==|===)$/).test(operator); +} + +/** + * Determines whether an operator is one used in a range test. + * Allowed operators are `<` and `<=`. + * @param {string} operator The operator to check. + * @returns {boolean} Whether the operator is used in range tests. + */ +function isRangeTestOperator(operator) { + return ["<", "<="].indexOf(operator) >= 0; +} + +/** + * Determines whether a non-Literal node is a negative number that should be + * treated as if it were a single Literal node. + * @param {ASTNode} node Node to test. + * @returns {boolean} True if the node is a negative number that looks like a + * real literal and should be treated as such. + */ +function looksLikeLiteral(node) { + return (node.type === "UnaryExpression" && + node.operator === "-" && + node.prefix && + node.argument.type === "Literal" && + typeof node.argument.value === "number"); +} + +/** + * Attempts to derive a Literal node from nodes that are treated like literals. + * @param {ASTNode} node Node to normalize. + * @param {number} [defaultValue] The default value to be returned if the node + * is not a Literal. + * @returns {ASTNode} One of the following options. + * 1. The original node if the node is already a Literal + * 2. A normalized Literal node with the negative number as the value if the + * node represents a negative number literal. + * 3. The Literal node which has the `defaultValue` argument if it exists. + * 4. Otherwise `null`. + */ +function getNormalizedLiteral(node, defaultValue) { + if (node.type === "Literal") { + return node; + } + + if (looksLikeLiteral(node)) { + return { + type: "Literal", + value: -node.argument.value, + raw: `-${node.argument.value}` + }; + } + + if (defaultValue) { + return { + type: "Literal", + value: defaultValue, + raw: String(defaultValue) + }; + } + + return null; +} + +/** + * Checks whether two expressions reference the same value. For example: + * a = a + * a.b = a.b + * a[0] = a[0] + * a['b'] = a['b'] + * @param {ASTNode} a Left side of the comparison. + * @param {ASTNode} b Right side of the comparison. + * @returns {boolean} True if both sides match and reference the same value. + */ +function same(a, b) { + if (a.type !== b.type) { + return false; + } + + switch (a.type) { + case "Identifier": + return a.name === b.name; + + case "Literal": + return a.value === b.value; + + case "MemberExpression": { + const nameA = astUtils.getStaticPropertyName(a); + + // x.y = x["y"] + if (nameA) { + return ( + same(a.object, b.object) && + nameA === astUtils.getStaticPropertyName(b) + ); + } + + /* + * x[0] = x[0] + * x[y] = x[y] + * x.y = x.y + */ + return ( + a.computed === b.computed && + same(a.object, b.object) && + same(a.property, b.property) + ); + } + + case "ThisExpression": + return true; + + default: + return false; + } +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require or disallow \"Yoda\" conditions", + category: "Best Practices", + recommended: false + }, + + schema: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + exceptRange: { + type: "boolean" + }, + onlyEquality: { + type: "boolean" + } + }, + additionalProperties: false + } + ], + + fixable: "code" + }, + + create(context) { + + // Default to "never" (!always) if no option + const always = (context.options[0] === "always"); + const exceptRange = (context.options[1] && context.options[1].exceptRange); + const onlyEquality = (context.options[1] && context.options[1].onlyEquality); + + const sourceCode = context.getSourceCode(); + + /** + * 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) { + const 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() { + let leftLiteral, rightLiteral; + + return (node.operator === "&&" && + (leftLiteral = getNormalizedLiteral(left.left)) && + (rightLiteral = getNormalizedLiteral(right.right, Number.POSITIVE_INFINITY)) && + leftLiteral.value <= rightLiteral.value && + same(left.right, right.left)); + } + + /** + * Determines whether node is of the form `x < 0 || 1 <= x`. + * @returns {boolean} Whether node is an "outside" range test. + */ + function isOutsideTest() { + let leftLiteral, rightLiteral; + + return (node.operator === "||" && + (leftLiteral = getNormalizedLiteral(left.right, Number.NEGATIVE_INFINITY)) && + (rightLiteral = getNormalizedLiteral(right.left)) && + leftLiteral.value <= rightLiteral.value && + same(left.left, right.right)); + } + + /** + * Determines whether node is wrapped in parentheses. + * @returns {boolean} Whether node is preceded immediately by an open + * paren token and followed immediately by a close + * paren token. + */ + function isParenWrapped() { + return astUtils.isParenthesised(sourceCode, node); + } + + return (node.type === "LogicalExpression" && + left.type === "BinaryExpression" && + right.type === "BinaryExpression" && + isRangeTestOperator(left.operator) && + isRangeTestOperator(right.operator) && + (isBetweenTest() || isOutsideTest()) && + isParenWrapped()); + } + + const OPERATOR_FLIP_MAP = { + "===": "===", + "!==": "!==", + "==": "==", + "!=": "!=", + "<": ">", + ">": "<", + "<=": ">=", + ">=": "<=" + }; + + /** + * Returns a string representation of a BinaryExpression node with its sides/operator flipped around. + * @param {ASTNode} node The BinaryExpression node + * @returns {string} A string representation of the node with the sides and operator flipped + */ + function getFlippedString(node) { + const operatorToken = sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator); + const textBeforeOperator = sourceCode.getText().slice(sourceCode.getTokenBefore(operatorToken).range[1], operatorToken.range[0]); + const textAfterOperator = sourceCode.getText().slice(operatorToken.range[1], sourceCode.getTokenAfter(operatorToken).range[0]); + const leftText = sourceCode.getText().slice(node.range[0], sourceCode.getTokenBefore(operatorToken).range[1]); + const rightText = sourceCode.getText().slice(sourceCode.getTokenAfter(operatorToken).range[0], node.range[1]); + + return rightText + textBeforeOperator + OPERATOR_FLIP_MAP[operatorToken.value] + textAfterOperator + leftText; + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + BinaryExpression(node) { + const expectedLiteral = always ? node.left : node.right; + const expectedNonLiteral = always ? node.right : node.left; + + // If `expectedLiteral` is not a literal, and `expectedNonLiteral` is a literal, raise an error. + if ( + (expectedNonLiteral.type === "Literal" || looksLikeLiteral(expectedNonLiteral)) && + !(expectedLiteral.type === "Literal" || looksLikeLiteral(expectedLiteral)) && + !(!isEqualityOperator(node.operator) && onlyEquality) && + isComparisonOperator(node.operator) && + !(exceptRange && isRangeTest(context.getAncestors().pop())) + ) { + context.report({ + node, + message: "Expected literal to be on the {{expectedSide}} side of {{operator}}.", + data: { + operator: node.operator, + expectedSide: always ? "left" : "right" + }, + fix: fixer => fixer.replaceText(node, getFlippedString(node)) + }); + } + + } + }; + + } +}; |