From 6ea3178776eae0e40c3f5498893e8aab0e23686b Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sun, 19 Jul 2020 01:40:43 +0900 Subject: [PATCH] Update: optional chaining support (fixes #12642) (#13416) * update deps (to branch) * trivial fix for debug output * update code path analysis * update accessor-pairs * update array-callback-return * add tests for camelcase * add tests for computed-property-spacing * update dot-location * update dot-notation * update func-name-matching * update global-require * update indent * add tests for getter-return * update new-cap * update newline-per-chained-call * update no-alert * update no-extend-native * update no-extra-bind * update no-extra-parens * update no-eval * update no-implicit-coercion * update eslint-utils * update no-implied-eval * update no-import-assign * update no-magic-numbers * update no-obj-calls * update no-prototype-builtins * add tests for no-restricted-syntax * update no-self-assign * update no-setter-return * update no-unexpected-multiline * update no-unused-expression * update no-useless-call * update no-whitespace-before-property * update operator-assignment * update padding-line-between-statements * update prefer-arrow-callback * add tests for prefer-destructuring * update prefer-exponentiation-operator * update prefer-numeric-literals * update prefer-promise-reject-errors * update prefer-regex-literals * update prefer-spread * update use-isnan * update yoda * update wrap-iife * remove __proto__ * fix no-import-assign for delete op * update eslint-visitor-keys * fix no-unexpected-multiline to just ignore optional chaining * update func-call-spacing * update constructor-super * update dot-location for unstable sort * update no-extra-boolean-cast * update func-call-spacing * update no-extra-parens for false positive on IIFE * update array-callback-return * update no-invalid-this (astUtils.isDefaultThisBinding) * update radix * update a comment in no-implicit-coercion * update comments in no-extra-bind * remove unnecessary change from array-callback-return * update dot-notation for autofix about `let?.[` * update new-cap * update wrap-iife * update prefer-arrow-callback * change isSameReference to handle `a.b` and `a?.b` are same * fix code path analysis for `node.arguments.length == 0` case * update `astUtils.couldBeError` * update `astUtils.isMethodWhichHasThisArg` * improve coverage * fix isMethodWhichHasThisArg * update no-self-assign * Upgrade: espree@7.2.0 Co-authored-by: Kai Cataldo --- .../code-path-analysis/code-path-analyzer.js | 38 + .../code-path-analysis/code-path-segment.js | 1 - .../code-path-analysis/code-path-state.js | 59 + .../code-path-analysis/debug-helpers.js | 45 +- lib/rules/accessor-pairs.js | 15 +- lib/rules/array-callback-return.js | 12 +- lib/rules/consistent-return.js | 13 +- lib/rules/constructor-super.js | 1 + lib/rules/dot-location.js | 34 +- lib/rules/dot-notation.js | 69 +- lib/rules/func-call-spacing.js | 48 +- lib/rules/func-name-matching.js | 5 +- lib/rules/global-require.js | 3 +- lib/rules/indent.js | 19 + lib/rules/new-cap.js | 24 +- lib/rules/newline-per-chained-call.js | 20 +- lib/rules/no-alert.js | 13 +- lib/rules/no-eval.js | 46 +- lib/rules/no-extend-native.js | 77 +- lib/rules/no-extra-bind.js | 74 +- lib/rules/no-extra-boolean-cast.js | 7 + lib/rules/no-extra-parens.js | 34 +- lib/rules/no-implicit-coercion.js | 17 +- lib/rules/no-implied-eval.js | 35 +- lib/rules/no-import-assign.js | 65 +- lib/rules/no-magic-numbers.js | 12 +- lib/rules/no-obj-calls.js | 11 +- lib/rules/no-prototype-builtins.js | 16 +- lib/rules/no-self-assign.js | 56 +- lib/rules/no-setter-return.js | 13 +- lib/rules/no-unexpected-multiline.js | 4 +- lib/rules/no-unused-expressions.js | 78 +- lib/rules/no-useless-call.js | 17 +- lib/rules/no-whitespace-before-property.js | 20 +- lib/rules/operator-assignment.js | 45 +- lib/rules/padding-line-between-statements.js | 4 +- lib/rules/prefer-arrow-callback.js | 115 +- lib/rules/prefer-exponentiation-operator.js | 2 +- lib/rules/prefer-numeric-literals.js | 17 +- lib/rules/prefer-promise-reject-errors.js | 4 +- lib/rules/prefer-regex-literals.js | 7 +- lib/rules/prefer-spread.js | 8 +- lib/rules/radix.js | 7 +- lib/rules/use-isnan.js | 2 +- lib/rules/utils/ast-utils.js | 470 ++++-- lib/rules/wrap-iife.js | 11 +- lib/rules/yoda.js | 57 +- package.json | 6 +- .../code-path-analysis/logical--if-qdot-1.js | 38 + .../optional-chaining--complex-1.js | 38 + .../optional-chaining--complex-2.js | 38 + .../optional-chaining--complex-3.js | 41 + .../optional-chaining--simple-1.js | 19 + .../optional-chaining--simple-2.js | 19 + .../optional-chaining--simple-3.js | 25 + .../optional-chaining--simple-4.js | 19 + tests/lib/rules/accessor-pairs.js | 40 + tests/lib/rules/array-callback-return.js | 27 + tests/lib/rules/camelcase.js | 14 + tests/lib/rules/computed-property-spacing.js | 22 + tests/lib/rules/constructor-super.js | 7 +- tests/lib/rules/dot-location.js | 85 ++ tests/lib/rules/dot-notation.js | 28 + tests/lib/rules/func-call-spacing.js | 134 +- tests/lib/rules/func-name-matching.js | 73 + tests/lib/rules/getter-return.js | 26 +- tests/lib/rules/global-require.js | 8 +- tests/lib/rules/id-blacklist.js | 1359 +++++++++++++++++ tests/lib/rules/indent.js | 99 ++ tests/lib/rules/new-cap.js | 51 +- tests/lib/rules/newline-per-chained-call.js | 66 +- tests/lib/rules/no-alert.js | 12 + tests/lib/rules/no-eval.js | 27 +- tests/lib/rules/no-extend-native.js | 27 +- tests/lib/rules/no-extra-bind.js | 48 + tests/lib/rules/no-extra-boolean-cast.js | 15 + tests/lib/rules/no-extra-parens.js | 55 +- tests/lib/rules/no-implicit-coercion.js | 22 + tests/lib/rules/no-implied-eval.js | 14 + tests/lib/rules/no-import-assign.js | 17 + tests/lib/rules/no-invalid-this.js | 48 + tests/lib/rules/no-magic-numbers.js | 19 + tests/lib/rules/no-obj-calls.js | 12 + tests/lib/rules/no-prototype-builtins.js | 12 + tests/lib/rules/no-restricted-syntax.js | 29 + tests/lib/rules/no-self-assign.js | 14 +- tests/lib/rules/no-setter-return.js | 14 +- tests/lib/rules/no-throw-literal.js | 4 +- tests/lib/rules/no-unexpected-multiline.js | 18 + tests/lib/rules/no-unused-expressions.js | 25 + tests/lib/rules/no-useless-call.js | 88 +- .../rules/no-whitespace-before-property.js | 90 +- tests/lib/rules/operator-assignment.js | 18 +- .../rules/padding-line-between-statements.js | 16 + tests/lib/rules/prefer-arrow-callback.js | 40 +- tests/lib/rules/prefer-destructuring.js | 8 +- .../rules/prefer-exponentiation-operator.js | 11 +- tests/lib/rules/prefer-numeric-literals.js | 29 +- .../lib/rules/prefer-promise-reject-errors.js | 17 +- tests/lib/rules/prefer-regex-literals.js | 9 +- tests/lib/rules/prefer-spread.js | 46 +- tests/lib/rules/radix.js | 22 + tests/lib/rules/use-isnan.js | 18 + tests/lib/rules/utils/ast-utils.js | 151 ++ tests/lib/rules/wrap-iife.js | 30 + tests/lib/rules/yoda.js | 5 + 106 files changed, 4293 insertions(+), 769 deletions(-) create mode 100644 tests/fixtures/code-path-analysis/logical--if-qdot-1.js create mode 100644 tests/fixtures/code-path-analysis/optional-chaining--complex-1.js create mode 100644 tests/fixtures/code-path-analysis/optional-chaining--complex-2.js create mode 100644 tests/fixtures/code-path-analysis/optional-chaining--complex-3.js create mode 100644 tests/fixtures/code-path-analysis/optional-chaining--simple-1.js create mode 100644 tests/fixtures/code-path-analysis/optional-chaining--simple-2.js create mode 100644 tests/fixtures/code-path-analysis/optional-chaining--simple-3.js create mode 100644 tests/fixtures/code-path-analysis/optional-chaining--simple-4.js diff --git a/lib/linter/code-path-analysis/code-path-analyzer.js b/lib/linter/code-path-analysis/code-path-analyzer.js index b612cf43566..3a0cda51118 100644 --- a/lib/linter/code-path-analysis/code-path-analyzer.js +++ b/lib/linter/code-path-analysis/code-path-analyzer.js @@ -244,6 +244,19 @@ function preprocess(analyzer, node) { const parent = node.parent; switch (parent.type) { + + // The `arguments.length == 0` case is in `postprocess` function. + case "CallExpression": + if (parent.optional === true && parent.arguments.length >= 1 && parent.arguments[0] === node) { + state.makeOptionalRight(); + } + break; + case "MemberExpression": + if (parent.optional === true && parent.property === node) { + state.makeOptionalRight(); + } + break; + case "LogicalExpression": if ( parent.right === node && @@ -377,6 +390,20 @@ function processCodePathToEnter(analyzer, node) { analyzer.emitter.emit("onCodePathStart", codePath, node); break; + case "ChainExpression": + state.pushChainContext(); + break; + case "CallExpression": + if (node.optional === true) { + state.makeOptionalNode(); + } + break; + case "MemberExpression": + if (node.optional === true) { + state.makeOptionalNode(); + } + break; + case "LogicalExpression": if (isHandledLogicalOperator(node.operator)) { state.pushChoiceContext( @@ -449,6 +476,10 @@ function processCodePathToExit(analyzer, node) { let dontForward = false; switch (node.type) { + case "ChainExpression": + state.popChainContext(); + break; + case "IfStatement": case "ConditionalExpression": state.popChoiceContext(); @@ -583,6 +614,13 @@ function postprocess(analyzer, node) { break; } + // The `arguments.length >= 1` case is in `preprocess` function. + case "CallExpression": + if (node.optional === true && node.arguments.length === 0) { + CodePath.getState(analyzer.codePath).makeOptionalRight(); + } + break; + default: break; } diff --git a/lib/linter/code-path-analysis/code-path-segment.js b/lib/linter/code-path-analysis/code-path-segment.js index 6b17b25c7fd..ca96ad34189 100644 --- a/lib/linter/code-path-analysis/code-path-segment.js +++ b/lib/linter/code-path-analysis/code-path-segment.js @@ -92,7 +92,6 @@ class CodePathSegment { /* istanbul ignore if */ if (debug.enabled) { this.internal.nodes = []; - this.internal.exitNodes = []; } } diff --git a/lib/linter/code-path-analysis/code-path-state.js b/lib/linter/code-path-analysis/code-path-state.js index 9e760601a0f..f2b16d07e0d 100644 --- a/lib/linter/code-path-analysis/code-path-state.js +++ b/lib/linter/code-path-analysis/code-path-state.js @@ -234,6 +234,7 @@ class CodePathState { this.tryContext = null; this.loopContext = null; this.breakContext = null; + this.chainContext = null; this.currentSegments = []; this.initialSegment = this.forkContext.head[0]; @@ -555,6 +556,64 @@ class CodePathState { ); } + //-------------------------------------------------------------------------- + // ChainExpression + //-------------------------------------------------------------------------- + + /** + * Push a new `ChainExpression` context to the stack. + * This method is called on entering to each `ChainExpression` node. + * This context is used to count forking in the optional chain then merge them on the exiting from the `ChainExpression` node. + * @returns {void} + */ + pushChainContext() { + this.chainContext = { + upper: this.chainContext, + countChoiceContexts: 0 + }; + } + + /** + * Pop a `ChainExpression` context from the stack. + * This method is called on exiting from each `ChainExpression` node. + * This merges all forks of the last optional chaining. + * @returns {void} + */ + popChainContext() { + const context = this.chainContext; + + this.chainContext = context.upper; + + // pop all choice contexts of this. + for (let i = context.countChoiceContexts; i > 0; --i) { + this.popChoiceContext(); + } + } + + /** + * Create a choice context for optional access. + * This method is called on entering to each `(Call|Member)Expression[optional=true]` node. + * This creates a choice context as similar to `LogicalExpression[operator="??"]` node. + * @returns {void} + */ + makeOptionalNode() { + if (this.chainContext) { + this.chainContext.countChoiceContexts += 1; + this.pushChoiceContext("??", false); + } + } + + /** + * Create a fork. + * This method is called on entering to the `arguments|property` property of each `(Call|Member)Expression` node. + * @returns {void} + */ + makeOptionalRight() { + if (this.chainContext) { + this.makeLogicalRight(); + } + } + //-------------------------------------------------------------------------- // SwitchStatement //-------------------------------------------------------------------------- diff --git a/lib/linter/code-path-analysis/debug-helpers.js b/lib/linter/code-path-analysis/debug-helpers.js index bde4e0a38ad..a4cb99a22e0 100644 --- a/lib/linter/code-path-analysis/debug-helpers.js +++ b/lib/linter/code-path-analysis/debug-helpers.js @@ -25,6 +25,22 @@ function getId(segment) { // eslint-disable-line jsdoc/require-jsdoc return segment.id + (segment.reachable ? "" : "!"); } +/** + * Get string for the given node and operation. + * @param {ASTNode} node The node to convert. + * @param {"enter" | "exit" | undefined} label The operation label. + * @returns {string} The string representation. + */ +function nodeToString(node, label) { + const suffix = label ? `:${label}` : ""; + + switch (node.type) { + case "Identifier": return `${node.type}${suffix} (${node.name})`; + case "Literal": return `${node.type}${suffix} (${node.value})`; + default: return `${node.type}${suffix}`; + } +} + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -56,9 +72,15 @@ module.exports = { const segInternal = state.currentSegments[i].internal; if (leaving) { - segInternal.exitNodes.push(node); + const last = segInternal.nodes.length - 1; + + if (last >= 0 && segInternal.nodes[last] === nodeToString(node, "enter")) { + segInternal.nodes[last] = nodeToString(node, void 0); + } else { + segInternal.nodes.push(nodeToString(node, "exit")); + } } else { - segInternal.nodes.push(node); + segInternal.nodes.push(nodeToString(node, "enter")); } } @@ -104,23 +126,8 @@ module.exports = { text += "style=\"rounded,dashed,filled\",fillcolor=\"#FF9800\",label=\"<>\\n"; } - if (segment.internal.nodes.length > 0 || segment.internal.exitNodes.length > 0) { - text += [].concat( - segment.internal.nodes.map(node => { - switch (node.type) { - case "Identifier": return `${node.type} (${node.name})`; - case "Literal": return `${node.type} (${node.value})`; - default: return node.type; - } - }), - segment.internal.exitNodes.map(node => { - switch (node.type) { - case "Identifier": return `${node.type}:exit (${node.name})`; - case "Literal": return `${node.type}:exit (${node.value})`; - default: return `${node.type}:exit`; - } - }) - ).join("\\n"); + if (segment.internal.nodes.length > 0) { + text += segment.internal.nodes.join("\\n"); } else { text += "????"; } diff --git a/lib/rules/accessor-pairs.js b/lib/rules/accessor-pairs.js index cf994ad2574..0e0d07a00c9 100644 --- a/lib/rules/accessor-pairs.js +++ b/lib/rules/accessor-pairs.js @@ -86,16 +86,6 @@ function isAccessorKind(node) { return node.kind === "get" || node.kind === "set"; } -/** - * 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. @@ -109,10 +99,7 @@ function isArgumentOfMethodCall(node, index, object, property) { return ( parent.type === "CallExpression" && - parent.callee.type === "MemberExpression" && - parent.callee.computed === false && - isIdentifier(parent.callee.object, object) && - isIdentifier(parent.callee.property, property) && + astUtils.isSpecificMemberAccess(parent.callee, object, property) && parent.arguments[index] === node ); } diff --git a/lib/rules/array-callback-return.js b/lib/rules/array-callback-return.js index 02a96e311e5..7267347149d 100644 --- a/lib/rules/array-callback-return.js +++ b/lib/rules/array-callback-return.js @@ -28,17 +28,14 @@ function isReachable(segment) { } /** - * Checks a given node is a MemberExpression node which has the specified name's + * Checks a given node is a member access 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 + * @returns {boolean} `true` if the node is a member access which has + * the specified name's property. The node may be a `(Chain|Member)Expression` node. */ function isTargetMethod(node) { - return ( - node.type === "MemberExpression" && - TARGET_METHODS.test(astUtils.getStaticPropertyName(node) || "") - ); + return astUtils.isSpecificMemberAccess(node, null, TARGET_METHODS); } /** @@ -76,6 +73,7 @@ function getArrayMethodName(node) { */ case "LogicalExpression": case "ConditionalExpression": + case "ChainExpression": currentNode = parent; break; diff --git a/lib/rules/consistent-return.js b/lib/rules/consistent-return.js index 22667fa4707..94db253d25b 100644 --- a/lib/rules/consistent-return.js +++ b/lib/rules/consistent-return.js @@ -9,23 +9,12 @@ //------------------------------------------------------------------------------ const lodash = require("lodash"); - const astUtils = require("./utils/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. @@ -165,7 +154,7 @@ module.exports = { let hasReturnValue = Boolean(argument); if (treatUndefinedAsUnspecified && hasReturnValue) { - hasReturnValue = !isIdentifier(argument, "undefined") && argument.operator !== "void"; + hasReturnValue = !astUtils.isSpecificId(argument, "undefined") && argument.operator !== "void"; } if (!funcInfo.hasReturn) { diff --git a/lib/rules/constructor-super.js b/lib/rules/constructor-super.js index 5a848f210ca..65ed7422c25 100644 --- a/lib/rules/constructor-super.js +++ b/lib/rules/constructor-super.js @@ -50,6 +50,7 @@ function isPossibleConstructor(node) { case "MemberExpression": case "CallExpression": case "NewExpression": + case "ChainExpression": case "YieldExpression": case "TaggedTemplateExpression": case "MetaProperty": diff --git a/lib/rules/dot-location.js b/lib/rules/dot-location.js index d483e217a94..0a739b1712b 100644 --- a/lib/rules/dot-location.js +++ b/lib/rules/dot-location.js @@ -52,31 +52,37 @@ module.exports = { */ function checkDotLocation(node) { const property = node.property; - const dot = sourceCode.getTokenBefore(property); - - // `obj` expression can be parenthesized, but those paren tokens are not a part of the `obj` node. - const tokenBeforeDot = sourceCode.getTokenBefore(dot); - - const textBeforeDot = sourceCode.getText().slice(tokenBeforeDot.range[1], dot.range[0]); - const textAfterDot = sourceCode.getText().slice(dot.range[1], property.range[0]); + const dotToken = sourceCode.getTokenBefore(property); if (onObject) { - if (!astUtils.isTokenOnSameLine(tokenBeforeDot, dot)) { - const neededTextAfterToken = astUtils.isDecimalIntegerNumericToken(tokenBeforeDot) ? " " : ""; + // `obj` expression can be parenthesized, but those paren tokens are not a part of the `obj` node. + const tokenBeforeDot = sourceCode.getTokenBefore(dotToken); + + if (!astUtils.isTokenOnSameLine(tokenBeforeDot, dotToken)) { context.report({ node, - loc: dot.loc, + loc: dotToken.loc, messageId: "expectedDotAfterObject", - fix: fixer => fixer.replaceTextRange([tokenBeforeDot.range[1], property.range[0]], `${neededTextAfterToken}.${textBeforeDot}${textAfterDot}`) + *fix(fixer) { + if (dotToken.value.startsWith(".") && astUtils.isDecimalIntegerNumericToken(tokenBeforeDot)) { + yield fixer.insertTextAfter(tokenBeforeDot, ` ${dotToken.value}`); + } else { + yield fixer.insertTextAfter(tokenBeforeDot, dotToken.value); + } + yield fixer.remove(dotToken); + } }); } - } else if (!astUtils.isTokenOnSameLine(dot, property)) { + } else if (!astUtils.isTokenOnSameLine(dotToken, property)) { context.report({ node, - loc: dot.loc, + loc: dotToken.loc, messageId: "expectedDotBeforeProperty", - fix: fixer => fixer.replaceTextRange([tokenBeforeDot.range[1], property.range[0]], `${textBeforeDot}${textAfterDot}.`) + *fix(fixer) { + yield fixer.remove(dotToken); + yield fixer.insertTextBefore(property, dotToken.value); + } }); } } diff --git a/lib/rules/dot-notation.js b/lib/rules/dot-notation.js index 2e8fff8b90e..751b4628edc 100644 --- a/lib/rules/dot-notation.js +++ b/lib/rules/dot-notation.js @@ -87,28 +87,36 @@ module.exports = { data: { key: formattedValue }, - fix(fixer) { + *fix(fixer) { const leftBracket = sourceCode.getTokenAfter(node.object, astUtils.isOpeningBracketToken); const rightBracket = sourceCode.getLastToken(node); + const nextToken = sourceCode.getTokenAfter(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; + // Don't perform any fixes if there are comments inside the brackets. + if (sourceCode.commentsExistBetween(leftBracket, rightBracket)) { + return; // eslint-disable-line eslint-plugin/fixer-return -- false positive } - 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( + // Replace the brackets by an identifier. + if (!node.optional) { + yield fixer.insertTextBefore( + leftBracket, + astUtils.isDecimalInteger(node.object) ? " ." : "." + ); + } + yield fixer.replaceTextRange( [leftBracket.range[0], rightBracket.range[1]], - `${textBeforeDot}.${value}${textAfterProperty}` + value ); + + // Insert a space after the property if it will be connected to the next token. + if ( + nextToken && + rightBracket.range[1] === nextToken.range[0] && + !astUtils.canTokensBeAdjacent(String(value), nextToken) + ) { + yield fixer.insertTextAfter(node, " "); + } } }); } @@ -141,29 +149,24 @@ module.exports = { data: { key: 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()) { + *fix(fixer) { + const dotToken = sourceCode.getTokenBefore(node.property); - // Don't perform any fixes if there are comments between the dot and the property name. - return null; + // A statement that starts with `let[` is parsed as a destructuring variable declaration, not a MemberExpression. + if (node.object.type === "Identifier" && node.object.name === "let" && !node.optional) { + return; // eslint-disable-line eslint-plugin/fixer-return -- false positive } - 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; + // Don't perform any fixes if there are comments between the dot and the property name. + if (sourceCode.commentsExistBetween(dotToken, node.property)) { + return; // eslint-disable-line eslint-plugin/fixer-return -- false positive } - return fixer.replaceTextRange( - [dot.range[0], node.property.range[1]], - `[${textAfterDot}"${node.property.name}"]` - ); + // Replace the identifier to brackets. + if (!node.optional) { + yield fixer.remove(dotToken); + } + yield fixer.replaceText(node.property, `["${node.property.name}"]`); } }); } diff --git a/lib/rules/func-call-spacing.js b/lib/rules/func-call-spacing.js index 5ecb63ecfa7..8fe690d4a6b 100644 --- a/lib/rules/func-call-spacing.js +++ b/lib/rules/func-call-spacing.js @@ -126,15 +126,24 @@ module.exports = { messageId: "unexpectedWhitespace", fix(fixer) { + // Don't remove comments. + if (sourceCode.commentsExistBetween(leftToken, rightToken)) { + return null; + } + + // If `?.` exsits, it doesn't hide no-undexpected-multiline errors + if (node.optional) { + return fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], "?."); + } + /* * Only autofix if there is no newline * https://github.com/eslint/eslint/issues/7787 */ - if (!hasNewline) { - return fixer.removeRange([leftToken.range[1], rightToken.range[0]]); + if (hasNewline) { + return null; } - - return null; + return fixer.removeRange([leftToken.range[1], rightToken.range[0]]); } }); } else if (!never && !hasWhitespace) { @@ -149,6 +158,9 @@ module.exports = { }, messageId: "missing", fix(fixer) { + if (node.optional) { + return null; // Not sure if inserting a space to either before/after `?.` token. + } return fixer.insertTextBefore(rightToken, " "); } }); @@ -161,7 +173,31 @@ module.exports = { }, messageId: "unexpectedNewline", fix(fixer) { - return fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], " "); + + /* + * Only autofix if there is no newline + * https://github.com/eslint/eslint/issues/7787 + * But if `?.` exsits, it doesn't hide no-undexpected-multiline errors + */ + if (!node.optional) { + return null; + } + + // Don't remove comments. + if (sourceCode.commentsExistBetween(leftToken, rightToken)) { + return null; + } + + const range = [leftToken.range[1], rightToken.range[0]]; + const qdToken = sourceCode.getTokenAfter(leftToken); + + if (qdToken.range[0] === leftToken.range[1]) { + return fixer.replaceTextRange(range, "?. "); + } + if (qdToken.range[1] === rightToken.range[0]) { + return fixer.replaceTextRange(range, " ?."); + } + return fixer.replaceTextRange(range, " ?. "); } }); } @@ -172,7 +208,7 @@ module.exports = { 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); + const prevToken = parenToken && sourceCode.getTokenBefore(parenToken, astUtils.isNotQuestionDotToken); // Parens in NewExpression are optional if (!(parenToken && parenToken.range[1] < node.range[1])) { diff --git a/lib/rules/func-name-matching.js b/lib/rules/func-name-matching.js index 83430ffadfc..755c2ee5075 100644 --- a/lib/rules/func-name-matching.js +++ b/lib/rules/func-name-matching.js @@ -117,10 +117,7 @@ module.exports = { if (!node) { return false; } - return node.type === "CallExpression" && - node.callee.type === "MemberExpression" && - node.callee.object.name === objName && - node.callee.property.name === funcName; + return node.type === "CallExpression" && astUtils.isSpecificMemberAccess(node.callee, objName, funcName); } /** diff --git a/lib/rules/global-require.js b/lib/rules/global-require.js index 469c0175d25..09d0332007e 100644 --- a/lib/rules/global-require.js +++ b/lib/rules/global-require.js @@ -13,7 +13,8 @@ const ACCEPTABLE_PARENTS = [ "CallExpression", "ConditionalExpression", "Program", - "VariableDeclaration" + "VariableDeclaration", + "ChainExpression" ]; /** diff --git a/lib/rules/indent.js b/lib/rules/indent.js index d576fde0382..22b633845b5 100644 --- a/lib/rules/indent.js +++ b/lib/rules/indent.js @@ -32,6 +32,7 @@ const KNOWN_NODES = new Set([ "BreakStatement", "CallExpression", "CatchClause", + "ChainExpression", "ClassBody", "ClassDeclaration", "ClassExpression", @@ -934,6 +935,24 @@ module.exports = { parameterParens.add(openingParen); parameterParens.add(closingParen); + /* + * If `?.` token exists, set desired offset for that. + * This logic is copied from `MemberExpression`'s. + */ + if (node.optional) { + const dotToken = sourceCode.getTokenAfter(node.callee, astUtils.isQuestionDotToken); + const calleeParenCount = sourceCode.getTokensBetween(node.callee, dotToken, { filter: astUtils.isClosingParenToken }).length; + const firstTokenOfCallee = calleeParenCount + ? sourceCode.getTokenBefore(node.callee, { skip: calleeParenCount - 1 }) + : sourceCode.getFirstToken(node.callee); + const lastTokenOfCallee = sourceCode.getTokenBefore(dotToken); + const offsetBase = lastTokenOfCallee.loc.end.line === openingParen.loc.start.line + ? lastTokenOfCallee + : firstTokenOfCallee; + + offsets.setDesiredOffset(dotToken, offsetBase, 1); + } + const offsetAfterToken = node.callee.type === "TaggedTemplateExpression" ? sourceCode.getFirstToken(node.callee.quasi) : openingParen; const offsetToken = sourceCode.getTokenBefore(offsetAfterToken); diff --git a/lib/rules/new-cap.js b/lib/rules/new-cap.js index 0faf45efb92..4249a542802 100644 --- a/lib/rules/new-cap.js +++ b/lib/rules/new-cap.js @@ -158,15 +158,9 @@ module.exports = { * @returns {string} name */ function extractNameFromExpression(node) { - - let name = ""; - - if (node.callee.type === "MemberExpression") { - name = astUtils.getStaticPropertyName(node.callee) || ""; - } else { - name = node.callee.name; - } - return name; + return node.callee.type === "Identifier" + ? node.callee.name + : astUtils.getStaticPropertyName(node.callee) || ""; } /** @@ -212,14 +206,16 @@ module.exports = { return true; } - if (calleeName === "UTC" && node.callee.type === "MemberExpression") { + const callee = astUtils.skipChainExpression(node.callee); + + if (calleeName === "UTC" && callee.type === "MemberExpression") { // allow if callee is Date.UTC - return node.callee.object.type === "Identifier" && - node.callee.object.name === "Date"; + return callee.object.type === "Identifier" && + callee.object.name === "Date"; } - return skipProperties && node.callee.type === "MemberExpression"; + return skipProperties && callee.type === "MemberExpression"; } /** @@ -229,7 +225,7 @@ module.exports = { * @returns {void} */ function report(node, messageId) { - let callee = node.callee; + let callee = astUtils.skipChainExpression(node.callee); if (callee.type === "MemberExpression") { callee = callee.property; diff --git a/lib/rules/newline-per-chained-call.js b/lib/rules/newline-per-chained-call.js index 4254fec185e..46c9d6c10f8 100644 --- a/lib/rules/newline-per-chained-call.js +++ b/lib/rules/newline-per-chained-call.js @@ -57,7 +57,16 @@ module.exports = { * @returns {string} The prefix of the node. */ function getPrefix(node) { - return node.computed ? "[" : "."; + if (node.computed) { + if (node.optional) { + return "?.["; + } + return "["; + } + if (node.optional) { + return "?."; + } + return "."; } /** @@ -76,17 +85,18 @@ module.exports = { return { "CallExpression:exit"(node) { - if (!node.callee || node.callee.type !== "MemberExpression") { + const callee = astUtils.skipChainExpression(node.callee); + + if (callee.type !== "MemberExpression") { return; } - const callee = node.callee; - let parent = callee.object; + let parent = astUtils.skipChainExpression(callee.object); let depth = 1; while (parent && parent.callee) { depth += 1; - parent = parent.callee.object; + parent = astUtils.skipChainExpression(astUtils.skipChainExpression(parent.callee).object); } if (depth > ignoreChainWithDepth && astUtils.isTokenOnSameLine(callee.object, callee.property)) { diff --git a/lib/rules/no-alert.js b/lib/rules/no-alert.js index 22d0dd57bdd..702b4d2ba7c 100644 --- a/lib/rules/no-alert.js +++ b/lib/rules/no-alert.js @@ -10,7 +10,8 @@ const { getStaticPropertyName: getPropertyName, - getVariableByName + getVariableByName, + skipChainExpression } = require("./utils/ast-utils"); //------------------------------------------------------------------------------ @@ -64,7 +65,13 @@ function isGlobalThisReferenceOrGlobalWindow(scope, node) { if (scope.type === "global" && node.type === "ThisExpression") { return true; } - if (node.name === "window" || (node.name === "globalThis" && getVariableByName(scope, "globalThis"))) { + if ( + node.type === "Identifier" && + ( + node.name === "window" || + (node.name === "globalThis" && getVariableByName(scope, "globalThis")) + ) + ) { return !isShadowed(scope, node); } @@ -96,7 +103,7 @@ module.exports = { create(context) { return { CallExpression(node) { - const callee = node.callee, + const callee = skipChainExpression(node.callee), currentScope = context.getScope(); // without window. diff --git a/lib/rules/no-eval.js b/lib/rules/no-eval.js index 811ad4e5d73..a020fdee014 100644 --- a/lib/rules/no-eval.js +++ b/lib/rules/no-eval.js @@ -21,38 +21,6 @@ const candidatesOfGlobalObject = Object.freeze([ "globalThis" ]); -/** - * 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. @@ -62,10 +30,7 @@ function isConstant(node, name) { * the specified name's property */ function isMember(node, name) { - return ( - node.type === "MemberExpression" && - (node.computed ? isConstant : isIdentifier)(node.property, name) - ); + return astUtils.isSpecificMemberAccess(node, null, name); } //------------------------------------------------------------------------------ @@ -230,7 +195,12 @@ module.exports = { "CallExpression:exit"(node) { const callee = node.callee; - if (isIdentifier(callee, "eval")) { + /* + * Optional call (`eval?.("code")`) is not direct eval. + * The direct eval is only step 6.a.vi of https://tc39.es/ecma262/#sec-function-calls-runtime-semantics-evaluation + * But the optional call is https://tc39.es/ecma262/#sec-optional-chaining-chain-evaluation + */ + if (!node.optional && astUtils.isSpecificId(callee, "eval")) { report(callee); } } @@ -241,7 +211,7 @@ module.exports = { "CallExpression:exit"(node) { const callee = node.callee; - if (isIdentifier(callee, "eval")) { + if (astUtils.isSpecificId(callee, "eval")) { report(callee); } }, diff --git a/lib/rules/no-extend-native.js b/lib/rules/no-extend-native.js index 7ab25ab4895..db365b50924 100644 --- a/lib/rules/no-extend-native.js +++ b/lib/rules/no-extend-native.js @@ -12,12 +12,6 @@ const astUtils = require("./utils/ast-utils"); const globals = require("globals"); -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -const propertyDefinitionMethods = new Set(["defineProperty", "defineProperties"]); - //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -100,40 +94,30 @@ module.exports = { } /** - * 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. + * Check if it's an assignment to the property of the given node. + * Example: `*.prop = 0` // the `*` is the given node. + * @param {ASTNode} node The node to check. + * @returns {boolean} True if an assignment to the property of the node. */ - 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 + function isAssigningToPropertyOf(node) { + return ( + node.parent.type === "MemberExpression" && + node.parent.object === node && + node.parent.parent.type === "AssignmentExpression" && + node.parent.parent.left === node.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. + * Checks if the given node is at the first argument of the method call of `Object.defineProperty()` or `Object.defineProperties()`. + * @param {ASTNode} node The node to check. + * @returns {boolean} True if the node is at the first argument of the method call of `Object.defineProperty()` or `Object.defineProperties()`. */ - 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) + function isInDefinePropertyCall(node) { + return ( + node.parent.type === "CallExpression" && + node.parent.arguments[0] === node && + astUtils.isSpecificMemberAccess(node.parent.callee, "Object", /^definePropert(?:y|ies)$/u) ); } @@ -149,14 +133,27 @@ module.exports = { * @returns {void} */ function checkAndReportPrototypeExtension(identifierNode) { - if (isInPrototypePropertyAssignment(identifierNode)) { + if (!isPrototypePropertyAccessed(identifierNode)) { + return; // This is not `*.prototype` access. + } + + /* + * `identifierNode.parent` is a MamberExpression `*.prototype`. + * If it's an optional member access, it may be wrapped by a `ChainExpression` node. + */ + const prototypeNode = + identifierNode.parent.parent.type === "ChainExpression" + ? identifierNode.parent.parent + : identifierNode.parent; + + if (isAssigningToPropertyOf(prototypeNode)) { - // Identifier --> MemberExpression --> MemberExpression --> AssignmentExpression - reportNode(identifierNode.parent.parent.parent, identifierNode.name); - } else if (isInDefinePropertyCall(identifierNode)) { + // `*.prototype` -> MemberExpression -> AssignmentExpression + reportNode(prototypeNode.parent.parent, identifierNode.name); + } else if (isInDefinePropertyCall(prototypeNode)) { - // Identifier --> MemberExpression --> CallExpression - reportNode(identifierNode.parent.parent, identifierNode.name); + // `*.prototype` -> CallExpression + reportNode(prototypeNode.parent, identifierNode.name); } } diff --git a/lib/rules/no-extra-bind.js b/lib/rules/no-extra-bind.js index df695924ab5..2db440dc1ea 100644 --- a/lib/rules/no-extra-bind.js +++ b/lib/rules/no-extra-bind.js @@ -61,24 +61,62 @@ module.exports = { * @returns {void} */ function report(node) { + const memberNode = node.parent; + const callNode = memberNode.parent.type === "ChainExpression" + ? memberNode.parent.parent + : memberNode.parent; + context.report({ - node: node.parent.parent, + node: callNode, messageId: "unexpected", - loc: node.parent.property.loc, + loc: memberNode.property.loc, + fix(fixer) { - if (node.parent.parent.arguments.length && !isSideEffectFree(node.parent.parent.arguments[0])) { + if (!isSideEffectFree(callNode.arguments[0])) { return null; } - const firstTokenToRemove = sourceCode - .getFirstTokenBetween(node.parent.object, node.parent.property, astUtils.isNotClosingParenToken); - const lastTokenToRemove = sourceCode.getLastToken(node.parent.parent); + /* + * The list of the first/last token pair of a removal range. + * This is two parts because closing parentheses may exist between the method name and arguments. + * E.g. `(function(){}.bind ) (obj)` + * ^^^^^ ^^^^^ < removal ranges + * E.g. `(function(){}?.['bind'] ) ?.(obj)` + * ^^^^^^^^^^ ^^^^^^^ < removal ranges + */ + const tokenPairs = [ + [ + + // `.`, `?.`, or `[` token. + sourceCode.getTokenAfter( + memberNode.object, + astUtils.isNotClosingParenToken + ), + + // property name or `]` token. + sourceCode.getLastToken(memberNode) + ], + [ + + // `?.` or `(` token of arguments. + sourceCode.getTokenAfter( + memberNode, + astUtils.isNotClosingParenToken + ), + + // `)` token of arguments. + sourceCode.getLastToken(callNode) + ] + ]; + const firstTokenToRemove = tokenPairs[0][0]; + const lastTokenToRemove = tokenPairs[1][1]; if (sourceCode.commentsExistBetween(firstTokenToRemove, lastTokenToRemove)) { return null; } - return fixer.removeRange([firstTokenToRemove.range[0], node.parent.parent.range[1]]); + return tokenPairs.map(([start, end]) => + fixer.removeRange([start.range[0], end.range[1]])); } }); } @@ -93,18 +131,20 @@ module.exports = { * @returns {boolean} `true` if the node is the callee of `.bind()` method. */ function isCalleeOfBindMethod(node) { - const parent = node.parent; - const grandparent = parent.parent; + if (!astUtils.isSpecificMemberAccess(node.parent, null, "bind")) { + return false; + } + + // The node of `*.bind` member access. + const bindNode = node.parent.parent.type === "ChainExpression" + ? node.parent.parent + : node.parent; return ( - grandparent && - grandparent.type === "CallExpression" && - grandparent.callee === parent && - grandparent.arguments.length === 1 && - grandparent.arguments[0].type !== "SpreadElement" && - parent.type === "MemberExpression" && - parent.object === node && - astUtils.getStaticPropertyName(parent) === "bind" + bindNode.parent.type === "CallExpression" && + bindNode.parent.callee === bindNode && + bindNode.parent.arguments.length === 1 && + bindNode.parent.arguments[0].type !== "SpreadElement" ); } diff --git a/lib/rules/no-extra-boolean-cast.js b/lib/rules/no-extra-boolean-cast.js index b90757b1126..6ae3ea62ca7 100644 --- a/lib/rules/no-extra-boolean-cast.js +++ b/lib/rules/no-extra-boolean-cast.js @@ -111,6 +111,10 @@ module.exports = { * @returns {boolean} If the node is in one of the flagged contexts */ function isInFlaggedContext(node) { + if (node.parent.type === "ChainExpression") { + return isInFlaggedContext(node.parent); + } + return isInBooleanContext(node) || (isLogicalContext(node.parent) && @@ -149,6 +153,9 @@ module.exports = { * @returns {boolean} `true` if the node needs to be parenthesized. */ function needsParens(previousNode, node) { + if (previousNode.parent.type === "ChainExpression") { + return needsParens(previousNode.parent, node); + } if (isParenthesized(previousNode)) { // parentheses around the previous node will stay, so there is no need for an additional pair diff --git a/lib/rules/no-extra-parens.js b/lib/rules/no-extra-parens.js index 1ece81eee81..e9d394c616a 100644 --- a/lib/rules/no-extra-parens.js +++ b/lib/rules/no-extra-parens.js @@ -100,10 +100,18 @@ module.exports = { * @private */ function isImmediateFunctionPrototypeMethodCall(node) { - return node.type === "CallExpression" && - node.callee.type === "MemberExpression" && - node.callee.object.type === "FunctionExpression" && - ["call", "apply"].includes(astUtils.getStaticPropertyName(node.callee)); + const callNode = astUtils.skipChainExpression(node); + + if (callNode.type !== "CallExpression") { + return false; + } + const callee = astUtils.skipChainExpression(callNode.callee); + + return ( + callee.type === "MemberExpression" && + callee.object.type === "FunctionExpression" && + ["call", "apply"].includes(astUtils.getStaticPropertyName(callee)) + ); } /** @@ -360,7 +368,9 @@ module.exports = { * @returns {boolean} `true` if the given node is an IIFE */ function isIIFE(node) { - return node.type === "CallExpression" && node.callee.type === "FunctionExpression"; + const maybeCallNode = astUtils.skipChainExpression(node); + + return maybeCallNode.type === "CallExpression" && maybeCallNode.callee.type === "FunctionExpression"; } /** @@ -466,13 +476,16 @@ module.exports = { if ( hasDoubleExcessParens(callee) || - !isIIFE(node) && !hasNewParensException && !( + !isIIFE(node) && + !hasNewParensException && + !( // Allow extra parens around a new expression if they are intervening parentheses. node.type === "NewExpression" && callee.type === "MemberExpression" && doesMemberExpressionContainCallExpression(callee) - ) + ) && + !(!node.optional && callee.type === "ChainExpression") ) { report(node.callee); } @@ -1004,6 +1017,13 @@ module.exports = { report(node.object); } + if (nodeObjHasExcessParens && + node.optional && + node.object.type === "ChainExpression" + ) { + report(node.object); + } + if (node.computed && hasExcessParens(node.property)) { report(node.property); } diff --git a/lib/rules/no-implicit-coercion.js b/lib/rules/no-implicit-coercion.js index 6d5ee61e96b..a639711ecea 100644 --- a/lib/rules/no-implicit-coercion.js +++ b/lib/rules/no-implicit-coercion.js @@ -47,12 +47,14 @@ function isDoubleLogicalNegating(node) { * @returns {boolean} Whether or not the node is a binary negating of `.indexOf()` method calling. */ function isBinaryNegatingOfIndexOf(node) { + if (node.operator !== "~") { + return false; + } + const callNode = astUtils.skipChainExpression(node.argument); + 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) + callNode.type === "CallExpression" && + astUtils.isSpecificMemberAccess(callNode.callee, null, INDEX_OF_PATTERN) ); } @@ -246,7 +248,10 @@ module.exports = { // ~foo.indexOf(bar) operatorAllowed = options.allow.indexOf("~") >= 0; if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) { - const recommendation = `${sourceCode.getText(node.argument)} !== -1`; + + // `foo?.indexOf(bar) !== -1` will be true (== found) if the `foo` is nullish. So use `>= 0` in that case. + const comparison = node.argument.type === "ChainExpression" ? ">= 0" : "!== -1"; + const recommendation = `${sourceCode.getText(node.argument)} ${comparison}`; report(node, recommendation, false); } diff --git a/lib/rules/no-implied-eval.js b/lib/rules/no-implied-eval.js index 1668a0432a5..b8120a64887 100644 --- a/lib/rules/no-implied-eval.js +++ b/lib/rules/no-implied-eval.js @@ -35,8 +35,8 @@ module.exports = { }, create(context) { - const EVAL_LIKE_FUNCS = Object.freeze(["setTimeout", "execScript", "setInterval"]); const GLOBAL_CANDIDATES = Object.freeze(["global", "window", "globalThis"]); + const EVAL_LIKE_FUNC_PATTERN = /^(?:set(?:Interval|Timeout)|execScript)$/u; /** * Checks whether a node is evaluated as a string or not. @@ -56,28 +56,6 @@ module.exports = { return false; } - /** - * Checks whether a node is an Identifier node named one of the specified names. - * @param {ASTNode} node A node to check. - * @param {string[]} specifiers Array of specified name. - * @returns {boolean} True if the node is a Identifier node which has specified name. - */ - function isSpecifiedIdentifier(node, specifiers) { - return node.type === "Identifier" && specifiers.includes(node.name); - } - - /** - * Checks a given node is a MemberExpression node which has the specified name's - * property. - * @param {ASTNode} node A node to check. - * @param {string[]} specifiers Array of specified name. - * @returns {boolean} `true` if the node is a MemberExpression node which has - * the specified name's property - */ - function isSpecifiedMember(node, specifiers) { - return node.type === "MemberExpression" && specifiers.includes(astUtils.getStaticPropertyName(node)); - } - /** * Reports if the `CallExpression` node has evaluated argument. * @param {ASTNode} node A CallExpression to check. @@ -114,14 +92,15 @@ module.exports = { const identifier = ref.identifier; let node = identifier.parent; - while (isSpecifiedMember(node, [name])) { + while (astUtils.isSpecificMemberAccess(node, null, name)) { node = node.parent; } - if (isSpecifiedMember(node, EVAL_LIKE_FUNCS)) { - const parent = node.parent; + if (astUtils.isSpecificMemberAccess(node, null, EVAL_LIKE_FUNC_PATTERN)) { + const calleeNode = node.parent.type === "ChainExpression" ? node.parent : node; + const parent = calleeNode.parent; - if (parent.type === "CallExpression" && parent.callee === node) { + if (parent.type === "CallExpression" && parent.callee === calleeNode) { reportImpliedEvalCallExpression(parent); } } @@ -134,7 +113,7 @@ module.exports = { return { CallExpression(node) { - if (isSpecifiedIdentifier(node.callee, EVAL_LIKE_FUNCS)) { + if (astUtils.isSpecificId(node.callee, EVAL_LIKE_FUNC_PATTERN)) { reportImpliedEvalCallExpression(node); } }, diff --git a/lib/rules/no-import-assign.js b/lib/rules/no-import-assign.js index 32e445ff68b..7a349bb730b 100644 --- a/lib/rules/no-import-assign.js +++ b/lib/rules/no-import-assign.js @@ -9,16 +9,12 @@ // Helpers //------------------------------------------------------------------------------ -const { findVariable, getPropertyName } = require("eslint-utils"); - -const MutationMethods = { - Object: new Set([ - "assign", "defineProperties", "defineProperty", "freeze", - "setPrototypeOf" - ]), - Reflect: new Set([ - "defineProperty", "deleteProperty", "set", "setPrototypeOf" - ]) +const { findVariable } = require("eslint-utils"); +const astUtils = require("./utils/ast-utils"); + +const WellKnownMutationFunctions = { + Object: /^(?:assign|definePropert(?:y|ies)|freeze|setPrototypeOf)$/u, + Reflect: /^(?:(?:define|delete)Property|set(?:PrototypeOf)?)$/u }; /** @@ -56,17 +52,20 @@ function isAssignmentLeft(node) { * @returns {boolean} `true` if the node is the operand of mutation unary operator. */ function isOperandOfMutationUnaryOperator(node) { - const { parent } = node; + const argumentNode = node.parent.type === "ChainExpression" + ? node.parent + : node; + const { parent } = argumentNode; return ( ( parent.type === "UpdateExpression" && - parent.argument === node + parent.argument === argumentNode ) || ( parent.type === "UnaryExpression" && parent.operator === "delete" && - parent.argument === node + parent.argument === argumentNode ) ); } @@ -92,35 +91,37 @@ function isIterationVariable(node) { } /** - * Check if a given node is the iteration variable of `for-in`/`for-of` syntax. + * Check if a given node is at the first argument of a well-known mutation function. + * - `Object.assign` + * - `Object.defineProperty` + * - `Object.defineProperties` + * - `Object.freeze` + * - `Object.setPrototypeOf` + * - `Refrect.defineProperty` + * - `Refrect.deleteProperty` + * - `Refrect.set` + * - `Refrect.setPrototypeOf` * @param {ASTNode} node The node to check. * @param {Scope} scope A `escope.Scope` object to find variable (whichever). - * @returns {boolean} `true` if the node is the iteration variable. + * @returns {boolean} `true` if the node is at the first argument of a well-known mutation function. */ function isArgumentOfWellKnownMutationFunction(node, scope) { const { parent } = node; + if (parent.type !== "CallExpression" || parent.arguments[0] !== node) { + return false; + } + const callee = astUtils.skipChainExpression(parent.callee); + if ( - parent.type === "CallExpression" && - parent.arguments[0] === node && - parent.callee.type === "MemberExpression" && - parent.callee.object.type === "Identifier" + !astUtils.isSpecificMemberAccess(callee, "Object", WellKnownMutationFunctions.Object) && + !astUtils.isSpecificMemberAccess(callee, "Reflect", WellKnownMutationFunctions.Reflect) ) { - const { callee } = parent; - const { object } = callee; - - if (Object.keys(MutationMethods).includes(object.name)) { - const variable = findVariable(scope, object); - - return ( - variable !== null && - variable.scope.type === "global" && - MutationMethods[object.name].has(getPropertyName(callee, scope)) - ); - } + return false; } + const variable = findVariable(scope, callee.object); - return false; + return variable !== null && variable.scope.type === "global"; } /** diff --git a/lib/rules/no-magic-numbers.js b/lib/rules/no-magic-numbers.js index cd07f5c3bda..6f6a156eb75 100644 --- a/lib/rules/no-magic-numbers.js +++ b/lib/rules/no-magic-numbers.js @@ -5,7 +5,7 @@ "use strict"; -const { isNumericLiteral } = require("./utils/ast-utils"); +const astUtils = require("./utils/ast-utils"); // Maximum array length by the ECMAScript Specification. const MAX_ARRAY_LENGTH = 2 ** 32 - 1; @@ -100,12 +100,8 @@ module.exports = { return parent.type === "CallExpression" && fullNumberNode === parent.arguments[1] && ( - parent.callee.name === "parseInt" || - ( - parent.callee.type === "MemberExpression" && - parent.callee.object.name === "Number" && - parent.callee.property.name === "parseInt" - ) + astUtils.isSpecificId(parent.callee, "parseInt") || + astUtils.isSpecificMemberAccess(parent.callee, "Number", "parseInt") ); } @@ -157,7 +153,7 @@ module.exports = { return { Literal(node) { - if (!isNumericLiteral(node)) { + if (!astUtils.isNumericLiteral(node)) { return; } diff --git a/lib/rules/no-obj-calls.js b/lib/rules/no-obj-calls.js index 6139ba2c182..6eb200c9b87 100644 --- a/lib/rules/no-obj-calls.js +++ b/lib/rules/no-obj-calls.js @@ -24,10 +24,13 @@ const nonCallableGlobals = ["Atomics", "JSON", "Math", "Reflect"]; * @returns {string} name to report */ function getReportNodeName(node) { - if (node.callee.type === "MemberExpression") { - return getPropertyName(node.callee); + if (node.type === "ChainExpression") { + return getReportNodeName(node.expression); } - return node.callee.name; + if (node.type === "MemberExpression") { + return getPropertyName(node); + } + return node.name; } //------------------------------------------------------------------------------ @@ -69,7 +72,7 @@ module.exports = { } for (const { node, path } of tracker.iterateGlobalReferences(traceMap)) { - const name = getReportNodeName(node); + const name = getReportNodeName(node.callee); const ref = path[0]; const messageId = name === ref ? "unexpectedCall" : "unexpectedRefCall"; diff --git a/lib/rules/no-prototype-builtins.js b/lib/rules/no-prototype-builtins.js index a00d3707204..ccec86c30da 100644 --- a/lib/rules/no-prototype-builtins.js +++ b/lib/rules/no-prototype-builtins.js @@ -4,6 +4,12 @@ */ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("./utils/ast-utils"); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -39,15 +45,19 @@ module.exports = { * @returns {void} */ function disallowBuiltIns(node) { - if (node.callee.type !== "MemberExpression" || node.callee.computed) { + + // TODO: just use `astUtils.getStaticPropertyName(node.callee)` + const callee = astUtils.skipChainExpression(node.callee); + + if (callee.type !== "MemberExpression" || callee.computed) { return; } - const propName = node.callee.property.name; + const propName = callee.property.name; if (DISALLOWED_PROPS.indexOf(propName) > -1) { context.report({ messageId: "prototypeBuildIn", - loc: node.callee.property.loc, + loc: callee.property.loc, data: { prop: propName }, node }); diff --git a/lib/rules/no-self-assign.js b/lib/rules/no-self-assign.js index 170e46b0593..705be324cf0 100644 --- a/lib/rules/no-self-assign.js +++ b/lib/rules/no-self-assign.js @@ -17,56 +17,6 @@ const astUtils = require("./utils/ast-utils"); const SPACES = /\s+/gu; -/** - * 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); - } - if (lobj.type === "ThisExpression") { - return true; - } - 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 @@ -162,9 +112,9 @@ function eachSelfAssignment(left, right, props, report) { } } else if ( props && - left.type === "MemberExpression" && - right.type === "MemberExpression" && - isSameMember(left, right) + astUtils.skipChainExpression(left).type === "MemberExpression" && + astUtils.skipChainExpression(right).type === "MemberExpression" && + astUtils.isSameReference(left, right) ) { report(right); } diff --git a/lib/rules/no-setter-return.js b/lib/rules/no-setter-return.js index a558640c357..9c79240dda1 100644 --- a/lib/rules/no-setter-return.js +++ b/lib/rules/no-setter-return.js @@ -39,15 +39,12 @@ function isGlobalReference(node, scope) { * @returns {boolean} `true` if the node is argument at the given position. */ function isArgumentOfGlobalMethodCall(node, scope, objectName, methodName, index) { - const parent = node.parent; + const callNode = node.parent; - return parent.type === "CallExpression" && - parent.arguments[index] === node && - parent.callee.type === "MemberExpression" && - astUtils.getStaticPropertyName(parent.callee) === methodName && - parent.callee.object.type === "Identifier" && - parent.callee.object.name === objectName && - isGlobalReference(parent.callee.object, scope); + return callNode.type === "CallExpression" && + callNode.arguments[index] === node && + astUtils.isSpecificMemberAccess(callNode.callee, objectName, methodName) && + isGlobalReference(astUtils.skipChainExpression(callNode.callee).object, scope); } /** diff --git a/lib/rules/no-unexpected-multiline.js b/lib/rules/no-unexpected-multiline.js index b5ec20de4b2..7af3fe67090 100644 --- a/lib/rules/no-unexpected-multiline.js +++ b/lib/rules/no-unexpected-multiline.js @@ -68,7 +68,7 @@ module.exports = { return { MemberExpression(node) { - if (!node.computed) { + if (!node.computed || node.optional) { return; } checkForBreakAfter(node.object, "property"); @@ -96,7 +96,7 @@ module.exports = { }, CallExpression(node) { - if (node.arguments.length === 0) { + if (node.arguments.length === 0 || node.optional) { return; } checkForBreakAfter(node.callee, "function"); diff --git a/lib/rules/no-unused-expressions.js b/lib/rules/no-unused-expressions.js index 8c049f556ff..882a0fd1c11 100644 --- a/lib/rules/no-unused-expressions.js +++ b/lib/rules/no-unused-expressions.js @@ -8,6 +8,22 @@ // Rule Definition //------------------------------------------------------------------------------ +/** + * Returns `true`. + * @returns {boolean} `true`. + */ +function alwaysTrue() { + return true; +} + +/** + * Returns `false`. + * @returns {boolean} `false`. + */ +function alwaysFalse() { + return false; +} + module.exports = { meta: { type: "suggestion", @@ -101,40 +117,56 @@ module.exports = { } /** - * 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 + * The member functions return `true` if the type has no side-effects. + * Unknown nodes are handled as `false`, then this rule ignores those. */ - function isValidExpression(node) { - if (allowTernary) { - - // Recursive check for ternary and logical expressions - if (node.type === "ConditionalExpression") { - return isValidExpression(node.consequent) && isValidExpression(node.alternate); + const Checker = Object.assign(Object.create(null), { + isDisallowed(node) { + return (Checker[node.type] || alwaysFalse)(node); + }, + + ArrayExpression: alwaysTrue, + ArrowFunctionExpression: alwaysTrue, + BinaryExpression: alwaysTrue, + ChainExpression(node) { + return Checker.isDisallowed(node.expression); + }, + ClassExpression: alwaysTrue, + ConditionalExpression(node) { + if (allowTernary) { + return Checker.isDisallowed(node.consequent) || Checker.isDisallowed(node.alternate); } - } - - if (allowShortCircuit) { - if (node.type === "LogicalExpression") { - return isValidExpression(node.right); + return true; + }, + FunctionExpression: alwaysTrue, + Identifier: alwaysTrue, + Literal: alwaysTrue, + LogicalExpression(node) { + if (allowShortCircuit) { + return Checker.isDisallowed(node.right); } - } - - if (allowTaggedTemplates && node.type === "TaggedTemplateExpression") { return true; + }, + MemberExpression: alwaysTrue, + MetaProperty: alwaysTrue, + ObjectExpression: alwaysTrue, + SequenceExpression: alwaysTrue, + TaggedTemplateExpression() { + return !allowTaggedTemplates; + }, + TemplateLiteral: alwaysTrue, + ThisExpression: alwaysTrue, + UnaryExpression(node) { + return node.operator !== "void" && node.operator !== "delete"; } - - return /^(?:Assignment|Call|New|Update|Yield|Await|Import)Expression$/u.test(node.type) || - (node.type === "UnaryExpression" && ["delete", "void"].indexOf(node.operator) >= 0); - } + }); return { ExpressionStatement(node) { - if (!isValidExpression(node.expression) && !isDirective(node, context.getAncestors())) { + if (Checker.isDisallowed(node.expression) && !isDirective(node, context.getAncestors())) { context.report({ node, messageId: "unusedExpression" }); } } }; - } }; diff --git a/lib/rules/no-useless-call.js b/lib/rules/no-useless-call.js index afc729d5de0..b1382a2fa28 100644 --- a/lib/rules/no-useless-call.js +++ b/lib/rules/no-useless-call.js @@ -17,13 +17,15 @@ const astUtils = require("./utils/ast-utils"); * @returns {boolean} Whether or not the node is a `.call()`/`.apply()`. */ function isCallOrNonVariadicApply(node) { + const callee = astUtils.skipChainExpression(node.callee); + return ( - node.callee.type === "MemberExpression" && - node.callee.property.type === "Identifier" && - node.callee.computed === false && + callee.type === "MemberExpression" && + callee.property.type === "Identifier" && + 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") + (callee.property.name === "call" && node.arguments.length >= 1) || + (callee.property.name === "apply" && node.arguments.length === 2 && node.arguments[1].type === "ArrayExpression") ) ); } @@ -74,12 +76,13 @@ module.exports = { return; } - const applied = node.callee.object; + const callee = astUtils.skipChainExpression(node.callee); + const applied = astUtils.skipChainExpression(callee.object); const expectedThis = (applied.type === "MemberExpression") ? applied.object : null; const thisArg = node.arguments[0]; if (isValidThisArg(expectedThis, thisArg, sourceCode)) { - context.report({ node, messageId: "unnecessaryCall", data: { name: node.callee.property.name } }); + context.report({ node, messageId: "unnecessaryCall", data: { name: callee.property.name } }); } } }; diff --git a/lib/rules/no-whitespace-before-property.js b/lib/rules/no-whitespace-before-property.js index ccd0b091b74..226f873c5f6 100644 --- a/lib/rules/no-whitespace-before-property.js +++ b/lib/rules/no-whitespace-before-property.js @@ -49,8 +49,6 @@ module.exports = { * @private */ function reportError(node, leftToken, rightToken) { - const replacementText = node.computed ? "" : "."; - context.report({ node, messageId: "unexpectedWhitespace", @@ -58,7 +56,9 @@ module.exports = { propName: sourceCode.getText(node.property) }, fix(fixer) { - if (!node.computed && astUtils.isDecimalInteger(node.object)) { + let replacementText = ""; + + if (!node.computed && !node.optional && astUtils.isDecimalInteger(node.object)) { /* * If the object is a number literal, fixing it to something like 5.toString() would cause a SyntaxError. @@ -66,6 +66,18 @@ module.exports = { */ return null; } + + // Don't fix if comments exist. + if (sourceCode.commentsExistBetween(leftToken, rightToken)) { + return null; + } + + if (node.optional) { + replacementText = "?."; + } else if (!node.computed) { + replacementText = "."; + } + return fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], replacementText); } }); @@ -86,7 +98,7 @@ module.exports = { if (node.computed) { rightToken = sourceCode.getTokenBefore(node.property, astUtils.isOpeningBracketToken); - leftToken = sourceCode.getTokenBefore(rightToken); + leftToken = sourceCode.getTokenBefore(rightToken, node.optional ? 1 : 0); } else { rightToken = sourceCode.getFirstToken(node.property); leftToken = sourceCode.getTokenBefore(rightToken, 1); diff --git a/lib/rules/operator-assignment.js b/lib/rules/operator-assignment.js index 6820793439c..aee79077f44 100644 --- a/lib/rules/operator-assignment.js +++ b/lib/rules/operator-assignment.js @@ -40,45 +40,6 @@ function isNonCommutativeOperatorWithShorthand(operator) { // 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); - - case "ThisExpression": - return true; - - 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) @@ -148,12 +109,12 @@ module.exports = { const operator = expr.operator; if (isCommutativeOperatorWithShorthand(operator) || isNonCommutativeOperatorWithShorthand(operator)) { - if (same(left, expr.left)) { + if (astUtils.isSameReference(left, expr.left, true)) { context.report({ node, messageId: "replaced", fix(fixer) { - if (canBeFixed(left)) { + if (canBeFixed(left) && canBeFixed(expr.left)) { const equalsToken = getOperatorToken(node); const operatorToken = getOperatorToken(expr); const leftText = sourceCode.getText().slice(node.range[0], equalsToken.range[0]); @@ -169,7 +130,7 @@ module.exports = { return null; } }); - } else if (same(left, expr.right) && isCommutativeOperatorWithShorthand(operator)) { + } else if (astUtils.isSameReference(left, expr.right, true) && isCommutativeOperatorWithShorthand(operator)) { /* * This case can't be fixed safely. diff --git a/lib/rules/padding-line-between-statements.js b/lib/rules/padding-line-between-statements.js index eea19f5ce58..c97b9956b71 100644 --- a/lib/rules/padding-line-between-statements.js +++ b/lib/rules/padding-line-between-statements.js @@ -85,10 +85,10 @@ function newNodeTypeTester(type) { */ function isIIFEStatement(node) { if (node.type === "ExpressionStatement") { - let call = node.expression; + let call = astUtils.skipChainExpression(node.expression); if (call.type === "UnaryExpression") { - call = call.argument; + call = astUtils.skipChainExpression(call.argument); } return call.type === "CallExpression" && astUtils.isFunction(call.callee); } diff --git a/lib/rules/prefer-arrow-callback.js b/lib/rules/prefer-arrow-callback.js index d4e0251940c..ee5cfe3c8c7 100644 --- a/lib/rules/prefer-arrow-callback.js +++ b/lib/rules/prefer-arrow-callback.js @@ -5,6 +5,8 @@ "use strict"; +const astUtils = require("./utils/ast-utils"); + //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ @@ -66,6 +68,7 @@ function getCallbackInfo(node) { const retv = { isCallback: false, isLexicalThis: false }; let currentNode = node; let parent = node.parent; + let bound = false; while (currentNode) { switch (parent.type) { @@ -73,23 +76,34 @@ function getCallbackInfo(node) { // Checks parents recursively. case "LogicalExpression": + case "ChainExpression": case "ConditionalExpression": break; // Checks whether the parent node is `.bind(this)` call. case "MemberExpression": - if (parent.object === currentNode && + if ( + parent.object === currentNode && !parent.property.computed && parent.property.type === "Identifier" && - parent.property.name === "bind" && - parent.parent.type === "CallExpression" && - parent.parent.callee === parent + parent.property.name === "bind" ) { - retv.isLexicalThis = ( - parent.parent.arguments.length === 1 && - parent.parent.arguments[0].type === "ThisExpression" - ); - parent = parent.parent; + const maybeCallee = parent.parent.type === "ChainExpression" + ? parent.parent + : parent; + + if (astUtils.isCallee(maybeCallee)) { + if (!bound) { + bound = true; // Use only the first `.bind()` to make `isLexicalThis` value. + retv.isLexicalThis = ( + maybeCallee.parent.arguments.length === 1 && + maybeCallee.parent.arguments[0].type === "ThisExpression" + ); + } + parent = maybeCallee.parent; + } else { + return retv; + } } else { return retv; } @@ -272,7 +286,7 @@ module.exports = { context.report({ node, messageId: "preferArrowCallback", - fix(fixer) { + *fix(fixer) { if ((!callbackInfo.isLexicalThis && scopeInfo.this) || hasDuplicateParams(node.params)) { /* @@ -281,30 +295,81 @@ module.exports = { * 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; + return; // eslint-disable-line eslint-plugin/fixer-return -- false positive } - 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)}`; + // Remove `.bind(this)` if exists. + if (callbackInfo.isLexicalThis) { + const memberNode = node.parent; - /* - * 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 `.bind(this)` exists but the parent is not `.bind(this)`, don't remove it automatically. + * E.g. `(foo || function(){}).bind(this)` + */ + if (memberNode.type !== "MemberExpression") { + return; // eslint-disable-line eslint-plugin/fixer-return -- false positive + } + + const callNode = memberNode.parent; + const firstTokenToRemove = sourceCode.getTokenAfter(memberNode.object, astUtils.isNotClosingParenToken); + const lastTokenToRemove = sourceCode.getLastToken(callNode); + + /* + * If the member expression is parenthesized, don't remove the right paren. + * E.g. `(function(){}.bind)(this)` + * ^^^^^^^^^^^^ + */ + if (astUtils.isParenthesised(sourceCode, memberNode)) { + return; // eslint-disable-line eslint-plugin/fixer-return -- false positive + } + + // If comments exist in the `.bind(this)`, don't remove those. + if (sourceCode.commentsExistBetween(firstTokenToRemove, lastTokenToRemove)) { + return; // eslint-disable-line eslint-plugin/fixer-return -- false positive + } + + yield fixer.removeRange([firstTokenToRemove.range[0], lastTokenToRemove.range[1]]); + } + + // Convert the function expression to an arrow function. + const functionToken = sourceCode.getFirstToken(node, node.async ? 1 : 0); + const leftParenToken = sourceCode.getTokenAfter(functionToken, astUtils.isOpeningParenToken); + + if (sourceCode.commentsExistBetween(functionToken, leftParenToken)) { + + // Remove only extra tokens to keep comments. + yield fixer.remove(functionToken); + if (node.id) { + yield fixer.remove(node.id); + } + } else { + + // Remove extra tokens and spaces. + yield fixer.removeRange([functionToken.range[0], leftParenToken.range[0]]); + } + yield fixer.insertTextBefore(node.body, "=> "); + + // Get the node that will become the new arrow function. + let replacedNode = callbackInfo.isLexicalThis ? node.parent.parent : node; + + if (replacedNode.type === "ChainExpression") { + replacedNode = replacedNode.parent; + } /* * 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); + if ( + replacedNode.parent.type !== "CallExpression" && + replacedNode.parent.type !== "ConditionalExpression" && + !astUtils.isParenthesised(sourceCode, replacedNode) && + !astUtils.isParenthesised(sourceCode, node) + ) { + yield fixer.insertTextBefore(replacedNode, "("); + yield fixer.insertTextAfter(replacedNode, ")"); + } } }); } diff --git a/lib/rules/prefer-exponentiation-operator.js b/lib/rules/prefer-exponentiation-operator.js index 5e75ef4724f..d1a00d6209e 100644 --- a/lib/rules/prefer-exponentiation-operator.js +++ b/lib/rules/prefer-exponentiation-operator.js @@ -52,7 +52,7 @@ function doesExponentNeedParens(exponent) { * @returns {boolean} `true` if the expression needs to be parenthesised. */ function doesExponentiationExpressionNeedParens(node, sourceCode) { - const parent = node.parent; + const parent = node.parent.type === "ChainExpression" ? node.parent.parent : node.parent; const needsParens = ( parent.type === "ClassDeclaration" || diff --git a/lib/rules/prefer-numeric-literals.js b/lib/rules/prefer-numeric-literals.js index 2a4fb5d954a..662136c4aad 100644 --- a/lib/rules/prefer-numeric-literals.js +++ b/lib/rules/prefer-numeric-literals.js @@ -29,19 +29,10 @@ const radixMap = new Map([ * 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; + return ( + astUtils.isSpecificId(calleeNode, "parseInt") || + astUtils.isSpecificMemberAccess(calleeNode, "Number", "parseInt") + ); } //------------------------------------------------------------------------------ diff --git a/lib/rules/prefer-promise-reject-errors.js b/lib/rules/prefer-promise-reject-errors.js index 56911b67adc..ec16e445555 100644 --- a/lib/rules/prefer-promise-reject-errors.js +++ b/lib/rules/prefer-promise-reject-errors.js @@ -73,9 +73,7 @@ module.exports = { * @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"; + return astUtils.isSpecificMemberAccess(node.callee, "Promise", "reject"); } //---------------------------------------------------------------------- diff --git a/lib/rules/prefer-regex-literals.js b/lib/rules/prefer-regex-literals.js index 8a5d209c1e2..9e8ce023547 100644 --- a/lib/rules/prefer-regex-literals.js +++ b/lib/rules/prefer-regex-literals.js @@ -102,11 +102,8 @@ module.exports = { */ function isStringRawTaggedStaticTemplateLiteral(node) { return node.type === "TaggedTemplateExpression" && - node.tag.type === "MemberExpression" && - node.tag.object.type === "Identifier" && - node.tag.object.name === "String" && - isGlobalReference(node.tag.object) && - astUtils.getStaticPropertyName(node.tag) === "raw" && + astUtils.isSpecificMemberAccess(node.tag, "String", "raw") && + isGlobalReference(astUtils.skipChainExpression(node.tag).object) && isStaticTemplateLiteral(node.quasi); } diff --git a/lib/rules/prefer-spread.js b/lib/rules/prefer-spread.js index bcb0dc0dd4c..d3c3c4d2297 100644 --- a/lib/rules/prefer-spread.js +++ b/lib/rules/prefer-spread.js @@ -18,17 +18,13 @@ const astUtils = require("./utils/ast-utils"); */ function isVariadicApplyCalling(node) { return ( - node.callee.type === "MemberExpression" && - node.callee.property.type === "Identifier" && - node.callee.property.name === "apply" && - node.callee.computed === false && + astUtils.isSpecificMemberAccess(node.callee, null, "apply") && 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. @@ -75,7 +71,7 @@ module.exports = { return; } - const applied = node.callee.object; + const applied = astUtils.skipChainExpression(astUtils.skipChainExpression(node.callee).object); const expectedThis = (applied.type === "MemberExpression") ? applied.object : null; const thisArg = node.arguments[0]; diff --git a/lib/rules/radix.js b/lib/rules/radix.js index 3903cb2a6a2..e3225662388 100644 --- a/lib/rules/radix.js +++ b/lib/rules/radix.js @@ -166,9 +166,12 @@ module.exports = { if (variable && !isShadowed(variable)) { variable.references.forEach(reference => { const node = reference.identifier.parent; + const maybeCallee = node.parent.type === "ChainExpression" + ? node.parent + : node; - if (isParseIntMethod(node) && astUtils.isCallee(node)) { - checkArguments(node.parent); + if (isParseIntMethod(node) && astUtils.isCallee(maybeCallee)) { + checkArguments(maybeCallee.parent); } }); } diff --git a/lib/rules/use-isnan.js b/lib/rules/use-isnan.js index 7b466be75f2..53ffeb7e6d1 100644 --- a/lib/rules/use-isnan.js +++ b/lib/rules/use-isnan.js @@ -106,7 +106,7 @@ module.exports = { * @returns {void} */ function checkCallExpression(node) { - const callee = node.callee; + const callee = astUtils.skipChainExpression(node.callee); if (callee.type === "MemberExpression") { const methodName = astUtils.getStaticPropertyName(callee); diff --git a/lib/rules/utils/ast-utils.js b/lib/rules/utils/ast-utils.js index ecea6948da2..d0dd770d199 100644 --- a/lib/rules/utils/ast-utils.js +++ b/lib/rules/utils/ast-utils.js @@ -143,6 +143,23 @@ function isInLoop(node) { return false; } +/** + * Determines whether the given node is a `null` literal. + * @param {ASTNode} node The node to check + * @returns {boolean} `true` if the node is a `null` literal + */ +function isNullLiteral(node) { + + /* + * Checking `node.value === null` does not guarantee that a literal is a null literal. + * When parsing values that cannot be represented in the current environment (e.g. unicode + * regexes in Node 4), `node.value` is set to `null` because it wouldn't be possible to + * set `node.value` to a unicode regex. To make sure a literal is actually `null`, check + * `node.regex` instead. Also see: https://github.com/eslint/eslint/issues/8020 + */ + return node.type === "Literal" && node.value === null && !node.regex && !node.bigint; +} + /** * Checks whether or not a node is `null` or `undefined`. * @param {ASTNode} node A node to check. @@ -151,7 +168,7 @@ function isInLoop(node) { */ function isNullOrUndefined(node) { return ( - module.exports.isNullLiteral(node) || + isNullLiteral(node) || (node.type === "Identifier" && node.name === "undefined") || (node.type === "UnaryExpression" && node.operator === "void") ); @@ -166,20 +183,270 @@ function isCallee(node) { return node.parent.type === "CallExpression" && node.parent.callee === node; } +/** + * Returns the result of the string conversion applied to the evaluated value of the given expression node, + * if it can be determined statically. + * + * This function returns a `string` value for all `Literal` nodes and simple `TemplateLiteral` nodes only. + * In all other cases, this function returns `null`. + * @param {ASTNode} node Expression node. + * @returns {string|null} String value if it can be determined. Otherwise, `null`. + */ +function getStaticStringValue(node) { + switch (node.type) { + case "Literal": + if (node.value === null) { + if (isNullLiteral(node)) { + return String(node.value); // "null" + } + if (node.regex) { + return `/${node.regex.pattern}/${node.regex.flags}`; + } + if (node.bigint) { + return node.bigint; + } + + // Otherwise, this is an unknown literal. The function will return null. + + } else { + return String(node.value); + } + break; + case "TemplateLiteral": + if (node.expressions.length === 0 && node.quasis.length === 1) { + return node.quasis[0].value.cooked; + } + break; + + // no default + } + + return null; +} + +/** + * Gets the property name of a given node. + * The node can be a MemberExpression, a Property, or a MethodDefinition. + * + * If the name is dynamic, this returns `null`. + * + * For examples: + * + * a.b // => "b" + * a["b"] // => "b" + * a['b'] // => "b" + * a[`b`] // => "b" + * a[100] // => "100" + * a[b] // => null + * a["a" + "b"] // => null + * a[tag`b`] // => null + * a[`${b}`] // => null + * + * let a = {b: 1} // => "b" + * let a = {["b"]: 1} // => "b" + * let a = {['b']: 1} // => "b" + * let a = {[`b`]: 1} // => "b" + * let a = {[100]: 1} // => "100" + * let a = {[b]: 1} // => null + * let a = {["a" + "b"]: 1} // => null + * let a = {[tag`b`]: 1} // => null + * let a = {[`${b}`]: 1} // => null + * @param {ASTNode} node The node to get. + * @returns {string|null} The property name if static. Otherwise, null. + */ +function getStaticPropertyName(node) { + let prop; + + switch (node && node.type) { + case "ChainExpression": + return getStaticPropertyName(node.expression); + + case "Property": + case "MethodDefinition": + prop = node.key; + break; + + case "MemberExpression": + prop = node.property; + break; + + // no default + } + + if (prop) { + if (prop.type === "Identifier" && !node.computed) { + return prop.name; + } + + return getStaticStringValue(prop); + } + + return null; +} + +/** + * Retrieve `ChainExpression#expression` value if the given node a `ChainExpression` node. Otherwise, pass through it. + * @param {ASTNode} node The node to address. + * @returns {ASTNode} The `ChainExpression#expression` value if the node is a `ChainExpression` node. Otherwise, the node. + */ +function skipChainExpression(node) { + return node && node.type === "ChainExpression" ? node.expression : node; +} + +/** + * Check if the `actual` is an expected value. + * @param {string} actual The string value to check. + * @param {string | RegExp} expected The expected string value or pattern. + * @returns {boolean} `true` if the `actual` is an expected value. + */ +function checkText(actual, expected) { + return typeof expected === "string" + ? actual === expected + : expected.test(actual); +} + +/** + * Check if a given node is an Identifier node with a given name. + * @param {ASTNode} node The node to check. + * @param {string | RegExp} name The expected name or the expected pattern of the object name. + * @returns {boolean} `true` if the node is an Identifier node with the name. + */ +function isSpecificId(node, name) { + return node.type === "Identifier" && checkText(node.name, name); +} + +/** + * Check if a given node is member access with a given object name and property name pair. + * This is regardless of optional or not. + * @param {ASTNode} node The node to check. + * @param {string | RegExp | null} objectName The expected name or the expected pattern of the object name. If this is nullish, this method doesn't check object. + * @param {string | RegExp | null} propertyName The expected name or the expected pattern of the property name. If this is nullish, this method doesn't check property. + * @returns {boolean} `true` if the node is member access with the object name and property name pair. + * The node is a `MemberExpression` or `ChainExpression`. + */ +function isSpecificMemberAccess(node, objectName, propertyName) { + const checkNode = skipChainExpression(node); + + if (checkNode.type !== "MemberExpression") { + return false; + } + + if (objectName && !isSpecificId(checkNode.object, objectName)) { + return false; + } + + if (propertyName) { + const actualPropertyName = getStaticPropertyName(checkNode); + + if (typeof actualPropertyName !== "string" || !checkText(actualPropertyName, propertyName)) { + return false; + } + } + + return true; +} + +/** + * Check if two literal nodes are the same value. + * @param {ASTNode} left The Literal node to compare. + * @param {ASTNode} right The other Literal node to compare. + * @returns {boolean} `true` if the two literal nodes are the same value. + */ +function equalLiteralValue(left, right) { + + // RegExp literal. + if (left.regex || right.regex) { + return Boolean( + left.regex && + right.regex && + left.regex.pattern === right.regex.pattern && + left.regex.flags === right.regex.flags + ); + } + + // BigInt literal. + if (left.bigint || right.bigint) { + return left.bigint === right.bigint; + } + + return left.value === right.value; +} + +/** + * Check if two expressions reference the same value. For example: + * a = a + * a.b = a.b + * a[0] = a[0] + * a['b'] = a['b'] + * @param {ASTNode} left The left side of the comparison. + * @param {ASTNode} right The right side of the comparison. + * @param {boolean} [disableStaticComputedKey] Don't address `a.b` and `a["b"]` are the same if `true`. For backward compatibility. + * @returns {boolean} `true` if both sides match and reference the same value. + */ +function isSameReference(left, right, disableStaticComputedKey = false) { + if (left.type !== right.type) { + + // Handle `a.b` and `a?.b` are samely. + if (left.type === "ChainExpression") { + return isSameReference(left.expression, right, disableStaticComputedKey); + } + if (right.type === "ChainExpression") { + return isSameReference(left, right.expression, disableStaticComputedKey); + } + + return false; + } + + switch (left.type) { + case "Super": + case "ThisExpression": + return true; + + case "Identifier": + return left.name === right.name; + case "Literal": + return equalLiteralValue(left, right); + + case "ChainExpression": + return isSameReference(left.expression, right.expression, disableStaticComputedKey); + + case "MemberExpression": { + if (!disableStaticComputedKey) { + const nameA = getStaticPropertyName(left); + + // x.y = x["y"] + if (nameA !== null) { + return ( + isSameReference(left.object, right.object, disableStaticComputedKey) && + nameA === getStaticPropertyName(right) + ); + } + } + + /* + * x[0] = x[0] + * x[y] = x[y] + * x.y = x.y + */ + return ( + left.computed === right.computed && + isSameReference(left.object, right.object, disableStaticComputedKey) && + isSameReference(left.property, right.property, disableStaticComputedKey) + ); + } + + default: + return false; + } +} + /** * Checks whether or not a node is `Reflect.apply`. * @param {ASTNode} node A node to check. * @returns {boolean} Whether or not the node is a `Reflect.apply`. */ function isReflectApply(node) { - return ( - node.type === "MemberExpression" && - node.object.type === "Identifier" && - node.object.name === "Reflect" && - node.property.type === "Identifier" && - node.property.name === "apply" && - node.computed === false - ); + return isSpecificMemberAccess(node, "Reflect", "apply"); } /** @@ -188,14 +455,7 @@ function isReflectApply(node) { * @returns {boolean} Whether or not the node is a `Array.from`. */ function isArrayFromMethod(node) { - return ( - node.type === "MemberExpression" && - node.object.type === "Identifier" && - arrayOrTypedArrayPattern.test(node.object.name) && - node.property.type === "Identifier" && - node.property.name === "from" && - node.computed === false - ); + return isSpecificMemberAccess(node, arrayOrTypedArrayPattern, "from"); } /** @@ -204,17 +464,7 @@ function isArrayFromMethod(node) { * @returns {boolean} Whether or not the node is a method which has `thisArg`. */ function isMethodWhichHasThisArg(node) { - for ( - let currentNode = node; - currentNode.type === "MemberExpression" && !currentNode.computed; - currentNode = currentNode.property - ) { - if (currentNode.property.type === "Identifier") { - return arrayMethodPattern.test(currentNode.property.name); - } - } - - return false; + return isSpecificMemberAccess(node, null, arrayMethodPattern); } /** @@ -289,6 +539,15 @@ function isDotToken(token) { return token.value === "." && token.type === "Punctuator"; } +/** + * 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 isQuestionDotToken(token) { + return token.value === "?." && token.type === "Punctuator"; +} + /** * Checks if the given token is a semicolon token or not. * @param {Token} token The token to check. @@ -505,6 +764,7 @@ module.exports = { isCommaToken, isCommentToken, isDotToken, + isQuestionDotToken, isKeywordToken, isNotClosingBraceToken: negate(isClosingBraceToken), isNotClosingBracketToken: negate(isClosingBracketToken), @@ -512,6 +772,7 @@ module.exports = { isNotColonToken: negate(isColonToken), isNotCommaToken: negate(isCommaToken), isNotDotToken: negate(isDotToken), + isNotQuestionDotToken: negate(isQuestionDotToken), isNotOpeningBraceToken: negate(isOpeningBraceToken), isNotOpeningBracketToken: negate(isOpeningBracketToken), isNotOpeningParenToken: negate(isOpeningParenToken), @@ -669,6 +930,7 @@ module.exports = { */ case "LogicalExpression": case "ConditionalExpression": + case "ChainExpression": currentNode = parent; break; @@ -755,14 +1017,21 @@ module.exports = { * (function foo() { ... }).apply(obj, []); */ case "MemberExpression": - return ( - parent.object !== currentNode || - parent.property.type !== "Identifier" || - !bindOrCallOrApplyPattern.test(parent.property.name) || - !isCallee(parent) || - parent.parent.arguments.length === 0 || - isNullOrUndefined(parent.parent.arguments[0]) - ); + if ( + parent.object === currentNode && + isSpecificMemberAccess(parent, null, bindOrCallOrApplyPattern) + ) { + const maybeCalleeNode = parent.parent.type === "ChainExpression" + ? parent.parent + : parent; + + return !( + isCallee(maybeCalleeNode) && + maybeCalleeNode.parent.arguments.length >= 1 && + !isNullOrUndefined(maybeCalleeNode.parent.arguments[0]) + ); + } + return true; /* * e.g. @@ -884,6 +1153,7 @@ module.exports = { return 17; case "CallExpression": + case "ChainExpression": case "ImportExpression": return 18; @@ -913,104 +1183,6 @@ module.exports = { return isFunction(node) && module.exports.isEmptyBlock(node.body); }, - /** - * Returns the result of the string conversion applied to the evaluated value of the given expression node, - * if it can be determined statically. - * - * This function returns a `string` value for all `Literal` nodes and simple `TemplateLiteral` nodes only. - * In all other cases, this function returns `null`. - * @param {ASTNode} node Expression node. - * @returns {string|null} String value if it can be determined. Otherwise, `null`. - */ - getStaticStringValue(node) { - switch (node.type) { - case "Literal": - if (node.value === null) { - if (module.exports.isNullLiteral(node)) { - return String(node.value); // "null" - } - if (node.regex) { - return `/${node.regex.pattern}/${node.regex.flags}`; - } - if (node.bigint) { - return node.bigint; - } - - // Otherwise, this is an unknown literal. The function will return null. - - } else { - return String(node.value); - } - break; - case "TemplateLiteral": - if (node.expressions.length === 0 && node.quasis.length === 1) { - return node.quasis[0].value.cooked; - } - break; - - // no default - } - - return null; - }, - - /** - * Gets the property name of a given node. - * The node can be a MemberExpression, a Property, or a MethodDefinition. - * - * If the name is dynamic, this returns `null`. - * - * For examples: - * - * a.b // => "b" - * a["b"] // => "b" - * a['b'] // => "b" - * a[`b`] // => "b" - * a[100] // => "100" - * a[b] // => null - * a["a" + "b"] // => null - * a[tag`b`] // => null - * a[`${b}`] // => null - * - * let a = {b: 1} // => "b" - * let a = {["b"]: 1} // => "b" - * let a = {['b']: 1} // => "b" - * let a = {[`b`]: 1} // => "b" - * let a = {[100]: 1} // => "100" - * let a = {[b]: 1} // => null - * let a = {["a" + "b"]: 1} // => null - * let a = {[tag`b`]: 1} // => null - * let a = {[`${b}`]: 1} // => null - * @param {ASTNode} node The node to get. - * @returns {string|null} The property name if static. Otherwise, null. - */ - getStaticPropertyName(node) { - let prop; - - switch (node && node.type) { - case "Property": - case "MethodDefinition": - prop = node.key; - break; - - case "MemberExpression": - prop = node.property; - break; - - // no default - } - - if (prop) { - if (prop.type === "Identifier" && !node.computed) { - return prop.name; - } - - return module.exports.getStaticStringValue(prop); - } - - return null; - }, - /** * Get directives from directive prologue of a Program or Function node. * @param {ASTNode} node The node to check. @@ -1164,7 +1336,7 @@ module.exports = { if (node.id) { tokens.push(`'${node.id.name}'`); } else { - const name = module.exports.getStaticPropertyName(parent); + const name = getStaticPropertyName(parent); if (name !== null) { tokens.push(`'${name}'`); @@ -1391,6 +1563,7 @@ module.exports = { case "TaggedTemplateExpression": case "YieldExpression": case "AwaitExpression": + case "ChainExpression": return true; // possibly an error object. case "AssignmentExpression": @@ -1413,23 +1586,6 @@ module.exports = { } }, - /** - * Determines whether the given node is a `null` literal. - * @param {ASTNode} node The node to check - * @returns {boolean} `true` if the node is a `null` literal - */ - isNullLiteral(node) { - - /* - * Checking `node.value === null` does not guarantee that a literal is a null literal. - * When parsing values that cannot be represented in the current environment (e.g. unicode - * regexes in Node 4), `node.value` is set to `null` because it wouldn't be possible to - * set `node.value` to a unicode regex. To make sure a literal is actually `null`, check - * `node.regex` instead. Also see: https://github.com/eslint/eslint/issues/8020 - */ - return node.type === "Literal" && node.value === null && !node.regex && !node.bigint; - }, - /** * Check if a given node is a numeric literal or not. * @param {ASTNode} node The node to check. @@ -1590,5 +1746,13 @@ module.exports = { isLogicalExpression, isCoalesceExpression, - isMixedLogicalAndCoalesceExpressions + isMixedLogicalAndCoalesceExpressions, + isNullLiteral, + getStaticStringValue, + getStaticPropertyName, + skipChainExpression, + isSpecificId, + isSpecificMemberAccess, + equalLiteralValue, + isSameReference }; diff --git a/lib/rules/wrap-iife.js b/lib/rules/wrap-iife.js index 896aed63de5..07e5b84a4a5 100644 --- a/lib/rules/wrap-iife.js +++ b/lib/rules/wrap-iife.js @@ -23,7 +23,14 @@ const eslintUtils = require("eslint-utils"); * @private */ function isCalleeOfNewExpression(node) { - return node.parent.type === "NewExpression" && node.parent.callee === node; + const maybeCallee = node.parent.type === "ChainExpression" + ? node.parent + : node; + + return ( + maybeCallee.parent.type === "NewExpression" && + maybeCallee.parent.callee === maybeCallee + ); } //------------------------------------------------------------------------------ @@ -98,7 +105,7 @@ module.exports = { * @returns {ASTNode} node that is the function expression of the given IIFE, or null if none exist */ function getFunctionNodeFromIIFE(node) { - const callee = node.callee; + const callee = astUtils.skipChainExpression(node.callee); if (callee.type === "FunctionExpression") { return callee; diff --git a/lib/rules/yoda.js b/lib/rules/yoda.js index f1159e5255d..87dd5ed6c57 100644 --- a/lib/rules/yoda.js +++ b/lib/rules/yoda.js @@ -111,59 +111,6 @@ function getNormalizedLiteral(node) { 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 !== null) { - 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 //------------------------------------------------------------------------------ @@ -236,7 +183,7 @@ module.exports = { * @returns {boolean} Whether node is a "between" range test. */ function isBetweenTest() { - if (node.operator === "&&" && same(left.right, right.left)) { + if (node.operator === "&&" && astUtils.isSameReference(left.right, right.left)) { const leftLiteral = getNormalizedLiteral(left.left); const rightLiteral = getNormalizedLiteral(right.right); @@ -260,7 +207,7 @@ module.exports = { * @returns {boolean} Whether node is an "outside" range test. */ function isOutsideTest() { - if (node.operator === "||" && same(left.left, right.right)) { + if (node.operator === "||" && astUtils.isSameReference(left.left, right.right)) { const leftLiteral = getNormalizedLiteral(left.right); const rightLiteral = getNormalizedLiteral(right.left); diff --git a/package.json b/package.json index 442639e6d67..727a7a3a41c 100644 --- a/package.json +++ b/package.json @@ -54,9 +54,9 @@ "doctrine": "^3.0.0", "enquirer": "^2.3.5", "eslint-scope": "^5.1.0", - "eslint-utils": "^2.0.0", - "eslint-visitor-keys": "^1.2.0", - "espree": "^7.1.0", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^1.3.0", + "espree": "^7.2.0", "esquery": "^1.2.0", "esutils": "^2.0.2", "file-entry-cache": "^5.0.1", diff --git a/tests/fixtures/code-path-analysis/logical--if-qdot-1.js b/tests/fixtures/code-path-analysis/logical--if-qdot-1.js new file mode 100644 index 00000000000..4e1d7b000c7 --- /dev/null +++ b/tests/fixtures/code-path-analysis/logical--if-qdot-1.js @@ -0,0 +1,38 @@ +/*expected +initial->s1_1->s1_2->s1_3->s1_4->s1_5->s1_6->s1_7->s1_9->s1_11; +s1_1->s1_3->s1_10->s1_11; +s1_4->s1_6->s1_8->s1_9; +s1_11->final; +*/ +if (obj?.foo) { + if (obj?.bar) { + foo(); + } else { + bar(); + } +} else { + qiz(); +} + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nIfStatement:enter\nChainExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_2[label="Identifier (foo)\nMemberExpression:exit"]; + s1_3[label="ChainExpression:exit"]; + s1_4[label="BlockStatement:enter\nIfStatement:enter\nChainExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_5[label="Identifier (bar)\nMemberExpression:exit"]; + s1_6[label="ChainExpression:exit"]; + s1_7[label="BlockStatement:enter\nExpressionStatement:enter\nCallExpression:enter\nIdentifier (foo)\nCallExpression:exit\nExpressionStatement:exit\nBlockStatement:exit"]; + s1_9[label="IfStatement:exit\nBlockStatement:exit"]; + s1_11[label="IfStatement:exit\nProgram:exit"]; + s1_10[label="BlockStatement:enter\nExpressionStatement:enter\nCallExpression:enter\nIdentifier (qiz)\nCallExpression:exit\nExpressionStatement:exit\nBlockStatement:exit"]; + s1_8[label="BlockStatement:enter\nExpressionStatement:enter\nCallExpression:enter\nIdentifier (bar)\nCallExpression:exit\nExpressionStatement:exit\nBlockStatement:exit"]; + initial->s1_1->s1_2->s1_3->s1_4->s1_5->s1_6->s1_7->s1_9->s1_11; + s1_1->s1_3->s1_10->s1_11; + s1_4->s1_6->s1_8->s1_9; + s1_11->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--complex-1.js b/tests/fixtures/code-path-analysis/optional-chaining--complex-1.js new file mode 100644 index 00000000000..5444269a841 --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--complex-1.js @@ -0,0 +1,38 @@ +/*expected +initial->s1_1->s1_2->s1_3->s1_5->s1_6->s1_7->s1_8->s1_9->s1_10->s1_11->s1_12->s1_13->s1_16; +s1_1->s1_16; +s1_2->s1_4->s1_5->s1_16; +s1_6->s1_8->s1_16; +s1_9->s1_11->s1_13; +s1_16->final; +*/ + +obj?.[cond ? k1 : k2]?.[k3 || k4]?.(a1 && a2, b1 ?? b2).foo(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nChainExpression:enter\nCallExpression:enter\nMemberExpression:enter\nCallExpression:enter\nMemberExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_2[label="ConditionalExpression:enter\nIdentifier (cond)"]; + s1_3[label="Identifier (k1)"]; + s1_5[label="ConditionalExpression:exit\nMemberExpression:exit"]; + s1_6[label="LogicalExpression:enter\nIdentifier (k3)"]; + s1_7[label="Identifier (k4)"]; + s1_8[label="LogicalExpression:exit\nMemberExpression:exit"]; + s1_9[label="LogicalExpression:enter\nIdentifier (a1)"]; + s1_10[label="Identifier (a2)"]; + s1_11[label="LogicalExpression:exit\nLogicalExpression:enter\nIdentifier (b1)"]; + s1_12[label="Identifier (b2)"]; + s1_13[label="LogicalExpression:exit\nCallExpression:exit\nIdentifier (foo)\nMemberExpression:exit\nIdentifier (arg)\nCallExpression:exit"]; + s1_16[label="ChainExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + s1_4[label="Identifier (k2)"]; + initial->s1_1->s1_2->s1_3->s1_5->s1_6->s1_7->s1_8->s1_9->s1_10->s1_11->s1_12->s1_13->s1_16; + s1_1->s1_16; + s1_2->s1_4->s1_5->s1_16; + s1_6->s1_8->s1_16; + s1_9->s1_11->s1_13; + s1_16->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--complex-2.js b/tests/fixtures/code-path-analysis/optional-chaining--complex-2.js new file mode 100644 index 00000000000..8d53c07e5f5 --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--complex-2.js @@ -0,0 +1,38 @@ +/*expected +initial->s1_1->s1_2->s1_3->s1_5->s1_6->s1_7->s1_8->s1_9->s1_10->s1_11->s1_12->s1_13->s1_16; +s1_1->s1_16; +s1_2->s1_4->s1_5->s1_16; +s1_6->s1_8->s1_16; +s1_9->s1_11->s1_13; +s1_16->final; +*/ + +(obj?.[cond ? k1 : k2]?.[k3 || k4]?.(a1 && a2, b1 ?? b2)).foo(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nCallExpression:enter\nMemberExpression:enter\nChainExpression:enter\nCallExpression:enter\nMemberExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_2[label="ConditionalExpression:enter\nIdentifier (cond)"]; + s1_3[label="Identifier (k1)"]; + s1_5[label="ConditionalExpression:exit\nMemberExpression:exit"]; + s1_6[label="LogicalExpression:enter\nIdentifier (k3)"]; + s1_7[label="Identifier (k4)"]; + s1_8[label="LogicalExpression:exit\nMemberExpression:exit"]; + s1_9[label="LogicalExpression:enter\nIdentifier (a1)"]; + s1_10[label="Identifier (a2)"]; + s1_11[label="LogicalExpression:exit\nLogicalExpression:enter\nIdentifier (b1)"]; + s1_12[label="Identifier (b2)"]; + s1_13[label="LogicalExpression:exit\nCallExpression:exit"]; + s1_16[label="ChainExpression:exit\nIdentifier (foo)\nMemberExpression:exit\nIdentifier (arg)\nCallExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + s1_4[label="Identifier (k2)"]; + initial->s1_1->s1_2->s1_3->s1_5->s1_6->s1_7->s1_8->s1_9->s1_10->s1_11->s1_12->s1_13->s1_16; + s1_1->s1_16; + s1_2->s1_4->s1_5->s1_16; + s1_6->s1_8->s1_16; + s1_9->s1_11->s1_13; + s1_16->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--complex-3.js b/tests/fixtures/code-path-analysis/optional-chaining--complex-3.js new file mode 100644 index 00000000000..0afef1cd172 --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--complex-3.js @@ -0,0 +1,41 @@ +/*expected +initial->s1_1->s1_2->s1_3->s1_5->s1_6->s1_7->s1_8->s1_10->s1_11->s1_12->s1_13->s1_14->s1_15->s1_16; +s1_1->s1_10; +s1_2->s1_4->s1_5->s1_10; +s1_6->s1_8; +s1_10->s1_16; +s1_11->s1_13->s1_15; +s1_16->final; +*/ + +(obj?.[cond ? k1 : k2]?.[k3 || k4])?.(a1 && a2, b1 ?? b2).foo(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nChainExpression:enter\nCallExpression:enter\nMemberExpression:enter\nCallExpression:enter\nChainExpression:enter\nMemberExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_2[label="ConditionalExpression:enter\nIdentifier (cond)"]; + s1_3[label="Identifier (k1)"]; + s1_5[label="ConditionalExpression:exit\nMemberExpression:exit"]; + s1_6[label="LogicalExpression:enter\nIdentifier (k3)"]; + s1_7[label="Identifier (k4)"]; + s1_8[label="LogicalExpression:exit\nMemberExpression:exit"]; + s1_10[label="ChainExpression:exit"]; + s1_11[label="LogicalExpression:enter\nIdentifier (a1)"]; + s1_12[label="Identifier (a2)"]; + s1_13[label="LogicalExpression:exit\nLogicalExpression:enter\nIdentifier (b1)"]; + s1_14[label="Identifier (b2)"]; + s1_15[label="LogicalExpression:exit\nCallExpression:exit\nIdentifier (foo)\nMemberExpression:exit\nIdentifier (arg)\nCallExpression:exit"]; + s1_16[label="ChainExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + s1_4[label="Identifier (k2)"]; + initial->s1_1->s1_2->s1_3->s1_5->s1_6->s1_7->s1_8->s1_10->s1_11->s1_12->s1_13->s1_14->s1_15->s1_16; + s1_1->s1_10; + s1_2->s1_4->s1_5->s1_10; + s1_6->s1_8; + s1_10->s1_16; + s1_11->s1_13->s1_15; + s1_16->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--simple-1.js b/tests/fixtures/code-path-analysis/optional-chaining--simple-1.js new file mode 100644 index 00000000000..d466a4e72d7 --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--simple-1.js @@ -0,0 +1,19 @@ +/*expected +initial->s1_1->s1_2->s1_3; +s1_1->s1_3->final; +*/ + +obj?.aaa.bbb(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nChainExpression:enter\nCallExpression:enter\nMemberExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_2[label="Identifier (aaa)\nMemberExpression:exit\nIdentifier (bbb)\nMemberExpression:exit\nIdentifier (arg)\nCallExpression:exit"]; + s1_3[label="ChainExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + initial->s1_1->s1_2->s1_3; + s1_1->s1_3->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--simple-2.js b/tests/fixtures/code-path-analysis/optional-chaining--simple-2.js new file mode 100644 index 00000000000..a5867528678 --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--simple-2.js @@ -0,0 +1,19 @@ +/*expected +initial->s1_1->s1_2->s1_3; +s1_1->s1_3->final; +*/ + +obj.foo?.(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nChainExpression:enter\nCallExpression:enter\nMemberExpression:enter\nIdentifier (obj)\nIdentifier (foo)\nMemberExpression:exit"]; + s1_2[label="Identifier (arg)\nCallExpression:exit"]; + s1_3[label="ChainExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + initial->s1_1->s1_2->s1_3; + s1_1->s1_3->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--simple-3.js b/tests/fixtures/code-path-analysis/optional-chaining--simple-3.js new file mode 100644 index 00000000000..fa43af967ed --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--simple-3.js @@ -0,0 +1,25 @@ +/*expected +initial->s1_1->s1_2->s1_3->s1_4->s1_7; +s1_1->s1_7; +s1_2->s1_7; +s1_3->s1_7->final; +*/ + +obj?.aaa?.bbb?.(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nChainExpression:enter\nCallExpression:enter\nMemberExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_2[label="Identifier (aaa)\nMemberExpression:exit"]; + s1_3[label="Identifier (bbb)\nMemberExpression:exit"]; + s1_4[label="Identifier (arg)\nCallExpression:exit"]; + s1_7[label="ChainExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + initial->s1_1->s1_2->s1_3->s1_4->s1_7; + s1_1->s1_7; + s1_2->s1_7; + s1_3->s1_7->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--simple-4.js b/tests/fixtures/code-path-analysis/optional-chaining--simple-4.js new file mode 100644 index 00000000000..c10174646bd --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--simple-4.js @@ -0,0 +1,19 @@ +/*expected +initial->s1_1->s1_2->s1_3; +s1_1->s1_3->final; +*/ + +func?.()(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nChainExpression:enter\nCallExpression:enter\nCallExpression:enter\nIdentifier (func)\nCallExpression:exit"]; + s1_2[label="Identifier (arg)\nCallExpression:exit"]; + s1_3[label="ChainExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + initial->s1_1->s1_2->s1_3; + s1_1->s1_3->final; +} +*/ diff --git a/tests/lib/rules/accessor-pairs.js b/tests/lib/rules/accessor-pairs.js index e8f143ada4c..c6341326647 100644 --- a/tests/lib/rules/accessor-pairs.js +++ b/tests/lib/rules/accessor-pairs.js @@ -1087,6 +1087,46 @@ ruleTester.run("accessor-pairs", rule, { code: "Object.create(null, {foo: {set: function(value) {}}});", errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] }, + { + code: "var o = {d: 1};\n Object?.defineProperty(o, 'c', \n{set: function(value) {\n val = value; \n} \n});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "Reflect?.defineProperty(obj, 'foo', {set: function(value) {}});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "Object?.defineProperties(obj, {foo: {set: function(value) {}}});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "Object?.create(null, {foo: {set: function(value) {}}});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "var o = {d: 1};\n (Object?.defineProperty)(o, 'c', \n{set: function(value) {\n val = value; \n} \n});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "(Reflect?.defineProperty)(obj, 'foo', {set: function(value) {}});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "(Object?.defineProperties)(obj, {foo: {set: function(value) {}}});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "(Object?.create)(null, {foo: {set: function(value) {}}});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, //------------------------------------------------------------------------------ // Classes diff --git a/tests/lib/rules/array-callback-return.js b/tests/lib/rules/array-callback-return.js index 61aabaeb32c..6d6a4d86242 100644 --- a/tests/lib/rules/array-callback-return.js +++ b/tests/lib/rules/array-callback-return.js @@ -443,6 +443,33 @@ ruleTester.run("array-callback-return", rule, { endLine: 2, endColumn: 20 }] + }, + + // Optional chaining + { + code: "foo?.filter(() => { console.log('hello') })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedInside", data: { name: "arrow function", arrayMethodName: "Array.prototype.filter" } }] + }, + { + code: "(foo?.filter)(() => { console.log('hello') })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedInside", data: { name: "arrow function", arrayMethodName: "Array.prototype.filter" } }] + }, + { + code: "Array?.from([], () => { console.log('hello') })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedInside", data: { name: "arrow function", arrayMethodName: "Array.from" } }] + }, + { + code: "(Array?.from)([], () => { console.log('hello') })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedInside", data: { name: "arrow function", arrayMethodName: "Array.from" } }] + }, + { + code: "foo?.filter((function() { return () => { console.log('hello') } })?.())", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedInside", data: { name: "arrow function", arrayMethodName: "Array.prototype.filter" } }] } ] }); diff --git a/tests/lib/rules/camelcase.js b/tests/lib/rules/camelcase.js index eab0f354aec..4f9cdca78fe 100644 --- a/tests/lib/rules/camelcase.js +++ b/tests/lib/rules/camelcase.js @@ -1306,6 +1306,20 @@ ruleTester.run("camelcase", rule, { type: "Identifier" } ] + }, + + // Optional chaining. + { + code: "obj.o_k.non_camelcase = 0", + options: [{ properties: "always" }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "notCamelCase", data: { name: "non_camelcase" } }] + }, + { + code: "(obj?.o_k).non_camelcase = 0", + options: [{ properties: "always" }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "notCamelCase", data: { name: "non_camelcase" } }] } ] }); diff --git a/tests/lib/rules/computed-property-spacing.js b/tests/lib/rules/computed-property-spacing.js index b0ecef6eb81..a1e5834243e 100644 --- a/tests/lib/rules/computed-property-spacing.js +++ b/tests/lib/rules/computed-property-spacing.js @@ -1906,6 +1906,28 @@ ruleTester.run("computed-property-spacing", rule, { endColumn: 19 } ] + }, + + // Optional chaining + { + code: "obj?.[1];", + output: "obj?.[ 1 ];", + options: ["always"], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "missingSpaceAfter", data: { tokenValue: "[" } }, + { messageId: "missingSpaceBefore", data: { tokenValue: "]" } } + ] + }, + { + code: "obj?.[ 1 ];", + output: "obj?.[1];", + options: ["never"], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "unexpectedSpaceAfter", data: { tokenValue: "[" } }, + { messageId: "unexpectedSpaceBefore", data: { tokenValue: "]" } } + ] } ] }); diff --git a/tests/lib/rules/constructor-super.js b/tests/lib/rules/constructor-super.js index b6c223dec7d..e20da576b5e 100644 --- a/tests/lib/rules/constructor-super.js +++ b/tests/lib/rules/constructor-super.js @@ -16,7 +16,7 @@ const { RuleTester } = require("../../../lib/rule-tester"); // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("constructor-super", rule, { valid: [ @@ -88,7 +88,10 @@ ruleTester.run("constructor-super", rule, { } } } - ` + `, + + // Optional chaining + "class A extends obj?.prop { constructor() { super(); } }" ], invalid: [ diff --git a/tests/lib/rules/dot-location.js b/tests/lib/rules/dot-location.js index 1a6ea37a733..e102bd6927d 100644 --- a/tests/lib/rules/dot-location.js +++ b/tests/lib/rules/dot-location.js @@ -136,6 +136,68 @@ ruleTester.run("dot-location", rule, { { code: "(\na &&\nb()\n).toString()", options: ["object"] + }, + + // Optional chaining + { + code: "obj?.prop", + options: ["object"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.[key]", + options: ["object"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.\nprop", + options: ["object"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj\n?.[key]", + options: ["object"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.\n[key]", + options: ["object"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.[\nkey]", + options: ["object"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.prop", + options: ["property"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.[key]", + options: ["property"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj\n?.prop", + options: ["property"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj\n?.[key]", + options: ["property"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.\n[key]", + options: ["property"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.[\nkey]", + options: ["property"], + parserOptions: { ecmaVersion: 2020 } } ], invalid: [ @@ -255,6 +317,29 @@ ruleTester.run("dot-location", rule, { output: "(5).\ntoExponential()", options: ["object"], errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 2, column: 1 }] + }, + + // Optional chaining + { + code: "obj\n?.prop", + output: "obj?.\nprop", + options: ["object"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedDotAfterObject" }] + }, + { + code: "10\n?.prop", + output: "10?.\nprop", + options: ["object"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedDotAfterObject" }] + }, + { + code: "obj?.\nprop", + output: "obj\n?.prop", + options: ["property"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedDotBeforeProperty" }] } ] }); diff --git a/tests/lib/rules/dot-notation.js b/tests/lib/rules/dot-notation.js index 5532ea35870..341ef25ec67 100644 --- a/tests/lib/rules/dot-notation.js +++ b/tests/lib/rules/dot-notation.js @@ -218,6 +218,34 @@ ruleTester.run("dot-notation", rule, { output: null, // `let["if"]()` is a syntax error because `let[` indicates a destructuring variable declaration options: [{ allowKeywords: false }], errors: [{ messageId: "useBrackets", data: { key: "if" } }] + }, + + // Optional chaining + { + code: "obj?.['prop']", + output: "obj?.prop", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "useDot", data: { key: q("prop") } }] + }, + { + code: "0?.['prop']", + output: "0?.prop", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "useDot", data: { key: q("prop") } }] + }, + { + code: "obj?.true", + output: "obj?.[\"true\"]", + options: [{ allowKeywords: false }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "useBrackets", data: { key: "true" } }] + }, + { + code: "let?.true", + output: "let?.[\"true\"]", + options: [{ allowKeywords: false }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "useBrackets", data: { key: "true" } }] } ] }); diff --git a/tests/lib/rules/func-call-spacing.js b/tests/lib/rules/func-call-spacing.js index f0199fd0a4e..3520882b019 100644 --- a/tests/lib/rules/func-call-spacing.js +++ b/tests/lib/rules/func-call-spacing.js @@ -218,6 +218,28 @@ ruleTester.run("func-call-spacing", rule, { code: "import\n(source)", options: ["always", { allowNewlines: true }], parserOptions: { ecmaVersion: 2020 } + }, + + // Optional chaining + { + code: "func?.()", + options: ["never"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "func ?.()", + options: ["always"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "func?. ()", + options: ["always"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "func ?. ()", + options: ["always"], + parserOptions: { ecmaVersion: 2020 } } ], invalid: [ @@ -560,7 +582,7 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "f\n();", - output: "f ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, @@ -572,7 +594,7 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "f\n(a, b);", - output: "f (a, b);", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, @@ -593,7 +615,7 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "f.b\n();", - output: "f.b ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [ { @@ -614,7 +636,7 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "f.b\n().c ();", - output: "f.b ().c ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [ { @@ -635,13 +657,13 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "f\n() ()", - output: "f () ()", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, { code: "f\n()()", - output: "f () ()", + output: "f\n() ()", // Don't fix the first error to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [ { messageId: "unexpectedNewline", type: "CallExpression" }, @@ -696,25 +718,25 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "f\r();", - output: "f ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, { code: "f\u2028();", - output: "f ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, { code: "f\u2029();", - output: "f ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, { code: "f\r\n();", - output: "f ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, @@ -841,7 +863,7 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "fnn\n (a, b);", - output: "fnn (a, b);", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [ { @@ -853,6 +875,96 @@ ruleTester.run("func-call-spacing", rule, { endColumn: 2 } ] + }, + { + code: "f /*comment*/ ()", + output: null, // Don't remove comments + options: ["never"], + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "f /*\n*/ ()", + output: null, // Don't remove comments + options: ["never"], + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "f/*comment*/()", + output: "f/*comment*/ ()", + options: ["always"], + errors: [{ messageId: "missing" }] + }, + + // Optional chaining + { + code: "func ?.()", + output: "func?.()", + options: ["never"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "func?. ()", + output: "func?.()", + options: ["never"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "func ?. ()", + output: "func?.()", + options: ["never"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "func\n?.()", + output: "func?.()", + options: ["never"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "func\n//comment\n?.()", + output: null, // Don't remove comments + options: ["never"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "func?.()", + output: null, // Not sure inserting a space into either before/after `?.`. + options: ["always"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "missing" }] + }, + { + code: "func\n ?.()", + output: "func ?.()", + options: ["always"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedNewline" }] + }, + { + code: "func?.\n ()", + output: "func?. ()", + options: ["always"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedNewline" }] + }, + { + code: "func ?.\n ()", + output: "func ?. ()", + options: ["always"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedNewline" }] + }, + { + code: "func\n /*comment*/ ?.()", + output: null, // Don't remove comments + options: ["always"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedNewline" }] } ] }); diff --git a/tests/lib/rules/func-name-matching.js b/tests/lib/rules/func-name-matching.js index 72ee66b41ed..4add17ea9d0 100644 --- a/tests/lib/rules/func-name-matching.js +++ b/tests/lib/rules/func-name-matching.js @@ -457,6 +457,79 @@ ruleTester.run("func-name-matching", rule, { errors: [ { messageId: "matchProperty", data: { funcName: "bar", name: "value" } } ] + }, + + // Optional chaining + { + code: "(obj?.aaa).foo = function bar() {};", + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "matchProperty", data: { funcName: "bar", name: "foo" } } + ] + }, + { + code: "Object?.defineProperty(foo, 'bar', { value: function baz() {} })", + options: ["always", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "matchProperty", data: { funcName: "baz", name: "bar" } } + ] + }, + { + code: "(Object?.defineProperty)(foo, 'bar', { value: function baz() {} })", + options: ["always", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "matchProperty", data: { funcName: "baz", name: "bar" } } + ] + }, + { + code: "Object?.defineProperty(foo, 'bar', { value: function bar() {} })", + options: ["never", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "notMatchProperty", data: { funcName: "bar", name: "bar" } } + ] + }, + { + code: "(Object?.defineProperty)(foo, 'bar', { value: function bar() {} })", + options: ["never", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "notMatchProperty", data: { funcName: "bar", name: "bar" } } + ] + }, + { + code: "Object?.defineProperties(foo, { bar: { value: function baz() {} } })", + options: ["always", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "matchProperty", data: { funcName: "baz", name: "bar" } } + ] + }, + { + code: "(Object?.defineProperties)(foo, { bar: { value: function baz() {} } })", + options: ["always", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "matchProperty", data: { funcName: "baz", name: "bar" } } + ] + }, + { + code: "Object?.defineProperties(foo, { bar: { value: function bar() {} } })", + options: ["never", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "notMatchProperty", data: { funcName: "bar", name: "bar" } } + ] + }, + { + code: "(Object?.defineProperties)(foo, { bar: { value: function bar() {} } })", + options: ["never", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "notMatchProperty", data: { funcName: "bar", name: "bar" } } + ] } ] }); diff --git a/tests/lib/rules/getter-return.js b/tests/lib/rules/getter-return.js index ea181650dee..7d7cce078ae 100644 --- a/tests/lib/rules/getter-return.js +++ b/tests/lib/rules/getter-return.js @@ -223,6 +223,30 @@ ruleTester.run("getter-return", rule, { { code: "Object.defineProperties(foo, { bar: { get: function () {~function () { return true; }()}} });", options, errors: [{ messageId: "expected" }] }, // option: {allowImplicit: true} - { code: "Object.defineProperty(foo, \"bar\", { get: function (){}});", options, errors: [{ messageId: "expected" }] } + { code: "Object.defineProperty(foo, \"bar\", { get: function (){}});", options, errors: [{ messageId: "expected" }] }, + + // Optional chaining + { + code: "Object?.defineProperty(foo, 'bar', { get: function (){} });", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expected", data: { name: "method 'get'" } }] + }, + { + code: "(Object?.defineProperty)(foo, 'bar', { get: function (){} });", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expected", data: { name: "method 'get'" } }] + }, + { + code: "Object?.defineProperty(foo, 'bar', { get: function (){} });", + options, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expected", data: { name: "method 'get'" } }] + }, + { + code: "(Object?.defineProperty)(foo, 'bar', { get: function (){} });", + options, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expected", data: { name: "method 'get'" } }] + } ] }); diff --git a/tests/lib/rules/global-require.js b/tests/lib/rules/global-require.js index 3fb6ccca459..4993962bd05 100644 --- a/tests/lib/rules/global-require.js +++ b/tests/lib/rules/global-require.js @@ -30,7 +30,13 @@ const valid = [ { code: "var logger = require(DEBUG ? 'dev-logger' : 'logger');" }, { code: "var logger = DEBUG ? require('dev-logger') : require('logger');" }, { code: "function localScopedRequire(require) { require('y'); }" }, - { code: "var someFunc = require('./someFunc'); someFunc(function(require) { return('bananas'); });" } + { code: "var someFunc = require('./someFunc'); someFunc(function(require) { return('bananas'); });" }, + + // Optional chaining + { + code: "var x = require('y')?.foo;", + parserOptions: { ecmaVersion: 2020 } + } ]; const error = { messageId: "unexpected", type: "CallExpression" }; diff --git a/tests/lib/rules/id-blacklist.js b/tests/lib/rules/id-blacklist.js index e69de29bb2d..4d13459acf6 100644 --- a/tests/lib/rules/id-blacklist.js +++ b/tests/lib/rules/id-blacklist.js @@ -0,0 +1,1359 @@ +/** + * @fileoverview Tests for id-blacklist rule. + * @author Keith Cirkel + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/id-blacklist"), + { RuleTester } = require("../../../lib/rule-tester"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester(); +const error = { messageId: "restricted", type: "Identifier" }; + +ruleTester.run("id-blacklist", rule, { + valid: [ + { + code: "foo = \"bar\"", + options: ["bar"] + }, + { + code: "bar = \"bar\"", + options: ["foo"] + }, + { + code: "foo = \"bar\"", + options: ["f", "fo", "fooo", "bar"] + }, + { + code: "function foo(){}", + options: ["bar"] + }, + { + code: "foo()", + options: ["f", "fo", "fooo", "bar"] + }, + { + code: "import { foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" } + }, + { + code: "export { foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" } + }, + { + code: "foo.bar()", + options: ["f", "fo", "fooo", "b", "ba", "baz"] + }, + { + code: "var foo = bar.baz;", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz"] + }, + { + code: "var foo = bar.baz.bing;", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "foo.bar.baz = bing.bong.bash;", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "if (foo.bar) {}", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "var obj = { key: foo.bar };", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "const {foo: bar} = baz", + options: ["foo"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "const {foo: {bar: baz}} = qux", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "function foo({ bar: baz }) {}", + options: ["bar"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "function foo({ bar: {baz: qux} }) {}", + options: ["bar", "baz"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "function foo({baz} = obj.qux) {}", + options: ["qux"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "function foo({ foo: {baz} = obj.qux }) {}", + options: ["qux"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "({a: bar = obj.baz});", + options: ["baz"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "({foo: {a: bar = obj.baz}} = qux);", + options: ["baz"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "var arr = [foo.bar];", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "[foo.bar]", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "[foo.bar.nesting]", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "if (foo.bar === bar.baz) { [foo.bar] }", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "var myArray = new Array(); var myDate = new Date();", + options: ["array", "date", "mydate", "myarray", "new", "var"] + }, + { + code: "foo()", + options: ["foo"] + }, + { + code: "foo.bar()", + options: ["bar"] + }, + { + code: "foo.bar", + options: ["bar"] + }, + { + code: "({foo: obj.bar.bar.bar.baz} = {});", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "({[obj.bar]: a = baz} = qux);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 } + }, + + // references to global variables + { + code: "Number.parseInt()", + options: ["Number"] + }, + { + code: "x = Number.NaN;", + options: ["Number"] + }, + { + code: "var foo = undefined;", + options: ["undefined"] + }, + { + code: "if (foo === undefined);", + options: ["undefined"] + }, + { + code: "obj[undefined] = 5;", // creates obj["undefined"]. It should be disallowed, but the rule doesn't know values of globals and can't control computed access. + options: ["undefined"] + }, + { + code: "foo = { [myGlobal]: 1 };", + options: ["myGlobal"], + parserOptions: { ecmaVersion: 6 }, + globals: { myGlobal: "readonly" } + }, + { + code: "({ myGlobal } = foo);", // writability doesn't affect the logic, it's always assumed that user doesn't have control over the names of globals. + options: ["myGlobal"], + parserOptions: { ecmaVersion: 6 }, + globals: { myGlobal: "writable" } + }, + { + code: "/* global myGlobal: readonly */ myGlobal = 5;", + options: ["myGlobal"] + }, + { + code: "var foo = [Map];", + options: ["Map"], + env: { es6: true } + }, + { + code: "var foo = { bar: window.baz };", + options: ["window"], + env: { browser: true } + } + ], + invalid: [ + { + code: "foo = \"bar\"", + options: ["foo"], + errors: [ + error + ] + }, + { + code: "bar = \"bar\"", + options: ["bar"], + errors: [ + error + ] + }, + { + code: "foo = \"bar\"", + options: ["f", "fo", "foo", "bar"], + errors: [ + error + ] + }, + { + code: "function foo(){}", + options: ["f", "fo", "foo", "bar"], + errors: [ + error + ] + }, + { + code: "import foo from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "import * as foo from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "export * as foo from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 2020, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "import { foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "import { foo as bar } from 'mod'", + options: ["bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "import { foo as bar } from 'mod'", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "import { foo as foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "import { foo, foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 10 + }] + }, + { + code: "import { foo as bar, foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 22 + }] + }, + { + code: "import foo, { foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 8 + }] + }, + { + code: "var foo; export { foo as bar };", + options: ["bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 26 + }] + }, + { + code: "var foo; export { foo };", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 19 + } + ] + }, + { + code: "var foo; export { foo as bar };", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + + // reports each occurrence of local identifier, although it's renamed in this export specifier + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 19 + } + ] + }, + { + code: "var foo; export { foo as foo };", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 19 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 26 + } + ] + }, + { + code: "var foo; export { foo as bar };", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 19 + }, + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 26 + } + ] + }, + { + code: "export { foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "export { foo as bar } from 'mod'", + options: ["bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "export { foo as bar } from 'mod'", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "export { foo as foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "export { foo, foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 10 + }] + }, + { + code: "export { foo as bar, foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 22 + }] + }, + { + code: "foo.bar()", + options: ["f", "fo", "foo", "b", "ba", "baz"], + errors: [ + error + ] + }, + { + code: "foo[bar] = baz;", + options: ["bar"], + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier" + }] + }, + { + code: "baz = foo[bar];", + options: ["bar"], + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier" + }] + }, + { + code: "var foo = bar.baz;", + options: ["f", "fo", "foo", "b", "ba", "barr", "bazz"], + errors: [ + error + ] + }, + { + code: "var foo = bar.baz;", + options: ["f", "fo", "fooo", "b", "ba", "bar", "bazz"], + errors: [ + error + ] + }, + { + code: "if (foo.bar) {}", + options: ["f", "fo", "foo", "b", "ba", "barr", "bazz", "bingg"], + errors: [ + error + ] + }, + { + code: "var obj = { key: foo.bar };", + options: ["obj"], + errors: [ + error + ] + }, + { + code: "var obj = { key: foo.bar };", + options: ["key"], + errors: [ + error + ] + }, + { + code: "var obj = { key: foo.bar };", + options: ["foo"], + errors: [ + error + ] + }, + { + code: "var arr = [foo.bar];", + options: ["arr"], + errors: [ + error + ] + }, + { + code: "var arr = [foo.bar];", + options: ["foo"], + errors: [ + error + ] + }, + { + code: "[foo.bar]", + options: ["f", "fo", "foo", "b", "ba", "barr", "bazz", "bingg"], + errors: [ + error + ] + }, + { + code: "if (foo.bar === bar.baz) { [bing.baz] }", + options: ["f", "fo", "foo", "b", "ba", "barr", "bazz", "bingg"], + errors: [ + error + ] + }, + { + code: "if (foo.bar === bar.baz) { [foo.bar] }", + options: ["f", "fo", "fooo", "b", "ba", "bar", "bazz", "bingg"], + errors: [ + error + ] + }, + { + code: "var myArray = new Array(); var myDate = new Date();", + options: ["array", "date", "myDate", "myarray", "new", "var"], + errors: [ + error + ] + }, + { + code: "var myArray = new Array(); var myDate = new Date();", + options: ["array", "date", "mydate", "myArray", "new", "var"], + errors: [ + error + ] + }, + { + code: "foo.bar = 1", + options: ["bar"], + errors: [ + error + ] + }, + { + code: "foo.bar.baz = 1", + options: ["bar", "baz"], + errors: [ + error + ] + }, + { + code: "const {foo} = baz", + options: ["foo"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 8 + } + ] + }, + { + code: "const {foo: bar} = baz", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 13 + } + ] + }, + { + code: "const {[foo]: bar} = baz", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 9 + }, + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 15 + } + ] + }, + { + code: "const {foo: {bar: baz}} = qux", + options: ["foo", "bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 19 + } + ] + }, + { + code: "const {foo: {[bar]: baz}} = qux", + options: ["foo", "bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 15 + }, + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 21 + } + ] + }, + { + code: "const {[foo]: {[bar]: baz}} = qux", + options: ["foo", "bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 9 + }, + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }, + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 23 + } + ] + }, + { + code: "function foo({ bar: baz }) {}", + options: ["bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 21 + } + ] + }, + { + code: "function foo({ bar: {baz: qux} }) {}", + options: ["bar", "baz", "qux"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "qux" }, + type: "Identifier", + column: 27 + } + ] + }, + { + code: "({foo: obj.bar} = baz);", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 12 + } + ] + }, + { + code: "({foo: obj.bar.bar.bar.baz} = {});", + options: ["foo", "bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 24 + } + ] + }, + { + code: "({[foo]: obj.bar} = baz);", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 4 + }, + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 14 + } + ] + }, + { + code: "({foo: { a: obj.bar }} = baz);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + } + ] + }, + { + code: "({a: obj.bar = baz} = qux);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 10 + } + ] + }, + { + code: "({a: obj.bar.bar.baz = obj.qux} = obj.qux);", + options: ["a", "bar", "baz", "qux"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 18 + } + ] + }, + { + code: "({a: obj[bar] = obj.qux} = obj.qux);", + options: ["a", "bar", "baz", "qux"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 10 + } + ] + }, + { + code: "({a: [obj.bar] = baz} = qux);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 11 + } + ] + }, + { + code: "({foo: { a: obj.bar = baz}} = qux);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + } + ] + }, + { + code: "({foo: { [a]: obj.bar }} = baz);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 19 + } + ] + }, + { + code: "({...obj.bar} = baz);", + options: ["bar"], + parserOptions: { ecmaVersion: 9 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 10 + } + ] + }, + { + code: "([obj.bar] = baz);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 7 + } + ] + }, + { + code: "const [bar] = baz;", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 8 + } + ] + }, + + // not a reference to a global variable, because it isn't a reference to a variable + { + code: "foo.undefined = 1;", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + }, + { + code: "var foo = { undefined: 1 };", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + }, + { + code: "var foo = { undefined: undefined };", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 13 + } + ] + }, + { + code: "var foo = { Number() {} };", + options: ["Number"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier" + } + ] + }, + { + code: "class Foo { Number() {} }", + options: ["Number"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier" + } + ] + }, + { + code: "myGlobal: while(foo) { break myGlobal; } ", + options: ["myGlobal"], + globals: { myGlobal: "readonly" }, + errors: [ + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 1 + }, + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 30 + } + ] + }, + + // globals declared in the given source code are not excluded from consideration + { + code: "const foo = 1; bar = foo;", + options: ["foo"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 22 + } + ] + }, + { + code: "let foo; foo = bar;", + options: ["foo"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 10 + } + ] + }, + { + code: "bar = foo; var foo;", + options: ["foo"], + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 16 + } + ] + }, + { + code: "function foo() {} var bar = foo;", + options: ["foo"], + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 10 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 29 + } + ] + }, + { + code: "class Foo {} var bar = Foo;", + options: ["Foo"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "Foo" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "Foo" }, + type: "Identifier", + column: 24 + } + ] + }, + + // redeclared globals are not excluded from consideration + { + code: "let undefined; undefined = 1;", + options: ["undefined"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 16 + } + ] + }, + { + code: "foo = undefined; var undefined;", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 22 + } + ] + }, + { + code: "function undefined(){} x = undefined;", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 10 + }, + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 28 + } + ] + }, + { + code: "class Number {} x = Number.NaN;", + options: ["Number"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 21 + } + ] + }, + + /* + * Assignment to a property with a restricted name isn't allowed, in general. + * In this case, that restriction prevents creating a global variable with a restricted name. + */ + { + code: "/* globals myGlobal */ window.myGlobal = 5; foo = myGlobal;", + options: ["myGlobal"], + env: { browser: true }, + errors: [ + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 31 + } + ] + }, + + // disabled global variables + { + code: "var foo = undefined;", + options: ["undefined"], + globals: { undefined: "off" }, + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + }, + { + code: "/* globals Number: off */ Number.parseInt()", + options: ["Number"], + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier" + } + ] + }, + { + code: "var foo = [Map];", // this actually isn't a disabled global: it was never enabled because es6 environment isn't enabled + options: ["Map"], + errors: [ + { + messageId: "restricted", + data: { name: "Map" }, + type: "Identifier" + } + ] + }, + + // shadowed global variables + { + code: "if (foo) { let undefined; bar = undefined; }", + options: ["undefined"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 16 + }, + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 33 + } + ] + }, + { + code: "function foo(Number) { var x = Number.NaN; }", + options: ["Number"], + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 14 + }, + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 32 + } + ] + }, + { + code: "function foo() { var myGlobal; x = myGlobal; }", + options: ["myGlobal"], + globals: { myGlobal: "readonly" }, + errors: [ + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 22 + }, + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 36 + } + ] + }, + { + code: "function foo(bar) { return Number.parseInt(bar); } const Number = 1;", + options: ["Number"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 28 + }, + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 58 + } + ] + }, + { + code: "import Number from 'myNumber'; const foo = Number.parseInt(bar);", + options: ["Number"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 8 + }, + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 44 + } + ] + }, + { + code: "var foo = function undefined() {};", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + }, + + // this is a reference to a global variable, but at the same time creates a property with a restricted name + { + code: "var foo = { undefined }", + options: ["undefined"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + } + ] +}); diff --git a/tests/lib/rules/indent.js b/tests/lib/rules/indent.js index 56589be5169..b253be22067 100644 --- a/tests/lib/rules/indent.js +++ b/tests/lib/rules/indent.js @@ -11374,6 +11374,105 @@ ruleTester.run("indent", rule, { [5, 4, 0, "Identifier"], [6, 0, 4, "Punctuator"] ]) + }, + + // Optional chaining + { + code: unIndent` + obj + ?.prop + ?.[key] + ?. + [key] + `, + output: unIndent` + obj + ?.prop + ?.[key] + ?. + [key] + `, + options: [4], + parserOptions: { ecmaVersion: 2020 }, + errors: expectedErrors([ + [2, 4, 0, "Punctuator"], + [3, 4, 0, "Punctuator"], + [4, 4, 0, "Punctuator"], + [5, 8, 0, "Punctuator"] + ]) + }, + { + code: unIndent` + ( + longSomething + ?.prop + ?.[key] + ) + ?.prop + ?.[key] + `, + output: unIndent` + ( + longSomething + ?.prop + ?.[key] + ) + ?.prop + ?.[key] + `, + options: [4], + parserOptions: { ecmaVersion: 2020 }, + errors: expectedErrors([ + [6, 4, 0, "Punctuator"], + [7, 4, 0, "Punctuator"] + ]) + }, + { + code: unIndent` + obj + ?.(arg) + ?. + (arg) + `, + output: unIndent` + obj + ?.(arg) + ?. + (arg) + `, + options: [4], + parserOptions: { ecmaVersion: 2020 }, + errors: expectedErrors([ + [2, 4, 0, "Punctuator"], + [3, 4, 0, "Punctuator"], + [4, 4, 0, "Punctuator"] + ]) + }, + { + code: unIndent` + ( + longSomething + ?.(arg) + ?.(arg) + ) + ?.(arg) + ?.(arg) + `, + output: unIndent` + ( + longSomething + ?.(arg) + ?.(arg) + ) + ?.(arg) + ?.(arg) + `, + options: [4], + parserOptions: { ecmaVersion: 2020 }, + errors: expectedErrors([ + [6, 4, 0, "Punctuator"], + [7, 4, 0, "Punctuator"] + ]) } ] }); diff --git a/tests/lib/rules/new-cap.js b/tests/lib/rules/new-cap.js index ec85c4bf6fd..5953c87c24a 100644 --- a/tests/lib/rules/new-cap.js +++ b/tests/lib/rules/new-cap.js @@ -71,7 +71,39 @@ ruleTester.run("new-cap", rule, { { code: "var x = new foo.bar(42);", options: [{ newIsCapExceptionPattern: "^foo\\.." }] }, { code: "var x = new foo.bar(42);", options: [{ properties: false }] }, { code: "var x = Foo.bar(42);", options: [{ properties: false }] }, - { code: "var x = foo.Bar(42);", options: [{ capIsNew: false, properties: false }] } + { code: "var x = foo.Bar(42);", options: [{ capIsNew: false, properties: false }] }, + + // Optional chaining + { + code: "foo?.bar();", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "(foo?.bar)();", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "new (foo?.Bar)();", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "(foo?.Bar)();", + options: [{ properties: false }], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "new (foo?.bar)();", + options: [{ properties: false }], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "Date?.UTC();", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "(Date?.UTC)();", + parserOptions: { ecmaVersion: 2020 } + } ], invalid: [ { @@ -302,6 +334,23 @@ ruleTester.run("new-cap", rule, { options: [{ newIsCapExceptionPattern: "^foo\\.." }], errors: [{ type: "NewExpression", messageId: "lower" }] + }, + + // Optional chaining + { + code: "new (foo?.bar)();", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "lower", column: 11, endColumn: 14 }] + }, + { + code: "foo?.Bar();", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "upper", column: 6, endColumn: 9 }] + }, + { + code: "(foo?.Bar)();", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "upper", column: 7, endColumn: 10 }] } ] }); diff --git a/tests/lib/rules/newline-per-chained-call.js b/tests/lib/rules/newline-per-chained-call.js index 28d45c69b82..2a26937ae62 100644 --- a/tests/lib/rules/newline-per-chained-call.js +++ b/tests/lib/rules/newline-per-chained-call.js @@ -340,5 +340,69 @@ ruleTester.run("newline-per-chained-call", rule, { endLine: 1, endColumn: 35 }] - }] + }, + + // Optional chaining + { + code: "obj?.foo1()?.foo2()?.foo3()", + output: "obj?.foo1()\n?.foo2()\n?.foo3()", + options: [{ ignoreChainWithDepth: 1 }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "expected", data: { callee: "?.foo2" } }, + { messageId: "expected", data: { callee: "?.foo3" } } + ] + }, + { + code: "(obj?.foo1()?.foo2)()?.foo3()", + output: "(obj?.foo1()\n?.foo2)()\n?.foo3()", + options: [{ ignoreChainWithDepth: 1 }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "expected", data: { callee: "?.foo2" } }, + { messageId: "expected", data: { callee: "?.foo3" } } + ] + }, + { + code: "(obj?.foo1())?.foo2()?.foo3()", + output: "(obj?.foo1())\n?.foo2()\n?.foo3()", + options: [{ ignoreChainWithDepth: 1 }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "expected", data: { callee: "?.foo2" } }, + { messageId: "expected", data: { callee: "?.foo3" } } + ] + }, + { + code: "obj?.[foo1]()?.[foo2]()?.[foo3]()", + output: "obj?.[foo1]()\n?.[foo2]()\n?.[foo3]()", + options: [{ ignoreChainWithDepth: 1 }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "expected", data: { callee: "?.[foo2]" } }, + { messageId: "expected", data: { callee: "?.[foo3]" } } + ] + }, + { + code: "(obj?.[foo1]()?.[foo2])()?.[foo3]()", + output: "(obj?.[foo1]()\n?.[foo2])()\n?.[foo3]()", + options: [{ ignoreChainWithDepth: 1 }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "expected", data: { callee: "?.[foo2]" } }, + { messageId: "expected", data: { callee: "?.[foo3]" } } + ] + }, + { + code: "(obj?.[foo1]())?.[foo2]()?.[foo3]()", + output: "(obj?.[foo1]())\n?.[foo2]()\n?.[foo3]()", + options: [{ ignoreChainWithDepth: 1 }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "expected", data: { callee: "?.[foo2]" } }, + { messageId: "expected", data: { callee: "?.[foo3]" } } + ] + } + + ] }); diff --git a/tests/lib/rules/no-alert.js b/tests/lib/rules/no-alert.js index 4851d5b0a7a..03fcddb6994 100644 --- a/tests/lib/rules/no-alert.js +++ b/tests/lib/rules/no-alert.js @@ -124,6 +124,18 @@ ruleTester.run("no-alert", rule, { code: "function foo() { var globalThis = bar; globalThis.alert(); }\nglobalThis.alert();", env: { es2020: true }, errors: [{ messageId: "unexpected", data: { name: "alert" }, type: "CallExpression", line: 2, column: 1 }] + }, + + // Optional chaining + { + code: "window?.alert(foo)", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected", data: { name: "alert" } }] + }, + { + code: "(window?.alert)(foo)", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected", data: { name: "alert" } }] } ] }); diff --git a/tests/lib/rules/no-eval.js b/tests/lib/rules/no-eval.js index ad184bd9370..bc8d38471bb 100644 --- a/tests/lib/rules/no-eval.js +++ b/tests/lib/rules/no-eval.js @@ -67,7 +67,10 @@ ruleTester.run("no-eval", rule, { { code: "(0, globalThis['eval'])('foo')", options: [{ allowIndirect: true }], env: { es2020: true } }, { code: "var EVAL = globalThis.eval; EVAL('foo')", options: [{ allowIndirect: true }] }, { code: "function foo() { globalThis.eval('foo') }", options: [{ allowIndirect: true }], env: { es2020: true } }, - { code: "globalThis.globalThis.eval('foo');", options: [{ allowIndirect: true }], env: { es2020: true } } + { code: "globalThis.globalThis.eval('foo');", options: [{ allowIndirect: true }], env: { es2020: true } }, + { code: "eval?.('foo')", options: [{ allowIndirect: true }], parserOptions: { ecmaVersion: 2020 } }, + { code: "window?.eval('foo')", options: [{ allowIndirect: true }], parserOptions: { ecmaVersion: 2020 }, env: { browser: true } }, + { code: "(window?.eval)('foo')", options: [{ allowIndirect: true }], parserOptions: { ecmaVersion: 2020 }, env: { browser: true } } ], invalid: [ @@ -100,6 +103,26 @@ ruleTester.run("no-eval", rule, { { code: "globalThis.globalThis.eval('foo')", env: { es2020: true }, errors: [{ messageId: "unexpected", type: "CallExpression", column: 23, endColumn: 27 }] }, { code: "globalThis.globalThis['eval']('foo')", env: { es2020: true }, errors: [{ messageId: "unexpected", type: "CallExpression", column: 23, endColumn: 29 }] }, { code: "(0, globalThis.eval)('foo')", env: { es2020: true }, errors: [{ messageId: "unexpected", type: "MemberExpression", column: 16, endColumn: 20 }] }, - { code: "(0, globalThis['eval'])('foo')", env: { es2020: true }, errors: [{ messageId: "unexpected", type: "MemberExpression", column: 16, endColumn: 22 }] } + { code: "(0, globalThis['eval'])('foo')", env: { es2020: true }, errors: [{ messageId: "unexpected", type: "MemberExpression", column: 16, endColumn: 22 }] }, + + // Optional chaining + { + code: "window?.eval('foo')", + parserOptions: { ecmaVersion: 2020 }, + globals: { window: "readonly" }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "(window?.eval)('foo')", + parserOptions: { ecmaVersion: 2020 }, + globals: { window: "readonly" }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "(window?.window).eval('foo')", + parserOptions: { ecmaVersion: 2020 }, + globals: { window: "readonly" }, + errors: [{ messageId: "unexpected" }] + } ] }); diff --git a/tests/lib/rules/no-extend-native.js b/tests/lib/rules/no-extend-native.js index a38177431d2..db07c123314 100644 --- a/tests/lib/rules/no-extend-native.js +++ b/tests/lib/rules/no-extend-native.js @@ -37,6 +37,7 @@ ruleTester.run("no-extend-native", rule, { code: "Object.prototype.g = 0", options: [{ exceptions: ["Object"] }] }, + "obj[Object.prototype] = 0", // https://github.com/eslint/eslint/issues/4438 "Object.defineProperty()", @@ -137,5 +138,29 @@ ruleTester.run("no-extend-native", rule, { data: { builtin: "Object" }, type: "AssignmentExpression" }] - }] + }, + + // Optional chaining + { + code: "(Object?.prototype).p = 0", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected", data: { builtin: "Object" } }] + }, + { + code: "Object.defineProperty(Object?.prototype, 'p', { value: 0 })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected", data: { builtin: "Object" } }] + }, + { + code: "Object?.defineProperty(Object.prototype, 'p', { value: 0 })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected", data: { builtin: "Object" } }] + }, + { + code: "(Object?.defineProperty)(Object.prototype, 'p', { value: 0 })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected", data: { builtin: "Object" } }] + } + + ] }); diff --git a/tests/lib/rules/no-extra-bind.js b/tests/lib/rules/no-extra-bind.js index 12197967176..8422f3de771 100644 --- a/tests/lib/rules/no-extra-bind.js +++ b/tests/lib/rules/no-extra-bind.js @@ -102,6 +102,16 @@ ruleTester.run("no-extra-bind", rule, { output: "var a = function() { (function(){ (function(){ this.d }.bind(c)) }) }", errors: [{ messageId: "unexpected", type: "CallExpression", column: 71 }] }, + { + code: "var a = (function() { return 1; }).bind(this)", + output: "var a = (function() { return 1; })", + errors + }, + { + code: "var a = (function() { return 1; }.bind)(this)", + output: "var a = (function() { return 1; })", + errors + }, // Should not autofix if bind expression args have side effects { @@ -180,6 +190,44 @@ ruleTester.run("no-extra-bind", rule, { code: "var a = function() {}.bind(b)/**/", output: "var a = function() {}/**/", errors + }, + + // Optional chaining + { + code: "var a = function() { return 1; }.bind?.(b)", + output: "var a = function() { return 1; }", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var a = function() { return 1; }?.bind(b)", + output: "var a = function() { return 1; }", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var a = (function() { return 1; }?.bind)(b)", + output: "var a = (function() { return 1; })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var a = function() { return 1; }['bind']?.(b)", + output: "var a = function() { return 1; }", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var a = function() { return 1; }?.['bind'](b)", + output: "var a = function() { return 1; }", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var a = (function() { return 1; }?.['bind'])(b)", + output: "var a = (function() { return 1; })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] } ] }); diff --git a/tests/lib/rules/no-extra-boolean-cast.js b/tests/lib/rules/no-extra-boolean-cast.js index 70ec8bd4f61..8dda6992d21 100644 --- a/tests/lib/rules/no-extra-boolean-cast.js +++ b/tests/lib/rules/no-extra-boolean-cast.js @@ -2408,6 +2408,21 @@ ruleTester.run("no-extra-boolean-cast", rule, { options: [{ enforceForLogicalOperands: true }], parserOptions: { ecmaVersion: 2020 }, errors: [{ messageId: "unexpectedCall", type: "CallExpression" }] + }, + + // Optional chaining + { + code: "if (Boolean?.(foo)) ;", + output: "if (foo) ;", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedCall" }] + }, + { + code: "if (Boolean?.(a ?? b) || c) {}", + output: "if ((a ?? b) || c) {}", + options: [{ enforceForLogicalOperands: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedCall" }] } ] }); diff --git a/tests/lib/rules/no-extra-parens.js b/tests/lib/rules/no-extra-parens.js index e2501bb45a0..6d7f8ef7da0 100644 --- a/tests/lib/rules/no-extra-parens.js +++ b/tests/lib/rules/no-extra-parens.js @@ -628,7 +628,32 @@ ruleTester.run("no-extra-parens", rule, { { code: "var v = (a || b) ?? c", parserOptions: { ecmaVersion: 2020 } }, { code: "var v = a || (b ?? c)", parserOptions: { ecmaVersion: 2020 } }, { code: "var v = (a && b) ?? c", parserOptions: { ecmaVersion: 2020 } }, - { code: "var v = a && (b ?? c)", parserOptions: { ecmaVersion: 2020 } } + { code: "var v = a && (b ?? c)", parserOptions: { ecmaVersion: 2020 } }, + + // Optional chaining + { code: "var v = (obj?.aaa).bbb", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = (obj?.aaa)()", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = new (obj?.aaa)()", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = new (obj?.aaa)", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = (obj?.aaa)`template`", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = (obj?.()).bbb", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = (obj?.())()", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = new (obj?.())()", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = new (obj?.())", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = (obj?.())`template`", parserOptions: { ecmaVersion: 2020 } }, + { code: "(obj?.aaa).bbb = 0", parserOptions: { ecmaVersion: 2020 } }, + { code: "var foo = (function(){})?.()", parserOptions: { ecmaVersion: 2020 } }, + { code: "var foo = (function(){}?.())", parserOptions: { ecmaVersion: 2020 } }, + { + code: "var foo = (function(){})?.call()", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "var foo = (function(){}?.call())", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + parserOptions: { ecmaVersion: 2020 } + } ], invalid: [ @@ -2715,6 +2740,34 @@ ruleTester.run("no-extra-parens", rule, { output: "var v = a | b ?? c | d", parserOptions: { ecmaVersion: 2020 }, errors: [{ messageId: "unexpected" }] + }, + + // Optional chaining + { + code: "var v = (obj?.aaa)?.aaa", + output: "var v = obj?.aaa?.aaa", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var v = (obj.aaa)?.aaa", + output: "var v = obj.aaa?.aaa", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var foo = (function(){})?.call()", + output: "var foo = function(){}?.call()", + options: ["all", { enforceForFunctionPrototypeMethods: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var foo = (function(){}?.call())", + output: "var foo = function(){}?.call()", + options: ["all", { enforceForFunctionPrototypeMethods: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] } ] }); diff --git a/tests/lib/rules/no-implicit-coercion.js b/tests/lib/rules/no-implicit-coercion.js index 73bfe7df6d8..fa2b68b4975 100644 --- a/tests/lib/rules/no-implicit-coercion.js +++ b/tests/lib/rules/no-implicit-coercion.js @@ -355,6 +355,28 @@ ruleTester.run("no-implicit-coercion", rule, { data: { recommendation: "String(1n)" }, type: "BinaryExpression" }] + }, + + // Optional chaining + { + code: "~foo?.indexOf(1)", + output: null, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "useRecommendation", + data: { recommendation: "foo?.indexOf(1) >= 0" }, + type: "UnaryExpression" + }] + }, + { + code: "~(foo?.indexOf)(1)", + output: null, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "useRecommendation", + data: { recommendation: "(foo?.indexOf)(1) !== -1" }, + type: "UnaryExpression" + }] } ] }); diff --git a/tests/lib/rules/no-implied-eval.js b/tests/lib/rules/no-implied-eval.js index 1c712f450f0..ce06f662043 100644 --- a/tests/lib/rules/no-implied-eval.js +++ b/tests/lib/rules/no-implied-eval.js @@ -235,6 +235,20 @@ ruleTester.run("no-implied-eval", rule, { line: 3 } ] + }, + + // Optional chaining + { + code: "window?.setTimeout('code', 0)", + parserOptions: { ecmaVersion: 2020 }, + globals: { window: "readonly" }, + errors: [{ messageId: "impliedEval" }] + }, + { + code: "(window?.setTimeout)('code', 0)", + parserOptions: { ecmaVersion: 2020 }, + globals: { window: "readonly" }, + errors: [{ messageId: "impliedEval" }] } ] }); diff --git a/tests/lib/rules/no-import-assign.js b/tests/lib/rules/no-import-assign.js index ab65451e33a..babfdfc3445 100644 --- a/tests/lib/rules/no-import-assign.js +++ b/tests/lib/rules/no-import-assign.js @@ -310,6 +310,23 @@ ruleTester.run("no-import-assign", rule, { { code: "import mod, * as mod_ns from 'mod'; mod.prop = 0; mod_ns.prop = 0", errors: [{ messageId: "readonlyMember", data: { name: "mod_ns" }, column: 51 }] + }, + + // Optional chaining + { + code: "import * as mod from 'mod'; Object?.defineProperty(mod, key, d)", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "readonlyMember", data: { name: "mod" }, column: 29 }] + }, + { + code: "import * as mod from 'mod'; (Object?.defineProperty)(mod, key, d)", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "readonlyMember", data: { name: "mod" }, column: 29 }] + }, + { + code: "import * as mod from 'mod'; delete mod?.prop", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "readonlyMember", data: { name: "mod" }, column: 29 }] } ] }); diff --git a/tests/lib/rules/no-invalid-this.js b/tests/lib/rules/no-invalid-this.js index 3662a836766..3eb4e7b0960 100644 --- a/tests/lib/rules/no-invalid-this.js +++ b/tests/lib/rules/no-invalid-this.js @@ -366,6 +366,12 @@ const patterns = [ invalid: [USE_STRICT, IMPLIED_STRICT, MODULES], errors }, + { + code: "obj.foo = (function() { return function() { console.log(this); z(x => console.log(x, this)); }; })?.();", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, // Class Instance Methods. { @@ -421,6 +427,24 @@ const patterns = [ valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], invalid: [] }, + { + code: "var foo = function() { console.log(this); z(x => console.log(x, this)); }?.bind(obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, + { + code: "var foo = (function() { console.log(this); z(x => console.log(x, this)); }?.bind)(obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, + { + code: "var foo = function() { console.log(this); z(x => console.log(x, this)); }.bind?.(obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, // Array methods. { @@ -534,6 +558,30 @@ const patterns = [ valid: [NORMAL], invalid: [USE_STRICT, IMPLIED_STRICT, MODULES] }, + { + code: "Array?.from([], function() { console.log(this); z(x => console.log(x, this)); }, obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, + { + code: "foo?.every(function() { console.log(this); z(x => console.log(x, this)); }, obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, + { + code: "(Array?.from)([], function() { console.log(this); z(x => console.log(x, this)); }, obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, + { + code: "(foo?.every)(function() { console.log(this); z(x => console.log(x, this)); }, obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, // @this tag. { diff --git a/tests/lib/rules/no-magic-numbers.js b/tests/lib/rules/no-magic-numbers.js index da75571ec28..a47b2a10555 100644 --- a/tests/lib/rules/no-magic-numbers.js +++ b/tests/lib/rules/no-magic-numbers.js @@ -213,6 +213,25 @@ ruleTester.run("no-magic-numbers", rule, { code: "f(-100n)", options: [{ ignore: ["-100n"] }], parserOptions: { ecmaVersion: 2020 } + }, + + // Optional chaining + { + code: "var x = parseInt?.(y, 10);", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "var x = Number?.parseInt(y, 10);", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "var x = (Number?.parseInt)(y, 10);", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "foo?.[777]", + options: [{ ignoreArrayIndexes: true }], + parserOptions: { ecmaVersion: 2020 } } ], invalid: [ diff --git a/tests/lib/rules/no-obj-calls.js b/tests/lib/rules/no-obj-calls.js index 4c21d9be4c8..6270e7a7280 100644 --- a/tests/lib/rules/no-obj-calls.js +++ b/tests/lib/rules/no-obj-calls.js @@ -315,6 +315,18 @@ ruleTester.run("no-obj-calls", rule, { code: "var foo = window.Atomics; new foo;", env: { es2020: true, browser: true }, errors: [{ messageId: "unexpectedRefCall", data: { name: "foo", ref: "Atomics" }, type: "NewExpression" }] + }, + + // Optional chaining + { + code: "var x = globalThis?.Reflect();", + env: { es2020: true }, + errors: [{ messageId: "unexpectedCall", data: { name: "Reflect" }, type: "CallExpression" }] + }, + { + code: "var x = (globalThis?.Reflect)();", + env: { es2020: true }, + errors: [{ messageId: "unexpectedCall", data: { name: "Reflect" }, type: "CallExpression" }] } ] }); diff --git a/tests/lib/rules/no-prototype-builtins.js b/tests/lib/rules/no-prototype-builtins.js index e4f0fa30f78..8f57545bef9 100644 --- a/tests/lib/rules/no-prototype-builtins.js +++ b/tests/lib/rules/no-prototype-builtins.js @@ -94,6 +94,18 @@ const invalid = [ data: { prop: "isPrototypeOf" }, type: "CallExpression" }] + }, + + // Optional chaining + { + code: "foo?.hasOwnProperty('bar')", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" } }] + }, + { + code: "(foo?.hasOwnProperty)('bar')", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" } }] } ]; diff --git a/tests/lib/rules/no-restricted-syntax.js b/tests/lib/rules/no-restricted-syntax.js index c8ba48fb8ec..cf8bc412366 100644 --- a/tests/lib/rules/no-restricted-syntax.js +++ b/tests/lib/rules/no-restricted-syntax.js @@ -128,6 +128,35 @@ ruleTester.run("no-restricted-syntax", rule, { code: "console.log(/a/i);", options: ["Literal[regex.flags=/./]"], errors: [{ messageId: "restrictedSyntax", data: { message: "Using 'Literal[regex.flags=/./]' is not allowed." }, type: "Literal" }] + }, + + // Optional chaining + { + code: "var foo = foo?.bar?.();", + options: ["ChainExpression"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "restrictedSyntax", data: { message: "Using 'ChainExpression' is not allowed." }, type: "ChainExpression" }] + }, + { + code: "var foo = foo?.bar?.();", + options: ["[optional=true]"], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "restrictedSyntax", data: { message: "Using '[optional=true]' is not allowed." }, type: "CallExpression" }, + { messageId: "restrictedSyntax", data: { message: "Using '[optional=true]' is not allowed." }, type: "MemberExpression" } + ] } + + /* + * TODO(mysticatea): fix https://github.com/estools/esquery/issues/110 + * { + * code: "a?.b", + * options: [":nth-child(1)"], + * parserOptions: { ecmaVersion: 2020 }, + * errors: [ + * { messageId: "restrictedSyntax", data: { message: "Using ':nth-child(1)' is not allowed." }, type: "ExpressionStatement" } + * ] + * } + */ ] }); diff --git a/tests/lib/rules/no-self-assign.js b/tests/lib/rules/no-self-assign.js index 5149dac65b2..5a9bb6fcfed 100644 --- a/tests/lib/rules/no-self-assign.js +++ b/tests/lib/rules/no-self-assign.js @@ -135,6 +135,18 @@ ruleTester.run("no-self-assign", rule, { options: [{ props: true }], errors: [{ messageId: "selfAssignment", data: { name: "this.x" } }] }, - { code: "a['/(?0)/'] = a[/(?0)/]", options: [{ props: true }], parserOptions: { ecmaVersion: 2018 }, errors: [{ messageId: "selfAssignment", data: { name: "a[/(?0)/]" } }] } + { code: "a['/(?0)/'] = a[/(?0)/]", options: [{ props: true }], parserOptions: { ecmaVersion: 2018 }, errors: [{ messageId: "selfAssignment", data: { name: "a[/(?0)/]" } }] }, + + // Optional chaining + { + code: "(a?.b).c = (a?.b).c", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "selfAssignment", data: { name: "(a?.b).c" } }] + }, + { + code: "a.b = a?.b", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "selfAssignment", data: { name: "a?.b" } }] + } ] }); diff --git a/tests/lib/rules/no-setter-return.js b/tests/lib/rules/no-setter-return.js index b70b4e3f1c0..0c64e8b4811 100644 --- a/tests/lib/rules/no-setter-return.js +++ b/tests/lib/rules/no-setter-return.js @@ -39,7 +39,7 @@ function error(column, type = "ReturnStatement") { // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2018 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("no-setter-return", rule, { valid: [ @@ -505,6 +505,18 @@ ruleTester.run("no-setter-return", rule, { { code: "Object.defineProperty(foo, 'bar', { set: function(Object) { return 1; } })", errors: [error()] + }, + + // Optional chaining + { + code: "Object?.defineProperty(foo, 'bar', { set(val) { return 1; } })", + parserOptions: { ecmaVersion: 2020 }, + errors: [error()] + }, + { + code: "(Object?.defineProperty)(foo, 'bar', { set(val) { return 1; } })", + parserOptions: { ecmaVersion: 2020 }, + errors: [error()] } ] }); diff --git a/tests/lib/rules/no-throw-literal.js b/tests/lib/rules/no-throw-literal.js index 05002117793..7836cec7837 100644 --- a/tests/lib/rules/no-throw-literal.js +++ b/tests/lib/rules/no-throw-literal.js @@ -38,7 +38,9 @@ ruleTester.run("no-throw-literal", rule, { "throw foo ? 'literal' : new Error();", // ConditionalExpression (alternate) { code: "throw tag `${foo}`;", parserOptions: { ecmaVersion: 6 } }, // TaggedTemplateExpression { code: "function* foo() { var index = 0; throw yield index++; }", parserOptions: { ecmaVersion: 6 } }, // YieldExpression - { code: "async function foo() { throw await bar; }", parserOptions: { ecmaVersion: 8 } } // AwaitExpression + { code: "async function foo() { throw await bar; }", parserOptions: { ecmaVersion: 8 } }, // AwaitExpression + { code: "throw obj?.foo", parserOptions: { ecmaVersion: 2020 } }, // ChainExpression + { code: "throw obj?.foo()", parserOptions: { ecmaVersion: 2020 } } // ChainExpression ], invalid: [ { diff --git a/tests/lib/rules/no-unexpected-multiline.js b/tests/lib/rules/no-unexpected-multiline.js index 29d05a9215b..83c7bf67570 100644 --- a/tests/lib/rules/no-unexpected-multiline.js +++ b/tests/lib/rules/no-unexpected-multiline.js @@ -122,6 +122,24 @@ ruleTester.run("no-unexpected-multiline", rule, { >\`multiline\`; `, parser: require.resolve("../../fixtures/parsers/typescript-parsers/tagged-template-with-generic/tagged-template-with-generic-3") + }, + + // Optional chaining + { + code: "var a = b\n ?.(x || y).doSomething()", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "var a = b\n ?.[a, b, c].forEach(doSomething)", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "var a = b?.\n (x || y).doSomething()", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "var a = b?.\n [a, b, c].forEach(doSomething)", + parserOptions: { ecmaVersion: 2020 } } ], invalid: [ diff --git a/tests/lib/rules/no-unused-expressions.js b/tests/lib/rules/no-unused-expressions.js index 8ef2028c662..1674629c90c 100644 --- a/tests/lib/rules/no-unused-expressions.js +++ b/tests/lib/rules/no-unused-expressions.js @@ -74,6 +74,14 @@ ruleTester.run("no-unused-expressions", rule, { { code: "import(\"foo\")", parserOptions: { ecmaVersion: 11 } + }, + { + code: "func?.(\"foo\")", + parserOptions: { ecmaVersion: 11 } + }, + { + code: "obj?.foo(\"bar\")", + parserOptions: { ecmaVersion: 11 } } ], invalid: [ @@ -127,6 +135,23 @@ ruleTester.run("no-unused-expressions", rule, { options: [{ allowTaggedTemplates: false }], parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "unusedExpression" }] + }, + + // Optional chaining + { + code: "obj?.foo", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unusedExpression", type: "ExpressionStatement" }] + }, + { + code: "obj?.foo.bar", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unusedExpression", type: "ExpressionStatement" }] + }, + { + code: "obj?.foo().bar", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unusedExpression", type: "ExpressionStatement" }] } ] }); diff --git a/tests/lib/rules/no-useless-call.js b/tests/lib/rules/no-useless-call.js index 8713ee92c4d..528f2f1b129 100644 --- a/tests/lib/rules/no-useless-call.js +++ b/tests/lib/rules/no-useless-call.js @@ -44,7 +44,13 @@ ruleTester.run("no-useless-call", rule, { "foo.call();", "obj.foo.call();", "foo.apply();", - "obj.foo.apply();" + "obj.foo.apply();", + + // Optional chaining + { + code: "obj?.foo.bar.call(obj.foo, 1, 2);", + parserOptions: { ecmaVersion: 2020 } + } ], invalid: [ @@ -170,6 +176,86 @@ ruleTester.run("no-useless-call", rule, { data: { name: "apply" }, type: "CallExpression" }] + }, + + // Optional chaining + { + code: "foo.call?.(undefined, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unnecessaryCall", data: { name: "call" } }] + }, + { + code: "foo?.call(undefined, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unnecessaryCall", data: { name: "call" } }] + }, + { + code: "(foo?.call)(undefined, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unnecessaryCall", data: { name: "call" } }] + }, + { + code: "obj.foo.call?.(obj, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] + }, + { + code: "obj?.foo.call(obj, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] + }, + { + code: "(obj?.foo).call(obj, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] + }, + { + code: "(obj?.foo.call)(obj, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] + }, + { + code: "obj?.foo.bar.call(obj?.foo, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] + }, + { + code: "(obj?.foo).bar.call(obj?.foo, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] + }, + { + code: "obj.foo?.bar.call(obj.foo, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] } ] }); diff --git a/tests/lib/rules/no-whitespace-before-property.js b/tests/lib/rules/no-whitespace-before-property.js index d10b30e20e8..31d50449583 100644 --- a/tests/lib/rules/no-whitespace-before-property.js +++ b/tests/lib/rules/no-whitespace-before-property.js @@ -99,7 +99,45 @@ ruleTester.run("no-whitespace-before-property", rule, { "foo[bar.baz('qux')]", "foo[(bar.baz() + 0) + qux]", "foo['bar ' + 1 + ' baz']", - "5['toExponential']()" + "5['toExponential']()", + + // Optional chaining + { + code: "obj?.prop", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "( obj )?.prop", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj\n ?.prop", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.\n prop", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.[key]", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "( obj )?.[ key ]", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj\n ?.[key]", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.\n [key]", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj\n ?.\n [key]", + parserOptions: { ecmaVersion: 2020 } + } ], invalid: [ @@ -859,6 +897,56 @@ ruleTester.run("no-whitespace-before-property", rule, { messageId: "unexpectedWhitespace", data: { propName: "toExponential" } }] + }, + + // Optional chaining + { + code: "obj?. prop", + output: "obj?.prop", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "prop" } }] + }, + { + code: "obj ?.prop", + output: "obj?.prop", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "prop" } }] + }, + { + code: "obj?. [key]", + output: "obj?.[key]", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "key" } }] + }, + { + code: "obj ?.[key]", + output: "obj?.[key]", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "key" } }] + }, + { + code: "5 ?. prop", + output: "5?.prop", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "prop" } }] + }, + { + code: "5 ?. [key]", + output: "5?.[key]", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "key" } }] + }, + { + code: "obj/* comment */?. prop", + output: null, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "prop" } }] + }, + { + code: "obj ?./* comment */prop", + output: null, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "prop" } }] } ] }); diff --git a/tests/lib/rules/operator-assignment.js b/tests/lib/rules/operator-assignment.js index f690f7eb7f9..ca7dfce6911 100644 --- a/tests/lib/rules/operator-assignment.js +++ b/tests/lib/rules/operator-assignment.js @@ -16,7 +16,7 @@ const rule = require("../../../lib/rules/operator-assignment"), // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 7 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); const EXPECTED_OPERATOR_ASSIGNMENT = [{ messageId: "replaced", type: "AssignmentExpression" }]; const UNEXPECTED_OPERATOR_ASSIGNMENT = [{ messageId: "unexpected", type: "AssignmentExpression" }]; @@ -398,6 +398,20 @@ ruleTester.run("operator-assignment", rule, { output: "foo= foo+(+bar===baz)", // tokens cannot be adjacent, but the right side will be parenthesised options: ["never"], errors: UNEXPECTED_OPERATOR_ASSIGNMENT - }] + }, + + // Optional chaining + { + code: "(obj?.a).b = (obj?.a).b + y", + output: null, + errors: EXPECTED_OPERATOR_ASSIGNMENT + }, + { + code: "obj.a = obj?.a + b", + output: null, + errors: EXPECTED_OPERATOR_ASSIGNMENT + } + + ] }); diff --git a/tests/lib/rules/padding-line-between-statements.js b/tests/lib/rules/padding-line-between-statements.js index e553610e353..931cdd24ccb 100644 --- a/tests/lib/rules/padding-line-between-statements.js +++ b/tests/lib/rules/padding-line-between-statements.js @@ -3632,6 +3632,22 @@ ruleTester.run("padding-line-between-statements", rule, { errors: [{ messageId: "expectedBlankLine" }] }, + // Optional chaining + { + code: "(function(){\n})?.()\nvar a = 2;", + output: "(function(){\n})?.()\n\nvar a = 2;", + options: [{ blankLine: "always", prev: "iife", next: "*" }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedBlankLine" }] + }, + { + code: "void (function(){\n})?.()\nvar a = 2;", + output: "void (function(){\n})?.()\n\nvar a = 2;", + options: [{ blankLine: "always", prev: "iife", next: "*" }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedBlankLine" }] + }, + //---------------------------------------------------------------------- // import //---------------------------------------------------------------------- diff --git a/tests/lib/rules/prefer-arrow-callback.js b/tests/lib/rules/prefer-arrow-callback.js index cb380de251d..cb46c4a5021 100644 --- a/tests/lib/rules/prefer-arrow-callback.js +++ b/tests/lib/rules/prefer-arrow-callback.js @@ -21,7 +21,7 @@ const errors = [{ type: "FunctionExpression" }]; -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-arrow-callback", rule, { valid: [ @@ -40,7 +40,9 @@ ruleTester.run("prefer-arrow-callback", rule, { "foo(function bar() { arguments; }.bind(this));", "foo(function bar() { new.target; });", "foo(function bar() { new.target; }.bind(this));", - "foo(function bar() { this; }.bind(this, somethingElse));" + "foo(function bar() { this; }.bind(this, somethingElse));", + "foo((function() {}).bind.bar)", + "foo((function() { this.bar(); }).bind(obj).bind(this))" ], invalid: [ { @@ -165,13 +167,43 @@ ruleTester.run("prefer-arrow-callback", rule, { { code: "qux(async function (foo = 1, bar = 2, baz = 3) { return baz; })", output: "qux(async (foo = 1, bar = 2, baz = 3) => { return baz; })", - parserOptions: { ecmaVersion: 8 }, errors }, { code: "qux(async function (foo = 1, bar = 2, baz = 3) { return this; }.bind(this))", output: "qux(async (foo = 1, bar = 2, baz = 3) => { return this; })", - parserOptions: { ecmaVersion: 8 }, + errors + }, + { + code: "foo((bar || function() {}).bind(this))", + output: null, + errors + }, + { + code: "foo(function() {}.bind(this).bind(obj))", + output: "foo((() => {}).bind(obj))", + errors + }, + + // Optional chaining + { + code: "foo?.(function() {});", + output: "foo?.(() => {});", + errors + }, + { + code: "foo?.(function() { return this; }.bind(this));", + output: "foo?.(() => { return this; });", + errors + }, + { + code: "foo(function() { return this; }?.bind(this));", + output: "foo(() => { return this; });", + errors + }, + { + code: "foo((function() { return this; }?.bind)(this));", + output: null, errors } ] diff --git a/tests/lib/rules/prefer-destructuring.js b/tests/lib/rules/prefer-destructuring.js index 6f1b6e3a1be..c3d9c65706d 100644 --- a/tests/lib/rules/prefer-destructuring.js +++ b/tests/lib/rules/prefer-destructuring.js @@ -16,7 +16,7 @@ const rule = require("../../../lib/rules/prefer-destructuring"), // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-destructuring", rule, { valid: [ @@ -141,7 +141,11 @@ ruleTester.run("prefer-destructuring", rule, { { code: "var {bar} = object.foo;", options: [{ object: true }] - } + }, + + // Optional chaining + "var foo = array?.[0];", // because the fixed code can throw TypeError. + "var foo = object?.foo;" ], invalid: [ diff --git a/tests/lib/rules/prefer-exponentiation-operator.js b/tests/lib/rules/prefer-exponentiation-operator.js index 42bf8ec047d..da147b021fd 100644 --- a/tests/lib/rules/prefer-exponentiation-operator.js +++ b/tests/lib/rules/prefer-exponentiation-operator.js @@ -40,7 +40,7 @@ function invalid(code, output) { // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2018 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-exponentiation-operator", rule, { valid: [ @@ -346,6 +346,13 @@ ruleTester.run("prefer-exponentiation-operator", rule, { invalid("Math.pow(a, b/**/)", null), invalid("Math.pow(a, b//\n)", null), invalid("Math.pow(a, b)/* comment */;", "a**b/* comment */;"), - invalid("Math.pow(a, b)// comment\n;", "a**b// comment\n;") + invalid("Math.pow(a, b)// comment\n;", "a**b// comment\n;"), + + // Optional chaining + invalid("Math.pow?.(a, b)", "a**b"), + invalid("Math?.pow(a, b)", "a**b"), + invalid("Math?.pow?.(a, b)", "a**b"), + invalid("(Math?.pow)(a, b)", "a**b"), + invalid("(Math?.pow)?.(a, b)", "a**b") ] }); diff --git a/tests/lib/rules/prefer-numeric-literals.js b/tests/lib/rules/prefer-numeric-literals.js index d8ce4117140..d5d5751a6e3 100644 --- a/tests/lib/rules/prefer-numeric-literals.js +++ b/tests/lib/rules/prefer-numeric-literals.js @@ -16,7 +16,7 @@ const rule = require("../../../lib/rules/prefer-numeric-literals"), // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-numeric-literals", rule, { valid: [ @@ -318,6 +318,33 @@ ruleTester.run("prefer-numeric-literals", rule, { code: "parseInt('11', 2)//comment\n;", output: "0b11//comment\n;", errors: 1 + }, + + // Optional chaining + { + code: "parseInt?.(\"1F7\", 16) === 255;", + output: "0x1F7 === 255;", + errors: [{ message: "Use hexadecimal literals instead of parseInt()." }] + }, + { + code: "Number?.parseInt(\"1F7\", 16) === 255;", + output: "0x1F7 === 255;", + errors: [{ message: "Use hexadecimal literals instead of Number?.parseInt()." }] + }, + { + code: "Number?.parseInt?.(\"1F7\", 16) === 255;", + output: "0x1F7 === 255;", + errors: [{ message: "Use hexadecimal literals instead of Number?.parseInt()." }] + }, + { + code: "(Number?.parseInt)(\"1F7\", 16) === 255;", + output: "0x1F7 === 255;", + errors: [{ message: "Use hexadecimal literals instead of Number?.parseInt()." }] + }, + { + code: "(Number?.parseInt)?.(\"1F7\", 16) === 255;", + output: "0x1F7 === 255;", + errors: [{ message: "Use hexadecimal literals instead of Number?.parseInt()." }] } ] }); diff --git a/tests/lib/rules/prefer-promise-reject-errors.js b/tests/lib/rules/prefer-promise-reject-errors.js index 012e44ce7a5..de8f4c72524 100644 --- a/tests/lib/rules/prefer-promise-reject-errors.js +++ b/tests/lib/rules/prefer-promise-reject-errors.js @@ -16,7 +16,7 @@ const { RuleTester } = require("../../../lib/rule-tester"); // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 8 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-promise-reject-errors", rule, { @@ -42,7 +42,11 @@ ruleTester.run("prefer-promise-reject-errors", rule, { { code: "new Promise(function(resolve, reject) { reject() })", options: [{ allowEmptyReject: true }] - } + }, + + // Optional chaining + "Promise.reject(obj?.foo)", + "Promise.reject(obj?.foo())" ], invalid: [ @@ -87,7 +91,14 @@ ruleTester.run("prefer-promise-reject-errors", rule, { "new Promise((foo, arguments) => arguments(5))", "new Promise(function({}, reject) { reject(5) })", "new Promise(({}, reject) => reject(5))", - "new Promise((resolve, reject, somethingElse = reject(5)) => {})" + "new Promise((resolve, reject, somethingElse = reject(5)) => {})", + + // Optional chaining + "Promise.reject?.(5)", + "Promise?.reject(5)", + "Promise?.reject?.(5)", + "(Promise?.reject)(5)", + "(Promise?.reject)?.(5)" ].map(invalidCase => { const errors = { errors: [{ messageId: "rejectAnError", type: "CallExpression" }] }; diff --git a/tests/lib/rules/prefer-regex-literals.js b/tests/lib/rules/prefer-regex-literals.js index 9f6a2e6fbae..0ddaa8dae3d 100644 --- a/tests/lib/rules/prefer-regex-literals.js +++ b/tests/lib/rules/prefer-regex-literals.js @@ -16,7 +16,7 @@ const { RuleTester } = require("../../../lib/rule-tester"); // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-regex-literals", rule, { valid: [ @@ -222,6 +222,7 @@ ruleTester.run("prefer-regex-literals", rule, { env: { es2020: true }, errors: [{ messageId: "unexpectedRegExp", type: "CallExpression" }] }, + { code: "new RegExp(/a/);", options: [{ disallowRedundantWrapping: true }], @@ -241,6 +242,12 @@ ruleTester.run("prefer-regex-literals", rule, { code: "new RegExp('a');", options: [{ disallowRedundantWrapping: true }], errors: [{ messageId: "unexpectedRegExp", type: "NewExpression", line: 1, column: 1 }] + }, + + // Optional chaining + { + code: "new RegExp((String?.raw)`a`);", + errors: [{ messageId: "unexpectedRegExp" }] } ] }); diff --git a/tests/lib/rules/prefer-spread.js b/tests/lib/rules/prefer-spread.js index 580d7faeba9..7f48d845f1b 100644 --- a/tests/lib/rules/prefer-spread.js +++ b/tests/lib/rules/prefer-spread.js @@ -18,7 +18,7 @@ const { RuleTester } = require("../../../lib/rule-tester"); const errors = [{ messageId: "preferSpread", type: "CallExpression" }]; -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-spread", rule, { valid: [ @@ -39,7 +39,11 @@ ruleTester.run("prefer-spread", rule, { // ignores incomplete things. "foo.apply();", "obj.foo.apply();", - "obj.foo.apply(obj, ...args)" + "obj.foo.apply(obj, ...args)", + + // Optional chaining + "(a?.b).c.foo.apply(a?.b.c, args);", + "a?.b.c.foo.apply((a?.b).c, args);" ], invalid: [ { @@ -73,6 +77,44 @@ ruleTester.run("prefer-spread", rule, { { code: "[].concat.apply([\n/*empty*/\n], args);", errors + }, + + // Optional chaining + { + code: "foo.apply?.(undefined, args);", + errors + }, + { + code: "foo?.apply(undefined, args);", + errors + }, + { + code: "foo?.apply?.(undefined, args);", + errors + }, + { + code: "(foo?.apply)(undefined, args);", + errors + }, + { + code: "(foo?.apply)?.(undefined, args);", + errors + }, + { + code: "(obj?.foo).apply(obj, args);", + errors + }, + { + code: "a?.b.c.foo.apply(a?.b.c, args);", + errors + }, + { + code: "(a?.b.c).foo.apply(a?.b.c, args);", + errors + }, + { + code: "(a?.b).c.foo.apply((a?.b).c, args);", + errors } ] }); diff --git a/tests/lib/rules/radix.js b/tests/lib/rules/radix.js index 2766e80b597..52801c3d183 100644 --- a/tests/lib/rules/radix.js +++ b/tests/lib/rules/radix.js @@ -185,6 +185,28 @@ ruleTester.run("radix", rule, { messageId: "redundantRadix", type: "CallExpression" }] + }, + + // Optional chaining + { + code: "parseInt?.(\"10\");", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "missingRadix" }] + }, + { + code: "Number.parseInt?.(\"10\");", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "missingRadix" }] + }, + { + code: "Number?.parseInt(\"10\");", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "missingRadix" }] + }, + { + code: "(Number?.parseInt)(\"10\");", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "missingRadix" }] } ] }); diff --git a/tests/lib/rules/use-isnan.js b/tests/lib/rules/use-isnan.js index aa1b1746fcd..c5a4b6bd686 100644 --- a/tests/lib/rules/use-isnan.js +++ b/tests/lib/rules/use-isnan.js @@ -385,6 +385,24 @@ ruleTester.run("use-isnan", rule, { code: "foo.bar.lastIndexOf(NaN)", options: [{ enforceForIndexOf: true }], errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "lastIndexOf" } }] + }, + { + code: "foo.indexOf?.(NaN)", + options: [{ enforceForIndexOf: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + }, + { + code: "foo?.indexOf(NaN)", + options: [{ enforceForIndexOf: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + }, + { + code: "(foo?.indexOf)(NaN)", + options: [{ enforceForIndexOf: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] } ] }); diff --git a/tests/lib/rules/utils/ast-utils.js b/tests/lib/rules/utils/ast-utils.js index 683e5ed2a6e..e3790f50f3f 100644 --- a/tests/lib/rules/utils/ast-utils.js +++ b/tests/lib/rules/utils/ast-utils.js @@ -10,6 +10,7 @@ //------------------------------------------------------------------------------ const assert = require("chai").assert, + util = require("util"), espree = require("espree"), astUtils = require("../../../../lib/rules/utils/ast-utils"), { Linter } = require("../../../../lib/linter"), @@ -478,6 +479,28 @@ describe("ast-utils", () => { assert.strictEqual(astUtils.getStaticStringValue(ast.body[0].expression), expectedResults[key]); }); }); + + it("should return text of regex literal even if it's not supported natively.", () => { + const node = { + type: "Literal", + value: null, + regex: { pattern: "(?:)", flags: "u" } + }; + const expectedText = "/(?:)/u"; + + assert.strictEqual(astUtils.getStaticStringValue(node), expectedText); + }); + + it("should return text of bigint literal even if it's not supported natively.", () => { + const node = { + type: "Literal", + value: null, + bigint: "100n" + }; + const expectedText = "100n"; + + assert.strictEqual(astUtils.getStaticStringValue(node), expectedText); + }); }); describe("getStaticPropertyName", () => { @@ -1411,6 +1434,134 @@ describe("ast-utils", () => { }); }); + describe("equalLiteralValue", () => { + describe("should return true if two regex values are same, even if it's not supported natively.", () => { + const patterns = [ + { + nodeA: { + type: "Literal", + value: /(?:)/u, + regex: { pattern: "(?:)", flags: "u" } + }, + nodeB: { + type: "Literal", + value: /(?:)/u, + regex: { pattern: "(?:)", flags: "u" } + }, + expected: true + }, + { + nodeA: { + type: "Literal", + value: null, + regex: { pattern: "(?:)", flags: "u" } + }, + nodeB: { + type: "Literal", + value: null, + regex: { pattern: "(?:)", flags: "u" } + }, + expected: true + }, + { + nodeA: { + type: "Literal", + value: null, + regex: { pattern: "(?:)", flags: "u" } + }, + nodeB: { + type: "Literal", + value: /(?:)/, // eslint-disable-line require-unicode-regexp + regex: { pattern: "(?:)", flags: "" } + }, + expected: false + }, + { + nodeA: { + type: "Literal", + value: null, + regex: { pattern: "(?:a)", flags: "u" } + }, + nodeB: { + type: "Literal", + value: null, + regex: { pattern: "(?:b)", flags: "u" } + }, + expected: false + } + ]; + + for (const { nodeA, nodeB, expected } of patterns) { + it(`should return ${expected} if it compared ${util.format("%o", nodeA)} and ${util.format("%o", nodeB)}`, () => { + assert.strictEqual(astUtils.equalLiteralValue(nodeA, nodeB), expected); + }); + } + }); + + describe("should return true if two bigint values are same, even if it's not supported natively.", () => { + const patterns = [ + { + nodeA: { + type: "Literal", + value: null, + bigint: "1" + }, + nodeB: { + type: "Literal", + value: null, + bigint: "1" + }, + expected: true + }, + { + nodeA: { + type: "Literal", + value: null, + bigint: "1" + }, + nodeB: { + type: "Literal", + value: null, + bigint: "2" + }, + expected: false + }, + { + nodeA: { + type: "Literal", + value: 1n, + bigint: "1" + }, + nodeB: { + type: "Literal", + value: 1n, + bigint: "1" + }, + expected: true + }, + { + nodeA: { + type: "Literal", + value: 1n, + bigint: "1" + }, + nodeB: { + type: "Literal", + value: 2n, + bigint: "2" + }, + expected: false + } + ]; + + for (const { nodeA, nodeB, expected } of patterns) { + it(`should return ${expected} if it compared ${util.format("%o", nodeA)} and ${util.format("%o", nodeB)}`, () => { + assert.strictEqual(astUtils.equalLiteralValue(nodeA, nodeB), expected); + }); + } + }); + }); + describe("hasOctalEscapeSequence", () => { /* eslint-disable quote-props */ diff --git a/tests/lib/rules/wrap-iife.js b/tests/lib/rules/wrap-iife.js index d18948f429a..035e265da40 100644 --- a/tests/lib/rules/wrap-iife.js +++ b/tests/lib/rules/wrap-iife.js @@ -603,6 +603,36 @@ ruleTester.run("wrap-iife", rule, { options: ["inside", { functionPrototypeMethods: true }], parserOptions: { ecmaVersion: 2020 }, errors: [wrapExpressionError] + }, + + // Optional chaining + { + code: "window.bar = function() { return 3; }.call?.(this, arg1);", + output: "window.bar = (function() { return 3; }).call?.(this, arg1);", + options: ["inside", { functionPrototypeMethods: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [wrapInvocationError] + }, + { + code: "window.bar = function() { return 3; }?.call(this, arg1);", + output: "window.bar = (function() { return 3; })?.call(this, arg1);", + options: ["inside", { functionPrototypeMethods: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [wrapInvocationError] + }, + { + code: "window.bar = (function() { return 3; }?.call)(this, arg1);", + output: "window.bar = ((function() { return 3; })?.call)(this, arg1);", + options: ["inside", { functionPrototypeMethods: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [wrapInvocationError] + }, + { + code: "new (function () {} ?.());", + output: "new ((function () {}) ?.());", + options: ["inside"], + parserOptions: { ecmaVersion: 2020 }, + errors: [wrapExpressionError] } ] }); diff --git a/tests/lib/rules/yoda.js b/tests/lib/rules/yoda.js index 43449401873..0fd2c3ee78f 100644 --- a/tests/lib/rules/yoda.js +++ b/tests/lib/rules/yoda.js @@ -288,6 +288,11 @@ ruleTester.run("yoda", rule, { code: "if('a' <= x && x < MAX) {}", options: ["never", { exceptRange: true }] }, + { + code: "if (0 <= obj?.a && obj?.a < 1) {}", + options: ["never", { exceptRange: true }], + parserOptions: { ecmaVersion: 2020 } + }, // onlyEquality {