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
|
|
package/lib/rules/id-match.js
CHANGED
@@ -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) {
|
package/lib/rules/index.js
CHANGED
@@ -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
|
-
|
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)
|
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);
|