eslint 8.23.0 → 8.24.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.
@@ -122,7 +122,8 @@ function statSafeSync(filePath) {
122
122
  try {
123
123
  return fs.statSync(filePath);
124
124
  } catch (error) {
125
- /* istanbul ignore next */
125
+
126
+ /* c8 ignore next */
126
127
  if (error.code !== "ENOENT") {
127
128
  throw error;
128
129
  }
@@ -141,7 +142,8 @@ function readdirSafeSync(directoryPath) {
141
142
  try {
142
143
  return fs.readdirSync(directoryPath, { withFileTypes: true });
143
144
  } catch (error) {
144
- /* istanbul ignore next */
145
+
146
+ /* c8 ignore next */
145
147
  if (error.code !== "ENOENT") {
146
148
  throw error;
147
149
  }
@@ -15,6 +15,7 @@ const fsp = fs.promises;
15
15
  const isGlob = require("is-glob");
16
16
  const globby = require("globby");
17
17
  const hash = require("../cli-engine/hash");
18
+ const minimatch = require("minimatch");
18
19
 
19
20
  //-----------------------------------------------------------------------------
20
21
  // Errors
@@ -126,14 +127,14 @@ async function findFiles({
126
127
  const filePaths = patterns.map(filePath => path.resolve(cwd, filePath));
127
128
  const stats = await Promise.all(
128
129
  filePaths.map(
129
- filePath => fsp.stat(filePath).catch(() => {})
130
+ filePath => fsp.stat(filePath).catch(() => { })
130
131
  )
131
132
  );
132
133
 
133
134
  stats.forEach((stat, index) => {
134
135
 
135
136
  const filePath = filePaths[index];
136
- const pattern = patterns[index];
137
+ const pattern = normalizeToPosix(patterns[index]);
137
138
 
138
139
  if (stat) {
139
140
 
@@ -157,6 +158,11 @@ async function findFiles({
157
158
  return false;
158
159
  }
159
160
 
161
+ // patterns starting with ** always apply
162
+ if (filePattern.startsWith("**")) {
163
+ return true;
164
+ }
165
+
160
166
  // patterns ending with * are not used for file search
161
167
  if (filePattern.endsWith("*")) {
162
168
  return false;
@@ -167,11 +173,27 @@ async function findFiles({
167
173
  return false;
168
174
  }
169
175
 
170
- // check if the pattern would be inside the cwd or not
176
+ // check if the pattern would be inside the config base path or not
171
177
  const fullFilePattern = path.join(cwd, filePattern);
172
- const relativeFilePattern = path.relative(configs.basePath, fullFilePattern);
178
+ const patternRelativeToConfigBasePath = path.relative(configs.basePath, fullFilePattern);
179
+
180
+ if (patternRelativeToConfigBasePath.startsWith("..")) {
181
+ return false;
182
+ }
183
+
184
+ // check if the pattern matches
185
+ if (minimatch(filePath, path.dirname(fullFilePattern), { partial: true })) {
186
+ return true;
187
+ }
188
+
189
+ // check if the pattern is inside the directory or not
190
+ const patternRelativeToFilePath = path.relative(filePath, fullFilePattern);
191
+
192
+ if (patternRelativeToFilePath.startsWith("..")) {
193
+ return false;
194
+ }
173
195
 
174
- return !relativeFilePattern.startsWith("..");
196
+ return true;
175
197
  })
176
198
  .map(filePattern => {
177
199
  if (filePattern.startsWith("**")) {
@@ -88,10 +88,10 @@ class CodePathSegment {
88
88
  }
89
89
  });
90
90
 
91
- /* istanbul ignore if */
91
+ /* c8 ignore start */
92
92
  if (debug.enabled) {
93
93
  this.internal.nodes = [];
94
- }
94
+ }/* c8 ignore stop */
95
95
  }
96
96
 
97
97
  /**
@@ -59,7 +59,7 @@ function getContinueContext(state, label) {
59
59
  context = context.upper;
60
60
  }
61
61
 
62
- /* istanbul ignore next: foolproof (syntax error) */
62
+ /* c8 ignore next */
63
63
  return null;
64
64
  }
65
65
 
@@ -79,7 +79,7 @@ function getBreakContext(state, label) {
79
79
  context = context.upper;
80
80
  }
81
81
 
82
- /* istanbul ignore next: foolproof (syntax error) */
82
+ /* c8 ignore next */
83
83
  return null;
84
84
  }
85
85
 
@@ -433,7 +433,7 @@ class CodePathState {
433
433
  */
434
434
  return context;
435
435
 
436
- /* istanbul ignore next */
436
+ /* c8 ignore next */
437
437
  default:
438
438
  throw new Error("unreachable");
439
439
  }
@@ -1030,7 +1030,7 @@ class CodePathState {
1030
1030
  };
1031
1031
  break;
1032
1032
 
1033
- /* istanbul ignore next */
1033
+ /* c8 ignore next */
1034
1034
  default:
1035
1035
  throw new Error(`unknown type: "${type}"`);
1036
1036
  }
@@ -1095,7 +1095,7 @@ class CodePathState {
1095
1095
  );
1096
1096
  break;
1097
1097
 
1098
- /* istanbul ignore next */
1098
+ /* c8 ignore next */
1099
1099
  default:
1100
1100
  throw new Error("unreachable");
1101
1101
  }
@@ -1392,11 +1392,12 @@ class CodePathState {
1392
1392
 
1393
1393
  const context = getBreakContext(this, label);
1394
1394
 
1395
- /* istanbul ignore else: foolproof (syntax error) */
1395
+
1396
1396
  if (context) {
1397
1397
  context.brokenForkContext.add(forkContext.head);
1398
1398
  }
1399
1399
 
1400
+ /* c8 ignore next */
1400
1401
  forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
1401
1402
  }
1402
1403
 
@@ -1417,7 +1418,6 @@ class CodePathState {
1417
1418
 
1418
1419
  const context = getContinueContext(this, label);
1419
1420
 
1420
- /* istanbul ignore else: foolproof (syntax error) */
1421
1421
  if (context) {
1422
1422
  if (context.continueDestSegments) {
1423
1423
  makeLooped(this, forkContext.head, context.continueDestSegments);
@@ -20,7 +20,7 @@ const debug = require("debug")("eslint:code-path");
20
20
  * @param {CodePathSegment} segment A segment to get.
21
21
  * @returns {string} Id of the segment.
22
22
  */
23
- /* istanbul ignore next */
23
+ /* c8 ignore next */
24
24
  function getId(segment) { // eslint-disable-line jsdoc/require-jsdoc -- Ignoring
25
25
  return segment.id + (segment.reachable ? "" : "!");
26
26
  }
@@ -67,7 +67,7 @@ module.exports = {
67
67
  * @param {boolean} leaving A flag whether or not it's leaving
68
68
  * @returns {void}
69
69
  */
70
- dumpState: !debug.enabled ? debug : /* istanbul ignore next */ function(node, state, leaving) {
70
+ dumpState: !debug.enabled ? debug : /* c8 ignore next */ function(node, state, leaving) {
71
71
  for (let i = 0; i < state.currentSegments.length; ++i) {
72
72
  const segInternal = state.currentSegments[i].internal;
73
73
 
@@ -98,7 +98,7 @@ module.exports = {
98
98
  * @see http://www.graphviz.org
99
99
  * @see http://www.webgraphviz.com
100
100
  */
101
- dumpDot: !debug.enabled ? debug : /* istanbul ignore next */ function(codePath) {
101
+ dumpDot: !debug.enabled ? debug : /* c8 ignore next */ function(codePath) {
102
102
  let text =
103
103
  "\n" +
104
104
  "digraph {\n" +
@@ -33,10 +33,10 @@ class IdGenerator {
33
33
  next() {
34
34
  this.n = 1 + this.n | 0;
35
35
 
36
- /* istanbul ignore if */
36
+ /* c8 ignore start */
37
37
  if (this.n < 0) {
38
38
  this.n = 1;
39
- }
39
+ }/* c8 ignore stop */
40
40
 
41
41
  return this.prefix + this.n;
42
42
  }
@@ -1601,12 +1601,18 @@ class Linter {
1601
1601
  languageOptions.ecmaVersion
1602
1602
  );
1603
1603
 
1604
- // add configured globals and language globals
1605
- const configuredGlobals = {
1606
- ...(getGlobalsForEcmaVersion(languageOptions.ecmaVersion)),
1607
- ...(languageOptions.sourceType === "commonjs" ? globals.commonjs : void 0),
1608
- ...languageOptions.globals
1609
- };
1604
+ /*
1605
+ * add configured globals and language globals
1606
+ *
1607
+ * using Object.assign instead of object spread for performance reasons
1608
+ * https://github.com/eslint/eslint/issues/16302
1609
+ */
1610
+ const configuredGlobals = Object.assign(
1611
+ {},
1612
+ getGlobalsForEcmaVersion(languageOptions.ecmaVersion),
1613
+ languageOptions.sourceType === "commonjs" ? globals.commonjs : void 0,
1614
+ languageOptions.globals
1615
+ );
1610
1616
 
1611
1617
  // double check that there is a parser to avoid mysterious error messages
1612
1618
  if (!languageOptions.parser) {
@@ -9,7 +9,7 @@
9
9
  // Helpers
10
10
  //------------------------------------------------------------------------------
11
11
 
12
- /* istanbul ignore next */
12
+ /* c8 ignore next */
13
13
  /**
14
14
  * Align the string to left
15
15
  * @param {string} str string to evaluate
@@ -22,7 +22,7 @@ function alignLeft(str, len, ch) {
22
22
  return str + new Array(len - str.length + 1).join(ch || " ");
23
23
  }
24
24
 
25
- /* istanbul ignore next */
25
+ /* c8 ignore next */
26
26
  /**
27
27
  * Align the string to right
28
28
  * @param {string} str string to evaluate
@@ -64,7 +64,7 @@ function getListSize() {
64
64
  return TIMING_ENV_VAR_AS_INTEGER > 10 ? TIMING_ENV_VAR_AS_INTEGER : MINIMUM_SIZE;
65
65
  }
66
66
 
67
- /* istanbul ignore next */
67
+ /* c8 ignore next */
68
68
  /**
69
69
  * display the data
70
70
  * @param {Object} data Data object to be displayed
@@ -119,7 +119,7 @@ function display(data) {
119
119
  console.log(table.join("\n")); // eslint-disable-line no-console -- Debugging function
120
120
  }
121
121
 
122
- /* istanbul ignore next */
122
+ /* c8 ignore next */
123
123
  module.exports = (function() {
124
124
 
125
125
  const data = Object.create(null);
@@ -16,7 +16,7 @@ const astUtils = require("./utils/ast-utils");
16
16
  //------------------------------------------------------------------------------
17
17
 
18
18
  const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/u;
19
- const TARGET_METHODS = /^(?:every|filter|find(?:Index)?|flatMap|forEach|map|reduce(?:Right)?|some|sort)$/u;
19
+ const TARGET_METHODS = /^(?:every|filter|find(?:Last)?(?:Index)?|flatMap|forEach|map|reduce(?:Right)?|some|sort)$/u;
20
20
 
21
21
  /**
22
22
  * Checks a given code path segment is reachable.
@@ -125,7 +125,7 @@ function getArrayMethodName(node) {
125
125
  }
126
126
  }
127
127
 
128
- /* istanbul ignore next: unreachable */
128
+ /* c8 ignore next */
129
129
  return null;
130
130
  }
131
131
 
@@ -28,10 +28,11 @@ function findReference(scope, node) {
28
28
  const references = scope.references.filter(reference => reference.identifier.range[0] === node.range[0] &&
29
29
  reference.identifier.range[1] === node.range[1]);
30
30
 
31
- /* istanbul ignore else: correctly returns null */
32
31
  if (references.length === 1) {
33
32
  return references[0];
34
33
  }
34
+
35
+ /* c8 ignore next */
35
36
  return null;
36
37
 
37
38
  }
@@ -18,8 +18,8 @@ const astUtils = require("./utils/ast-utils");
18
18
  //------------------------------------------------------------------------------
19
19
  // Rule Definition
20
20
  //------------------------------------------------------------------------------
21
-
22
- /* istanbul ignore next: this rule has known coverage issues, but it's deprecated and shouldn't be updated in the future anyway. */
21
+ // this rule has known coverage issues, but it's deprecated and shouldn't be updated in the future anyway.
22
+ /* c8 ignore next */
23
23
  /** @type {import('../shared/types').Rule} */
24
24
  module.exports = {
25
25
  meta: {
@@ -212,10 +212,10 @@ module.exports = {
212
212
  if (context.options[0] === "tab") {
213
213
  indentSize = 1;
214
214
  indentType = "tab";
215
- } else /* istanbul ignore else : this will be caught by options validation */ if (typeof context.options[0] === "number") {
215
+ } else /* c8 ignore start */ if (typeof context.options[0] === "number") {
216
216
  indentSize = context.options[0];
217
217
  indentType = "space";
218
- }
218
+ }/* c8 ignore stop */
219
219
 
220
220
  if (context.options[1]) {
221
221
  const opts = context.options[1];
@@ -12,7 +12,7 @@
12
12
  // Requirements
13
13
  //------------------------------------------------------------------------------
14
14
 
15
- const createTree = require("functional-red-black-tree");
15
+ const { OrderedMap } = require("js-sdsl");
16
16
 
17
17
  const astUtils = require("./utils/ast-utils");
18
18
 
@@ -135,7 +135,8 @@ class BinarySearchTree {
135
135
  * Creates an empty tree
136
136
  */
137
137
  constructor() {
138
- this._rbTree = createTree();
138
+ this._orderedMap = new OrderedMap();
139
+ this._orderedMapEnd = this._orderedMap.end();
139
140
  }
140
141
 
141
142
  /**
@@ -145,13 +146,7 @@ class BinarySearchTree {
145
146
  * @returns {void}
146
147
  */
147
148
  insert(key, value) {
148
- const iterator = this._rbTree.find(key);
149
-
150
- if (iterator.valid) {
151
- this._rbTree = iterator.update(value);
152
- } else {
153
- this._rbTree = this._rbTree.insert(key, value);
154
- }
149
+ this._orderedMap.setElement(key, value);
155
150
  }
156
151
 
157
152
  /**
@@ -160,9 +155,13 @@ class BinarySearchTree {
160
155
  * @returns {{key: number, value: *}|null} The found entry, or null if no such entry exists.
161
156
  */
162
157
  findLe(key) {
163
- const iterator = this._rbTree.le(key);
158
+ const iterator = this._orderedMap.reverseLowerBound(key);
164
159
 
165
- return iterator && { key: iterator.key, value: iterator.value };
160
+ if (iterator.equals(this._orderedMapEnd)) {
161
+ return {};
162
+ }
163
+
164
+ return { key: iterator.pointer[0], value: iterator.pointer[1] };
166
165
  }
167
166
 
168
167
  /**
@@ -177,11 +176,20 @@ class BinarySearchTree {
177
176
  if (start === end) {
178
177
  return;
179
178
  }
180
- const iterator = this._rbTree.ge(start);
179
+ const iterator = this._orderedMap.lowerBound(start);
181
180
 
182
- while (iterator.valid && iterator.key < end) {
183
- this._rbTree = this._rbTree.remove(iterator.key);
184
- iterator.next();
181
+ if (iterator.equals(this._orderedMapEnd)) {
182
+ return;
183
+ }
184
+
185
+ if (end > this._orderedMap.back()[0]) {
186
+ while (!iterator.equals(this._orderedMapEnd)) {
187
+ this._orderedMap.eraseElementByIterator(iterator);
188
+ }
189
+ } else {
190
+ while (iterator.pointer[0] < end) {
191
+ this._orderedMap.eraseElementByIterator(iterator);
192
+ }
185
193
  }
186
194
  }
187
195
  }
@@ -72,6 +72,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({
72
72
  "lines-around-comment": () => require("./lines-around-comment"),
73
73
  "lines-around-directive": () => require("./lines-around-directive"),
74
74
  "lines-between-class-members": () => require("./lines-between-class-members"),
75
+ "logical-assignment-operators": () => require("./logical-assignment-operators"),
75
76
  "max-classes-per-file": () => require("./max-classes-per-file"),
76
77
  "max-depth": () => require("./max-depth"),
77
78
  "max-len": () => require("./max-len"),
@@ -0,0 +1,474 @@
1
+ /**
2
+ * @fileoverview Rule to replace assignment expressions with logical operator assignment
3
+ * @author Daniel Martens
4
+ */
5
+ "use strict";
6
+
7
+ //------------------------------------------------------------------------------
8
+ // Requirements
9
+ //------------------------------------------------------------------------------
10
+ const astUtils = require("./utils/ast-utils.js");
11
+
12
+ //------------------------------------------------------------------------------
13
+ // Helpers
14
+ //------------------------------------------------------------------------------
15
+
16
+ const baseTypes = new Set(["Identifier", "Super", "ThisExpression"]);
17
+
18
+ /**
19
+ * Returns true iff either "undefined" or a void expression (eg. "void 0")
20
+ * @param {ASTNode} expression Expression to check
21
+ * @param {import('eslint-scope').Scope} scope Scope of the expression
22
+ * @returns {boolean} True iff "undefined" or "void ..."
23
+ */
24
+ function isUndefined(expression, scope) {
25
+ if (expression.type === "Identifier" && expression.name === "undefined") {
26
+ return astUtils.isReferenceToGlobalVariable(scope, expression);
27
+ }
28
+
29
+ return expression.type === "UnaryExpression" &&
30
+ expression.operator === "void" &&
31
+ expression.argument.type === "Literal" &&
32
+ expression.argument.value === 0;
33
+ }
34
+
35
+ /**
36
+ * Returns true iff the reference is either an identifier or member expression
37
+ * @param {ASTNode} expression Expression to check
38
+ * @returns {boolean} True for identifiers and member expressions
39
+ */
40
+ function isReference(expression) {
41
+ return (expression.type === "Identifier" && expression.name !== "undefined") ||
42
+ expression.type === "MemberExpression";
43
+ }
44
+
45
+ /**
46
+ * Returns true iff the expression checks for nullish with loose equals.
47
+ * Examples: value == null, value == void 0
48
+ * @param {ASTNode} expression Test condition
49
+ * @param {import('eslint-scope').Scope} scope Scope of the expression
50
+ * @returns {boolean} True iff implicit nullish comparison
51
+ */
52
+ function isImplicitNullishComparison(expression, scope) {
53
+ if (expression.type !== "BinaryExpression" || expression.operator !== "==") {
54
+ return false;
55
+ }
56
+
57
+ const reference = isReference(expression.left) ? "left" : "right";
58
+ const nullish = reference === "left" ? "right" : "left";
59
+
60
+ return isReference(expression[reference]) &&
61
+ (astUtils.isNullLiteral(expression[nullish]) || isUndefined(expression[nullish], scope));
62
+ }
63
+
64
+ /**
65
+ * Condition with two equal comparisons.
66
+ * @param {ASTNode} expression Condition
67
+ * @returns {boolean} True iff matches ? === ? || ? === ?
68
+ */
69
+ function isDoubleComparison(expression) {
70
+ return expression.type === "LogicalExpression" &&
71
+ expression.operator === "||" &&
72
+ expression.left.type === "BinaryExpression" &&
73
+ expression.left.operator === "===" &&
74
+ expression.right.type === "BinaryExpression" &&
75
+ expression.right.operator === "===";
76
+ }
77
+
78
+ /**
79
+ * Returns true iff the expression checks for undefined and null.
80
+ * Example: value === null || value === undefined
81
+ * @param {ASTNode} expression Test condition
82
+ * @param {import('eslint-scope').Scope} scope Scope of the expression
83
+ * @returns {boolean} True iff explicit nullish comparison
84
+ */
85
+ function isExplicitNullishComparison(expression, scope) {
86
+ if (!isDoubleComparison(expression)) {
87
+ return false;
88
+ }
89
+ const leftReference = isReference(expression.left.left) ? "left" : "right";
90
+ const leftNullish = leftReference === "left" ? "right" : "left";
91
+ const rightReference = isReference(expression.right.left) ? "left" : "right";
92
+ const rightNullish = rightReference === "left" ? "right" : "left";
93
+
94
+ return astUtils.isSameReference(expression.left[leftReference], expression.right[rightReference]) &&
95
+ ((astUtils.isNullLiteral(expression.left[leftNullish]) && isUndefined(expression.right[rightNullish], scope)) ||
96
+ (isUndefined(expression.left[leftNullish], scope) && astUtils.isNullLiteral(expression.right[rightNullish])));
97
+ }
98
+
99
+ /**
100
+ * Returns true for Boolean(arg) calls
101
+ * @param {ASTNode} expression Test condition
102
+ * @param {import('eslint-scope').Scope} scope Scope of the expression
103
+ * @returns {boolean} Whether the expression is a boolean cast
104
+ */
105
+ function isBooleanCast(expression, scope) {
106
+ return expression.type === "CallExpression" &&
107
+ expression.callee.name === "Boolean" &&
108
+ expression.arguments.length === 1 &&
109
+ astUtils.isReferenceToGlobalVariable(scope, expression.callee);
110
+ }
111
+
112
+ /**
113
+ * Returns true for:
114
+ * truthiness checks: value, Boolean(value), !!value
115
+ * falsyness checks: !value, !Boolean(value)
116
+ * nullish checks: value == null, value === undefined || value === null
117
+ * @param {ASTNode} expression Test condition
118
+ * @param {import('eslint-scope').Scope} scope Scope of the expression
119
+ * @returns {?{ reference: ASTNode, operator: '??'|'||'|'&&'}} Null if not a known existence
120
+ */
121
+ function getExistence(expression, scope) {
122
+ const isNegated = expression.type === "UnaryExpression" && expression.operator === "!";
123
+ const base = isNegated ? expression.argument : expression;
124
+
125
+ switch (true) {
126
+ case isReference(base):
127
+ return { reference: base, operator: isNegated ? "||" : "&&" };
128
+ case base.type === "UnaryExpression" && base.operator === "!" && isReference(base.argument):
129
+ return { reference: base.argument, operator: "&&" };
130
+ case isBooleanCast(base, scope) && isReference(base.arguments[0]):
131
+ return { reference: base.arguments[0], operator: isNegated ? "||" : "&&" };
132
+ case isImplicitNullishComparison(expression, scope):
133
+ return { reference: isReference(expression.left) ? expression.left : expression.right, operator: "??" };
134
+ case isExplicitNullishComparison(expression, scope):
135
+ return { reference: isReference(expression.left.left) ? expression.left.left : expression.left.right, operator: "??" };
136
+ default: return null;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Returns true iff the node is inside a with block
142
+ * @param {ASTNode} node Node to check
143
+ * @returns {boolean} True iff passed node is inside a with block
144
+ */
145
+ function isInsideWithBlock(node) {
146
+ if (node.type === "Program") {
147
+ return false;
148
+ }
149
+
150
+ return node.parent.type === "WithStatement" && node.parent.body === node ? true : isInsideWithBlock(node.parent);
151
+ }
152
+
153
+ //------------------------------------------------------------------------------
154
+ // Rule Definition
155
+ //------------------------------------------------------------------------------
156
+ /** @type {import('../shared/types').Rule} */
157
+ module.exports = {
158
+ meta: {
159
+ type: "suggestion",
160
+
161
+ docs: {
162
+ description: "Require or disallow logical assignment logical operator shorthand",
163
+ recommended: false,
164
+ url: "https://eslint.org/docs/rules/logical-assignment-operators"
165
+ },
166
+
167
+ schema: {
168
+ type: "array",
169
+ oneOf: [{
170
+ items: [
171
+ { const: "always" },
172
+ {
173
+ type: "object",
174
+ properties: {
175
+ enforceForIfStatements: {
176
+ type: "boolean"
177
+ }
178
+ },
179
+ additionalProperties: false
180
+ }
181
+ ],
182
+ minItems: 0, // 0 for allowing passing no options
183
+ maxItems: 2
184
+ }, {
185
+ items: [{ const: "never" }],
186
+ minItems: 1,
187
+ maxItems: 1
188
+ }]
189
+ },
190
+ fixable: "code",
191
+ // eslint-disable-next-line eslint-plugin/require-meta-has-suggestions -- Does not detect conditional suggestions
192
+ hasSuggestions: true,
193
+ messages: {
194
+ assignment: "Assignment (=) can be replaced with operator assignment ({{operator}}).",
195
+ useLogicalOperator: "Convert this assignment to use the operator {{ operator }}.",
196
+ logical: "Logical expression can be replaced with an assignment ({{ operator }}).",
197
+ convertLogical: "Replace this logical expression with an assignment with the operator {{ operator }}.",
198
+ if: "'if' statement can be replaced with a logical operator assignment with operator {{ operator }}.",
199
+ convertIf: "Replace this 'if' statement with a logical assignment with operator {{ operator }}.",
200
+ unexpected: "Unexpected logical operator assignment ({{operator}}) shorthand.",
201
+ separate: "Separate the logical assignment into an assignment with a logical operator."
202
+ }
203
+ },
204
+
205
+ create(context) {
206
+ const mode = context.options[0] === "never" ? "never" : "always";
207
+ const checkIf = mode === "always" && context.options.length > 1 && context.options[1].enforceForIfStatements;
208
+ const sourceCode = context.getSourceCode();
209
+ const isStrict = context.getScope().isStrict;
210
+
211
+ /**
212
+ * Returns false if the access could be a getter
213
+ * @param {ASTNode} node Assignment expression
214
+ * @returns {boolean} True iff the fix is safe
215
+ */
216
+ function cannotBeGetter(node) {
217
+ return node.type === "Identifier" &&
218
+ (isStrict || !isInsideWithBlock(node));
219
+ }
220
+
221
+ /**
222
+ * Check whether only a single property is accessed
223
+ * @param {ASTNode} node reference
224
+ * @returns {boolean} True iff a single property is accessed
225
+ */
226
+ function accessesSingleProperty(node) {
227
+ if (!isStrict && isInsideWithBlock(node)) {
228
+ return node.type === "Identifier";
229
+ }
230
+
231
+ return node.type === "MemberExpression" &&
232
+ baseTypes.has(node.object.type) &&
233
+ (!node.computed || (node.property.type !== "MemberExpression" && node.property.type !== "ChainExpression"));
234
+ }
235
+
236
+ /**
237
+ * Adds a fixer or suggestion whether on the fix is safe.
238
+ * @param {{ messageId: string, node: ASTNode }} descriptor Report descriptor without fix or suggest
239
+ * @param {{ messageId: string, fix: Function }} suggestion Adds the fix or the whole suggestion as only element in "suggest" to suggestion
240
+ * @param {boolean} shouldBeFixed Fix iff the condition is true
241
+ * @returns {Object} Descriptor with either an added fix or suggestion
242
+ */
243
+ function createConditionalFixer(descriptor, suggestion, shouldBeFixed) {
244
+ if (shouldBeFixed) {
245
+ return {
246
+ ...descriptor,
247
+ fix: suggestion.fix
248
+ };
249
+ }
250
+
251
+ return {
252
+ ...descriptor,
253
+ suggest: [suggestion]
254
+ };
255
+ }
256
+
257
+
258
+ /**
259
+ * Returns the operator token for assignments and binary expressions
260
+ * @param {ASTNode} node AssignmentExpression or BinaryExpression
261
+ * @returns {import('eslint').AST.Token} Operator token between the left and right expression
262
+ */
263
+ function getOperatorToken(node) {
264
+ return sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
265
+ }
266
+
267
+ if (mode === "never") {
268
+ return {
269
+
270
+ // foo ||= bar
271
+ "AssignmentExpression"(assignment) {
272
+ if (!astUtils.isLogicalAssignmentOperator(assignment.operator)) {
273
+ return;
274
+ }
275
+
276
+ const descriptor = {
277
+ messageId: "unexpected",
278
+ node: assignment,
279
+ data: { operator: assignment.operator }
280
+ };
281
+ const suggestion = {
282
+ messageId: "separate",
283
+ *fix(ruleFixer) {
284
+ if (sourceCode.getCommentsInside(assignment).length > 0) {
285
+ return;
286
+ }
287
+
288
+ const operatorToken = getOperatorToken(assignment);
289
+
290
+ // -> foo = bar
291
+ yield ruleFixer.replaceText(operatorToken, "=");
292
+
293
+ const assignmentText = sourceCode.getText(assignment.left);
294
+ const operator = assignment.operator.slice(0, -1);
295
+
296
+ // -> foo = foo || bar
297
+ yield ruleFixer.insertTextAfter(operatorToken, ` ${assignmentText} ${operator}`);
298
+
299
+ const precedence = astUtils.getPrecedence(assignment.right) <= astUtils.getPrecedence({ type: "LogicalExpression", operator });
300
+
301
+ // ?? and || / && cannot be mixed but have same precedence
302
+ const mixed = assignment.operator === "??=" && astUtils.isLogicalExpression(assignment.right);
303
+
304
+ if (!astUtils.isParenthesised(sourceCode, assignment.right) && (precedence || mixed)) {
305
+
306
+ // -> foo = foo || (bar)
307
+ yield ruleFixer.insertTextBefore(assignment.right, "(");
308
+ yield ruleFixer.insertTextAfter(assignment.right, ")");
309
+ }
310
+ }
311
+ };
312
+
313
+ context.report(createConditionalFixer(descriptor, suggestion, cannotBeGetter(assignment.left)));
314
+ }
315
+ };
316
+ }
317
+
318
+ return {
319
+
320
+ // foo = foo || bar
321
+ "AssignmentExpression[operator='='][right.type='LogicalExpression']"(assignment) {
322
+ if (!astUtils.isSameReference(assignment.left, assignment.right.left)) {
323
+ return;
324
+ }
325
+
326
+ const descriptor = {
327
+ messageId: "assignment",
328
+ node: assignment,
329
+ data: { operator: `${assignment.right.operator}=` }
330
+ };
331
+ const suggestion = {
332
+ messageId: "useLogicalOperator",
333
+ data: { operator: `${assignment.right.operator}=` },
334
+ *fix(ruleFixer) {
335
+ if (sourceCode.getCommentsInside(assignment).length > 0) {
336
+ return;
337
+ }
338
+
339
+ // No need for parenthesis around the assignment based on precedence as the precedence stays the same even with changed operator
340
+ const assignmentOperatorToken = getOperatorToken(assignment);
341
+
342
+ // -> foo ||= foo || bar
343
+ yield ruleFixer.insertTextBefore(assignmentOperatorToken, assignment.right.operator);
344
+
345
+ // -> foo ||= bar
346
+ const logicalOperatorToken = getOperatorToken(assignment.right);
347
+ const firstRightOperandToken = sourceCode.getTokenAfter(logicalOperatorToken);
348
+
349
+ yield ruleFixer.removeRange([assignment.right.range[0], firstRightOperandToken.range[0]]);
350
+ }
351
+ };
352
+
353
+ context.report(createConditionalFixer(descriptor, suggestion, cannotBeGetter(assignment.left)));
354
+ },
355
+
356
+ // foo || (foo = bar)
357
+ 'LogicalExpression[right.type="AssignmentExpression"][right.operator="="]'(logical) {
358
+
359
+ // Right side has to be parenthesized, otherwise would be parsed as (foo || foo) = bar which is illegal
360
+ if (isReference(logical.left) && astUtils.isSameReference(logical.left, logical.right.left)) {
361
+ const descriptor = {
362
+ messageId: "logical",
363
+ node: logical,
364
+ data: { operator: `${logical.operator}=` }
365
+ };
366
+ const suggestion = {
367
+ messageId: "convertLogical",
368
+ data: { operator: `${logical.operator}=` },
369
+ *fix(ruleFixer) {
370
+ if (sourceCode.getCommentsInside(logical).length > 0) {
371
+ return;
372
+ }
373
+
374
+ const requiresOuterParenthesis = logical.parent.type !== "ExpressionStatement" &&
375
+ (astUtils.getPrecedence({ type: "AssignmentExpression" }) < astUtils.getPrecedence(logical.parent));
376
+
377
+ if (!astUtils.isParenthesised(sourceCode, logical) && requiresOuterParenthesis) {
378
+ yield ruleFixer.insertTextBefore(logical, "(");
379
+ yield ruleFixer.insertTextAfter(logical, ")");
380
+ }
381
+
382
+ // Also removes all opening parenthesis
383
+ yield ruleFixer.removeRange([logical.range[0], logical.right.range[0]]); // -> foo = bar)
384
+
385
+ // Also removes all ending parenthesis
386
+ yield ruleFixer.removeRange([logical.right.range[1], logical.range[1]]); // -> foo = bar
387
+
388
+ const operatorToken = getOperatorToken(logical.right);
389
+
390
+ yield ruleFixer.insertTextBefore(operatorToken, logical.operator); // -> foo ||= bar
391
+ }
392
+ };
393
+ const fix = cannotBeGetter(logical.left) || accessesSingleProperty(logical.left);
394
+
395
+ context.report(createConditionalFixer(descriptor, suggestion, fix));
396
+ }
397
+ },
398
+
399
+ // if (foo) foo = bar
400
+ "IfStatement[alternate=null]"(ifNode) {
401
+ if (!checkIf) {
402
+ return;
403
+ }
404
+
405
+ const hasBody = ifNode.consequent.type === "BlockStatement";
406
+
407
+ if (hasBody && ifNode.consequent.body.length !== 1) {
408
+ return;
409
+ }
410
+
411
+ const body = hasBody ? ifNode.consequent.body[0] : ifNode.consequent;
412
+ const scope = context.getScope();
413
+ const existence = getExistence(ifNode.test, scope);
414
+
415
+ if (
416
+ body.type === "ExpressionStatement" &&
417
+ body.expression.type === "AssignmentExpression" &&
418
+ body.expression.operator === "=" &&
419
+ existence !== null &&
420
+ astUtils.isSameReference(existence.reference, body.expression.left)
421
+ ) {
422
+ const descriptor = {
423
+ messageId: "if",
424
+ node: ifNode,
425
+ data: { operator: `${existence.operator}=` }
426
+ };
427
+ const suggestion = {
428
+ messageId: "convertIf",
429
+ data: { operator: `${existence.operator}=` },
430
+ *fix(ruleFixer) {
431
+ if (sourceCode.getCommentsInside(ifNode).length > 0) {
432
+ return;
433
+ }
434
+
435
+ const firstBodyToken = sourceCode.getFirstToken(body);
436
+ const prevToken = sourceCode.getTokenBefore(ifNode);
437
+
438
+ if (
439
+ prevToken !== null &&
440
+ prevToken.value !== ";" &&
441
+ prevToken.value !== "{" &&
442
+ firstBodyToken.type !== "Identifier" &&
443
+ firstBodyToken.type !== "Keyword"
444
+ ) {
445
+
446
+ // Do not fix if the fixed statement could be part of the previous statement (eg. fn() if (a == null) (a) = b --> fn()(a) ??= b)
447
+ return;
448
+ }
449
+
450
+
451
+ const operatorToken = getOperatorToken(body.expression);
452
+
453
+ yield ruleFixer.insertTextBefore(operatorToken, existence.operator); // -> if (foo) foo ||= bar
454
+
455
+ yield ruleFixer.removeRange([ifNode.range[0], body.range[0]]); // -> foo ||= bar
456
+
457
+ yield ruleFixer.removeRange([body.range[1], ifNode.range[1]]); // -> foo ||= bar, only present if "if" had a body
458
+
459
+ const nextToken = sourceCode.getTokenAfter(body.expression);
460
+
461
+ if (hasBody && (nextToken !== null && nextToken.value !== ";")) {
462
+ yield ruleFixer.insertTextAfter(ifNode, ";");
463
+ }
464
+ }
465
+ };
466
+ const shouldBeFixed = cannotBeGetter(existence.reference) ||
467
+ (ifNode.test.type !== "LogicalExpression" && accessesSingleProperty(existence.reference));
468
+
469
+ context.report(createConditionalFixer(descriptor, suggestion, shouldBeFixed));
470
+ }
471
+ }
472
+ };
473
+ }
474
+ };
@@ -39,10 +39,10 @@ const CAPS_ALLOWED = [
39
39
  */
40
40
  function checkArray(obj, key, fallback) {
41
41
 
42
- /* istanbul ignore if */
42
+ /* c8 ignore start */
43
43
  if (Object.prototype.hasOwnProperty.call(obj, key) && !Array.isArray(obj[key])) {
44
44
  throw new TypeError(`${key}, if provided, must be an Array`);
45
- }
45
+ }/* c8 ignore stop */
46
46
  return obj[key] || fallback;
47
47
  }
48
48
 
@@ -188,7 +188,7 @@ module.exports = {
188
188
  }
189
189
  return precedence(node) <= precedence(parent);
190
190
 
191
- /* istanbul ignore next */
191
+ /* c8 ignore next */
192
192
  default:
193
193
  throw new Error(`Unexpected parent type: ${parent.type}`);
194
194
  }
@@ -634,10 +634,10 @@ module.exports = {
634
634
 
635
635
  currentNode = currentNode.parent;
636
636
 
637
- /* istanbul ignore if */
637
+ /* c8 ignore start */
638
638
  if (currentNode === null) {
639
639
  throw new Error("Nodes are not in the ancestor-descendant relationship.");
640
- }
640
+ }/* c8 ignore stop */
641
641
 
642
642
  path.push(currentNode);
643
643
  }
@@ -98,7 +98,7 @@ module.exports = {
98
98
  info = info.upper;
99
99
  }
100
100
 
101
- /* istanbul ignore next: syntax error */
101
+ /* c8 ignore next */
102
102
  return "other";
103
103
  }
104
104
 
@@ -74,7 +74,7 @@ function hasUselessComputedKey(node) {
74
74
 
75
75
  return value !== "constructor";
76
76
 
77
- /* istanbul ignore next */
77
+ /* c8 ignore next */
78
78
  default:
79
79
  throw new Error(`Unexpected node type: ${node.type}`);
80
80
  }
@@ -90,7 +90,7 @@ function getScopeNode(node) {
90
90
  }
91
91
  }
92
92
 
93
- /* istanbul ignore next : unreachable */
93
+ /* c8 ignore next */
94
94
  return null;
95
95
  }
96
96
 
@@ -186,7 +186,7 @@ module.exports = {
186
186
  case "ClassBody":
187
187
  return options.classes;
188
188
 
189
- /* istanbul ignore next */
189
+ /* c8 ignore next */
190
190
  default:
191
191
  throw new Error("unreachable");
192
192
  }
@@ -53,7 +53,7 @@ function getVariableOfArguments(scope) {
53
53
  }
54
54
  }
55
55
 
56
- /* istanbul ignore next */
56
+ /* c8 ignore next */
57
57
  return null;
58
58
  }
59
59
 
@@ -126,7 +126,7 @@ function getCallbackInfo(node) {
126
126
  parent = parent.parent;
127
127
  }
128
128
 
129
- /* istanbul ignore next */
129
+ /* c8 ignore next */
130
130
  throw new Error("unreachable");
131
131
  }
132
132
 
@@ -446,7 +446,19 @@ module.exports = {
446
446
 
447
447
  reportCount += nodesToReport.length;
448
448
 
449
- shouldFix = shouldFix && (reportCount === varDeclParent.declarations.length);
449
+ let totalDeclarationsCount = 0;
450
+
451
+ varDeclParent.declarations.forEach(declaration => {
452
+ if (declaration.id.type === "ObjectPattern") {
453
+ totalDeclarationsCount += declaration.id.properties.length;
454
+ } else if (declaration.id.type === "ArrayPattern") {
455
+ totalDeclarationsCount += declaration.id.elements.length;
456
+ } else {
457
+ totalDeclarationsCount += 1;
458
+ }
459
+ });
460
+
461
+ shouldFix = shouldFix && (reportCount === totalDeclarationsCount);
450
462
  }
451
463
  }
452
464
 
@@ -30,7 +30,7 @@ function getVariableOfArguments(scope) {
30
30
  }
31
31
  }
32
32
 
33
- /* istanbul ignore next : unreachable */
33
+ /* c8 ignore next */
34
34
  return null;
35
35
  }
36
36
 
@@ -68,7 +68,6 @@ module.exports = {
68
68
  // Increases the count of `yield` keyword.
69
69
  YieldExpression() {
70
70
 
71
- /* istanbul ignore else */
72
71
  if (stack.length > 0) {
73
72
  stack[stack.length - 1] += 1;
74
73
  }
@@ -105,7 +105,7 @@ module.exports = {
105
105
  if (ecmaFeatures.impliedStrict) {
106
106
  mode = "implied";
107
107
  } else if (mode === "safe") {
108
- mode = ecmaFeatures.globalReturn ? "global" : "function";
108
+ mode = ecmaFeatures.globalReturn || context.languageOptions.sourceType === "commonjs" ? "global" : "function";
109
109
  }
110
110
 
111
111
  /**
@@ -1350,7 +1350,7 @@ module.exports = {
1350
1350
  }
1351
1351
  }
1352
1352
 
1353
- /* istanbul ignore next */
1353
+ /* c8 ignore next */
1354
1354
  return true;
1355
1355
  },
1356
1356
 
@@ -7,7 +7,7 @@
7
7
 
8
8
  /* eslint no-console: "off" -- Logging util */
9
9
 
10
- /* istanbul ignore next */
10
+ /* c8 ignore next */
11
11
  module.exports = {
12
12
 
13
13
  /**
@@ -69,7 +69,7 @@ module.exports = class Cursor {
69
69
  * @returns {boolean} `true` if the next token exists.
70
70
  * @abstract
71
71
  */
72
- /* istanbul ignore next */
72
+ /* c8 ignore next */
73
73
  moveNext() { // eslint-disable-line class-methods-use-this -- Unused
74
74
  throw new Error("Not implemented.");
75
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint",
3
- "version": "8.23.0",
3
+ "version": "8.24.0",
4
4
  "author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",
5
5
  "description": "An AST-based pattern checker for JavaScript.",
6
6
  "bin": {
@@ -55,8 +55,8 @@
55
55
  "homepage": "https://eslint.org",
56
56
  "bugs": "https://github.com/eslint/eslint/issues/",
57
57
  "dependencies": {
58
- "@eslint/eslintrc": "^1.3.1",
59
- "@humanwhocodes/config-array": "^0.10.4",
58
+ "@eslint/eslintrc": "^1.3.2",
59
+ "@humanwhocodes/config-array": "^0.10.5",
60
60
  "@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
61
61
  "@humanwhocodes/module-importer": "^1.0.1",
62
62
  "ajv": "^6.10.0",
@@ -74,7 +74,6 @@
74
74
  "fast-deep-equal": "^3.1.3",
75
75
  "file-entry-cache": "^6.0.1",
76
76
  "find-up": "^5.0.0",
77
- "functional-red-black-tree": "^1.0.1",
78
77
  "glob-parent": "^6.0.1",
79
78
  "globals": "^13.15.0",
80
79
  "globby": "^11.1.0",
@@ -83,6 +82,7 @@
83
82
  "import-fresh": "^3.0.0",
84
83
  "imurmurhash": "^0.1.4",
85
84
  "is-glob": "^4.0.0",
85
+ "js-sdsl": "^4.1.4",
86
86
  "js-yaml": "^4.1.0",
87
87
  "json-stable-stringify-without-jsonify": "^1.0.1",
88
88
  "levn": "^0.4.1",
@@ -99,6 +99,7 @@
99
99
  "@babel/core": "^7.4.3",
100
100
  "@babel/preset-env": "^7.4.3",
101
101
  "babel-loader": "^8.0.5",
102
+ "c8": "^7.12.0",
102
103
  "chai": "^4.0.1",
103
104
  "cheerio": "^0.22.0",
104
105
  "common-tags": "^1.8.0",
@@ -142,7 +143,6 @@
142
143
  "mocha-junit-reporter": "^2.0.0",
143
144
  "node-polyfill-webpack-plugin": "^1.0.3",
144
145
  "npm-license": "^0.3.3",
145
- "nyc": "^15.0.1",
146
146
  "pirates": "^4.0.5",
147
147
  "progress": "^2.0.3",
148
148
  "proxyquire": "^2.0.1",