eslint 8.4.0 → 8.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.
@@ -11,6 +11,21 @@
11
11
 
12
12
  const astUtils = require("./utils/ast-utils");
13
13
 
14
+ //------------------------------------------------------------------------------
15
+ // Helpers
16
+ //------------------------------------------------------------------------------
17
+
18
+ /**
19
+ * Determines if the given code path is a code path with lexical `this` binding.
20
+ * That is, if `this` within the code path refers to `this` of surrounding code path.
21
+ * @param {CodePath} codePath Code path.
22
+ * @param {ASTNode} node Node that started the code path.
23
+ * @returns {boolean} `true` if it is a code path with lexical `this` binding.
24
+ */
25
+ function isCodePathWithLexicalThis(codePath, node) {
26
+ return codePath.origin === "function" && node.type === "ArrowFunctionExpression";
27
+ }
28
+
14
29
  //------------------------------------------------------------------------------
15
30
  // Rule Definition
16
31
  //------------------------------------------------------------------------------
@@ -72,71 +87,53 @@ module.exports = {
72
87
  return current;
73
88
  };
74
89
 
75
- /**
76
- * Pushs new checking context into the stack.
77
- *
78
- * The checking context is not initialized yet.
79
- * Because most functions don't have `this` keyword.
80
- * When `this` keyword was found, the checking context is initialized.
81
- * @param {ASTNode} node A function node that was entered.
82
- * @returns {void}
83
- */
84
- function enterFunction(node) {
85
-
86
- // `this` can be invalid only under strict mode.
87
- stack.push({
88
- init: !context.getScope().isStrict,
89
- node,
90
- valid: true
91
- });
92
- }
90
+ return {
93
91
 
94
- /**
95
- * Pops the current checking context from the stack.
96
- * @returns {void}
97
- */
98
- function exitFunction() {
99
- stack.pop();
100
- }
92
+ onCodePathStart(codePath, node) {
93
+ if (isCodePathWithLexicalThis(codePath, node)) {
94
+ return;
95
+ }
101
96
 
102
- return {
97
+ if (codePath.origin === "program") {
98
+ const scope = context.getScope();
99
+ const features = context.parserOptions.ecmaFeatures || {};
100
+
101
+ stack.push({
102
+ init: true,
103
+ node,
104
+ valid: !(
105
+ scope.isStrict ||
106
+ node.sourceType === "module" ||
107
+ (features.globalReturn && scope.childScopes[0].isStrict)
108
+ )
109
+ });
103
110
 
104
- /*
105
- * `this` is invalid only under strict mode.
106
- * Modules is always strict mode.
107
- */
108
- Program(node) {
109
- const scope = context.getScope(),
110
- features = context.parserOptions.ecmaFeatures || {};
111
+ return;
112
+ }
111
113
 
114
+ /*
115
+ * `init: false` means that `valid` isn't determined yet.
116
+ * Most functions don't use `this`, and the calculation for `valid`
117
+ * is relatively costly, so we'll calculate it lazily when the first
118
+ * `this` within the function is traversed. A special case are non-strict
119
+ * functions, because `this` refers to the global object and therefore is
120
+ * always valid, so we can set `init: true` right away.
121
+ */
112
122
  stack.push({
113
- init: true,
123
+ init: !context.getScope().isStrict,
114
124
  node,
115
- valid: !(
116
- scope.isStrict ||
117
- node.sourceType === "module" ||
118
- (features.globalReturn && scope.childScopes[0].isStrict)
119
- )
125
+ valid: true
120
126
  });
121
127
  },
122
128
 
123
- "Program:exit"() {
129
+ onCodePathEnd(codePath, node) {
130
+ if (isCodePathWithLexicalThis(codePath, node)) {
131
+ return;
132
+ }
133
+
124
134
  stack.pop();
125
135
  },
126
136
 
127
- FunctionDeclaration: enterFunction,
128
- "FunctionDeclaration:exit": exitFunction,
129
- FunctionExpression: enterFunction,
130
- "FunctionExpression:exit": exitFunction,
131
-
132
- // Field initializers are implicit functions.
133
- "PropertyDefinition > *.value": enterFunction,
134
- "PropertyDefinition > *.value:exit": exitFunction,
135
-
136
- // Class static blocks are implicit functions.
137
- StaticBlock: enterFunction,
138
- "StaticBlock:exit": exitFunction,
139
-
140
137
  // Reports if `this` of the current context is invalid.
141
138
  ThisExpression(node) {
142
139
  const current = stack.getCurrent();
@@ -5,6 +5,12 @@
5
5
 
6
6
  "use strict";
7
7
 
8
+ //------------------------------------------------------------------------------
9
+ // Requirements
10
+ //------------------------------------------------------------------------------
11
+
12
+ const astUtils = require("./utils/ast-utils");
13
+
8
14
  //------------------------------------------------------------------------------
9
15
  // Rule Definition
10
16
  //------------------------------------------------------------------------------
@@ -44,12 +50,12 @@ module.exports = {
44
50
  const restrictedNames = new Set(context.options[0] && context.options[0].restrictedNamedExports);
45
51
 
46
52
  /**
47
- * Checks and reports given exported identifier.
48
- * @param {ASTNode} node exported `Identifier` node to check.
53
+ * Checks and reports given exported name.
54
+ * @param {ASTNode} node exported `Identifier` or string `Literal` node to check.
49
55
  * @returns {void}
50
56
  */
51
57
  function checkExportedName(node) {
52
- const name = node.name;
58
+ const name = astUtils.getModuleExportName(node);
53
59
 
54
60
  if (restrictedNames.has(name)) {
55
61
  context.report({
@@ -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
  //------------------------------------------------------------------------------
@@ -63,6 +69,9 @@ const arrayOfStringsOrObjectPatterns = {
63
69
  message: {
64
70
  type: "string",
65
71
  minLength: 1
72
+ },
73
+ caseSensitive: {
74
+ type: "boolean"
66
75
  }
67
76
  },
68
77
  additionalProperties: false,
@@ -142,10 +151,18 @@ module.exports = {
142
151
  }, {});
143
152
 
144
153
  // Handle patterns too, either as strings or groups
145
- const restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || [];
146
- const restrictedPatternGroups = restrictedPatterns.length > 0 && typeof restrictedPatterns[0] === "string"
147
- ? [{ matcher: ignore().add(restrictedPatterns) }]
148
- : restrictedPatterns.map(({ group, message }) => ({ matcher: ignore().add(group), customMessage: message }));
154
+ let restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || [];
155
+
156
+ // standardize to array of objects if we have an array of strings
157
+ if (restrictedPatterns.length > 0 && typeof restrictedPatterns[0] === "string") {
158
+ restrictedPatterns = [{ group: restrictedPatterns }];
159
+ }
160
+
161
+ // relative paths are supported for this rule
162
+ const restrictedPatternGroups = restrictedPatterns.map(({ group, message, caseSensitive }) => ({
163
+ matcher: ignore({ allowRelativePaths: true, ignorecase: !caseSensitive }).add(group),
164
+ customMessage: message
165
+ }));
149
166
 
150
167
  // if no imports are restricted we don't need to check
151
168
  if (Object.keys(restrictedPaths).length === 0 && restrictedPatternGroups.length === 0) {
@@ -269,12 +286,12 @@ module.exports = {
269
286
  } else if (specifier.type === "ImportNamespaceSpecifier") {
270
287
  name = "*";
271
288
  } else if (specifier.imported) {
272
- name = specifier.imported.name;
289
+ name = astUtils.getModuleExportName(specifier.imported);
273
290
  } else if (specifier.local) {
274
- name = specifier.local.name;
291
+ name = astUtils.getModuleExportName(specifier.local);
275
292
  }
276
293
 
277
- if (name) {
294
+ if (typeof name === "string") {
278
295
  if (importNames.has(name)) {
279
296
  importNames.get(name).push(specifierData);
280
297
  } else {
@@ -103,7 +103,8 @@ module.exports = {
103
103
  return {};
104
104
  }
105
105
 
106
- const ig = ignore().add(restrictedPatterns);
106
+ // relative paths are supported for this rule
107
+ const ig = ignore({ allowRelativePaths: true }).add(restrictedPatterns);
107
108
 
108
109
 
109
110
  /**
@@ -174,7 +174,7 @@ module.exports = {
174
174
 
175
175
  return {
176
176
  AssignmentExpression(node) {
177
- if (node.operator === "=") {
177
+ if (["=", "&&=", "||=", "??="].includes(node.operator)) {
178
178
  eachSelfAssignment(node.left, node.right, props, report);
179
179
  }
180
180
  }
@@ -132,8 +132,10 @@ module.exports = {
132
132
  return;
133
133
  }
134
134
 
135
- if (node.imported.name === node.local.name &&
136
- node.imported.range[0] !== node.local.range[0]) {
135
+ if (
136
+ node.imported.range[0] !== node.local.range[0] &&
137
+ astUtils.getModuleExportName(node.imported) === node.local.name
138
+ ) {
137
139
  reportError(node, node.imported, "Import");
138
140
  }
139
141
  }
@@ -148,8 +150,10 @@ module.exports = {
148
150
  return;
149
151
  }
150
152
 
151
- if (node.local.name === node.exported.name &&
152
- node.local.range[0] !== node.exported.range[0]) {
153
+ if (
154
+ node.local.range[0] !== node.exported.range[0] &&
155
+ astUtils.getModuleExportName(node.local) === astUtils.getModuleExportName(node.exported)
156
+ ) {
153
157
  reportError(node, node.local, "Export");
154
158
  }
155
159
 
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @fileoverview Prefers Object.hasOwn() instead of Object.prototype.hasOwnProperty.call()
3
+ * @author Nitin Kumar
4
+ * @author Gautam Arora
5
+ */
6
+
7
+ "use strict";
8
+
9
+ //------------------------------------------------------------------------------
10
+ // Requirements
11
+ //------------------------------------------------------------------------------
12
+
13
+ const astUtils = require("./utils/ast-utils");
14
+
15
+ //------------------------------------------------------------------------------
16
+ // Helpers
17
+ //------------------------------------------------------------------------------
18
+
19
+ /**
20
+ * Checks if the given node is considered to be an access to a property of `Object.prototype`.
21
+ * @param {ASTNode} node `MemberExpression` node to evaluate.
22
+ * @returns {boolean} `true` if `node.object` is `Object`, `Object.prototype`, or `{}` (empty 'ObjectExpression' node).
23
+ */
24
+ function hasLeftHandObject(node) {
25
+
26
+ /*
27
+ * ({}).hasOwnProperty.call(obj, prop) - `true`
28
+ * ({ foo }.hasOwnProperty.call(obj, prop)) - `false`, object literal should be empty
29
+ */
30
+ if (node.object.type === "ObjectExpression" && node.object.properties.length === 0) {
31
+ return true;
32
+ }
33
+
34
+ const objectNodeToCheck = node.object.type === "MemberExpression" && astUtils.getStaticPropertyName(node.object) === "prototype" ? node.object.object : node.object;
35
+
36
+ if (objectNodeToCheck.type === "Identifier" && objectNodeToCheck.name === "Object") {
37
+ return true;
38
+ }
39
+
40
+ return false;
41
+ }
42
+
43
+ //------------------------------------------------------------------------------
44
+ // Rule Definition
45
+ //------------------------------------------------------------------------------
46
+
47
+ /** @type {import('../shared/types').Rule} */
48
+ module.exports = {
49
+ meta: {
50
+ type: "suggestion",
51
+ docs: {
52
+ description:
53
+ "disallow use of `Object.prototype.hasOwnProperty.call()` and prefer use of `Object.hasOwn()`",
54
+ recommended: false,
55
+ url: "https://eslint.org/docs/rules/prefer-object-has-own"
56
+ },
57
+ schema: [],
58
+ messages: {
59
+ useHasOwn: "Use 'Object.hasOwn()' instead of 'Object.prototype.hasOwnProperty.call()'."
60
+ },
61
+ fixable: "code"
62
+ },
63
+ create(context) {
64
+ return {
65
+ CallExpression(node) {
66
+ if (!(node.callee.type === "MemberExpression" && node.callee.object.type === "MemberExpression")) {
67
+ return;
68
+ }
69
+
70
+ const calleePropertyName = astUtils.getStaticPropertyName(node.callee);
71
+ const objectPropertyName = astUtils.getStaticPropertyName(node.callee.object);
72
+ const isObject = hasLeftHandObject(node.callee.object);
73
+
74
+ // check `Object` scope
75
+ const scope = context.getScope();
76
+ const variable = astUtils.getVariableByName(scope, "Object");
77
+
78
+ if (
79
+ calleePropertyName === "call" &&
80
+ objectPropertyName === "hasOwnProperty" &&
81
+ isObject &&
82
+ variable && variable.scope.type === "global"
83
+ ) {
84
+ context.report({
85
+ node,
86
+ messageId: "useHasOwn",
87
+ fix(fixer) {
88
+ const sourceCode = context.getSourceCode();
89
+
90
+ if (sourceCode.getCommentsInside(node.callee).length > 0) {
91
+ return null;
92
+ }
93
+
94
+ const tokenJustBeforeNode = sourceCode.getTokenBefore(node.callee, { includeComments: true });
95
+
96
+ // for https://github.com/eslint/eslint/pull/15346#issuecomment-991417335
97
+ if (
98
+ tokenJustBeforeNode &&
99
+ tokenJustBeforeNode.range[1] === node.callee.range[0] &&
100
+ !astUtils.canTokensBeAdjacent(tokenJustBeforeNode, "Object.hasOwn")
101
+ ) {
102
+ return fixer.replaceText(node.callee, " Object.hasOwn");
103
+ }
104
+
105
+ return fixer.replaceText(node.callee, "Object.hasOwn");
106
+ }
107
+ });
108
+ }
109
+ }
110
+ };
111
+ }
112
+ };
@@ -11,11 +11,15 @@
11
11
 
12
12
  const astUtils = require("./utils/ast-utils");
13
13
  const { CALL, CONSTRUCT, ReferenceTracker, findVariable } = require("eslint-utils");
14
+ const { RegExpValidator, visitRegExpAST, RegExpParser } = require("regexpp");
15
+ const { canTokensBeAdjacent } = require("./utils/ast-utils");
14
16
 
15
17
  //------------------------------------------------------------------------------
16
18
  // Helpers
17
19
  //------------------------------------------------------------------------------
18
20
 
21
+ const REGEXPP_LATEST_ECMA_VERSION = 2022;
22
+
19
23
  /**
20
24
  * Determines whether the given node is a string literal.
21
25
  * @param {ASTNode} node Node to check.
@@ -43,6 +47,71 @@ function isStaticTemplateLiteral(node) {
43
47
  return node.type === "TemplateLiteral" && node.expressions.length === 0;
44
48
  }
45
49
 
50
+ const validPrecedingTokens = [
51
+ "(",
52
+ ";",
53
+ "[",
54
+ ",",
55
+ "=",
56
+ "+",
57
+ "*",
58
+ "-",
59
+ "?",
60
+ "~",
61
+ "%",
62
+ "**",
63
+ "!",
64
+ "typeof",
65
+ "instanceof",
66
+ "&&",
67
+ "||",
68
+ "??",
69
+ "return",
70
+ "...",
71
+ "delete",
72
+ "void",
73
+ "in",
74
+ "<",
75
+ ">",
76
+ "<=",
77
+ ">=",
78
+ "==",
79
+ "===",
80
+ "!=",
81
+ "!==",
82
+ "<<",
83
+ ">>",
84
+ ">>>",
85
+ "&",
86
+ "|",
87
+ "^",
88
+ ":",
89
+ "{",
90
+ "=>",
91
+ "*=",
92
+ "<<=",
93
+ ">>=",
94
+ ">>>=",
95
+ "^=",
96
+ "|=",
97
+ "&=",
98
+ "??=",
99
+ "||=",
100
+ "&&=",
101
+ "**=",
102
+ "+=",
103
+ "-=",
104
+ "/=",
105
+ "%=",
106
+ "/",
107
+ "do",
108
+ "break",
109
+ "continue",
110
+ "debugger",
111
+ "case",
112
+ "throw"
113
+ ];
114
+
46
115
 
47
116
  //------------------------------------------------------------------------------
48
117
  // Rule Definition
@@ -59,6 +128,8 @@ module.exports = {
59
128
  url: "https://eslint.org/docs/rules/prefer-regex-literals"
60
129
  },
61
130
 
131
+ hasSuggestions: true,
132
+
62
133
  schema: [
63
134
  {
64
135
  type: "object",
@@ -74,6 +145,7 @@ module.exports = {
74
145
 
75
146
  messages: {
76
147
  unexpectedRegExp: "Use a regular expression literal instead of the 'RegExp' constructor.",
148
+ replaceWithLiteral: "Replace with an equivalent regular expression literal.",
77
149
  unexpectedRedundantRegExp: "Regular expression literal is unnecessarily wrapped within a 'RegExp' constructor.",
78
150
  unexpectedRedundantRegExpWithFlags: "Use regular expression literal with flags instead of the 'RegExp' constructor."
79
151
  }
@@ -81,6 +153,7 @@ module.exports = {
81
153
 
82
154
  create(context) {
83
155
  const [{ disallowRedundantWrapping = false } = {}] = context.options;
156
+ const sourceCode = context.getSourceCode();
84
157
 
85
158
  /**
86
159
  * Determines whether the given identifier node is a reference to a global variable.
@@ -107,6 +180,27 @@ module.exports = {
107
180
  isStaticTemplateLiteral(node.quasi);
108
181
  }
109
182
 
183
+ /**
184
+ * Gets the value of a string
185
+ * @param {ASTNode} node The node to get the string of.
186
+ * @returns {string|null} The value of the node.
187
+ */
188
+ function getStringValue(node) {
189
+ if (isStringLiteral(node)) {
190
+ return node.value;
191
+ }
192
+
193
+ if (isStaticTemplateLiteral(node)) {
194
+ return node.quasis[0].value.cooked;
195
+ }
196
+
197
+ if (isStringRawTaggedStaticTemplateLiteral(node)) {
198
+ return node.quasi.quasis[0].value.raw;
199
+ }
200
+
201
+ return null;
202
+ }
203
+
110
204
  /**
111
205
  * Determines whether the given node is considered to be a static string by the logic of this rule.
112
206
  * @param {ASTNode} node Node to check.
@@ -152,6 +246,53 @@ module.exports = {
152
246
  return false;
153
247
  }
154
248
 
249
+ /**
250
+ * Returns a ecmaVersion compatible for regexpp.
251
+ * @param {any} ecmaVersion The ecmaVersion to convert.
252
+ * @returns {import("regexpp/ecma-versions").EcmaVersion} The resulting ecmaVersion compatible for regexpp.
253
+ */
254
+ function getRegexppEcmaVersion(ecmaVersion) {
255
+ if (typeof ecmaVersion !== "number" || ecmaVersion <= 5) {
256
+ return 5;
257
+ }
258
+ return Math.min(ecmaVersion + 2009, REGEXPP_LATEST_ECMA_VERSION);
259
+ }
260
+
261
+ /**
262
+ * Makes a character escaped or else returns null.
263
+ * @param {string} character The character to escape.
264
+ * @returns {string} The resulting escaped character.
265
+ */
266
+ function resolveEscapes(character) {
267
+ switch (character) {
268
+ case "\n":
269
+ case "\\\n":
270
+ return "\\n";
271
+
272
+ case "\r":
273
+ case "\\\r":
274
+ return "\\r";
275
+
276
+ case "\t":
277
+ case "\\\t":
278
+ return "\\t";
279
+
280
+ case "\v":
281
+ case "\\\v":
282
+ return "\\v";
283
+
284
+ case "\f":
285
+ case "\\\f":
286
+ return "\\f";
287
+
288
+ case "/":
289
+ return "\\/";
290
+
291
+ default:
292
+ return null;
293
+ }
294
+ }
295
+
155
296
  return {
156
297
  Program() {
157
298
  const scope = context.getScope();
@@ -171,7 +312,82 @@ module.exports = {
171
312
  context.report({ node, messageId: "unexpectedRedundantRegExp" });
172
313
  }
173
314
  } else if (hasOnlyStaticStringArguments(node)) {
174
- context.report({ node, messageId: "unexpectedRegExp" });
315
+ let regexContent = getStringValue(node.arguments[0]);
316
+ let noFix = false;
317
+ let flags;
318
+
319
+ if (node.arguments[1]) {
320
+ flags = getStringValue(node.arguments[1]);
321
+ }
322
+
323
+ const regexppEcmaVersion = getRegexppEcmaVersion(context.parserOptions.ecmaVersion);
324
+ const RegExpValidatorInstance = new RegExpValidator({ ecmaVersion: regexppEcmaVersion });
325
+
326
+ try {
327
+ RegExpValidatorInstance.validatePattern(regexContent, 0, regexContent.length, flags ? flags.includes("u") : false);
328
+ if (flags) {
329
+ RegExpValidatorInstance.validateFlags(flags);
330
+ }
331
+ } catch {
332
+ noFix = true;
333
+ }
334
+
335
+ const tokenBefore = sourceCode.getTokenBefore(node);
336
+
337
+ if (tokenBefore && !validPrecedingTokens.includes(tokenBefore.value)) {
338
+ noFix = true;
339
+ }
340
+
341
+ if (!/^[-a-zA-Z0-9\\[\](){} \t\r\n\v\f!@#$%^&*+^_=/~`.><?,'"|:;]*$/u.test(regexContent)) {
342
+ noFix = true;
343
+ }
344
+
345
+ if (sourceCode.getCommentsInside(node).length > 0) {
346
+ noFix = true;
347
+ }
348
+
349
+ if (regexContent && !noFix) {
350
+ let charIncrease = 0;
351
+
352
+ const ast = new RegExpParser({ ecmaVersion: regexppEcmaVersion }).parsePattern(regexContent, 0, regexContent.length, flags ? flags.includes("u") : false);
353
+
354
+ visitRegExpAST(ast, {
355
+ onCharacterEnter(characterNode) {
356
+ const escaped = resolveEscapes(characterNode.raw);
357
+
358
+ if (escaped) {
359
+ regexContent =
360
+ regexContent.slice(0, characterNode.start + charIncrease) +
361
+ escaped +
362
+ regexContent.slice(characterNode.end + charIncrease);
363
+
364
+ if (characterNode.raw.length === 1) {
365
+ charIncrease += 1;
366
+ }
367
+ }
368
+ }
369
+ });
370
+ }
371
+
372
+ const newRegExpValue = `/${regexContent || "(?:)"}/${flags || ""}`;
373
+
374
+ context.report({
375
+ node,
376
+ messageId: "unexpectedRegExp",
377
+ suggest: noFix ? [] : [{
378
+ messageId: "replaceWithLiteral",
379
+ fix(fixer) {
380
+ const tokenAfter = sourceCode.getTokenAfter(node);
381
+
382
+ return fixer.replaceText(
383
+ node,
384
+ (tokenBefore && !canTokensBeAdjacent(tokenBefore, newRegExpValue) && tokenBefore.range[1] === node.range[0] ? " " : "") +
385
+ newRegExpValue +
386
+ (tokenAfter && !canTokensBeAdjacent(newRegExpValue, tokenAfter) && node.range[1] === tokenAfter.range[0] ? " " : "")
387
+ );
388
+ }
389
+ }]
390
+ });
175
391
  }
176
392
  }
177
393
  }
@@ -188,7 +188,7 @@ module.exports = {
188
188
  return sourceCode.getText(currentNode);
189
189
  }
190
190
 
191
- if (isConcatenation(currentNode) && hasStringLiteral(currentNode) && hasNonStringLiteral(currentNode)) {
191
+ if (isConcatenation(currentNode) && hasStringLiteral(currentNode)) {
192
192
  const plusSign = sourceCode.getFirstTokenBetween(currentNode.left, currentNode.right, token => token.value === "+");
193
193
  const textBeforePlus = getTextBetween(currentNode.left, plusSign);
194
194
  const textAfterPlus = getTextBetween(plusSign, currentNode.right);