eslint 8.13.0 → 8.17.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.
- package/README.md +17 -12
- package/bin/eslint.js +1 -1
- package/lib/cli-engine/cli-engine.js +2 -4
- package/lib/cli-engine/lint-result-cache.js +1 -1
- package/lib/eslint/eslint.js +3 -3
- package/lib/linter/code-path-analysis/code-path-segment.js +1 -1
- package/lib/linter/code-path-analysis/code-path-state.js +1 -1
- package/lib/linter/code-path-analysis/code-path.js +1 -1
- package/lib/rules/accessor-pairs.js +4 -4
- package/lib/rules/callback-return.js +2 -2
- package/lib/rules/capitalized-comments.js +1 -1
- package/lib/rules/consistent-this.js +1 -1
- package/lib/rules/dot-notation.js +2 -2
- package/lib/rules/function-paren-newline.js +8 -5
- package/lib/rules/global-require.js +3 -3
- package/lib/rules/indent-legacy.js +2 -2
- package/lib/rules/indent.js +45 -13
- package/lib/rules/index.js +1 -0
- package/lib/rules/jsx-quotes.js +1 -1
- package/lib/rules/lines-around-comment.js +3 -3
- package/lib/rules/max-lines.js +2 -2
- package/lib/rules/max-statements.js +1 -1
- package/lib/rules/newline-before-return.js +1 -1
- package/lib/rules/no-bitwise.js +2 -2
- package/lib/rules/no-console.js +1 -1
- package/lib/rules/no-constant-binary-expression.js +500 -0
- package/lib/rules/no-constant-condition.js +4 -197
- package/lib/rules/no-control-regex.js +23 -10
- package/lib/rules/no-empty-function.js +1 -1
- package/lib/rules/no-extra-boolean-cast.js +3 -3
- package/lib/rules/no-extra-semi.js +1 -1
- package/lib/rules/no-global-assign.js +1 -1
- package/lib/rules/no-implicit-coercion.js +8 -8
- package/lib/rules/no-loop-func.js +1 -1
- package/lib/rules/no-magic-numbers.js +3 -3
- package/lib/rules/no-misleading-character-class.js +90 -17
- package/lib/rules/no-mixed-operators.js +1 -1
- package/lib/rules/no-mixed-requires.js +1 -1
- package/lib/rules/no-multi-spaces.js +1 -1
- package/lib/rules/no-native-reassign.js +1 -1
- package/lib/rules/no-new-object.js +1 -1
- package/lib/rules/no-new-wrappers.js +1 -1
- package/lib/rules/no-octal.js +2 -2
- package/lib/rules/no-prototype-builtins.js +3 -3
- package/lib/rules/no-shadow.js +5 -5
- package/lib/rules/no-sparse-arrays.js +1 -1
- package/lib/rules/no-underscore-dangle.js +31 -2
- package/lib/rules/no-unused-expressions.js +1 -1
- package/lib/rules/no-unused-vars.js +1 -1
- package/lib/rules/no-use-before-define.js +15 -2
- package/lib/rules/operator-assignment.js +2 -2
- package/lib/rules/prefer-const.js +1 -1
- package/lib/rules/prefer-reflect.js +2 -2
- package/lib/rules/prefer-regex-literals.js +3 -3
- package/lib/rules/quote-props.js +2 -2
- package/lib/rules/quotes.js +1 -1
- package/lib/rules/spaced-comment.js +1 -1
- package/lib/rules/utils/ast-utils.js +203 -7
- package/lib/rules/valid-jsdoc.js +1 -1
- package/lib/rules/valid-typeof.js +4 -4
- package/lib/rules/yoda.js +1 -1
- package/lib/shared/types.js +1 -1
- package/package.json +25 -8
@@ -5,6 +5,8 @@
|
|
5
5
|
|
6
6
|
"use strict";
|
7
7
|
|
8
|
+
const { isConstant } = require("./utils/ast-utils");
|
9
|
+
|
8
10
|
//------------------------------------------------------------------------------
|
9
11
|
// Helpers
|
10
12
|
//------------------------------------------------------------------------------
|
@@ -53,201 +55,6 @@ module.exports = {
|
|
53
55
|
// Helpers
|
54
56
|
//--------------------------------------------------------------------------
|
55
57
|
|
56
|
-
/**
|
57
|
-
* Returns literal's value converted to the Boolean type
|
58
|
-
* @param {ASTNode} node any `Literal` node
|
59
|
-
* @returns {boolean | null} `true` when node is truthy, `false` when node is falsy,
|
60
|
-
* `null` when it cannot be determined.
|
61
|
-
*/
|
62
|
-
function getBooleanValue(node) {
|
63
|
-
if (node.value === null) {
|
64
|
-
|
65
|
-
/*
|
66
|
-
* it might be a null literal or bigint/regex literal in unsupported environments .
|
67
|
-
* https://github.com/estree/estree/blob/14df8a024956ea289bd55b9c2226a1d5b8a473ee/es5.md#regexpliteral
|
68
|
-
* https://github.com/estree/estree/blob/14df8a024956ea289bd55b9c2226a1d5b8a473ee/es2020.md#bigintliteral
|
69
|
-
*/
|
70
|
-
|
71
|
-
if (node.raw === "null") {
|
72
|
-
return false;
|
73
|
-
}
|
74
|
-
|
75
|
-
// regex is always truthy
|
76
|
-
if (typeof node.regex === "object") {
|
77
|
-
return true;
|
78
|
-
}
|
79
|
-
|
80
|
-
return null;
|
81
|
-
}
|
82
|
-
|
83
|
-
return !!node.value;
|
84
|
-
}
|
85
|
-
|
86
|
-
/**
|
87
|
-
* Checks if a branch node of LogicalExpression short circuits the whole condition
|
88
|
-
* @param {ASTNode} node The branch of main condition which needs to be checked
|
89
|
-
* @param {string} operator The operator of the main LogicalExpression.
|
90
|
-
* @returns {boolean} true when condition short circuits whole condition
|
91
|
-
*/
|
92
|
-
function isLogicalIdentity(node, operator) {
|
93
|
-
switch (node.type) {
|
94
|
-
case "Literal":
|
95
|
-
return (operator === "||" && getBooleanValue(node) === true) ||
|
96
|
-
(operator === "&&" && getBooleanValue(node) === false);
|
97
|
-
|
98
|
-
case "UnaryExpression":
|
99
|
-
return (operator === "&&" && node.operator === "void");
|
100
|
-
|
101
|
-
case "LogicalExpression":
|
102
|
-
|
103
|
-
/*
|
104
|
-
* handles `a && false || b`
|
105
|
-
* `false` is an identity element of `&&` but not `||`
|
106
|
-
*/
|
107
|
-
return operator === node.operator &&
|
108
|
-
(
|
109
|
-
isLogicalIdentity(node.left, operator) ||
|
110
|
-
isLogicalIdentity(node.right, operator)
|
111
|
-
);
|
112
|
-
|
113
|
-
case "AssignmentExpression":
|
114
|
-
return ["||=", "&&="].includes(node.operator) &&
|
115
|
-
operator === node.operator.slice(0, -1) &&
|
116
|
-
isLogicalIdentity(node.right, operator);
|
117
|
-
|
118
|
-
// no default
|
119
|
-
}
|
120
|
-
return false;
|
121
|
-
}
|
122
|
-
|
123
|
-
/**
|
124
|
-
* Checks if an identifier is a reference to a global variable.
|
125
|
-
* @param {ASTNode} node An identifier node to check.
|
126
|
-
* @returns {boolean} `true` if the identifier is a reference to a global variable.
|
127
|
-
*/
|
128
|
-
function isReferenceToGlobalVariable(node) {
|
129
|
-
const scope = context.getScope();
|
130
|
-
const reference = scope.references.find(ref => ref.identifier === node);
|
131
|
-
|
132
|
-
return Boolean(
|
133
|
-
reference &&
|
134
|
-
reference.resolved &&
|
135
|
-
reference.resolved.scope.type === "global" &&
|
136
|
-
reference.resolved.defs.length === 0
|
137
|
-
);
|
138
|
-
}
|
139
|
-
|
140
|
-
/**
|
141
|
-
* Checks if a node has a constant truthiness value.
|
142
|
-
* @param {ASTNode} node The AST node to check.
|
143
|
-
* @param {boolean} inBooleanPosition `true` if checking the test of a
|
144
|
-
* condition. `false` in all other cases. When `false`, checks if -- for
|
145
|
-
* both string and number -- if coerced to that type, the value will
|
146
|
-
* be constant.
|
147
|
-
* @returns {Bool} true when node's truthiness is constant
|
148
|
-
* @private
|
149
|
-
*/
|
150
|
-
function isConstant(node, inBooleanPosition) {
|
151
|
-
|
152
|
-
// node.elements can return null values in the case of sparse arrays ex. [,]
|
153
|
-
if (!node) {
|
154
|
-
return true;
|
155
|
-
}
|
156
|
-
switch (node.type) {
|
157
|
-
case "Literal":
|
158
|
-
case "ArrowFunctionExpression":
|
159
|
-
case "FunctionExpression":
|
160
|
-
return true;
|
161
|
-
case "ClassExpression":
|
162
|
-
case "ObjectExpression":
|
163
|
-
|
164
|
-
/**
|
165
|
-
* In theory objects like:
|
166
|
-
*
|
167
|
-
* `{toString: () => a}`
|
168
|
-
* `{valueOf: () => a}`
|
169
|
-
*
|
170
|
-
* Or a classes like:
|
171
|
-
*
|
172
|
-
* `class { static toString() { return a } }`
|
173
|
-
* `class { static valueOf() { return a } }`
|
174
|
-
*
|
175
|
-
* Are not constant verifiably when `inBooleanPosition` is
|
176
|
-
* false, but it's an edge case we've opted not to handle.
|
177
|
-
*/
|
178
|
-
return true;
|
179
|
-
case "TemplateLiteral":
|
180
|
-
return (inBooleanPosition && node.quasis.some(quasi => quasi.value.cooked.length)) ||
|
181
|
-
node.expressions.every(exp => isConstant(exp, false));
|
182
|
-
|
183
|
-
case "ArrayExpression": {
|
184
|
-
if (!inBooleanPosition) {
|
185
|
-
return node.elements.every(element => isConstant(element, false));
|
186
|
-
}
|
187
|
-
return true;
|
188
|
-
}
|
189
|
-
|
190
|
-
case "UnaryExpression":
|
191
|
-
if (
|
192
|
-
node.operator === "void" ||
|
193
|
-
node.operator === "typeof" && inBooleanPosition
|
194
|
-
) {
|
195
|
-
return true;
|
196
|
-
}
|
197
|
-
|
198
|
-
if (node.operator === "!") {
|
199
|
-
return isConstant(node.argument, true);
|
200
|
-
}
|
201
|
-
|
202
|
-
return isConstant(node.argument, false);
|
203
|
-
|
204
|
-
case "BinaryExpression":
|
205
|
-
return isConstant(node.left, false) &&
|
206
|
-
isConstant(node.right, false) &&
|
207
|
-
node.operator !== "in";
|
208
|
-
|
209
|
-
case "LogicalExpression": {
|
210
|
-
const isLeftConstant = isConstant(node.left, inBooleanPosition);
|
211
|
-
const isRightConstant = isConstant(node.right, inBooleanPosition);
|
212
|
-
const isLeftShortCircuit = (isLeftConstant && isLogicalIdentity(node.left, node.operator));
|
213
|
-
const isRightShortCircuit = (inBooleanPosition && isRightConstant && isLogicalIdentity(node.right, node.operator));
|
214
|
-
|
215
|
-
return (isLeftConstant && isRightConstant) ||
|
216
|
-
isLeftShortCircuit ||
|
217
|
-
isRightShortCircuit;
|
218
|
-
}
|
219
|
-
case "NewExpression":
|
220
|
-
return inBooleanPosition;
|
221
|
-
case "AssignmentExpression":
|
222
|
-
if (node.operator === "=") {
|
223
|
-
return isConstant(node.right, inBooleanPosition);
|
224
|
-
}
|
225
|
-
|
226
|
-
if (["||=", "&&="].includes(node.operator) && inBooleanPosition) {
|
227
|
-
return isLogicalIdentity(node.right, node.operator.slice(0, -1));
|
228
|
-
}
|
229
|
-
|
230
|
-
return false;
|
231
|
-
|
232
|
-
case "SequenceExpression":
|
233
|
-
return isConstant(node.expressions[node.expressions.length - 1], inBooleanPosition);
|
234
|
-
case "SpreadElement":
|
235
|
-
return isConstant(node.argument, inBooleanPosition);
|
236
|
-
case "CallExpression":
|
237
|
-
if (node.callee.type === "Identifier" && node.callee.name === "Boolean") {
|
238
|
-
if (node.arguments.length === 0 || isConstant(node.arguments[0], true)) {
|
239
|
-
return isReferenceToGlobalVariable(node.callee);
|
240
|
-
}
|
241
|
-
}
|
242
|
-
return false;
|
243
|
-
case "Identifier":
|
244
|
-
return node.name === "undefined" && isReferenceToGlobalVariable(node);
|
245
|
-
|
246
|
-
// no default
|
247
|
-
}
|
248
|
-
return false;
|
249
|
-
}
|
250
|
-
|
251
58
|
/**
|
252
59
|
* Tracks when the given node contains a constant condition.
|
253
60
|
* @param {ASTNode} node The AST node to check.
|
@@ -255,7 +62,7 @@ module.exports = {
|
|
255
62
|
* @private
|
256
63
|
*/
|
257
64
|
function trackConstantConditionLoop(node) {
|
258
|
-
if (node.test && isConstant(node.test, true)) {
|
65
|
+
if (node.test && isConstant(context.getScope(), node.test, true)) {
|
259
66
|
loopsInCurrentScope.add(node);
|
260
67
|
}
|
261
68
|
}
|
@@ -280,7 +87,7 @@ module.exports = {
|
|
280
87
|
* @private
|
281
88
|
*/
|
282
89
|
function reportIfConstant(node) {
|
283
|
-
if (node.test && isConstant(node.test, true)) {
|
90
|
+
if (node.test && isConstant(context.getScope(), node.test, true)) {
|
284
91
|
context.report({ node: node.test, messageId: "unexpected" });
|
285
92
|
}
|
286
93
|
}
|
@@ -30,10 +30,12 @@ const collector = new (class {
|
|
30
30
|
}
|
31
31
|
}
|
32
32
|
|
33
|
-
collectControlChars(regexpStr) {
|
33
|
+
collectControlChars(regexpStr, flags) {
|
34
|
+
const uFlag = typeof flags === "string" && flags.includes("u");
|
35
|
+
|
34
36
|
try {
|
35
37
|
this._source = regexpStr;
|
36
|
-
this._validator.validatePattern(regexpStr); // Call onCharacter hook
|
38
|
+
this._validator.validatePattern(regexpStr, void 0, void 0, uFlag); // Call onCharacter hook
|
37
39
|
} catch {
|
38
40
|
|
39
41
|
// Ignore syntax errors in RegExp.
|
@@ -68,13 +70,15 @@ module.exports = {
|
|
68
70
|
|
69
71
|
/**
|
70
72
|
* Get the regex expression
|
71
|
-
* @param {ASTNode} node node to evaluate
|
72
|
-
* @returns {
|
73
|
+
* @param {ASTNode} node `Literal` node to evaluate
|
74
|
+
* @returns {{ pattern: string, flags: string | null } | null} Regex if found (the given node is either a regex literal
|
75
|
+
* or a string literal that is the pattern argument of a RegExp constructor call). Otherwise `null`. If flags cannot be determined,
|
76
|
+
* the `flags` property will be `null`.
|
73
77
|
* @private
|
74
78
|
*/
|
75
|
-
function
|
79
|
+
function getRegExp(node) {
|
76
80
|
if (node.regex) {
|
77
|
-
return node.regex
|
81
|
+
return node.regex;
|
78
82
|
}
|
79
83
|
if (typeof node.value === "string" &&
|
80
84
|
(node.parent.type === "NewExpression" || node.parent.type === "CallExpression") &&
|
@@ -82,7 +86,15 @@ module.exports = {
|
|
82
86
|
node.parent.callee.name === "RegExp" &&
|
83
87
|
node.parent.arguments[0] === node
|
84
88
|
) {
|
85
|
-
|
89
|
+
const pattern = node.value;
|
90
|
+
const flags =
|
91
|
+
node.parent.arguments.length > 1 &&
|
92
|
+
node.parent.arguments[1].type === "Literal" &&
|
93
|
+
typeof node.parent.arguments[1].value === "string"
|
94
|
+
? node.parent.arguments[1].value
|
95
|
+
: null;
|
96
|
+
|
97
|
+
return { pattern, flags };
|
86
98
|
}
|
87
99
|
|
88
100
|
return null;
|
@@ -90,10 +102,11 @@ module.exports = {
|
|
90
102
|
|
91
103
|
return {
|
92
104
|
Literal(node) {
|
93
|
-
const
|
105
|
+
const regExp = getRegExp(node);
|
94
106
|
|
95
|
-
if (
|
96
|
-
const
|
107
|
+
if (regExp) {
|
108
|
+
const { pattern, flags } = regExp;
|
109
|
+
const controlCharacters = collector.collectControlChars(pattern, flags);
|
97
110
|
|
98
111
|
if (controlCharacters.length > 0) {
|
99
112
|
context.report({
|
@@ -51,13 +51,13 @@ module.exports = {
|
|
51
51
|
const sourceCode = context.getSourceCode();
|
52
52
|
|
53
53
|
// Node types which have a test which will coerce values to booleans.
|
54
|
-
const BOOLEAN_NODE_TYPES = [
|
54
|
+
const BOOLEAN_NODE_TYPES = new Set([
|
55
55
|
"IfStatement",
|
56
56
|
"DoWhileStatement",
|
57
57
|
"WhileStatement",
|
58
58
|
"ConditionalExpression",
|
59
59
|
"ForStatement"
|
60
|
-
];
|
60
|
+
]);
|
61
61
|
|
62
62
|
/**
|
63
63
|
* Check if a node is a Boolean function or constructor.
|
@@ -95,7 +95,7 @@ module.exports = {
|
|
95
95
|
(isBooleanFunctionOrConstructorCall(node.parent) &&
|
96
96
|
node === node.parent.arguments[0]) ||
|
97
97
|
|
98
|
-
(BOOLEAN_NODE_TYPES.
|
98
|
+
(BOOLEAN_NODE_TYPES.has(node.parent.type) &&
|
99
99
|
node === node.parent.test) ||
|
100
100
|
|
101
101
|
// !<bool>
|
@@ -78,7 +78,7 @@ module.exports = {
|
|
78
78
|
* @returns {void}
|
79
79
|
*/
|
80
80
|
function checkVariable(variable) {
|
81
|
-
if (variable.writeable === false && exceptions.
|
81
|
+
if (variable.writeable === false && !exceptions.includes(variable.name)) {
|
82
82
|
variable.references.forEach(checkReference);
|
83
83
|
}
|
84
84
|
}
|
@@ -30,9 +30,9 @@ function parseOptions(options) {
|
|
30
30
|
}
|
31
31
|
|
32
32
|
/**
|
33
|
-
* Checks whether or not a node is a double logical
|
33
|
+
* Checks whether or not a node is a double logical negating.
|
34
34
|
* @param {ASTNode} node An UnaryExpression node to check.
|
35
|
-
* @returns {boolean} Whether or not the node is a double logical
|
35
|
+
* @returns {boolean} Whether or not the node is a double logical negating.
|
36
36
|
*/
|
37
37
|
function isDoubleLogicalNegating(node) {
|
38
38
|
return (
|
@@ -257,7 +257,7 @@ module.exports = {
|
|
257
257
|
let operatorAllowed;
|
258
258
|
|
259
259
|
// !!foo
|
260
|
-
operatorAllowed = options.allow.
|
260
|
+
operatorAllowed = options.allow.includes("!!");
|
261
261
|
if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) {
|
262
262
|
const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`;
|
263
263
|
|
@@ -265,7 +265,7 @@ module.exports = {
|
|
265
265
|
}
|
266
266
|
|
267
267
|
// ~foo.indexOf(bar)
|
268
|
-
operatorAllowed = options.allow.
|
268
|
+
operatorAllowed = options.allow.includes("~");
|
269
269
|
if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) {
|
270
270
|
|
271
271
|
// `foo?.indexOf(bar) !== -1` will be true (== found) if the `foo` is nullish. So use `>= 0` in that case.
|
@@ -276,7 +276,7 @@ module.exports = {
|
|
276
276
|
}
|
277
277
|
|
278
278
|
// +foo
|
279
|
-
operatorAllowed = options.allow.
|
279
|
+
operatorAllowed = options.allow.includes("+");
|
280
280
|
if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) {
|
281
281
|
const recommendation = `Number(${sourceCode.getText(node.argument)})`;
|
282
282
|
|
@@ -289,7 +289,7 @@ module.exports = {
|
|
289
289
|
let operatorAllowed;
|
290
290
|
|
291
291
|
// 1 * foo
|
292
|
-
operatorAllowed = options.allow.
|
292
|
+
operatorAllowed = options.allow.includes("*");
|
293
293
|
const nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && getNonNumericOperand(node);
|
294
294
|
|
295
295
|
if (nonNumericOperand) {
|
@@ -299,7 +299,7 @@ module.exports = {
|
|
299
299
|
}
|
300
300
|
|
301
301
|
// "" + foo
|
302
|
-
operatorAllowed = options.allow.
|
302
|
+
operatorAllowed = options.allow.includes("+");
|
303
303
|
if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) {
|
304
304
|
const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`;
|
305
305
|
|
@@ -310,7 +310,7 @@ module.exports = {
|
|
310
310
|
AssignmentExpression(node) {
|
311
311
|
|
312
312
|
// foo += ""
|
313
|
-
const operatorAllowed = options.allow.
|
313
|
+
const operatorAllowed = options.allow.includes("+");
|
314
314
|
|
315
315
|
if (!operatorAllowed && options.string && isAppendEmptyString(node)) {
|
316
316
|
const code = sourceCode.getText(getNonEmptyOperand(node));
|
@@ -125,7 +125,7 @@ function isSafe(loopNode, reference) {
|
|
125
125
|
* The reference is every reference of the upper scope's variable we are
|
126
126
|
* looking now.
|
127
127
|
*
|
128
|
-
* It's
|
128
|
+
* It's safe if the reference matches one of the following condition.
|
129
129
|
* - is readonly.
|
130
130
|
* - doesn't exist inside a local function and after the border.
|
131
131
|
* @param {eslint-scope.Reference} upperRef A reference to check.
|
@@ -80,7 +80,7 @@ module.exports = {
|
|
80
80
|
const config = context.options[0] || {},
|
81
81
|
detectObjects = !!config.detectObjects,
|
82
82
|
enforceConst = !!config.enforceConst,
|
83
|
-
ignore = (config.ignore || []).map(normalizeIgnoreValue),
|
83
|
+
ignore = new Set((config.ignore || []).map(normalizeIgnoreValue)),
|
84
84
|
ignoreArrayIndexes = !!config.ignoreArrayIndexes,
|
85
85
|
ignoreDefaultValues = !!config.ignoreDefaultValues;
|
86
86
|
|
@@ -92,7 +92,7 @@ module.exports = {
|
|
92
92
|
* @returns {boolean} true if the value is ignored
|
93
93
|
*/
|
94
94
|
function isIgnoredValue(value) {
|
95
|
-
return ignore.
|
95
|
+
return ignore.has(value);
|
96
96
|
}
|
97
97
|
|
98
98
|
/**
|
@@ -209,7 +209,7 @@ module.exports = {
|
|
209
209
|
});
|
210
210
|
}
|
211
211
|
} else if (
|
212
|
-
okTypes.
|
212
|
+
!okTypes.includes(parent.type) ||
|
213
213
|
(parent.type === "AssignmentExpression" && parent.left.type === "Identifier")
|
214
214
|
) {
|
215
215
|
context.report({
|
@@ -4,13 +4,16 @@
|
|
4
4
|
"use strict";
|
5
5
|
|
6
6
|
const { CALL, CONSTRUCT, ReferenceTracker, getStringIfConstant } = require("eslint-utils");
|
7
|
-
const { RegExpParser, visitRegExpAST } = require("regexpp");
|
7
|
+
const { RegExpValidator, RegExpParser, visitRegExpAST } = require("regexpp");
|
8
8
|
const { isCombiningCharacter, isEmojiModifier, isRegionalIndicatorSymbol, isSurrogatePair } = require("./utils/unicode");
|
9
|
+
const astUtils = require("./utils/ast-utils.js");
|
9
10
|
|
10
11
|
//------------------------------------------------------------------------------
|
11
12
|
// Helpers
|
12
13
|
//------------------------------------------------------------------------------
|
13
14
|
|
15
|
+
const REGEXPP_LATEST_ECMA_VERSION = 2022;
|
16
|
+
|
14
17
|
/**
|
15
18
|
* Iterate character sequences of a given nodes.
|
16
19
|
*
|
@@ -109,6 +112,8 @@ module.exports = {
|
|
109
112
|
url: "https://eslint.org/docs/rules/no-misleading-character-class"
|
110
113
|
},
|
111
114
|
|
115
|
+
hasSuggestions: true,
|
116
|
+
|
112
117
|
schema: [],
|
113
118
|
|
114
119
|
messages: {
|
@@ -116,10 +121,12 @@ module.exports = {
|
|
116
121
|
combiningClass: "Unexpected combined character in character class.",
|
117
122
|
emojiModifier: "Unexpected modified Emoji in character class.",
|
118
123
|
regionalIndicatorSymbol: "Unexpected national flag in character class.",
|
119
|
-
zwj: "Unexpected joined character sequence in character class."
|
124
|
+
zwj: "Unexpected joined character sequence in character class.",
|
125
|
+
suggestUnicodeFlag: "Add unicode 'u' flag to regex."
|
120
126
|
}
|
121
127
|
},
|
122
128
|
create(context) {
|
129
|
+
const sourceCode = context.getSourceCode();
|
123
130
|
const parser = new RegExpParser();
|
124
131
|
|
125
132
|
/**
|
@@ -127,17 +134,10 @@ module.exports = {
|
|
127
134
|
* @param {Node} node The node to report.
|
128
135
|
* @param {string} pattern The regular expression pattern to verify.
|
129
136
|
* @param {string} flags The flags of the regular expression.
|
137
|
+
* @param {Function} unicodeFixer Fixer for missing "u" flag.
|
130
138
|
* @returns {void}
|
131
139
|
*/
|
132
|
-
function verify(node, pattern, flags) {
|
133
|
-
const has = {
|
134
|
-
surrogatePairWithoutUFlag: false,
|
135
|
-
combiningClass: false,
|
136
|
-
variationSelector: false,
|
137
|
-
emojiModifier: false,
|
138
|
-
regionalIndicatorSymbol: false,
|
139
|
-
zwj: false
|
140
|
-
};
|
140
|
+
function verify(node, pattern, flags, unicodeFixer) {
|
141
141
|
let patternNode;
|
142
142
|
|
143
143
|
try {
|
@@ -153,26 +153,75 @@ module.exports = {
|
|
153
153
|
return;
|
154
154
|
}
|
155
155
|
|
156
|
+
const foundKinds = new Set();
|
157
|
+
|
156
158
|
visitRegExpAST(patternNode, {
|
157
159
|
onCharacterClassEnter(ccNode) {
|
158
160
|
for (const chars of iterateCharacterSequence(ccNode.elements)) {
|
159
161
|
for (const kind of kinds) {
|
160
|
-
|
162
|
+
if (hasCharacterSequence[kind](chars)) {
|
163
|
+
foundKinds.add(kind);
|
164
|
+
}
|
161
165
|
}
|
162
166
|
}
|
163
167
|
}
|
164
168
|
});
|
165
169
|
|
166
|
-
for (const kind of
|
167
|
-
|
168
|
-
|
170
|
+
for (const kind of foundKinds) {
|
171
|
+
let suggest;
|
172
|
+
|
173
|
+
if (kind === "surrogatePairWithoutUFlag") {
|
174
|
+
suggest = [{
|
175
|
+
messageId: "suggestUnicodeFlag",
|
176
|
+
fix: unicodeFixer
|
177
|
+
}];
|
169
178
|
}
|
179
|
+
|
180
|
+
context.report({
|
181
|
+
node,
|
182
|
+
messageId: kind,
|
183
|
+
suggest
|
184
|
+
});
|
170
185
|
}
|
171
186
|
}
|
172
187
|
|
188
|
+
/**
|
189
|
+
* Checks if the given regular expression pattern would be valid with the `u` flag.
|
190
|
+
* @param {string} pattern The regular expression pattern to verify.
|
191
|
+
* @returns {boolean} `true` if the pattern would be valid with the `u` flag.
|
192
|
+
* `false` if the pattern would be invalid with the `u` flag or the configured
|
193
|
+
* ecmaVersion doesn't support the `u` flag.
|
194
|
+
*/
|
195
|
+
function isValidWithUnicodeFlag(pattern) {
|
196
|
+
const { ecmaVersion } = context.parserOptions;
|
197
|
+
|
198
|
+
// ecmaVersion is unknown or it doesn't support the 'u' flag
|
199
|
+
if (typeof ecmaVersion !== "number" || ecmaVersion <= 5) {
|
200
|
+
return false;
|
201
|
+
}
|
202
|
+
|
203
|
+
const validator = new RegExpValidator({
|
204
|
+
ecmaVersion: Math.min(ecmaVersion + 2009, REGEXPP_LATEST_ECMA_VERSION)
|
205
|
+
});
|
206
|
+
|
207
|
+
try {
|
208
|
+
validator.validatePattern(pattern, void 0, void 0, /* uFlag = */ true);
|
209
|
+
} catch {
|
210
|
+
return false;
|
211
|
+
}
|
212
|
+
|
213
|
+
return true;
|
214
|
+
}
|
215
|
+
|
173
216
|
return {
|
174
217
|
"Literal[regex]"(node) {
|
175
|
-
verify(node, node.regex.pattern, node.regex.flags
|
218
|
+
verify(node, node.regex.pattern, node.regex.flags, fixer => {
|
219
|
+
if (!isValidWithUnicodeFlag(node.regex.pattern)) {
|
220
|
+
return null;
|
221
|
+
}
|
222
|
+
|
223
|
+
return fixer.insertTextAfter(node, "u");
|
224
|
+
});
|
176
225
|
},
|
177
226
|
"Program"() {
|
178
227
|
const scope = context.getScope();
|
@@ -191,7 +240,31 @@ module.exports = {
|
|
191
240
|
const flags = getStringIfConstant(flagsNode, scope);
|
192
241
|
|
193
242
|
if (typeof pattern === "string") {
|
194
|
-
verify(node, pattern, flags || ""
|
243
|
+
verify(node, pattern, flags || "", fixer => {
|
244
|
+
|
245
|
+
if (!isValidWithUnicodeFlag(pattern)) {
|
246
|
+
return null;
|
247
|
+
}
|
248
|
+
|
249
|
+
if (node.arguments.length === 1) {
|
250
|
+
const penultimateToken = sourceCode.getLastToken(node, { skip: 1 }); // skip closing parenthesis
|
251
|
+
|
252
|
+
return fixer.insertTextAfter(
|
253
|
+
penultimateToken,
|
254
|
+
astUtils.isCommaToken(penultimateToken)
|
255
|
+
? ' "u",'
|
256
|
+
: ', "u"'
|
257
|
+
);
|
258
|
+
}
|
259
|
+
|
260
|
+
if ((flagsNode.type === "Literal" && typeof flagsNode.value === "string") || flagsNode.type === "TemplateLiteral") {
|
261
|
+
const range = [flagsNode.range[0], flagsNode.range[1] - 1];
|
262
|
+
|
263
|
+
return fixer.insertTextAfterRange(range, "u");
|
264
|
+
}
|
265
|
+
|
266
|
+
return null;
|
267
|
+
});
|
195
268
|
}
|
196
269
|
}
|
197
270
|
}
|
@@ -64,7 +64,7 @@ function normalizeOptions(options = {}) {
|
|
64
64
|
* @returns {boolean} `true` if such group existed.
|
65
65
|
*/
|
66
66
|
function includesBothInAGroup(groups, left, right) {
|
67
|
-
return groups.some(group => group.
|
67
|
+
return groups.some(group => group.includes(left) && group.includes(right));
|
68
68
|
}
|
69
69
|
|
70
70
|
/**
|
@@ -56,7 +56,7 @@ module.exports = {
|
|
56
56
|
const options = context.options[0] || {};
|
57
57
|
const ignoreEOLComments = options.ignoreEOLComments;
|
58
58
|
const exceptions = Object.assign({ Property: true }, options.exceptions);
|
59
|
-
const hasExceptions = Object.keys(exceptions).
|
59
|
+
const hasExceptions = Object.keys(exceptions).some(key => exceptions[key]);
|
60
60
|
|
61
61
|
/**
|
62
62
|
* Formats value of given comment token for error message by truncating its length.
|
@@ -81,7 +81,7 @@ module.exports = {
|
|
81
81
|
* @returns {void}
|
82
82
|
*/
|
83
83
|
function checkVariable(variable) {
|
84
|
-
if (variable.writeable === false && exceptions.
|
84
|
+
if (variable.writeable === false && !exceptions.includes(variable.name)) {
|
85
85
|
variable.references.forEach(checkReference);
|
86
86
|
}
|
87
87
|
}
|