eslint 7.3.1 → 7.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +28 -16
  3. package/lib/cli-engine/config-array-factory.js +1 -33
  4. package/lib/cli-engine/formatters/checkstyle.js +2 -2
  5. package/lib/linter/code-path-analysis/code-path-analyzer.js +38 -0
  6. package/lib/linter/code-path-analysis/code-path-segment.js +0 -1
  7. package/lib/linter/code-path-analysis/code-path-state.js +59 -0
  8. package/lib/linter/code-path-analysis/debug-helpers.js +26 -19
  9. package/lib/rule-tester/rule-tester.js +10 -0
  10. package/lib/rules/accessor-pairs.js +1 -14
  11. package/lib/rules/array-callback-return.js +26 -17
  12. package/lib/rules/arrow-body-style.js +43 -8
  13. package/lib/rules/arrow-parens.js +91 -108
  14. package/lib/rules/camelcase.js +47 -0
  15. package/lib/rules/comma-dangle.js +1 -2
  16. package/lib/rules/consistent-return.js +1 -12
  17. package/lib/rules/constructor-super.js +1 -0
  18. package/lib/rules/dot-location.js +20 -14
  19. package/lib/rules/dot-notation.js +36 -33
  20. package/lib/rules/func-call-spacing.js +42 -6
  21. package/lib/rules/func-name-matching.js +1 -4
  22. package/lib/rules/global-require.js +2 -1
  23. package/lib/rules/id-blacklist.js +14 -11
  24. package/lib/rules/id-denylist.js +230 -0
  25. package/lib/rules/indent.js +23 -3
  26. package/lib/rules/index.js +1 -0
  27. package/lib/rules/keyword-spacing.js +2 -2
  28. package/lib/rules/max-len.js +13 -2
  29. package/lib/rules/new-cap.js +10 -14
  30. package/lib/rules/newline-per-chained-call.js +15 -5
  31. package/lib/rules/no-alert.js +10 -3
  32. package/lib/rules/no-duplicate-case.js +23 -4
  33. package/lib/rules/no-eval.js +8 -38
  34. package/lib/rules/no-extend-native.js +37 -40
  35. package/lib/rules/no-extra-bind.js +57 -17
  36. package/lib/rules/no-extra-boolean-cast.js +7 -0
  37. package/lib/rules/no-extra-parens.js +48 -10
  38. package/lib/rules/no-implicit-coercion.js +11 -6
  39. package/lib/rules/no-implied-eval.js +7 -28
  40. package/lib/rules/no-import-assign.js +33 -32
  41. package/lib/rules/no-irregular-whitespace.js +22 -12
  42. package/lib/rules/no-magic-numbers.js +4 -8
  43. package/lib/rules/no-obj-calls.js +7 -4
  44. package/lib/rules/no-prototype-builtins.js +13 -3
  45. package/lib/rules/no-self-assign.js +3 -53
  46. package/lib/rules/no-setter-return.js +5 -8
  47. package/lib/rules/no-underscore-dangle.js +66 -21
  48. package/lib/rules/no-unexpected-multiline.js +2 -2
  49. package/lib/rules/no-unneeded-ternary.js +0 -2
  50. package/lib/rules/no-unused-expressions.js +55 -23
  51. package/lib/rules/no-useless-call.js +10 -7
  52. package/lib/rules/no-whitespace-before-property.js +16 -4
  53. package/lib/rules/object-curly-newline.js +4 -4
  54. package/lib/rules/operator-assignment.js +3 -42
  55. package/lib/rules/padding-line-between-statements.js +2 -2
  56. package/lib/rules/prefer-arrow-callback.js +90 -25
  57. package/lib/rules/prefer-exponentiation-operator.js +1 -1
  58. package/lib/rules/prefer-numeric-literals.js +4 -13
  59. package/lib/rules/prefer-promise-reject-errors.js +1 -3
  60. package/lib/rules/prefer-regex-literals.js +68 -13
  61. package/lib/rules/prefer-spread.js +2 -6
  62. package/lib/rules/radix.js +5 -2
  63. package/lib/rules/sort-imports.js +28 -0
  64. package/lib/rules/use-isnan.js +1 -1
  65. package/lib/rules/utils/ast-utils.js +317 -153
  66. package/lib/rules/wrap-iife.js +9 -2
  67. package/lib/rules/yoda.js +2 -55
  68. package/messages/extend-config-missing.txt +1 -1
  69. package/messages/no-config-found.txt +1 -1
  70. package/messages/plugin-conflict.txt +1 -1
  71. package/messages/plugin-missing.txt +1 -1
  72. package/messages/whitespace-found.txt +1 -1
  73. package/package.json +6 -7
@@ -57,7 +57,16 @@ module.exports = {
57
57
  * @returns {string} The prefix of the node.
58
58
  */
59
59
  function getPrefix(node) {
60
- return node.computed ? "[" : ".";
60
+ if (node.computed) {
61
+ if (node.optional) {
62
+ return "?.[";
63
+ }
64
+ return "[";
65
+ }
66
+ if (node.optional) {
67
+ return "?.";
68
+ }
69
+ return ".";
61
70
  }
62
71
 
63
72
  /**
@@ -76,17 +85,18 @@ module.exports = {
76
85
 
77
86
  return {
78
87
  "CallExpression:exit"(node) {
79
- if (!node.callee || node.callee.type !== "MemberExpression") {
88
+ const callee = astUtils.skipChainExpression(node.callee);
89
+
90
+ if (callee.type !== "MemberExpression") {
80
91
  return;
81
92
  }
82
93
 
83
- const callee = node.callee;
84
- let parent = callee.object;
94
+ let parent = astUtils.skipChainExpression(callee.object);
85
95
  let depth = 1;
86
96
 
87
97
  while (parent && parent.callee) {
88
98
  depth += 1;
89
- parent = parent.callee.object;
99
+ parent = astUtils.skipChainExpression(astUtils.skipChainExpression(parent.callee).object);
90
100
  }
91
101
 
92
102
  if (depth > ignoreChainWithDepth && astUtils.isTokenOnSameLine(callee.object, callee.property)) {
@@ -10,7 +10,8 @@
10
10
 
11
11
  const {
12
12
  getStaticPropertyName: getPropertyName,
13
- getVariableByName
13
+ getVariableByName,
14
+ skipChainExpression
14
15
  } = require("./utils/ast-utils");
15
16
 
16
17
  //------------------------------------------------------------------------------
@@ -64,7 +65,13 @@ function isGlobalThisReferenceOrGlobalWindow(scope, node) {
64
65
  if (scope.type === "global" && node.type === "ThisExpression") {
65
66
  return true;
66
67
  }
67
- if (node.name === "window" || (node.name === "globalThis" && getVariableByName(scope, "globalThis"))) {
68
+ if (
69
+ node.type === "Identifier" &&
70
+ (
71
+ node.name === "window" ||
72
+ (node.name === "globalThis" && getVariableByName(scope, "globalThis"))
73
+ )
74
+ ) {
68
75
  return !isShadowed(scope, node);
69
76
  }
70
77
 
@@ -96,7 +103,7 @@ module.exports = {
96
103
  create(context) {
97
104
  return {
98
105
  CallExpression(node) {
99
- const callee = node.callee,
106
+ const callee = skipChainExpression(node.callee),
100
107
  currentScope = context.getScope();
101
108
 
102
109
  // without window.
@@ -6,6 +6,12 @@
6
6
 
7
7
  "use strict";
8
8
 
9
+ //------------------------------------------------------------------------------
10
+ // Requirements
11
+ //------------------------------------------------------------------------------
12
+
13
+ const astUtils = require("./utils/ast-utils");
14
+
9
15
  //------------------------------------------------------------------------------
10
16
  // Rule Definition
11
17
  //------------------------------------------------------------------------------
@@ -31,18 +37,31 @@ module.exports = {
31
37
  create(context) {
32
38
  const sourceCode = context.getSourceCode();
33
39
 
40
+ /**
41
+ * Determines whether the two given nodes are considered to be equal.
42
+ * @param {ASTNode} a First node.
43
+ * @param {ASTNode} b Second node.
44
+ * @returns {boolean} `true` if the nodes are considered to be equal.
45
+ */
46
+ function equal(a, b) {
47
+ if (a.type !== b.type) {
48
+ return false;
49
+ }
50
+
51
+ return astUtils.equalTokens(a, b, sourceCode);
52
+ }
34
53
  return {
35
54
  SwitchStatement(node) {
36
- const previousKeys = new Set();
55
+ const previousTests = [];
37
56
 
38
57
  for (const switchCase of node.cases) {
39
58
  if (switchCase.test) {
40
- const key = sourceCode.getText(switchCase.test);
59
+ const test = switchCase.test;
41
60
 
42
- if (previousKeys.has(key)) {
61
+ if (previousTests.some(previousTest => equal(previousTest, test))) {
43
62
  context.report({ node: switchCase, messageId: "unexpected" });
44
63
  } else {
45
- previousKeys.add(key);
64
+ previousTests.push(test);
46
65
  }
47
66
  }
48
67
  }
@@ -21,38 +21,6 @@ const candidatesOfGlobalObject = Object.freeze([
21
21
  "globalThis"
22
22
  ]);
23
23
 
24
- /**
25
- * Checks a given node is a Identifier node of the specified name.
26
- * @param {ASTNode} node A node to check.
27
- * @param {string} name A name to check.
28
- * @returns {boolean} `true` if the node is a Identifier node of the name.
29
- */
30
- function isIdentifier(node, name) {
31
- return node.type === "Identifier" && node.name === name;
32
- }
33
-
34
- /**
35
- * Checks a given node is a Literal node of the specified string value.
36
- * @param {ASTNode} node A node to check.
37
- * @param {string} name A name to check.
38
- * @returns {boolean} `true` if the node is a Literal node of the name.
39
- */
40
- function isConstant(node, name) {
41
- switch (node.type) {
42
- case "Literal":
43
- return node.value === name;
44
-
45
- case "TemplateLiteral":
46
- return (
47
- node.expressions.length === 0 &&
48
- node.quasis[0].value.cooked === name
49
- );
50
-
51
- default:
52
- return false;
53
- }
54
- }
55
-
56
24
  /**
57
25
  * Checks a given node is a MemberExpression node which has the specified name's
58
26
  * property.
@@ -62,10 +30,7 @@ function isConstant(node, name) {
62
30
  * the specified name's property
63
31
  */
64
32
  function isMember(node, name) {
65
- return (
66
- node.type === "MemberExpression" &&
67
- (node.computed ? isConstant : isIdentifier)(node.property, name)
68
- );
33
+ return astUtils.isSpecificMemberAccess(node, null, name);
69
34
  }
70
35
 
71
36
  //------------------------------------------------------------------------------
@@ -230,7 +195,12 @@ module.exports = {
230
195
  "CallExpression:exit"(node) {
231
196
  const callee = node.callee;
232
197
 
233
- if (isIdentifier(callee, "eval")) {
198
+ /*
199
+ * Optional call (`eval?.("code")`) is not direct eval.
200
+ * The direct eval is only step 6.a.vi of https://tc39.es/ecma262/#sec-function-calls-runtime-semantics-evaluation
201
+ * But the optional call is https://tc39.es/ecma262/#sec-optional-chaining-chain-evaluation
202
+ */
203
+ if (!node.optional && astUtils.isSpecificId(callee, "eval")) {
234
204
  report(callee);
235
205
  }
236
206
  }
@@ -241,7 +211,7 @@ module.exports = {
241
211
  "CallExpression:exit"(node) {
242
212
  const callee = node.callee;
243
213
 
244
- if (isIdentifier(callee, "eval")) {
214
+ if (astUtils.isSpecificId(callee, "eval")) {
245
215
  report(callee);
246
216
  }
247
217
  },
@@ -12,12 +12,6 @@
12
12
  const astUtils = require("./utils/ast-utils");
13
13
  const globals = require("globals");
14
14
 
15
- //------------------------------------------------------------------------------
16
- // Helpers
17
- //------------------------------------------------------------------------------
18
-
19
- const propertyDefinitionMethods = new Set(["defineProperty", "defineProperties"]);
20
-
21
15
  //------------------------------------------------------------------------------
22
16
  // Rule Definition
23
17
  //------------------------------------------------------------------------------
@@ -100,40 +94,30 @@ module.exports = {
100
94
  }
101
95
 
102
96
  /**
103
- * Checks that an identifier is an object of a prototype whose member
104
- * is being assigned in an AssignmentExpression.
105
- * Example: Object.prototype.foo = "bar"
106
- * @param {ASTNode} identifierNode The identifier to check.
107
- * @returns {boolean} True if the identifier's prototype is modified.
97
+ * Check if it's an assignment to the property of the given node.
98
+ * Example: `*.prop = 0` // the `*` is the given node.
99
+ * @param {ASTNode} node The node to check.
100
+ * @returns {boolean} True if an assignment to the property of the node.
108
101
  */
109
- function isInPrototypePropertyAssignment(identifierNode) {
110
- return Boolean(
111
- isPrototypePropertyAccessed(identifierNode) &&
112
- identifierNode.parent.parent.type === "MemberExpression" &&
113
- identifierNode.parent.parent.parent.type === "AssignmentExpression" &&
114
- identifierNode.parent.parent.parent.left === identifierNode.parent.parent
102
+ function isAssigningToPropertyOf(node) {
103
+ return (
104
+ node.parent.type === "MemberExpression" &&
105
+ node.parent.object === node &&
106
+ node.parent.parent.type === "AssignmentExpression" &&
107
+ node.parent.parent.left === node.parent
115
108
  );
116
109
  }
117
110
 
118
111
  /**
119
- * Checks that an identifier is an object of a prototype whose member
120
- * is being extended via the Object.defineProperty() or
121
- * Object.defineProperties() methods.
122
- * Example: Object.defineProperty(Array.prototype, "foo", ...)
123
- * Example: Object.defineProperties(Array.prototype, ...)
124
- * @param {ASTNode} identifierNode The identifier to check.
125
- * @returns {boolean} True if the identifier's prototype is modified.
112
+ * Checks if the given node is at the first argument of the method call of `Object.defineProperty()` or `Object.defineProperties()`.
113
+ * @param {ASTNode} node The node to check.
114
+ * @returns {boolean} True if the node is at the first argument of the method call of `Object.defineProperty()` or `Object.defineProperties()`.
126
115
  */
127
- function isInDefinePropertyCall(identifierNode) {
128
- return Boolean(
129
- isPrototypePropertyAccessed(identifierNode) &&
130
- identifierNode.parent.parent.type === "CallExpression" &&
131
- identifierNode.parent.parent.arguments[0] === identifierNode.parent &&
132
- identifierNode.parent.parent.callee.type === "MemberExpression" &&
133
- identifierNode.parent.parent.callee.object.type === "Identifier" &&
134
- identifierNode.parent.parent.callee.object.name === "Object" &&
135
- identifierNode.parent.parent.callee.property.type === "Identifier" &&
136
- propertyDefinitionMethods.has(identifierNode.parent.parent.callee.property.name)
116
+ function isInDefinePropertyCall(node) {
117
+ return (
118
+ node.parent.type === "CallExpression" &&
119
+ node.parent.arguments[0] === node &&
120
+ astUtils.isSpecificMemberAccess(node.parent.callee, "Object", /^definePropert(?:y|ies)$/u)
137
121
  );
138
122
  }
139
123
 
@@ -149,14 +133,27 @@ module.exports = {
149
133
  * @returns {void}
150
134
  */
151
135
  function checkAndReportPrototypeExtension(identifierNode) {
152
- if (isInPrototypePropertyAssignment(identifierNode)) {
136
+ if (!isPrototypePropertyAccessed(identifierNode)) {
137
+ return; // This is not `*.prototype` access.
138
+ }
139
+
140
+ /*
141
+ * `identifierNode.parent` is a MamberExpression `*.prototype`.
142
+ * If it's an optional member access, it may be wrapped by a `ChainExpression` node.
143
+ */
144
+ const prototypeNode =
145
+ identifierNode.parent.parent.type === "ChainExpression"
146
+ ? identifierNode.parent.parent
147
+ : identifierNode.parent;
148
+
149
+ if (isAssigningToPropertyOf(prototypeNode)) {
153
150
 
154
- // Identifier --> MemberExpression --> MemberExpression --> AssignmentExpression
155
- reportNode(identifierNode.parent.parent.parent, identifierNode.name);
156
- } else if (isInDefinePropertyCall(identifierNode)) {
151
+ // `*.prototype` -> MemberExpression -> AssignmentExpression
152
+ reportNode(prototypeNode.parent.parent, identifierNode.name);
153
+ } else if (isInDefinePropertyCall(prototypeNode)) {
157
154
 
158
- // Identifier --> MemberExpression --> CallExpression
159
- reportNode(identifierNode.parent.parent, identifierNode.name);
155
+ // `*.prototype` -> CallExpression
156
+ reportNode(prototypeNode.parent, identifierNode.name);
160
157
  }
161
158
  }
162
159
 
@@ -61,24 +61,62 @@ module.exports = {
61
61
  * @returns {void}
62
62
  */
63
63
  function report(node) {
64
+ const memberNode = node.parent;
65
+ const callNode = memberNode.parent.type === "ChainExpression"
66
+ ? memberNode.parent.parent
67
+ : memberNode.parent;
68
+
64
69
  context.report({
65
- node: node.parent.parent,
70
+ node: callNode,
66
71
  messageId: "unexpected",
67
- loc: node.parent.property.loc,
72
+ loc: memberNode.property.loc,
73
+
68
74
  fix(fixer) {
69
- if (node.parent.parent.arguments.length && !isSideEffectFree(node.parent.parent.arguments[0])) {
75
+ if (!isSideEffectFree(callNode.arguments[0])) {
70
76
  return null;
71
77
  }
72
78
 
73
- const firstTokenToRemove = sourceCode
74
- .getFirstTokenBetween(node.parent.object, node.parent.property, astUtils.isNotClosingParenToken);
75
- const lastTokenToRemove = sourceCode.getLastToken(node.parent.parent);
79
+ /*
80
+ * The list of the first/last token pair of a removal range.
81
+ * This is two parts because closing parentheses may exist between the method name and arguments.
82
+ * E.g. `(function(){}.bind ) (obj)`
83
+ * ^^^^^ ^^^^^ < removal ranges
84
+ * E.g. `(function(){}?.['bind'] ) ?.(obj)`
85
+ * ^^^^^^^^^^ ^^^^^^^ < removal ranges
86
+ */
87
+ const tokenPairs = [
88
+ [
89
+
90
+ // `.`, `?.`, or `[` token.
91
+ sourceCode.getTokenAfter(
92
+ memberNode.object,
93
+ astUtils.isNotClosingParenToken
94
+ ),
95
+
96
+ // property name or `]` token.
97
+ sourceCode.getLastToken(memberNode)
98
+ ],
99
+ [
100
+
101
+ // `?.` or `(` token of arguments.
102
+ sourceCode.getTokenAfter(
103
+ memberNode,
104
+ astUtils.isNotClosingParenToken
105
+ ),
106
+
107
+ // `)` token of arguments.
108
+ sourceCode.getLastToken(callNode)
109
+ ]
110
+ ];
111
+ const firstTokenToRemove = tokenPairs[0][0];
112
+ const lastTokenToRemove = tokenPairs[1][1];
76
113
 
77
114
  if (sourceCode.commentsExistBetween(firstTokenToRemove, lastTokenToRemove)) {
78
115
  return null;
79
116
  }
80
117
 
81
- return fixer.removeRange([firstTokenToRemove.range[0], node.parent.parent.range[1]]);
118
+ return tokenPairs.map(([start, end]) =>
119
+ fixer.removeRange([start.range[0], end.range[1]]));
82
120
  }
83
121
  });
84
122
  }
@@ -93,18 +131,20 @@ module.exports = {
93
131
  * @returns {boolean} `true` if the node is the callee of `.bind()` method.
94
132
  */
95
133
  function isCalleeOfBindMethod(node) {
96
- const parent = node.parent;
97
- const grandparent = parent.parent;
134
+ if (!astUtils.isSpecificMemberAccess(node.parent, null, "bind")) {
135
+ return false;
136
+ }
137
+
138
+ // The node of `*.bind` member access.
139
+ const bindNode = node.parent.parent.type === "ChainExpression"
140
+ ? node.parent.parent
141
+ : node.parent;
98
142
 
99
143
  return (
100
- grandparent &&
101
- grandparent.type === "CallExpression" &&
102
- grandparent.callee === parent &&
103
- grandparent.arguments.length === 1 &&
104
- grandparent.arguments[0].type !== "SpreadElement" &&
105
- parent.type === "MemberExpression" &&
106
- parent.object === node &&
107
- astUtils.getStaticPropertyName(parent) === "bind"
144
+ bindNode.parent.type === "CallExpression" &&
145
+ bindNode.parent.callee === bindNode &&
146
+ bindNode.parent.arguments.length === 1 &&
147
+ bindNode.parent.arguments[0].type !== "SpreadElement"
108
148
  );
109
149
  }
110
150
 
@@ -111,6 +111,10 @@ module.exports = {
111
111
  * @returns {boolean} If the node is in one of the flagged contexts
112
112
  */
113
113
  function isInFlaggedContext(node) {
114
+ if (node.parent.type === "ChainExpression") {
115
+ return isInFlaggedContext(node.parent);
116
+ }
117
+
114
118
  return isInBooleanContext(node) ||
115
119
  (isLogicalContext(node.parent) &&
116
120
 
@@ -149,6 +153,9 @@ module.exports = {
149
153
  * @returns {boolean} `true` if the node needs to be parenthesized.
150
154
  */
151
155
  function needsParens(previousNode, node) {
156
+ if (previousNode.parent.type === "ChainExpression") {
157
+ return needsParens(previousNode.parent, node);
158
+ }
152
159
  if (isParenthesized(previousNode)) {
153
160
 
154
161
  // parentheses around the previous node will stay, so there is no need for an additional pair
@@ -100,10 +100,18 @@ module.exports = {
100
100
  * @private
101
101
  */
102
102
  function isImmediateFunctionPrototypeMethodCall(node) {
103
- return node.type === "CallExpression" &&
104
- node.callee.type === "MemberExpression" &&
105
- node.callee.object.type === "FunctionExpression" &&
106
- ["call", "apply"].includes(astUtils.getStaticPropertyName(node.callee));
103
+ const callNode = astUtils.skipChainExpression(node);
104
+
105
+ if (callNode.type !== "CallExpression") {
106
+ return false;
107
+ }
108
+ const callee = astUtils.skipChainExpression(callNode.callee);
109
+
110
+ return (
111
+ callee.type === "MemberExpression" &&
112
+ callee.object.type === "FunctionExpression" &&
113
+ ["call", "apply"].includes(astUtils.getStaticPropertyName(callee))
114
+ );
107
115
  }
108
116
 
109
117
  /**
@@ -360,7 +368,9 @@ module.exports = {
360
368
  * @returns {boolean} `true` if the given node is an IIFE
361
369
  */
362
370
  function isIIFE(node) {
363
- return node.type === "CallExpression" && node.callee.type === "FunctionExpression";
371
+ const maybeCallNode = astUtils.skipChainExpression(node);
372
+
373
+ return maybeCallNode.type === "CallExpression" && maybeCallNode.callee.type === "FunctionExpression";
364
374
  }
365
375
 
366
376
  /**
@@ -466,13 +476,16 @@ module.exports = {
466
476
 
467
477
  if (
468
478
  hasDoubleExcessParens(callee) ||
469
- !isIIFE(node) && !hasNewParensException && !(
479
+ !isIIFE(node) &&
480
+ !hasNewParensException &&
481
+ !(
470
482
 
471
483
  // Allow extra parens around a new expression if they are intervening parentheses.
472
484
  node.type === "NewExpression" &&
473
485
  callee.type === "MemberExpression" &&
474
486
  doesMemberExpressionContainCallExpression(callee)
475
- )
487
+ ) &&
488
+ !(!node.optional && callee.type === "ChainExpression")
476
489
  ) {
477
490
  report(node.callee);
478
491
  }
@@ -710,6 +723,20 @@ module.exports = {
710
723
  reportsBuffer.reports = reportsBuffer.reports.filter(r => r.node !== node);
711
724
  }
712
725
 
726
+ /**
727
+ * Checks whether a node is a MemberExpression at NewExpression's callee.
728
+ * @param {ASTNode} node node to check.
729
+ * @returns {boolean} True if the node is a MemberExpression at NewExpression's callee. false otherwise.
730
+ */
731
+ function isMemberExpInNewCallee(node) {
732
+ if (node.type === "MemberExpression") {
733
+ return node.parent.type === "NewExpression" && node.parent.callee === node
734
+ ? true
735
+ : node.parent.object === node && isMemberExpInNewCallee(node.parent);
736
+ }
737
+ return false;
738
+ }
739
+
713
740
  return {
714
741
  ArrayExpression(node) {
715
742
  node.elements
@@ -950,7 +977,11 @@ module.exports = {
950
977
  LogicalExpression: checkBinaryLogical,
951
978
 
952
979
  MemberExpression(node) {
953
- const nodeObjHasExcessParens = hasExcessParens(node.object) &&
980
+ const shouldAllowWrapOnce = isMemberExpInNewCallee(node) &&
981
+ doesMemberExpressionContainCallExpression(node);
982
+ const nodeObjHasExcessParens = shouldAllowWrapOnce
983
+ ? hasDoubleExcessParens(node.object)
984
+ : hasExcessParens(node.object) &&
954
985
  !(
955
986
  isImmediateFunctionPrototypeMethodCall(node.parent) &&
956
987
  node.parent.callee === node &&
@@ -974,8 +1005,8 @@ module.exports = {
974
1005
  }
975
1006
 
976
1007
  if (nodeObjHasExcessParens &&
977
- node.object.type === "CallExpression" &&
978
- node.parent.type !== "NewExpression") {
1008
+ node.object.type === "CallExpression"
1009
+ ) {
979
1010
  report(node.object);
980
1011
  }
981
1012
 
@@ -986,6 +1017,13 @@ module.exports = {
986
1017
  report(node.object);
987
1018
  }
988
1019
 
1020
+ if (nodeObjHasExcessParens &&
1021
+ node.optional &&
1022
+ node.object.type === "ChainExpression"
1023
+ ) {
1024
+ report(node.object);
1025
+ }
1026
+
989
1027
  if (node.computed && hasExcessParens(node.property)) {
990
1028
  report(node.property);
991
1029
  }
@@ -47,12 +47,14 @@ function isDoubleLogicalNegating(node) {
47
47
  * @returns {boolean} Whether or not the node is a binary negating of `.indexOf()` method calling.
48
48
  */
49
49
  function isBinaryNegatingOfIndexOf(node) {
50
+ if (node.operator !== "~") {
51
+ return false;
52
+ }
53
+ const callNode = astUtils.skipChainExpression(node.argument);
54
+
50
55
  return (
51
- node.operator === "~" &&
52
- node.argument.type === "CallExpression" &&
53
- node.argument.callee.type === "MemberExpression" &&
54
- node.argument.callee.property.type === "Identifier" &&
55
- INDEX_OF_PATTERN.test(node.argument.callee.property.name)
56
+ callNode.type === "CallExpression" &&
57
+ astUtils.isSpecificMemberAccess(callNode.callee, null, INDEX_OF_PATTERN)
56
58
  );
57
59
  }
58
60
 
@@ -246,7 +248,10 @@ module.exports = {
246
248
  // ~foo.indexOf(bar)
247
249
  operatorAllowed = options.allow.indexOf("~") >= 0;
248
250
  if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) {
249
- const recommendation = `${sourceCode.getText(node.argument)} !== -1`;
251
+
252
+ // `foo?.indexOf(bar) !== -1` will be true (== found) if the `foo` is nullish. So use `>= 0` in that case.
253
+ const comparison = node.argument.type === "ChainExpression" ? ">= 0" : "!== -1";
254
+ const recommendation = `${sourceCode.getText(node.argument)} ${comparison}`;
250
255
 
251
256
  report(node, recommendation, false);
252
257
  }
@@ -35,8 +35,8 @@ module.exports = {
35
35
  },
36
36
 
37
37
  create(context) {
38
- const EVAL_LIKE_FUNCS = Object.freeze(["setTimeout", "execScript", "setInterval"]);
39
38
  const GLOBAL_CANDIDATES = Object.freeze(["global", "window", "globalThis"]);
39
+ const EVAL_LIKE_FUNC_PATTERN = /^(?:set(?:Interval|Timeout)|execScript)$/u;
40
40
 
41
41
  /**
42
42
  * Checks whether a node is evaluated as a string or not.
@@ -56,28 +56,6 @@ module.exports = {
56
56
  return false;
57
57
  }
58
58
 
59
- /**
60
- * Checks whether a node is an Identifier node named one of the specified names.
61
- * @param {ASTNode} node A node to check.
62
- * @param {string[]} specifiers Array of specified name.
63
- * @returns {boolean} True if the node is a Identifier node which has specified name.
64
- */
65
- function isSpecifiedIdentifier(node, specifiers) {
66
- return node.type === "Identifier" && specifiers.includes(node.name);
67
- }
68
-
69
- /**
70
- * Checks a given node is a MemberExpression node which has the specified name's
71
- * property.
72
- * @param {ASTNode} node A node to check.
73
- * @param {string[]} specifiers Array of specified name.
74
- * @returns {boolean} `true` if the node is a MemberExpression node which has
75
- * the specified name's property
76
- */
77
- function isSpecifiedMember(node, specifiers) {
78
- return node.type === "MemberExpression" && specifiers.includes(astUtils.getStaticPropertyName(node));
79
- }
80
-
81
59
  /**
82
60
  * Reports if the `CallExpression` node has evaluated argument.
83
61
  * @param {ASTNode} node A CallExpression to check.
@@ -114,14 +92,15 @@ module.exports = {
114
92
  const identifier = ref.identifier;
115
93
  let node = identifier.parent;
116
94
 
117
- while (isSpecifiedMember(node, [name])) {
95
+ while (astUtils.isSpecificMemberAccess(node, null, name)) {
118
96
  node = node.parent;
119
97
  }
120
98
 
121
- if (isSpecifiedMember(node, EVAL_LIKE_FUNCS)) {
122
- const parent = node.parent;
99
+ if (astUtils.isSpecificMemberAccess(node, null, EVAL_LIKE_FUNC_PATTERN)) {
100
+ const calleeNode = node.parent.type === "ChainExpression" ? node.parent : node;
101
+ const parent = calleeNode.parent;
123
102
 
124
- if (parent.type === "CallExpression" && parent.callee === node) {
103
+ if (parent.type === "CallExpression" && parent.callee === calleeNode) {
125
104
  reportImpliedEvalCallExpression(parent);
126
105
  }
127
106
  }
@@ -134,7 +113,7 @@ module.exports = {
134
113
 
135
114
  return {
136
115
  CallExpression(node) {
137
- if (isSpecifiedIdentifier(node.callee, EVAL_LIKE_FUNCS)) {
116
+ if (astUtils.isSpecificId(node.callee, EVAL_LIKE_FUNC_PATTERN)) {
138
117
  reportImpliedEvalCallExpression(node);
139
118
  }
140
119
  },