eslint 6.2.2 → 6.5.1
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/CHANGELOG.md +91 -0
- package/README.md +9 -8
- package/bin/eslint.js +38 -12
- package/conf/config-schema.js +1 -0
- package/conf/default-cli-options.js +1 -1
- package/lib/cli-engine/cli-engine.js +2 -4
- package/lib/cli-engine/config-array/config-array.js +6 -0
- package/lib/cli-engine/config-array/extracted-config.js +6 -0
- package/lib/cli-engine/config-array/override-tester.js +2 -2
- package/lib/cli-engine/config-array-factory.js +2 -0
- package/lib/cli-engine/formatters/stylish.js +2 -1
- package/lib/cli-engine/ignored-paths.js +3 -3
- package/lib/cli-engine/lint-result-cache.js +0 -1
- package/lib/cli.js +13 -12
- package/lib/init/config-initializer.js +29 -0
- package/lib/init/config-rule.js +2 -2
- package/lib/init/npm-utils.js +9 -9
- package/lib/linter/apply-disable-directives.js +17 -9
- package/lib/linter/code-path-analysis/debug-helpers.js +1 -1
- package/lib/linter/linter.js +23 -4
- package/lib/options.js +7 -1
- package/lib/rule-tester/rule-tester.js +1 -2
- package/lib/rules/accessor-pairs.js +51 -11
- package/lib/rules/capitalized-comments.js +2 -2
- package/lib/rules/computed-property-spacing.js +18 -1
- package/lib/rules/default-param-last.js +61 -0
- package/lib/rules/eqeqeq.js +7 -19
- package/lib/rules/func-name-matching.js +1 -0
- package/lib/rules/function-paren-newline.js +2 -2
- package/lib/rules/indent-legacy.js +1 -1
- package/lib/rules/indent.js +44 -6
- package/lib/rules/index.js +3 -0
- package/lib/rules/new-parens.js +5 -1
- package/lib/rules/no-extra-bind.js +7 -1
- package/lib/rules/no-extra-boolean-cast.js +12 -2
- package/lib/rules/no-extra-label.js +9 -1
- package/lib/rules/no-extra-parens.js +23 -3
- package/lib/rules/no-import-assign.js +238 -0
- package/lib/rules/no-lone-blocks.js +6 -1
- package/lib/rules/no-obj-calls.js +29 -9
- package/lib/rules/no-octal-escape.js +14 -8
- package/lib/rules/no-regex-spaces.js +106 -45
- package/lib/rules/no-self-assign.js +17 -6
- package/lib/rules/no-sequences.js +2 -2
- package/lib/rules/no-undef-init.js +7 -1
- package/lib/rules/no-unsafe-negation.js +2 -10
- package/lib/rules/no-useless-rename.js +25 -13
- package/lib/rules/no-useless-return.js +3 -2
- package/lib/rules/object-curly-spacing.js +1 -1
- package/lib/rules/object-shorthand.js +35 -9
- package/lib/rules/prefer-named-capture-group.js +3 -15
- package/lib/rules/prefer-numeric-literals.js +4 -0
- package/lib/rules/prefer-regex-literals.js +125 -0
- package/lib/rules/quotes.js +6 -0
- package/lib/rules/space-before-function-paren.js +12 -1
- package/lib/rules/space-in-parens.js +77 -71
- package/lib/rules/use-isnan.js +67 -6
- package/lib/rules/yoda.js +11 -2
- package/lib/shared/logging.js +2 -0
- package/lib/shared/runtime-info.js +163 -0
- package/lib/shared/types.js +2 -0
- package/lib/source-code/source-code.js +3 -4
- package/package.json +3 -1
@@ -116,7 +116,15 @@ module.exports = {
|
|
116
116
|
node: labelNode,
|
117
117
|
messageId: "unexpected",
|
118
118
|
data: labelNode,
|
119
|
-
fix
|
119
|
+
fix(fixer) {
|
120
|
+
const breakOrContinueToken = sourceCode.getFirstToken(node);
|
121
|
+
|
122
|
+
if (sourceCode.commentsExistBetween(breakOrContinueToken, labelNode)) {
|
123
|
+
return null;
|
124
|
+
}
|
125
|
+
|
126
|
+
return fixer.removeRange([breakOrContinueToken.range[1], labelNode.range[1]]);
|
127
|
+
}
|
120
128
|
});
|
121
129
|
}
|
122
130
|
return;
|
@@ -49,7 +49,8 @@ module.exports = {
|
|
49
49
|
nestedBinaryExpressions: { type: "boolean" },
|
50
50
|
returnAssign: { type: "boolean" },
|
51
51
|
ignoreJSX: { enum: ["none", "all", "single-line", "multi-line"] },
|
52
|
-
enforceForArrowConditionals: { type: "boolean" }
|
52
|
+
enforceForArrowConditionals: { type: "boolean" },
|
53
|
+
enforceForSequenceExpressions: { type: "boolean" }
|
53
54
|
},
|
54
55
|
additionalProperties: false
|
55
56
|
}
|
@@ -77,6 +78,8 @@ module.exports = {
|
|
77
78
|
const IGNORE_JSX = ALL_NODES && context.options[1] && context.options[1].ignoreJSX;
|
78
79
|
const IGNORE_ARROW_CONDITIONALS = ALL_NODES && context.options[1] &&
|
79
80
|
context.options[1].enforceForArrowConditionals === false;
|
81
|
+
const IGNORE_SEQUENCE_EXPRESSIONS = ALL_NODES && context.options[1] &&
|
82
|
+
context.options[1].enforceForSequenceExpressions === false;
|
80
83
|
|
81
84
|
const PRECEDENCE_OF_ASSIGNMENT_EXPR = precedence({ type: "AssignmentExpression" });
|
82
85
|
const PRECEDENCE_OF_UPDATE_EXPR = precedence({ type: "UpdateExpression" });
|
@@ -115,6 +118,10 @@ module.exports = {
|
|
115
118
|
}
|
116
119
|
}
|
117
120
|
|
121
|
+
if (node.type === "SequenceExpression" && IGNORE_SEQUENCE_EXPRESSIONS) {
|
122
|
+
return false;
|
123
|
+
}
|
124
|
+
|
118
125
|
return ALL_NODES || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
|
119
126
|
}
|
120
127
|
|
@@ -198,7 +205,14 @@ module.exports = {
|
|
198
205
|
const lastToken = sourceCode.getLastToken(newExpression);
|
199
206
|
const penultimateToken = sourceCode.getTokenBefore(lastToken);
|
200
207
|
|
201
|
-
return newExpression.arguments.length > 0 ||
|
208
|
+
return newExpression.arguments.length > 0 ||
|
209
|
+
(
|
210
|
+
|
211
|
+
// The expression should end with its own parens, e.g., new new foo() is not a new expression with parens
|
212
|
+
astUtils.isOpeningParenToken(penultimateToken) &&
|
213
|
+
astUtils.isClosingParenToken(lastToken) &&
|
214
|
+
newExpression.callee.range[1] < newExpression.range[1]
|
215
|
+
);
|
202
216
|
}
|
203
217
|
|
204
218
|
/**
|
@@ -331,7 +345,7 @@ module.exports = {
|
|
331
345
|
function finishReport() {
|
332
346
|
context.report({
|
333
347
|
node,
|
334
|
-
loc: leftParenToken.loc
|
348
|
+
loc: leftParenToken.loc,
|
335
349
|
messageId: "unexpected",
|
336
350
|
fix(fixer) {
|
337
351
|
const parenthesizedSource = sourceCode.text.slice(leftParenToken.range[1], rightParenToken.range[0]);
|
@@ -880,6 +894,12 @@ module.exports = {
|
|
880
894
|
report(node.object);
|
881
895
|
}
|
882
896
|
|
897
|
+
if (nodeObjHasExcessParens &&
|
898
|
+
node.object.type === "NewExpression" &&
|
899
|
+
isNewExpressionWithParens(node.object)) {
|
900
|
+
report(node.object);
|
901
|
+
}
|
902
|
+
|
883
903
|
if (node.computed && hasExcessParens(node.property)) {
|
884
904
|
report(node.property);
|
885
905
|
}
|
@@ -0,0 +1,238 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview Rule to flag updates of imported bindings.
|
3
|
+
* @author Toru Nagashima <https://github.com/mysticatea>
|
4
|
+
*/
|
5
|
+
|
6
|
+
"use strict";
|
7
|
+
|
8
|
+
//------------------------------------------------------------------------------
|
9
|
+
// Helpers
|
10
|
+
//------------------------------------------------------------------------------
|
11
|
+
|
12
|
+
const { findVariable, getPropertyName } = require("eslint-utils");
|
13
|
+
|
14
|
+
const MutationMethods = {
|
15
|
+
Object: new Set([
|
16
|
+
"assign", "defineProperties", "defineProperty", "freeze",
|
17
|
+
"setPrototypeOf"
|
18
|
+
]),
|
19
|
+
Reflect: new Set([
|
20
|
+
"defineProperty", "deleteProperty", "set", "setPrototypeOf"
|
21
|
+
])
|
22
|
+
};
|
23
|
+
|
24
|
+
/**
|
25
|
+
* Check if a given node is LHS of an assignment node.
|
26
|
+
* @param {ASTNode} node The node to check.
|
27
|
+
* @returns {boolean} `true` if the node is LHS.
|
28
|
+
*/
|
29
|
+
function isAssignmentLeft(node) {
|
30
|
+
const { parent } = node;
|
31
|
+
|
32
|
+
return (
|
33
|
+
(
|
34
|
+
parent.type === "AssignmentExpression" &&
|
35
|
+
parent.left === node
|
36
|
+
) ||
|
37
|
+
|
38
|
+
// Destructuring assignments
|
39
|
+
parent.type === "ArrayPattern" ||
|
40
|
+
(
|
41
|
+
parent.type === "Property" &&
|
42
|
+
parent.value === node &&
|
43
|
+
parent.parent.type === "ObjectPattern"
|
44
|
+
) ||
|
45
|
+
parent.type === "RestElement" ||
|
46
|
+
(
|
47
|
+
parent.type === "AssignmentPattern" &&
|
48
|
+
parent.left === node
|
49
|
+
)
|
50
|
+
);
|
51
|
+
}
|
52
|
+
|
53
|
+
/**
|
54
|
+
* Check if a given node is the operand of mutation unary operator.
|
55
|
+
* @param {ASTNode} node The node to check.
|
56
|
+
* @returns {boolean} `true` if the node is the operand of mutation unary operator.
|
57
|
+
*/
|
58
|
+
function isOperandOfMutationUnaryOperator(node) {
|
59
|
+
const { parent } = node;
|
60
|
+
|
61
|
+
return (
|
62
|
+
(
|
63
|
+
parent.type === "UpdateExpression" &&
|
64
|
+
parent.argument === node
|
65
|
+
) ||
|
66
|
+
(
|
67
|
+
parent.type === "UnaryExpression" &&
|
68
|
+
parent.operator === "delete" &&
|
69
|
+
parent.argument === node
|
70
|
+
)
|
71
|
+
);
|
72
|
+
}
|
73
|
+
|
74
|
+
/**
|
75
|
+
* Check if a given node is the iteration variable of `for-in`/`for-of` syntax.
|
76
|
+
* @param {ASTNode} node The node to check.
|
77
|
+
* @returns {boolean} `true` if the node is the iteration variable.
|
78
|
+
*/
|
79
|
+
function isIterationVariable(node) {
|
80
|
+
const { parent } = node;
|
81
|
+
|
82
|
+
return (
|
83
|
+
(
|
84
|
+
parent.type === "ForInStatement" &&
|
85
|
+
parent.left === node
|
86
|
+
) ||
|
87
|
+
(
|
88
|
+
parent.type === "ForOfStatement" &&
|
89
|
+
parent.left === node
|
90
|
+
)
|
91
|
+
);
|
92
|
+
}
|
93
|
+
|
94
|
+
/**
|
95
|
+
* Check if a given node is the iteration variable of `for-in`/`for-of` syntax.
|
96
|
+
* @param {ASTNode} node The node to check.
|
97
|
+
* @param {Scope} scope A `escope.Scope` object to find variable (whichever).
|
98
|
+
* @returns {boolean} `true` if the node is the iteration variable.
|
99
|
+
*/
|
100
|
+
function isArgumentOfWellKnownMutationFunction(node, scope) {
|
101
|
+
const { parent } = node;
|
102
|
+
|
103
|
+
if (
|
104
|
+
parent.type === "CallExpression" &&
|
105
|
+
parent.arguments[0] === node &&
|
106
|
+
parent.callee.type === "MemberExpression" &&
|
107
|
+
parent.callee.object.type === "Identifier"
|
108
|
+
) {
|
109
|
+
const { callee } = parent;
|
110
|
+
const { object } = callee;
|
111
|
+
|
112
|
+
if (Object.keys(MutationMethods).includes(object.name)) {
|
113
|
+
const variable = findVariable(scope, object);
|
114
|
+
|
115
|
+
return (
|
116
|
+
variable !== null &&
|
117
|
+
variable.scope.type === "global" &&
|
118
|
+
MutationMethods[object.name].has(getPropertyName(callee, scope))
|
119
|
+
);
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
return false;
|
124
|
+
}
|
125
|
+
|
126
|
+
/**
|
127
|
+
* Check if the identifier node is placed at to update members.
|
128
|
+
* @param {ASTNode} id The Identifier node to check.
|
129
|
+
* @param {Scope} scope A `escope.Scope` object to find variable (whichever).
|
130
|
+
* @returns {boolean} `true` if the member of `id` was updated.
|
131
|
+
*/
|
132
|
+
function isMemberWrite(id, scope) {
|
133
|
+
const { parent } = id;
|
134
|
+
|
135
|
+
return (
|
136
|
+
(
|
137
|
+
parent.type === "MemberExpression" &&
|
138
|
+
parent.object === id &&
|
139
|
+
(
|
140
|
+
isAssignmentLeft(parent) ||
|
141
|
+
isOperandOfMutationUnaryOperator(parent) ||
|
142
|
+
isIterationVariable(parent)
|
143
|
+
)
|
144
|
+
) ||
|
145
|
+
isArgumentOfWellKnownMutationFunction(id, scope)
|
146
|
+
);
|
147
|
+
}
|
148
|
+
|
149
|
+
/**
|
150
|
+
* Get the mutation node.
|
151
|
+
* @param {ASTNode} id The Identifier node to get.
|
152
|
+
* @returns {ASTNode} The mutation node.
|
153
|
+
*/
|
154
|
+
function getWriteNode(id) {
|
155
|
+
let node = id.parent;
|
156
|
+
|
157
|
+
while (
|
158
|
+
node &&
|
159
|
+
node.type !== "AssignmentExpression" &&
|
160
|
+
node.type !== "UpdateExpression" &&
|
161
|
+
node.type !== "UnaryExpression" &&
|
162
|
+
node.type !== "CallExpression" &&
|
163
|
+
node.type !== "ForInStatement" &&
|
164
|
+
node.type !== "ForOfStatement"
|
165
|
+
) {
|
166
|
+
node = node.parent;
|
167
|
+
}
|
168
|
+
|
169
|
+
return node || id;
|
170
|
+
}
|
171
|
+
|
172
|
+
//------------------------------------------------------------------------------
|
173
|
+
// Rule Definition
|
174
|
+
//------------------------------------------------------------------------------
|
175
|
+
|
176
|
+
module.exports = {
|
177
|
+
meta: {
|
178
|
+
type: "problem",
|
179
|
+
|
180
|
+
docs: {
|
181
|
+
description: "disallow assigning to imported bindings",
|
182
|
+
category: "Possible Errors",
|
183
|
+
recommended: false,
|
184
|
+
url: "https://eslint.org/docs/rules/no-import-assign"
|
185
|
+
},
|
186
|
+
|
187
|
+
schema: [],
|
188
|
+
|
189
|
+
messages: {
|
190
|
+
readonly: "'{{name}}' is read-only.",
|
191
|
+
readonlyMember: "The members of '{{name}}' are read-only."
|
192
|
+
}
|
193
|
+
},
|
194
|
+
|
195
|
+
create(context) {
|
196
|
+
return {
|
197
|
+
ImportDeclaration(node) {
|
198
|
+
const scope = context.getScope();
|
199
|
+
|
200
|
+
for (const variable of context.getDeclaredVariables(node)) {
|
201
|
+
const shouldCheckMembers = variable.defs.some(
|
202
|
+
d => d.node.type === "ImportNamespaceSpecifier"
|
203
|
+
);
|
204
|
+
let prevIdNode = null;
|
205
|
+
|
206
|
+
for (const reference of variable.references) {
|
207
|
+
const idNode = reference.identifier;
|
208
|
+
|
209
|
+
/*
|
210
|
+
* AssignmentPattern (e.g. `[a = 0] = b`) makes two write
|
211
|
+
* references for the same identifier. This should skip
|
212
|
+
* the one of the two in order to prevent redundant reports.
|
213
|
+
*/
|
214
|
+
if (idNode === prevIdNode) {
|
215
|
+
continue;
|
216
|
+
}
|
217
|
+
prevIdNode = idNode;
|
218
|
+
|
219
|
+
if (reference.isWrite()) {
|
220
|
+
context.report({
|
221
|
+
node: getWriteNode(idNode),
|
222
|
+
messageId: "readonly",
|
223
|
+
data: { name: idNode.name }
|
224
|
+
});
|
225
|
+
} else if (shouldCheckMembers && isMemberWrite(idNode, scope)) {
|
226
|
+
context.report({
|
227
|
+
node: getWriteNode(idNode),
|
228
|
+
messageId: "readonlyMember",
|
229
|
+
data: { name: idNode.name }
|
230
|
+
});
|
231
|
+
}
|
232
|
+
}
|
233
|
+
}
|
234
|
+
}
|
235
|
+
};
|
236
|
+
|
237
|
+
}
|
238
|
+
};
|
@@ -79,7 +79,7 @@ module.exports = {
|
|
79
79
|
}
|
80
80
|
};
|
81
81
|
|
82
|
-
// ES6: report blocks without block-level bindings
|
82
|
+
// ES6: report blocks without block-level bindings, or that's only child of another block
|
83
83
|
if (context.parserOptions.ecmaVersion >= 6) {
|
84
84
|
ruleDef = {
|
85
85
|
BlockStatement(node) {
|
@@ -91,6 +91,11 @@ module.exports = {
|
|
91
91
|
if (loneBlocks.length > 0 && loneBlocks[loneBlocks.length - 1] === node) {
|
92
92
|
loneBlocks.pop();
|
93
93
|
report(node);
|
94
|
+
} else if (
|
95
|
+
node.parent.type === "BlockStatement" &&
|
96
|
+
node.parent.body.length === 1
|
97
|
+
) {
|
98
|
+
report(node);
|
94
99
|
}
|
95
100
|
}
|
96
101
|
};
|
@@ -5,6 +5,18 @@
|
|
5
5
|
|
6
6
|
"use strict";
|
7
7
|
|
8
|
+
//------------------------------------------------------------------------------
|
9
|
+
// Requirements
|
10
|
+
//------------------------------------------------------------------------------
|
11
|
+
|
12
|
+
const { CALL, ReferenceTracker } = require("eslint-utils");
|
13
|
+
|
14
|
+
//------------------------------------------------------------------------------
|
15
|
+
// Helpers
|
16
|
+
//------------------------------------------------------------------------------
|
17
|
+
|
18
|
+
const nonCallableGlobals = ["Atomics", "JSON", "Math", "Reflect"];
|
19
|
+
|
8
20
|
//------------------------------------------------------------------------------
|
9
21
|
// Rule Definition
|
10
22
|
//------------------------------------------------------------------------------
|
@@ -20,23 +32,31 @@ module.exports = {
|
|
20
32
|
url: "https://eslint.org/docs/rules/no-obj-calls"
|
21
33
|
},
|
22
34
|
|
23
|
-
schema: []
|
35
|
+
schema: [],
|
36
|
+
|
37
|
+
messages: {
|
38
|
+
unexpectedCall: "'{{name}}' is not a function."
|
39
|
+
}
|
24
40
|
},
|
25
41
|
|
26
42
|
create(context) {
|
27
43
|
|
28
44
|
return {
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
45
|
+
Program() {
|
46
|
+
const scope = context.getScope();
|
47
|
+
const tracker = new ReferenceTracker(scope);
|
48
|
+
const traceMap = {};
|
49
|
+
|
50
|
+
for (const global of nonCallableGlobals) {
|
51
|
+
traceMap[global] = {
|
52
|
+
[CALL]: true
|
53
|
+
};
|
54
|
+
}
|
33
55
|
|
34
|
-
|
35
|
-
|
36
|
-
}
|
56
|
+
for (const { node } of tracker.iterateGlobalReferences(traceMap)) {
|
57
|
+
context.report({ node, messageId: "unexpectedCall", data: { name: node.callee.name } });
|
37
58
|
}
|
38
59
|
}
|
39
60
|
};
|
40
|
-
|
41
61
|
}
|
42
62
|
};
|
@@ -20,7 +20,11 @@ module.exports = {
|
|
20
20
|
url: "https://eslint.org/docs/rules/no-octal-escape"
|
21
21
|
},
|
22
22
|
|
23
|
-
schema: []
|
23
|
+
schema: [],
|
24
|
+
|
25
|
+
messages: {
|
26
|
+
octalEscapeSequence: "Don't use octal: '\\{{sequence}}'. Use '\\u....' instead."
|
27
|
+
}
|
24
28
|
},
|
25
29
|
|
26
30
|
create(context) {
|
@@ -32,15 +36,17 @@ module.exports = {
|
|
32
36
|
return;
|
33
37
|
}
|
34
38
|
|
35
|
-
|
39
|
+
// \0 represents a valid NULL character if it isn't followed by a digit.
|
40
|
+
const match = node.raw.match(
|
41
|
+
/^(?:[^\\]|\\.)*?\\([0-3][0-7]{1,2}|[4-7][0-7]|[1-7])/u
|
42
|
+
);
|
36
43
|
|
37
44
|
if (match) {
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
}
|
45
|
+
context.report({
|
46
|
+
node,
|
47
|
+
messageId: "octalEscapeSequence",
|
48
|
+
data: { sequence: match[1] }
|
49
|
+
});
|
44
50
|
}
|
45
51
|
}
|
46
52
|
|
@@ -5,7 +5,29 @@
|
|
5
5
|
|
6
6
|
"use strict";
|
7
7
|
|
8
|
+
//------------------------------------------------------------------------------
|
9
|
+
// Requirements
|
10
|
+
//------------------------------------------------------------------------------
|
11
|
+
|
8
12
|
const astUtils = require("./utils/ast-utils");
|
13
|
+
const regexpp = require("regexpp");
|
14
|
+
|
15
|
+
//------------------------------------------------------------------------------
|
16
|
+
// Helpers
|
17
|
+
//------------------------------------------------------------------------------
|
18
|
+
|
19
|
+
const regExpParser = new regexpp.RegExpParser();
|
20
|
+
const DOUBLE_SPACE = / {2}/u;
|
21
|
+
|
22
|
+
/**
|
23
|
+
* Check if node is a string
|
24
|
+
* @param {ASTNode} node node to evaluate
|
25
|
+
* @returns {boolean} True if its a string
|
26
|
+
* @private
|
27
|
+
*/
|
28
|
+
function isString(node) {
|
29
|
+
return node && node.type === "Literal" && typeof node.value === "string";
|
30
|
+
}
|
9
31
|
|
10
32
|
//------------------------------------------------------------------------------
|
11
33
|
// Rule Definition
|
@@ -27,40 +49,70 @@ module.exports = {
|
|
27
49
|
},
|
28
50
|
|
29
51
|
create(context) {
|
30
|
-
const sourceCode = context.getSourceCode();
|
31
52
|
|
32
53
|
/**
|
33
|
-
* Validate regular
|
34
|
-
*
|
35
|
-
* @param {
|
36
|
-
* @param {
|
37
|
-
*
|
54
|
+
* Validate regular expression
|
55
|
+
*
|
56
|
+
* @param {ASTNode} nodeToReport Node to report.
|
57
|
+
* @param {string} pattern Regular expression pattern to validate.
|
58
|
+
* @param {string} rawPattern Raw representation of the pattern in the source code.
|
59
|
+
* @param {number} rawPatternStartRange Start range of the pattern in the source code.
|
60
|
+
* @param {string} flags Regular expression flags.
|
38
61
|
* @returns {void}
|
39
62
|
* @private
|
40
63
|
*/
|
41
|
-
function checkRegex(
|
42
|
-
const multipleSpacesRegex = /( {2,})( [+*{?]|[^+*{?]|$)/u,
|
43
|
-
regexResults = multipleSpacesRegex.exec(value);
|
64
|
+
function checkRegex(nodeToReport, pattern, rawPattern, rawPatternStartRange, flags) {
|
44
65
|
|
45
|
-
if (
|
46
|
-
|
66
|
+
// Skip if there are no consecutive spaces in the source code, to avoid reporting e.g., RegExp(' \ ').
|
67
|
+
if (!DOUBLE_SPACE.test(rawPattern)) {
|
68
|
+
return;
|
69
|
+
}
|
47
70
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
71
|
+
const characterClassNodes = [];
|
72
|
+
let regExpAST;
|
73
|
+
|
74
|
+
try {
|
75
|
+
regExpAST = regExpParser.parsePattern(pattern, 0, pattern.length, flags.includes("u"));
|
76
|
+
} catch (e) {
|
77
|
+
|
78
|
+
// Ignore regular expressions with syntax errors
|
79
|
+
return;
|
80
|
+
}
|
81
|
+
|
82
|
+
regexpp.visitRegExpAST(regExpAST, {
|
83
|
+
onCharacterClassEnter(ccNode) {
|
84
|
+
characterClassNodes.push(ccNode);
|
85
|
+
}
|
86
|
+
});
|
87
|
+
|
88
|
+
const spacesPattern = /( {2,})(?: [+*{?]|[^+*{?]|$)/gu;
|
89
|
+
let match;
|
90
|
+
|
91
|
+
while ((match = spacesPattern.exec(pattern))) {
|
92
|
+
const { 1: { length }, index } = match;
|
93
|
+
|
94
|
+
// Report only consecutive spaces that are not in character classes.
|
95
|
+
if (
|
96
|
+
characterClassNodes.every(({ start, end }) => index < start || end <= index)
|
97
|
+
) {
|
98
|
+
context.report({
|
99
|
+
node: nodeToReport,
|
100
|
+
message: "Spaces are hard to count. Use {{{length}}}.",
|
101
|
+
data: { length },
|
102
|
+
fix(fixer) {
|
103
|
+
if (pattern !== rawPattern) {
|
104
|
+
return null;
|
105
|
+
}
|
106
|
+
return fixer.replaceTextRange(
|
107
|
+
[rawPatternStartRange + index, rawPatternStartRange + index + length],
|
108
|
+
` {${length}}`
|
109
|
+
);
|
110
|
+
}
|
111
|
+
});
|
112
|
+
|
113
|
+
// Report only the first occurence of consecutive spaces
|
114
|
+
return;
|
115
|
+
}
|
64
116
|
}
|
65
117
|
}
|
66
118
|
|
@@ -71,25 +123,22 @@ module.exports = {
|
|
71
123
|
* @private
|
72
124
|
*/
|
73
125
|
function checkLiteral(node) {
|
74
|
-
|
75
|
-
|
76
|
-
|
126
|
+
if (node.regex) {
|
127
|
+
const pattern = node.regex.pattern;
|
128
|
+
const rawPattern = node.raw.slice(1, node.raw.lastIndexOf("/"));
|
129
|
+
const rawPatternStartRange = node.range[0] + 1;
|
130
|
+
const flags = node.regex.flags;
|
77
131
|
|
78
|
-
|
79
|
-
|
132
|
+
checkRegex(
|
133
|
+
node,
|
134
|
+
pattern,
|
135
|
+
rawPattern,
|
136
|
+
rawPatternStartRange,
|
137
|
+
flags
|
138
|
+
);
|
80
139
|
}
|
81
140
|
}
|
82
141
|
|
83
|
-
/**
|
84
|
-
* Check if node is a string
|
85
|
-
* @param {ASTNode} node node to evaluate
|
86
|
-
* @returns {boolean} True if its a string
|
87
|
-
* @private
|
88
|
-
*/
|
89
|
-
function isString(node) {
|
90
|
-
return node && node.type === "Literal" && typeof node.value === "string";
|
91
|
-
}
|
92
|
-
|
93
142
|
/**
|
94
143
|
* Validate strings passed to the RegExp constructor
|
95
144
|
* @param {ASTNode} node node to validate
|
@@ -100,9 +149,22 @@ module.exports = {
|
|
100
149
|
const scope = context.getScope();
|
101
150
|
const regExpVar = astUtils.getVariableByName(scope, "RegExp");
|
102
151
|
const shadowed = regExpVar && regExpVar.defs.length > 0;
|
152
|
+
const patternNode = node.arguments[0];
|
153
|
+
const flagsNode = node.arguments[1];
|
103
154
|
|
104
|
-
if (node.callee.type === "Identifier" && node.callee.name === "RegExp" && isString(
|
105
|
-
|
155
|
+
if (node.callee.type === "Identifier" && node.callee.name === "RegExp" && isString(patternNode) && !shadowed) {
|
156
|
+
const pattern = patternNode.value;
|
157
|
+
const rawPattern = patternNode.raw.slice(1, -1);
|
158
|
+
const rawPatternStartRange = patternNode.range[0] + 1;
|
159
|
+
const flags = isString(flagsNode) ? flagsNode.value : "";
|
160
|
+
|
161
|
+
checkRegex(
|
162
|
+
node,
|
163
|
+
pattern,
|
164
|
+
rawPattern,
|
165
|
+
rawPatternStartRange,
|
166
|
+
flags
|
167
|
+
);
|
106
168
|
}
|
107
169
|
}
|
108
170
|
|
@@ -111,6 +173,5 @@ module.exports = {
|
|
111
173
|
CallExpression: checkFunction,
|
112
174
|
NewExpression: checkFunction
|
113
175
|
};
|
114
|
-
|
115
176
|
}
|
116
177
|
};
|
@@ -94,9 +94,19 @@ function eachSelfAssignment(left, right, props, report) {
|
|
94
94
|
const end = Math.min(left.elements.length, right.elements.length);
|
95
95
|
|
96
96
|
for (let i = 0; i < end; ++i) {
|
97
|
+
const leftElement = left.elements[i];
|
97
98
|
const rightElement = right.elements[i];
|
98
99
|
|
99
|
-
|
100
|
+
// Avoid cases such as [...a] = [...a, 1]
|
101
|
+
if (
|
102
|
+
leftElement &&
|
103
|
+
leftElement.type === "RestElement" &&
|
104
|
+
i < right.elements.length - 1
|
105
|
+
) {
|
106
|
+
break;
|
107
|
+
}
|
108
|
+
|
109
|
+
eachSelfAssignment(leftElement, rightElement, props, report);
|
100
110
|
|
101
111
|
// After a spread element, those indices are unknown.
|
102
112
|
if (rightElement && rightElement.type === "SpreadElement") {
|
@@ -142,13 +152,14 @@ function eachSelfAssignment(left, right, props, report) {
|
|
142
152
|
} else if (
|
143
153
|
left.type === "Property" &&
|
144
154
|
right.type === "Property" &&
|
145
|
-
!left.computed &&
|
146
|
-
!right.computed &&
|
147
155
|
right.kind === "init" &&
|
148
|
-
!right.method
|
149
|
-
left.key.name === right.key.name
|
156
|
+
!right.method
|
150
157
|
) {
|
151
|
-
|
158
|
+
const leftName = astUtils.getStaticPropertyName(left);
|
159
|
+
|
160
|
+
if (leftName !== null && leftName === astUtils.getStaticPropertyName(right)) {
|
161
|
+
eachSelfAssignment(left.value, right.value, props, report);
|
162
|
+
}
|
152
163
|
} else if (
|
153
164
|
props &&
|
154
165
|
left.type === "MemberExpression" &&
|