eslint 8.4.1 → 8.5.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.
@@ -216,6 +216,9 @@ function freezeDeeply(x) {
216
216
  * @returns {string} The sanitized text.
217
217
  */
218
218
  function sanitize(text) {
219
+ if (typeof text !== "string") {
220
+ return "";
221
+ }
219
222
  return text.replace(
220
223
  /[\u0000-\u0009\u000b-\u001a]/gu, // eslint-disable-line no-control-regex -- Escaping controls
221
224
  c => `\\u${c.codePointAt(0).toString(16).padStart(4, "0")}`
@@ -691,6 +694,13 @@ class RuleTester {
691
694
  * @private
692
695
  */
693
696
  function testValidTemplate(item) {
697
+ const code = typeof item === "object" ? item.code : item;
698
+
699
+ assert.ok(typeof code === "string", "Test case must specify a string value for 'code'");
700
+ if (item.name) {
701
+ assert.ok(typeof item.name === "string", "Optional test case property 'name' must be a string");
702
+ }
703
+
694
704
  const result = runRuleForItem(item);
695
705
  const messages = result.messages;
696
706
 
@@ -731,6 +741,10 @@ class RuleTester {
731
741
  * @private
732
742
  */
733
743
  function testInvalidTemplate(item) {
744
+ assert.ok(typeof item.code === "string", "Test case must specify a string value for 'code'");
745
+ if (item.name) {
746
+ assert.ok(typeof item.name === "string", "Optional test case property 'name' must be a string");
747
+ }
734
748
  assert.ok(item.errors || item.errors === 0,
735
749
  `Did not specify errors for an invalid test of ${ruleName}`);
736
750
 
@@ -67,6 +67,8 @@ module.exports = {
67
67
  onlyDeclarations = !!options.onlyDeclarations,
68
68
  ignoreDestructuring = !!options.ignoreDestructuring;
69
69
 
70
+ let globalScope;
71
+
70
72
  //--------------------------------------------------------------------------
71
73
  // Helpers
72
74
  //--------------------------------------------------------------------------
@@ -77,6 +79,19 @@ module.exports = {
77
79
  const DECLARATION_TYPES = new Set(["FunctionDeclaration", "VariableDeclarator"]);
78
80
  const IMPORT_TYPES = new Set(["ImportSpecifier", "ImportNamespaceSpecifier", "ImportDefaultSpecifier"]);
79
81
 
82
+ /**
83
+ * Checks whether the given node represents a reference to a global variable that is not declared in the source code.
84
+ * These identifiers will be allowed, as it is assumed that user has no control over the names of external global variables.
85
+ * @param {ASTNode} node `Identifier` node to check.
86
+ * @returns {boolean} `true` if the node is a reference to a global variable.
87
+ */
88
+ function isReferenceToGlobalVariable(node) {
89
+ const variable = globalScope.set.get(node.name);
90
+
91
+ return variable && variable.defs.length === 0 &&
92
+ variable.references.some(ref => ref.identifier === node);
93
+ }
94
+
80
95
  /**
81
96
  * Checks if a string matches the provided pattern
82
97
  * @param {string} name The string to check.
@@ -155,11 +170,19 @@ module.exports = {
155
170
 
156
171
  return {
157
172
 
173
+ Program() {
174
+ globalScope = context.getScope();
175
+ },
176
+
158
177
  Identifier(node) {
159
178
  const name = node.name,
160
179
  parent = node.parent,
161
180
  effectiveParent = (parent.type === "MemberExpression") ? parent.parent : parent;
162
181
 
182
+ if (isReferenceToGlobalVariable(node)) {
183
+ return;
184
+ }
185
+
163
186
  if (parent.type === "MemberExpression") {
164
187
 
165
188
  if (!checkProperties) {
@@ -255,6 +255,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({
255
255
  "prefer-exponentiation-operator": () => require("./prefer-exponentiation-operator"),
256
256
  "prefer-named-capture-group": () => require("./prefer-named-capture-group"),
257
257
  "prefer-numeric-literals": () => require("./prefer-numeric-literals"),
258
+ "prefer-object-has-own": () => require("./prefer-object-has-own"),
258
259
  "prefer-object-spread": () => require("./prefer-object-spread"),
259
260
  "prefer-promise-reject-errors": () => require("./prefer-promise-reject-errors"),
260
261
  "prefer-reflect": () => require("./prefer-reflect"),
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint",
3
- "version": "8.4.1",
3
+ "version": "8.5.0",
4
4
  "author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",
5
5
  "description": "An AST-based pattern checker for JavaScript.",
6
6
  "bin": {