eslint 7.3.0 → 7.6.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 (71) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +5 -3
  3. package/lib/cli-engine/config-array-factory.js +1 -28
  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/consistent-return.js +1 -12
  16. package/lib/rules/constructor-super.js +1 -0
  17. package/lib/rules/dot-location.js +20 -14
  18. package/lib/rules/dot-notation.js +36 -33
  19. package/lib/rules/func-call-spacing.js +42 -6
  20. package/lib/rules/func-name-matching.js +1 -4
  21. package/lib/rules/global-require.js +2 -1
  22. package/lib/rules/id-blacklist.js +14 -11
  23. package/lib/rules/id-denylist.js +230 -0
  24. package/lib/rules/indent.js +19 -0
  25. package/lib/rules/index.js +1 -0
  26. package/lib/rules/keyword-spacing.js +2 -2
  27. package/lib/rules/max-len.js +13 -2
  28. package/lib/rules/new-cap.js +10 -14
  29. package/lib/rules/newline-per-chained-call.js +15 -5
  30. package/lib/rules/no-alert.js +10 -3
  31. package/lib/rules/no-duplicate-case.js +23 -4
  32. package/lib/rules/no-eval.js +8 -38
  33. package/lib/rules/no-extend-native.js +37 -40
  34. package/lib/rules/no-extra-bind.js +57 -17
  35. package/lib/rules/no-extra-boolean-cast.js +7 -0
  36. package/lib/rules/no-extra-parens.js +48 -10
  37. package/lib/rules/no-implicit-coercion.js +11 -6
  38. package/lib/rules/no-implied-eval.js +7 -28
  39. package/lib/rules/no-import-assign.js +33 -32
  40. package/lib/rules/no-irregular-whitespace.js +22 -12
  41. package/lib/rules/no-magic-numbers.js +4 -8
  42. package/lib/rules/no-obj-calls.js +7 -4
  43. package/lib/rules/no-prototype-builtins.js +13 -3
  44. package/lib/rules/no-self-assign.js +3 -53
  45. package/lib/rules/no-setter-return.js +5 -8
  46. package/lib/rules/no-unexpected-multiline.js +2 -2
  47. package/lib/rules/no-unneeded-ternary.js +0 -2
  48. package/lib/rules/no-unused-expressions.js +55 -23
  49. package/lib/rules/no-useless-call.js +10 -7
  50. package/lib/rules/no-whitespace-before-property.js +16 -4
  51. package/lib/rules/object-curly-newline.js +4 -4
  52. package/lib/rules/operator-assignment.js +3 -42
  53. package/lib/rules/padding-line-between-statements.js +2 -2
  54. package/lib/rules/prefer-arrow-callback.js +90 -25
  55. package/lib/rules/prefer-exponentiation-operator.js +1 -1
  56. package/lib/rules/prefer-numeric-literals.js +4 -13
  57. package/lib/rules/prefer-promise-reject-errors.js +1 -3
  58. package/lib/rules/prefer-regex-literals.js +68 -13
  59. package/lib/rules/prefer-spread.js +2 -6
  60. package/lib/rules/radix.js +5 -2
  61. package/lib/rules/sort-imports.js +28 -0
  62. package/lib/rules/use-isnan.js +1 -1
  63. package/lib/rules/utils/ast-utils.js +317 -153
  64. package/lib/rules/wrap-iife.js +9 -2
  65. package/lib/rules/yoda.js +2 -55
  66. package/messages/extend-config-missing.txt +1 -1
  67. package/messages/no-config-found.txt +1 -1
  68. package/messages/plugin-conflict.txt +1 -1
  69. package/messages/plugin-missing.txt +1 -1
  70. package/messages/whitespace-found.txt +1 -1
  71. package/package.json +6 -6
@@ -24,10 +24,13 @@ const nonCallableGlobals = ["Atomics", "JSON", "Math", "Reflect"];
24
24
  * @returns {string} name to report
25
25
  */
26
26
  function getReportNodeName(node) {
27
- if (node.callee.type === "MemberExpression") {
28
- return getPropertyName(node.callee);
27
+ if (node.type === "ChainExpression") {
28
+ return getReportNodeName(node.expression);
29
29
  }
30
- return node.callee.name;
30
+ if (node.type === "MemberExpression") {
31
+ return getPropertyName(node);
32
+ }
33
+ return node.name;
31
34
  }
32
35
 
33
36
  //------------------------------------------------------------------------------
@@ -69,7 +72,7 @@ module.exports = {
69
72
  }
70
73
 
71
74
  for (const { node, path } of tracker.iterateGlobalReferences(traceMap)) {
72
- const name = getReportNodeName(node);
75
+ const name = getReportNodeName(node.callee);
73
76
  const ref = path[0];
74
77
  const messageId = name === ref ? "unexpectedCall" : "unexpectedRefCall";
75
78
 
@@ -4,6 +4,12 @@
4
4
  */
5
5
  "use strict";
6
6
 
7
+ //------------------------------------------------------------------------------
8
+ // Requirements
9
+ //------------------------------------------------------------------------------
10
+
11
+ const astUtils = require("./utils/ast-utils");
12
+
7
13
  //------------------------------------------------------------------------------
8
14
  // Rule Definition
9
15
  //------------------------------------------------------------------------------
@@ -39,15 +45,19 @@ module.exports = {
39
45
  * @returns {void}
40
46
  */
41
47
  function disallowBuiltIns(node) {
42
- if (node.callee.type !== "MemberExpression" || node.callee.computed) {
48
+
49
+ // TODO: just use `astUtils.getStaticPropertyName(node.callee)`
50
+ const callee = astUtils.skipChainExpression(node.callee);
51
+
52
+ if (callee.type !== "MemberExpression" || callee.computed) {
43
53
  return;
44
54
  }
45
- const propName = node.callee.property.name;
55
+ const propName = callee.property.name;
46
56
 
47
57
  if (DISALLOWED_PROPS.indexOf(propName) > -1) {
48
58
  context.report({
49
59
  messageId: "prototypeBuildIn",
50
- loc: node.callee.property.loc,
60
+ loc: callee.property.loc,
51
61
  data: { prop: propName },
52
62
  node
53
63
  });
@@ -17,56 +17,6 @@ const astUtils = require("./utils/ast-utils");
17
17
 
18
18
  const SPACES = /\s+/gu;
19
19
 
20
- /**
21
- * Checks whether the property of 2 given member expression nodes are the same
22
- * property or not.
23
- * @param {ASTNode} left A member expression node to check.
24
- * @param {ASTNode} right Another member expression node to check.
25
- * @returns {boolean} `true` if the member expressions have the same property.
26
- */
27
- function isSameProperty(left, right) {
28
- if (left.property.type === "Identifier" &&
29
- left.property.type === right.property.type &&
30
- left.property.name === right.property.name &&
31
- left.computed === right.computed
32
- ) {
33
- return true;
34
- }
35
-
36
- const lname = astUtils.getStaticPropertyName(left);
37
- const rname = astUtils.getStaticPropertyName(right);
38
-
39
- return lname !== null && lname === rname;
40
- }
41
-
42
- /**
43
- * Checks whether 2 given member expression nodes are the reference to the same
44
- * property or not.
45
- * @param {ASTNode} left A member expression node to check.
46
- * @param {ASTNode} right Another member expression node to check.
47
- * @returns {boolean} `true` if the member expressions are the reference to the
48
- * same property or not.
49
- */
50
- function isSameMember(left, right) {
51
- if (!isSameProperty(left, right)) {
52
- return false;
53
- }
54
-
55
- const lobj = left.object;
56
- const robj = right.object;
57
-
58
- if (lobj.type !== robj.type) {
59
- return false;
60
- }
61
- if (lobj.type === "MemberExpression") {
62
- return isSameMember(lobj, robj);
63
- }
64
- if (lobj.type === "ThisExpression") {
65
- return true;
66
- }
67
- return lobj.type === "Identifier" && lobj.name === robj.name;
68
- }
69
-
70
20
  /**
71
21
  * Traverses 2 Pattern nodes in parallel, then reports self-assignments.
72
22
  * @param {ASTNode|null} left A left node to traverse. This is a Pattern or
@@ -162,9 +112,9 @@ function eachSelfAssignment(left, right, props, report) {
162
112
  }
163
113
  } else if (
164
114
  props &&
165
- left.type === "MemberExpression" &&
166
- right.type === "MemberExpression" &&
167
- isSameMember(left, right)
115
+ astUtils.skipChainExpression(left).type === "MemberExpression" &&
116
+ astUtils.skipChainExpression(right).type === "MemberExpression" &&
117
+ astUtils.isSameReference(left, right)
168
118
  ) {
169
119
  report(right);
170
120
  }
@@ -39,15 +39,12 @@ function isGlobalReference(node, scope) {
39
39
  * @returns {boolean} `true` if the node is argument at the given position.
40
40
  */
41
41
  function isArgumentOfGlobalMethodCall(node, scope, objectName, methodName, index) {
42
- const parent = node.parent;
42
+ const callNode = node.parent;
43
43
 
44
- return parent.type === "CallExpression" &&
45
- parent.arguments[index] === node &&
46
- parent.callee.type === "MemberExpression" &&
47
- astUtils.getStaticPropertyName(parent.callee) === methodName &&
48
- parent.callee.object.type === "Identifier" &&
49
- parent.callee.object.name === objectName &&
50
- isGlobalReference(parent.callee.object, scope);
44
+ return callNode.type === "CallExpression" &&
45
+ callNode.arguments[index] === node &&
46
+ astUtils.isSpecificMemberAccess(callNode.callee, objectName, methodName) &&
47
+ isGlobalReference(astUtils.skipChainExpression(callNode.callee).object, scope);
51
48
  }
52
49
 
53
50
  /**
@@ -68,7 +68,7 @@ module.exports = {
68
68
  return {
69
69
 
70
70
  MemberExpression(node) {
71
- if (!node.computed) {
71
+ if (!node.computed || node.optional) {
72
72
  return;
73
73
  }
74
74
  checkForBreakAfter(node.object, "property");
@@ -96,7 +96,7 @@ module.exports = {
96
96
  },
97
97
 
98
98
  CallExpression(node) {
99
- if (node.arguments.length === 0) {
99
+ if (node.arguments.length === 0 || node.optional) {
100
100
  return;
101
101
  }
102
102
  checkForBreakAfter(node.callee, "function");
@@ -122,7 +122,6 @@ module.exports = {
122
122
  if (isBooleanLiteral(node.alternate) && isBooleanLiteral(node.consequent)) {
123
123
  context.report({
124
124
  node,
125
- loc: node.consequent.loc.start,
126
125
  messageId: "unnecessaryConditionalExpression",
127
126
  fix(fixer) {
128
127
  if (node.consequent.value === node.alternate.value) {
@@ -144,7 +143,6 @@ module.exports = {
144
143
  } else if (!defaultAssignment && matchesDefaultAssignment(node)) {
145
144
  context.report({
146
145
  node,
147
- loc: node.consequent.loc.start,
148
146
  messageId: "unnecessaryConditionalAssignment",
149
147
  fix: fixer => {
150
148
  const shouldParenthesizeAlternate =
@@ -8,6 +8,22 @@
8
8
  // Rule Definition
9
9
  //------------------------------------------------------------------------------
10
10
 
11
+ /**
12
+ * Returns `true`.
13
+ * @returns {boolean} `true`.
14
+ */
15
+ function alwaysTrue() {
16
+ return true;
17
+ }
18
+
19
+ /**
20
+ * Returns `false`.
21
+ * @returns {boolean} `false`.
22
+ */
23
+ function alwaysFalse() {
24
+ return false;
25
+ }
26
+
11
27
  module.exports = {
12
28
  meta: {
13
29
  type: "suggestion",
@@ -101,40 +117,56 @@ module.exports = {
101
117
  }
102
118
 
103
119
  /**
104
- * Determines whether or not a given node is a valid expression. Recurses on short circuit eval and ternary nodes if enabled by flags.
105
- * @param {ASTNode} node any node
106
- * @returns {boolean} whether the given node is a valid expression
120
+ * The member functions return `true` if the type has no side-effects.
121
+ * Unknown nodes are handled as `false`, then this rule ignores those.
107
122
  */
108
- function isValidExpression(node) {
109
- if (allowTernary) {
110
-
111
- // Recursive check for ternary and logical expressions
112
- if (node.type === "ConditionalExpression") {
113
- return isValidExpression(node.consequent) && isValidExpression(node.alternate);
123
+ const Checker = Object.assign(Object.create(null), {
124
+ isDisallowed(node) {
125
+ return (Checker[node.type] || alwaysFalse)(node);
126
+ },
127
+
128
+ ArrayExpression: alwaysTrue,
129
+ ArrowFunctionExpression: alwaysTrue,
130
+ BinaryExpression: alwaysTrue,
131
+ ChainExpression(node) {
132
+ return Checker.isDisallowed(node.expression);
133
+ },
134
+ ClassExpression: alwaysTrue,
135
+ ConditionalExpression(node) {
136
+ if (allowTernary) {
137
+ return Checker.isDisallowed(node.consequent) || Checker.isDisallowed(node.alternate);
114
138
  }
115
- }
116
-
117
- if (allowShortCircuit) {
118
- if (node.type === "LogicalExpression") {
119
- return isValidExpression(node.right);
139
+ return true;
140
+ },
141
+ FunctionExpression: alwaysTrue,
142
+ Identifier: alwaysTrue,
143
+ Literal: alwaysTrue,
144
+ LogicalExpression(node) {
145
+ if (allowShortCircuit) {
146
+ return Checker.isDisallowed(node.right);
120
147
  }
121
- }
122
-
123
- if (allowTaggedTemplates && node.type === "TaggedTemplateExpression") {
124
148
  return true;
149
+ },
150
+ MemberExpression: alwaysTrue,
151
+ MetaProperty: alwaysTrue,
152
+ ObjectExpression: alwaysTrue,
153
+ SequenceExpression: alwaysTrue,
154
+ TaggedTemplateExpression() {
155
+ return !allowTaggedTemplates;
156
+ },
157
+ TemplateLiteral: alwaysTrue,
158
+ ThisExpression: alwaysTrue,
159
+ UnaryExpression(node) {
160
+ return node.operator !== "void" && node.operator !== "delete";
125
161
  }
126
-
127
- return /^(?:Assignment|Call|New|Update|Yield|Await|Import)Expression$/u.test(node.type) ||
128
- (node.type === "UnaryExpression" && ["delete", "void"].indexOf(node.operator) >= 0);
129
- }
162
+ });
130
163
 
131
164
  return {
132
165
  ExpressionStatement(node) {
133
- if (!isValidExpression(node.expression) && !isDirective(node, context.getAncestors())) {
166
+ if (Checker.isDisallowed(node.expression) && !isDirective(node, context.getAncestors())) {
134
167
  context.report({ node, messageId: "unusedExpression" });
135
168
  }
136
169
  }
137
170
  };
138
-
139
171
  }
140
172
  };
@@ -17,13 +17,15 @@ const astUtils = require("./utils/ast-utils");
17
17
  * @returns {boolean} Whether or not the node is a `.call()`/`.apply()`.
18
18
  */
19
19
  function isCallOrNonVariadicApply(node) {
20
+ const callee = astUtils.skipChainExpression(node.callee);
21
+
20
22
  return (
21
- node.callee.type === "MemberExpression" &&
22
- node.callee.property.type === "Identifier" &&
23
- node.callee.computed === false &&
23
+ callee.type === "MemberExpression" &&
24
+ callee.property.type === "Identifier" &&
25
+ callee.computed === false &&
24
26
  (
25
- (node.callee.property.name === "call" && node.arguments.length >= 1) ||
26
- (node.callee.property.name === "apply" && node.arguments.length === 2 && node.arguments[1].type === "ArrayExpression")
27
+ (callee.property.name === "call" && node.arguments.length >= 1) ||
28
+ (callee.property.name === "apply" && node.arguments.length === 2 && node.arguments[1].type === "ArrayExpression")
27
29
  )
28
30
  );
29
31
  }
@@ -74,12 +76,13 @@ module.exports = {
74
76
  return;
75
77
  }
76
78
 
77
- const applied = node.callee.object;
79
+ const callee = astUtils.skipChainExpression(node.callee);
80
+ const applied = astUtils.skipChainExpression(callee.object);
78
81
  const expectedThis = (applied.type === "MemberExpression") ? applied.object : null;
79
82
  const thisArg = node.arguments[0];
80
83
 
81
84
  if (isValidThisArg(expectedThis, thisArg, sourceCode)) {
82
- context.report({ node, messageId: "unnecessaryCall", data: { name: node.callee.property.name } });
85
+ context.report({ node, messageId: "unnecessaryCall", data: { name: callee.property.name } });
83
86
  }
84
87
  }
85
88
  };
@@ -49,8 +49,6 @@ module.exports = {
49
49
  * @private
50
50
  */
51
51
  function reportError(node, leftToken, rightToken) {
52
- const replacementText = node.computed ? "" : ".";
53
-
54
52
  context.report({
55
53
  node,
56
54
  messageId: "unexpectedWhitespace",
@@ -58,7 +56,9 @@ module.exports = {
58
56
  propName: sourceCode.getText(node.property)
59
57
  },
60
58
  fix(fixer) {
61
- if (!node.computed && astUtils.isDecimalInteger(node.object)) {
59
+ let replacementText = "";
60
+
61
+ if (!node.computed && !node.optional && astUtils.isDecimalInteger(node.object)) {
62
62
 
63
63
  /*
64
64
  * If the object is a number literal, fixing it to something like 5.toString() would cause a SyntaxError.
@@ -66,6 +66,18 @@ module.exports = {
66
66
  */
67
67
  return null;
68
68
  }
69
+
70
+ // Don't fix if comments exist.
71
+ if (sourceCode.commentsExistBetween(leftToken, rightToken)) {
72
+ return null;
73
+ }
74
+
75
+ if (node.optional) {
76
+ replacementText = "?.";
77
+ } else if (!node.computed) {
78
+ replacementText = ".";
79
+ }
80
+
69
81
  return fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], replacementText);
70
82
  }
71
83
  });
@@ -86,7 +98,7 @@ module.exports = {
86
98
 
87
99
  if (node.computed) {
88
100
  rightToken = sourceCode.getTokenBefore(node.property, astUtils.isOpeningBracketToken);
89
- leftToken = sourceCode.getTokenBefore(rightToken);
101
+ leftToken = sourceCode.getTokenBefore(rightToken, node.optional ? 1 : 0);
90
102
  } else {
91
103
  rightToken = sourceCode.getFirstToken(node.property);
92
104
  leftToken = sourceCode.getTokenBefore(rightToken, 1);
@@ -224,7 +224,7 @@ module.exports = {
224
224
  context.report({
225
225
  messageId: "expectedLinebreakAfterOpeningBrace",
226
226
  node,
227
- loc: openBrace.loc.start,
227
+ loc: openBrace.loc,
228
228
  fix(fixer) {
229
229
  if (hasCommentsFirstToken) {
230
230
  return null;
@@ -238,7 +238,7 @@ module.exports = {
238
238
  context.report({
239
239
  messageId: "expectedLinebreakBeforeClosingBrace",
240
240
  node,
241
- loc: closeBrace.loc.start,
241
+ loc: closeBrace.loc,
242
242
  fix(fixer) {
243
243
  if (hasCommentsLastToken) {
244
244
  return null;
@@ -260,7 +260,7 @@ module.exports = {
260
260
  context.report({
261
261
  messageId: "unexpectedLinebreakAfterOpeningBrace",
262
262
  node,
263
- loc: openBrace.loc.start,
263
+ loc: openBrace.loc,
264
264
  fix(fixer) {
265
265
  if (hasCommentsFirstToken) {
266
266
  return null;
@@ -280,7 +280,7 @@ module.exports = {
280
280
  context.report({
281
281
  messageId: "unexpectedLinebreakBeforeClosingBrace",
282
282
  node,
283
- loc: closeBrace.loc.start,
283
+ loc: closeBrace.loc,
284
284
  fix(fixer) {
285
285
  if (hasCommentsLastToken) {
286
286
  return null;
@@ -40,45 +40,6 @@ function isNonCommutativeOperatorWithShorthand(operator) {
40
40
  // Rule Definition
41
41
  //------------------------------------------------------------------------------
42
42
 
43
- /**
44
- * Checks whether two expressions reference the same value. For example:
45
- * a = a
46
- * a.b = a.b
47
- * a[0] = a[0]
48
- * a['b'] = a['b']
49
- * @param {ASTNode} a Left side of the comparison.
50
- * @param {ASTNode} b Right side of the comparison.
51
- * @returns {boolean} True if both sides match and reference the same value.
52
- */
53
- function same(a, b) {
54
- if (a.type !== b.type) {
55
- return false;
56
- }
57
-
58
- switch (a.type) {
59
- case "Identifier":
60
- return a.name === b.name;
61
-
62
- case "Literal":
63
- return a.value === b.value;
64
-
65
- case "MemberExpression":
66
-
67
- /*
68
- * x[0] = x[0]
69
- * x[y] = x[y]
70
- * x.y = x.y
71
- */
72
- return same(a.object, b.object) && same(a.property, b.property);
73
-
74
- case "ThisExpression":
75
- return true;
76
-
77
- default:
78
- return false;
79
- }
80
- }
81
-
82
43
  /**
83
44
  * Determines if the left side of a node can be safely fixed (i.e. if it activates the same getters/setters and)
84
45
  * toString calls regardless of whether assignment shorthand is used)
@@ -148,12 +109,12 @@ module.exports = {
148
109
  const operator = expr.operator;
149
110
 
150
111
  if (isCommutativeOperatorWithShorthand(operator) || isNonCommutativeOperatorWithShorthand(operator)) {
151
- if (same(left, expr.left)) {
112
+ if (astUtils.isSameReference(left, expr.left, true)) {
152
113
  context.report({
153
114
  node,
154
115
  messageId: "replaced",
155
116
  fix(fixer) {
156
- if (canBeFixed(left)) {
117
+ if (canBeFixed(left) && canBeFixed(expr.left)) {
157
118
  const equalsToken = getOperatorToken(node);
158
119
  const operatorToken = getOperatorToken(expr);
159
120
  const leftText = sourceCode.getText().slice(node.range[0], equalsToken.range[0]);
@@ -169,7 +130,7 @@ module.exports = {
169
130
  return null;
170
131
  }
171
132
  });
172
- } else if (same(left, expr.right) && isCommutativeOperatorWithShorthand(operator)) {
133
+ } else if (astUtils.isSameReference(left, expr.right, true) && isCommutativeOperatorWithShorthand(operator)) {
173
134
 
174
135
  /*
175
136
  * This case can't be fixed safely.
@@ -85,10 +85,10 @@ function newNodeTypeTester(type) {
85
85
  */
86
86
  function isIIFEStatement(node) {
87
87
  if (node.type === "ExpressionStatement") {
88
- let call = node.expression;
88
+ let call = astUtils.skipChainExpression(node.expression);
89
89
 
90
90
  if (call.type === "UnaryExpression") {
91
- call = call.argument;
91
+ call = astUtils.skipChainExpression(call.argument);
92
92
  }
93
93
  return call.type === "CallExpression" && astUtils.isFunction(call.callee);
94
94
  }
@@ -5,6 +5,8 @@
5
5
 
6
6
  "use strict";
7
7
 
8
+ const astUtils = require("./utils/ast-utils");
9
+
8
10
  //------------------------------------------------------------------------------
9
11
  // Helpers
10
12
  //------------------------------------------------------------------------------
@@ -66,6 +68,7 @@ function getCallbackInfo(node) {
66
68
  const retv = { isCallback: false, isLexicalThis: false };
67
69
  let currentNode = node;
68
70
  let parent = node.parent;
71
+ let bound = false;
69
72
 
70
73
  while (currentNode) {
71
74
  switch (parent.type) {
@@ -73,23 +76,34 @@ function getCallbackInfo(node) {
73
76
  // Checks parents recursively.
74
77
 
75
78
  case "LogicalExpression":
79
+ case "ChainExpression":
76
80
  case "ConditionalExpression":
77
81
  break;
78
82
 
79
83
  // Checks whether the parent node is `.bind(this)` call.
80
84
  case "MemberExpression":
81
- if (parent.object === currentNode &&
85
+ if (
86
+ parent.object === currentNode &&
82
87
  !parent.property.computed &&
83
88
  parent.property.type === "Identifier" &&
84
- parent.property.name === "bind" &&
85
- parent.parent.type === "CallExpression" &&
86
- parent.parent.callee === parent
89
+ parent.property.name === "bind"
87
90
  ) {
88
- retv.isLexicalThis = (
89
- parent.parent.arguments.length === 1 &&
90
- parent.parent.arguments[0].type === "ThisExpression"
91
- );
92
- parent = parent.parent;
91
+ const maybeCallee = parent.parent.type === "ChainExpression"
92
+ ? parent.parent
93
+ : parent;
94
+
95
+ if (astUtils.isCallee(maybeCallee)) {
96
+ if (!bound) {
97
+ bound = true; // Use only the first `.bind()` to make `isLexicalThis` value.
98
+ retv.isLexicalThis = (
99
+ maybeCallee.parent.arguments.length === 1 &&
100
+ maybeCallee.parent.arguments[0].type === "ThisExpression"
101
+ );
102
+ }
103
+ parent = maybeCallee.parent;
104
+ } else {
105
+ return retv;
106
+ }
93
107
  } else {
94
108
  return retv;
95
109
  }
@@ -272,7 +286,7 @@ module.exports = {
272
286
  context.report({
273
287
  node,
274
288
  messageId: "preferArrowCallback",
275
- fix(fixer) {
289
+ *fix(fixer) {
276
290
  if ((!callbackInfo.isLexicalThis && scopeInfo.this) || hasDuplicateParams(node.params)) {
277
291
 
278
292
  /*
@@ -281,30 +295,81 @@ module.exports = {
281
295
  * If the callback function has duplicates in its list of parameters (possible in sloppy mode),
282
296
  * don't replace it with an arrow function, because this is a SyntaxError with arrow functions.
283
297
  */
284
- return null;
298
+ return; // eslint-disable-line eslint-plugin/fixer-return -- false positive
285
299
  }
286
300
 
287
- const paramsLeftParen = node.params.length ? sourceCode.getTokenBefore(node.params[0]) : sourceCode.getTokenBefore(node.body, 1);
288
- const paramsRightParen = sourceCode.getTokenBefore(node.body);
289
- const asyncKeyword = node.async ? "async " : "";
290
- const paramsFullText = sourceCode.text.slice(paramsLeftParen.range[0], paramsRightParen.range[1]);
291
- const arrowFunctionText = `${asyncKeyword}${paramsFullText} => ${sourceCode.getText(node.body)}`;
301
+ // Remove `.bind(this)` if exists.
302
+ if (callbackInfo.isLexicalThis) {
303
+ const memberNode = node.parent;
292
304
 
293
- /*
294
- * If the callback function has `.bind(this)`, replace it with an arrow function and remove the binding.
295
- * Otherwise, just replace the arrow function itself.
296
- */
297
- const replacedNode = callbackInfo.isLexicalThis ? node.parent.parent : node;
305
+ /*
306
+ * If `.bind(this)` exists but the parent is not `.bind(this)`, don't remove it automatically.
307
+ * E.g. `(foo || function(){}).bind(this)`
308
+ */
309
+ if (memberNode.type !== "MemberExpression") {
310
+ return; // eslint-disable-line eslint-plugin/fixer-return -- false positive
311
+ }
312
+
313
+ const callNode = memberNode.parent;
314
+ const firstTokenToRemove = sourceCode.getTokenAfter(memberNode.object, astUtils.isNotClosingParenToken);
315
+ const lastTokenToRemove = sourceCode.getLastToken(callNode);
316
+
317
+ /*
318
+ * If the member expression is parenthesized, don't remove the right paren.
319
+ * E.g. `(function(){}.bind)(this)`
320
+ * ^^^^^^^^^^^^
321
+ */
322
+ if (astUtils.isParenthesised(sourceCode, memberNode)) {
323
+ return; // eslint-disable-line eslint-plugin/fixer-return -- false positive
324
+ }
325
+
326
+ // If comments exist in the `.bind(this)`, don't remove those.
327
+ if (sourceCode.commentsExistBetween(firstTokenToRemove, lastTokenToRemove)) {
328
+ return; // eslint-disable-line eslint-plugin/fixer-return -- false positive
329
+ }
330
+
331
+ yield fixer.removeRange([firstTokenToRemove.range[0], lastTokenToRemove.range[1]]);
332
+ }
333
+
334
+ // Convert the function expression to an arrow function.
335
+ const functionToken = sourceCode.getFirstToken(node, node.async ? 1 : 0);
336
+ const leftParenToken = sourceCode.getTokenAfter(functionToken, astUtils.isOpeningParenToken);
337
+
338
+ if (sourceCode.commentsExistBetween(functionToken, leftParenToken)) {
339
+
340
+ // Remove only extra tokens to keep comments.
341
+ yield fixer.remove(functionToken);
342
+ if (node.id) {
343
+ yield fixer.remove(node.id);
344
+ }
345
+ } else {
346
+
347
+ // Remove extra tokens and spaces.
348
+ yield fixer.removeRange([functionToken.range[0], leftParenToken.range[0]]);
349
+ }
350
+ yield fixer.insertTextBefore(node.body, "=> ");
351
+
352
+ // Get the node that will become the new arrow function.
353
+ let replacedNode = callbackInfo.isLexicalThis ? node.parent.parent : node;
354
+
355
+ if (replacedNode.type === "ChainExpression") {
356
+ replacedNode = replacedNode.parent;
357
+ }
298
358
 
299
359
  /*
300
360
  * If the replaced node is part of a BinaryExpression, LogicalExpression, or MemberExpression, then
301
361
  * the arrow function needs to be parenthesized, because `foo || () => {}` is invalid syntax even
302
362
  * though `foo || function() {}` is valid.
303
363
  */
304
- const needsParens = replacedNode.parent.type !== "CallExpression" && replacedNode.parent.type !== "ConditionalExpression";
305
- const replacementText = needsParens ? `(${arrowFunctionText})` : arrowFunctionText;
306
-
307
- return fixer.replaceText(replacedNode, replacementText);
364
+ if (
365
+ replacedNode.parent.type !== "CallExpression" &&
366
+ replacedNode.parent.type !== "ConditionalExpression" &&
367
+ !astUtils.isParenthesised(sourceCode, replacedNode) &&
368
+ !astUtils.isParenthesised(sourceCode, node)
369
+ ) {
370
+ yield fixer.insertTextBefore(replacedNode, "(");
371
+ yield fixer.insertTextAfter(replacedNode, ")");
372
+ }
308
373
  }
309
374
  });
310
375
  }