eslint 6.0.0-alpha.2 → 6.1.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/CHANGELOG.md +74 -0
- package/README.md +4 -11
- package/bin/eslint.js +4 -1
- package/lib/cli-engine/cascading-config-array-factory.js +15 -3
- package/lib/cli-engine/cli-engine.js +13 -3
- package/lib/cli-engine/config-array/config-array.js +1 -2
- package/lib/cli-engine/config-array/override-tester.js +11 -1
- package/lib/cli-engine/config-array-factory.js +7 -6
- package/lib/cli-engine/file-enumerator.js +5 -13
- package/lib/cli-engine/formatters/junit.js +14 -2
- package/lib/init/config-initializer.js +19 -9
- package/lib/linter/linter.js +2 -4
- package/lib/linter/node-event-generator.js +6 -4
- package/lib/rule-tester/rule-tester.js +41 -11
- package/lib/rules/arrow-body-style.js +2 -2
- package/lib/rules/arrow-parens.js +21 -0
- package/lib/rules/dot-location.js +21 -17
- package/lib/rules/max-len.js +7 -0
- package/lib/rules/multiline-comment-style.js +2 -1
- package/lib/rules/no-else-return.js +127 -0
- package/lib/rules/no-extra-parens.js +241 -17
- package/lib/rules/no-octal.js +1 -1
- package/lib/rules/no-param-reassign.js +12 -1
- package/lib/rules/no-restricted-imports.js +18 -14
- package/lib/rules/no-useless-escape.js +6 -1
- package/lib/rules/no-var.js +14 -1
- package/lib/rules/prefer-const.js +9 -3
- package/lib/rules/require-atomic-updates.js +63 -84
- package/lib/rules/sort-keys.js +11 -3
- package/lib/rules/utils/ast-utils.js +26 -1
- package/lib/rules/valid-typeof.js +1 -1
- package/lib/{cli-engine → shared}/naming.js +0 -0
- package/messages/extend-config-missing.txt +2 -0
- package/messages/print-config-with-directory-path.txt +2 -0
- package/package.json +24 -27
@@ -10,6 +10,23 @@
|
|
10
10
|
|
11
11
|
const astUtils = require("./utils/ast-utils");
|
12
12
|
|
13
|
+
//------------------------------------------------------------------------------
|
14
|
+
// Helpers
|
15
|
+
//------------------------------------------------------------------------------
|
16
|
+
|
17
|
+
/**
|
18
|
+
* Get location should be reported by AST node.
|
19
|
+
*
|
20
|
+
* @param {ASTNode} node AST Node.
|
21
|
+
* @returns {Location} Location information.
|
22
|
+
*/
|
23
|
+
function getLocation(node) {
|
24
|
+
return {
|
25
|
+
start: node.params[0].loc.start,
|
26
|
+
end: node.params[node.params.length - 1].loc.end
|
27
|
+
};
|
28
|
+
}
|
29
|
+
|
13
30
|
//------------------------------------------------------------------------------
|
14
31
|
// Rule Definition
|
15
32
|
//------------------------------------------------------------------------------
|
@@ -102,6 +119,7 @@ module.exports = {
|
|
102
119
|
context.report({
|
103
120
|
node,
|
104
121
|
messageId: "unexpectedParensInline",
|
122
|
+
loc: getLocation(node),
|
105
123
|
fix: fixParamsWithParenthesis
|
106
124
|
});
|
107
125
|
}
|
@@ -116,6 +134,7 @@ module.exports = {
|
|
116
134
|
context.report({
|
117
135
|
node,
|
118
136
|
messageId: "expectedParensBlock",
|
137
|
+
loc: getLocation(node),
|
119
138
|
fix(fixer) {
|
120
139
|
return fixer.replaceText(firstTokenOfParam, `(${firstTokenOfParam.value})`);
|
121
140
|
}
|
@@ -135,6 +154,7 @@ module.exports = {
|
|
135
154
|
context.report({
|
136
155
|
node,
|
137
156
|
messageId: "unexpectedParens",
|
157
|
+
loc: getLocation(node),
|
138
158
|
fix: fixParamsWithParenthesis
|
139
159
|
});
|
140
160
|
}
|
@@ -149,6 +169,7 @@ module.exports = {
|
|
149
169
|
context.report({
|
150
170
|
node,
|
151
171
|
messageId: "expectedParens",
|
172
|
+
loc: getLocation(node),
|
152
173
|
fix(fixer) {
|
153
174
|
return fixer.replaceText(firstTokenOfParam, `(${firstTokenOfParam.value})`);
|
154
175
|
}
|
@@ -54,29 +54,31 @@ module.exports = {
|
|
54
54
|
*/
|
55
55
|
function checkDotLocation(obj, prop, node) {
|
56
56
|
const dot = sourceCode.getTokenBefore(prop);
|
57
|
-
|
57
|
+
|
58
|
+
// `obj` expression can be parenthesized, but those paren tokens are not a part of the `obj` node.
|
59
|
+
const tokenBeforeDot = sourceCode.getTokenBefore(dot);
|
60
|
+
|
61
|
+
const textBeforeDot = sourceCode.getText().slice(tokenBeforeDot.range[1], dot.range[0]);
|
58
62
|
const textAfterDot = sourceCode.getText().slice(dot.range[1], prop.range[0]);
|
59
63
|
|
60
|
-
if (
|
61
|
-
if (
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
context.report({
|
66
|
-
node,
|
67
|
-
loc: dot.loc.start,
|
68
|
-
messageId: "expectedDotAfterObject",
|
69
|
-
fix: fixer => fixer.replaceTextRange([obj.range[1], prop.range[0]], `${neededTextAfterObj}.${textBeforeDot}${textAfterDot}`)
|
70
|
-
});
|
71
|
-
}
|
72
|
-
} else if (!astUtils.isTokenOnSameLine(dot, prop)) {
|
64
|
+
if (onObject) {
|
65
|
+
if (!astUtils.isTokenOnSameLine(tokenBeforeDot, dot)) {
|
66
|
+
const neededTextAfterToken = astUtils.isDecimalIntegerNumericToken(tokenBeforeDot) ? " " : "";
|
67
|
+
|
73
68
|
context.report({
|
74
69
|
node,
|
75
70
|
loc: dot.loc.start,
|
76
|
-
messageId: "
|
77
|
-
fix: fixer => fixer.replaceTextRange([
|
71
|
+
messageId: "expectedDotAfterObject",
|
72
|
+
fix: fixer => fixer.replaceTextRange([tokenBeforeDot.range[1], prop.range[0]], `${neededTextAfterToken}.${textBeforeDot}${textAfterDot}`)
|
78
73
|
});
|
79
74
|
}
|
75
|
+
} else if (!astUtils.isTokenOnSameLine(dot, prop)) {
|
76
|
+
context.report({
|
77
|
+
node,
|
78
|
+
loc: dot.loc.start,
|
79
|
+
messageId: "expectedDotBeforeProperty",
|
80
|
+
fix: fixer => fixer.replaceTextRange([tokenBeforeDot.range[1], prop.range[0]], `${textBeforeDot}${textAfterDot}.`)
|
81
|
+
});
|
80
82
|
}
|
81
83
|
}
|
82
84
|
|
@@ -86,7 +88,9 @@ module.exports = {
|
|
86
88
|
* @returns {void}
|
87
89
|
*/
|
88
90
|
function checkNode(node) {
|
89
|
-
|
91
|
+
if (!node.computed) {
|
92
|
+
checkDotLocation(node.object, node.property, node);
|
93
|
+
}
|
90
94
|
}
|
91
95
|
|
92
96
|
return {
|
package/lib/rules/max-len.js
CHANGED
@@ -315,6 +315,13 @@ module.exports = {
|
|
315
315
|
textToMeasure = line;
|
316
316
|
} else if (ignoreTrailingComments && isTrailingComment(line, lineNumber, comment)) {
|
317
317
|
textToMeasure = stripTrailingComment(line, comment);
|
318
|
+
|
319
|
+
// ignore multiple trailing comments in the same line
|
320
|
+
let lastIndex = commentsIndex;
|
321
|
+
|
322
|
+
while (isTrailingComment(textToMeasure, lineNumber, comments[--lastIndex])) {
|
323
|
+
textToMeasure = stripTrailingComment(textToMeasure, comments[lastIndex]);
|
324
|
+
}
|
318
325
|
} else {
|
319
326
|
textToMeasure = line;
|
320
327
|
}
|
@@ -25,6 +25,7 @@ module.exports = {
|
|
25
25
|
schema: [{ enum: ["starred-block", "separate-lines", "bare-block"] }],
|
26
26
|
messages: {
|
27
27
|
expectedBlock: "Expected a block comment instead of consecutive line comments.",
|
28
|
+
expectedBareBlock: "Expected a block comment without padding stars.",
|
28
29
|
startNewline: "Expected a linebreak after '/*'.",
|
29
30
|
endNewline: "Expected a linebreak before '*/'.",
|
30
31
|
missingStar: "Expected a '*' at the start of this line.",
|
@@ -250,7 +251,7 @@ module.exports = {
|
|
250
251
|
start: block.loc.start,
|
251
252
|
end: { line: block.loc.start.line, column: block.loc.start.column + 2 }
|
252
253
|
},
|
253
|
-
messageId: "
|
254
|
+
messageId: "expectedBareBlock",
|
254
255
|
fix(fixer) {
|
255
256
|
return fixer.replaceText(block, convertToBlock(block, commentLines.filter(line => line)));
|
256
257
|
}
|
@@ -51,6 +51,124 @@ module.exports = {
|
|
51
51
|
// Helpers
|
52
52
|
//--------------------------------------------------------------------------
|
53
53
|
|
54
|
+
/**
|
55
|
+
* Checks whether the given names can be safely used to declare block-scoped variables
|
56
|
+
* in the given scope. Name collisions can produce redeclaration syntax errors,
|
57
|
+
* or silently change references and modify behavior of the original code.
|
58
|
+
*
|
59
|
+
* This is not a generic function. In particular, it is assumed that the scope is a function scope or
|
60
|
+
* a function's inner scope, and that the names can be valid identifiers in the given scope.
|
61
|
+
*
|
62
|
+
* @param {string[]} names Array of variable names.
|
63
|
+
* @param {eslint-scope.Scope} scope Function scope or a function's inner scope.
|
64
|
+
* @returns {boolean} True if all names can be safely declared, false otherwise.
|
65
|
+
*/
|
66
|
+
function isSafeToDeclare(names, scope) {
|
67
|
+
|
68
|
+
if (names.length === 0) {
|
69
|
+
return true;
|
70
|
+
}
|
71
|
+
|
72
|
+
const functionScope = scope.variableScope;
|
73
|
+
|
74
|
+
/*
|
75
|
+
* If this is a function scope, scope.variables will contain parameters, implicit variables such as "arguments",
|
76
|
+
* all function-scoped variables ('var'), and block-scoped variables defined in the scope.
|
77
|
+
* If this is an inner scope, scope.variables will contain block-scoped variables defined in the scope.
|
78
|
+
*
|
79
|
+
* Redeclaring any of these would cause a syntax error, except for the implicit variables.
|
80
|
+
*/
|
81
|
+
const declaredVariables = scope.variables.filter(({ defs }) => defs.length > 0);
|
82
|
+
|
83
|
+
if (declaredVariables.some(({ name }) => names.includes(name))) {
|
84
|
+
return false;
|
85
|
+
}
|
86
|
+
|
87
|
+
// Redeclaring a catch variable would also cause a syntax error.
|
88
|
+
if (scope !== functionScope && scope.upper.type === "catch") {
|
89
|
+
if (scope.upper.variables.some(({ name }) => names.includes(name))) {
|
90
|
+
return false;
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
/*
|
95
|
+
* Redeclaring an implicit variable, such as "arguments", would not cause a syntax error.
|
96
|
+
* However, if the variable was used, declaring a new one with the same name would change references
|
97
|
+
* and modify behavior.
|
98
|
+
*/
|
99
|
+
const usedImplicitVariables = scope.variables.filter(({ defs, references }) =>
|
100
|
+
defs.length === 0 && references.length > 0);
|
101
|
+
|
102
|
+
if (usedImplicitVariables.some(({ name }) => names.includes(name))) {
|
103
|
+
return false;
|
104
|
+
}
|
105
|
+
|
106
|
+
/*
|
107
|
+
* Declaring a variable with a name that was already used to reference a variable from an upper scope
|
108
|
+
* would change references and modify behavior.
|
109
|
+
*/
|
110
|
+
if (scope.through.some(t => names.includes(t.identifier.name))) {
|
111
|
+
return false;
|
112
|
+
}
|
113
|
+
|
114
|
+
/*
|
115
|
+
* If the scope is an inner scope (not the function scope), an uninitialized `var` variable declared inside
|
116
|
+
* the scope node (directly or in one of its descendants) is neither declared nor 'through' in the scope.
|
117
|
+
*
|
118
|
+
* For example, this would be a syntax error "Identifier 'a' has already been declared":
|
119
|
+
* function foo() { if (bar) { let a; if (baz) { var a; } } }
|
120
|
+
*/
|
121
|
+
if (scope !== functionScope) {
|
122
|
+
const scopeNodeRange = scope.block.range;
|
123
|
+
const variablesToCheck = functionScope.variables.filter(({ name }) => names.includes(name));
|
124
|
+
|
125
|
+
if (variablesToCheck.some(v => v.defs.some(({ node: { range } }) =>
|
126
|
+
scopeNodeRange[0] <= range[0] && range[1] <= scopeNodeRange[1]))) {
|
127
|
+
return false;
|
128
|
+
}
|
129
|
+
}
|
130
|
+
|
131
|
+
return true;
|
132
|
+
}
|
133
|
+
|
134
|
+
|
135
|
+
/**
|
136
|
+
* Checks whether the removal of `else` and its braces is safe from variable name collisions.
|
137
|
+
*
|
138
|
+
* @param {Node} node The 'else' node.
|
139
|
+
* @param {eslint-scope.Scope} scope The scope in which the node and the whole 'if' statement is.
|
140
|
+
* @returns {boolean} True if it is safe, false otherwise.
|
141
|
+
*/
|
142
|
+
function isSafeFromNameCollisions(node, scope) {
|
143
|
+
|
144
|
+
if (node.type === "FunctionDeclaration") {
|
145
|
+
|
146
|
+
// Conditional function declaration. Scope and hoisting are unpredictable, different engines work differently.
|
147
|
+
return false;
|
148
|
+
}
|
149
|
+
|
150
|
+
if (node.type !== "BlockStatement") {
|
151
|
+
return true;
|
152
|
+
}
|
153
|
+
|
154
|
+
const elseBlockScope = scope.childScopes.find(({ block }) => block === node);
|
155
|
+
|
156
|
+
if (!elseBlockScope) {
|
157
|
+
|
158
|
+
// ecmaVersion < 6, `else` block statement cannot have its own scope, no possible collisions.
|
159
|
+
return true;
|
160
|
+
}
|
161
|
+
|
162
|
+
/*
|
163
|
+
* elseBlockScope is supposed to merge into its upper scope. elseBlockScope.variables array contains
|
164
|
+
* only block-scoped variables (such as let and const variables or class and function declarations)
|
165
|
+
* defined directly in the elseBlockScope. These are exactly the only names that could cause collisions.
|
166
|
+
*/
|
167
|
+
const namesToCheck = elseBlockScope.variables.map(({ name }) => name);
|
168
|
+
|
169
|
+
return isSafeToDeclare(namesToCheck, scope);
|
170
|
+
}
|
171
|
+
|
54
172
|
/**
|
55
173
|
* Display the context report if rule is violated
|
56
174
|
*
|
@@ -58,10 +176,17 @@ module.exports = {
|
|
58
176
|
* @returns {void}
|
59
177
|
*/
|
60
178
|
function displayReport(node) {
|
179
|
+
const currentScope = context.getScope();
|
180
|
+
|
61
181
|
context.report({
|
62
182
|
node,
|
63
183
|
messageId: "unexpected",
|
64
184
|
fix: fixer => {
|
185
|
+
|
186
|
+
if (!isSafeFromNameCollisions(node, currentScope)) {
|
187
|
+
return null;
|
188
|
+
}
|
189
|
+
|
65
190
|
const sourceCode = context.getSourceCode();
|
66
191
|
const startToken = sourceCode.getFirstToken(node);
|
67
192
|
const elseToken = sourceCode.getTokenBefore(startToken);
|
@@ -118,6 +243,8 @@ module.exports = {
|
|
118
243
|
* Extend the replacement range to include the entire
|
119
244
|
* function to avoid conflicting with no-useless-return.
|
120
245
|
* https://github.com/eslint/eslint/issues/8026
|
246
|
+
*
|
247
|
+
* Also, to avoid name collisions between two else blocks.
|
121
248
|
*/
|
122
249
|
return new FixTracker(fixer, sourceCode)
|
123
250
|
.retainEnclosingFunction(node)
|
@@ -81,6 +81,8 @@ module.exports = {
|
|
81
81
|
const PRECEDENCE_OF_ASSIGNMENT_EXPR = precedence({ type: "AssignmentExpression" });
|
82
82
|
const PRECEDENCE_OF_UPDATE_EXPR = precedence({ type: "UpdateExpression" });
|
83
83
|
|
84
|
+
let reportsBuffer;
|
85
|
+
|
84
86
|
/**
|
85
87
|
* Determines if this rule should be enforced for a node given the current configuration.
|
86
88
|
* @param {ASTNode} node - The node to be checked.
|
@@ -316,19 +318,33 @@ module.exports = {
|
|
316
318
|
}
|
317
319
|
}
|
318
320
|
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
321
|
+
/**
|
322
|
+
* Finishes reporting
|
323
|
+
* @returns {void}
|
324
|
+
* @private
|
325
|
+
*/
|
326
|
+
function finishReport() {
|
327
|
+
context.report({
|
328
|
+
node,
|
329
|
+
loc: leftParenToken.loc.start,
|
330
|
+
messageId: "unexpected",
|
331
|
+
fix(fixer) {
|
332
|
+
const parenthesizedSource = sourceCode.text.slice(leftParenToken.range[1], rightParenToken.range[0]);
|
333
|
+
|
334
|
+
return fixer.replaceTextRange([
|
335
|
+
leftParenToken.range[0],
|
336
|
+
rightParenToken.range[1]
|
337
|
+
], (requiresLeadingSpace(node) ? " " : "") + parenthesizedSource + (requiresTrailingSpace(node) ? " " : ""));
|
338
|
+
}
|
339
|
+
});
|
340
|
+
}
|
325
341
|
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
342
|
+
if (reportsBuffer) {
|
343
|
+
reportsBuffer.reports.push({ node, finishReport });
|
344
|
+
return;
|
345
|
+
}
|
346
|
+
|
347
|
+
finishReport();
|
332
348
|
}
|
333
349
|
|
334
350
|
/**
|
@@ -498,6 +514,126 @@ module.exports = {
|
|
498
514
|
}
|
499
515
|
}
|
500
516
|
|
517
|
+
/**
|
518
|
+
* Finds the path from the given node to the specified ancestor.
|
519
|
+
* @param {ASTNode} node First node in the path.
|
520
|
+
* @param {ASTNode} ancestor Last node in the path.
|
521
|
+
* @returns {ASTNode[]} Path, including both nodes.
|
522
|
+
* @throws {Error} If the given node does not have the specified ancestor.
|
523
|
+
*/
|
524
|
+
function pathToAncestor(node, ancestor) {
|
525
|
+
const path = [node];
|
526
|
+
let currentNode = node;
|
527
|
+
|
528
|
+
while (currentNode !== ancestor) {
|
529
|
+
|
530
|
+
currentNode = currentNode.parent;
|
531
|
+
|
532
|
+
/* istanbul ignore if */
|
533
|
+
if (currentNode === null) {
|
534
|
+
throw new Error("Nodes are not in the ancestor-descendant relationship.");
|
535
|
+
}
|
536
|
+
|
537
|
+
path.push(currentNode);
|
538
|
+
}
|
539
|
+
|
540
|
+
return path;
|
541
|
+
}
|
542
|
+
|
543
|
+
/**
|
544
|
+
* Finds the path from the given node to the specified descendant.
|
545
|
+
* @param {ASTNode} node First node in the path.
|
546
|
+
* @param {ASTNode} descendant Last node in the path.
|
547
|
+
* @returns {ASTNode[]} Path, including both nodes.
|
548
|
+
* @throws {Error} If the given node does not have the specified descendant.
|
549
|
+
*/
|
550
|
+
function pathToDescendant(node, descendant) {
|
551
|
+
return pathToAncestor(descendant, node).reverse();
|
552
|
+
}
|
553
|
+
|
554
|
+
/**
|
555
|
+
* Checks whether the syntax of the given ancestor of an 'in' expression inside a for-loop initializer
|
556
|
+
* is preventing the 'in' keyword from being interpreted as a part of an ill-formed for-in loop.
|
557
|
+
*
|
558
|
+
* @param {ASTNode} node Ancestor of an 'in' expression.
|
559
|
+
* @param {ASTNode} child Child of the node, ancestor of the same 'in' expression or the 'in' expression itself.
|
560
|
+
* @returns {boolean} True if the keyword 'in' would be interpreted as the 'in' operator, without any parenthesis.
|
561
|
+
*/
|
562
|
+
function isSafelyEnclosingInExpression(node, child) {
|
563
|
+
switch (node.type) {
|
564
|
+
case "ArrayExpression":
|
565
|
+
case "ArrayPattern":
|
566
|
+
case "BlockStatement":
|
567
|
+
case "ObjectExpression":
|
568
|
+
case "ObjectPattern":
|
569
|
+
case "TemplateLiteral":
|
570
|
+
return true;
|
571
|
+
case "ArrowFunctionExpression":
|
572
|
+
case "FunctionExpression":
|
573
|
+
return node.params.includes(child);
|
574
|
+
case "CallExpression":
|
575
|
+
case "NewExpression":
|
576
|
+
return node.arguments.includes(child);
|
577
|
+
case "MemberExpression":
|
578
|
+
return node.computed && node.property === child;
|
579
|
+
case "ConditionalExpression":
|
580
|
+
return node.consequent === child;
|
581
|
+
default:
|
582
|
+
return false;
|
583
|
+
}
|
584
|
+
}
|
585
|
+
|
586
|
+
/**
|
587
|
+
* Starts a new reports buffering. Warnings will be stored in a buffer instead of being reported immediately.
|
588
|
+
* An additional logic that requires multiple nodes (e.g. a whole subtree) may dismiss some of the stored warnings.
|
589
|
+
*
|
590
|
+
* @returns {void}
|
591
|
+
*/
|
592
|
+
function startNewReportsBuffering() {
|
593
|
+
reportsBuffer = {
|
594
|
+
upper: reportsBuffer,
|
595
|
+
inExpressionNodes: [],
|
596
|
+
reports: []
|
597
|
+
};
|
598
|
+
}
|
599
|
+
|
600
|
+
/**
|
601
|
+
* Ends the current reports buffering.
|
602
|
+
* @returns {void}
|
603
|
+
*/
|
604
|
+
function endCurrentReportsBuffering() {
|
605
|
+
const { upper, inExpressionNodes, reports } = reportsBuffer;
|
606
|
+
|
607
|
+
if (upper) {
|
608
|
+
upper.inExpressionNodes.push(...inExpressionNodes);
|
609
|
+
upper.reports.push(...reports);
|
610
|
+
} else {
|
611
|
+
|
612
|
+
// flush remaining reports
|
613
|
+
reports.forEach(({ finishReport }) => finishReport());
|
614
|
+
}
|
615
|
+
|
616
|
+
reportsBuffer = upper;
|
617
|
+
}
|
618
|
+
|
619
|
+
/**
|
620
|
+
* Checks whether the given node is in the current reports buffer.
|
621
|
+
* @param {ASTNode} node Node to check.
|
622
|
+
* @returns {boolean} True if the node is in the current buffer, false otherwise.
|
623
|
+
*/
|
624
|
+
function isInCurrentReportsBuffer(node) {
|
625
|
+
return reportsBuffer.reports.some(r => r.node === node);
|
626
|
+
}
|
627
|
+
|
628
|
+
/**
|
629
|
+
* Removes the given node from the current reports buffer.
|
630
|
+
* @param {ASTNode} node Node to remove.
|
631
|
+
* @returns {void}
|
632
|
+
*/
|
633
|
+
function removeFromCurrentReportsBuffer(node) {
|
634
|
+
reportsBuffer.reports = reportsBuffer.reports.filter(r => r.node !== node);
|
635
|
+
}
|
636
|
+
|
501
637
|
return {
|
502
638
|
ArrayExpression(node) {
|
503
639
|
node.elements
|
@@ -540,7 +676,14 @@ module.exports = {
|
|
540
676
|
}
|
541
677
|
},
|
542
678
|
|
543
|
-
BinaryExpression
|
679
|
+
BinaryExpression(node) {
|
680
|
+
if (reportsBuffer && node.operator === "in") {
|
681
|
+
reportsBuffer.inExpressionNodes.push(node);
|
682
|
+
}
|
683
|
+
|
684
|
+
checkBinaryLogical(node);
|
685
|
+
},
|
686
|
+
|
544
687
|
CallExpression: checkCallNew,
|
545
688
|
|
546
689
|
ConditionalExpression(node) {
|
@@ -602,10 +745,6 @@ module.exports = {
|
|
602
745
|
},
|
603
746
|
|
604
747
|
ForStatement(node) {
|
605
|
-
if (node.init && hasExcessParens(node.init)) {
|
606
|
-
report(node.init);
|
607
|
-
}
|
608
|
-
|
609
748
|
if (node.test && hasExcessParens(node.test) && !isCondAssignException(node)) {
|
610
749
|
report(node.test);
|
611
750
|
}
|
@@ -613,6 +752,81 @@ module.exports = {
|
|
613
752
|
if (node.update && hasExcessParens(node.update)) {
|
614
753
|
report(node.update);
|
615
754
|
}
|
755
|
+
|
756
|
+
if (node.init) {
|
757
|
+
startNewReportsBuffering();
|
758
|
+
|
759
|
+
if (hasExcessParens(node.init)) {
|
760
|
+
report(node.init);
|
761
|
+
}
|
762
|
+
}
|
763
|
+
},
|
764
|
+
|
765
|
+
"ForStatement > *.init:exit"(node) {
|
766
|
+
|
767
|
+
/*
|
768
|
+
* Removing parentheses around `in` expressions might change semantics and cause errors.
|
769
|
+
*
|
770
|
+
* For example, this valid for loop:
|
771
|
+
* for (let a = (b in c); ;);
|
772
|
+
* after removing parentheses would be treated as an invalid for-in loop:
|
773
|
+
* for (let a = b in c; ;);
|
774
|
+
*/
|
775
|
+
|
776
|
+
if (reportsBuffer.reports.length) {
|
777
|
+
reportsBuffer.inExpressionNodes.forEach(inExpressionNode => {
|
778
|
+
const path = pathToDescendant(node, inExpressionNode);
|
779
|
+
let nodeToExclude;
|
780
|
+
|
781
|
+
for (let i = 0; i < path.length; i++) {
|
782
|
+
const pathNode = path[i];
|
783
|
+
|
784
|
+
if (i < path.length - 1) {
|
785
|
+
const nextPathNode = path[i + 1];
|
786
|
+
|
787
|
+
if (isSafelyEnclosingInExpression(pathNode, nextPathNode)) {
|
788
|
+
|
789
|
+
// The 'in' expression in safely enclosed by the syntax of its ancestor nodes (e.g. by '{}' or '[]').
|
790
|
+
return;
|
791
|
+
}
|
792
|
+
}
|
793
|
+
|
794
|
+
if (isParenthesised(pathNode)) {
|
795
|
+
if (isInCurrentReportsBuffer(pathNode)) {
|
796
|
+
|
797
|
+
// This node was supposed to be reported, but parentheses might be necessary.
|
798
|
+
|
799
|
+
if (isParenthesisedTwice(pathNode)) {
|
800
|
+
|
801
|
+
/*
|
802
|
+
* This node is parenthesised twice, it certainly has at least one pair of `extra` parentheses.
|
803
|
+
* If the --fix option is on, the current fixing iteration will remove only one pair of parentheses.
|
804
|
+
* The remaining pair is safely enclosing the 'in' expression.
|
805
|
+
*/
|
806
|
+
return;
|
807
|
+
}
|
808
|
+
|
809
|
+
// Exclude the outermost node only.
|
810
|
+
if (!nodeToExclude) {
|
811
|
+
nodeToExclude = pathNode;
|
812
|
+
}
|
813
|
+
|
814
|
+
// Don't break the loop here, there might be some safe nodes or parentheses that will stay inside.
|
815
|
+
|
816
|
+
} else {
|
817
|
+
|
818
|
+
// This node will stay parenthesised, the 'in' expression in safely enclosed by '()'.
|
819
|
+
return;
|
820
|
+
}
|
821
|
+
}
|
822
|
+
}
|
823
|
+
|
824
|
+
// Exclude the node from the list (i.e. treat parentheses as necessary)
|
825
|
+
removeFromCurrentReportsBuffer(nodeToExclude);
|
826
|
+
});
|
827
|
+
}
|
828
|
+
|
829
|
+
endCurrentReportsBuffering();
|
616
830
|
},
|
617
831
|
|
618
832
|
IfStatement(node) {
|
@@ -664,6 +878,16 @@ module.exports = {
|
|
664
878
|
}).forEach(property => report(property.value));
|
665
879
|
},
|
666
880
|
|
881
|
+
Property(node) {
|
882
|
+
if (node.computed) {
|
883
|
+
const { key } = node;
|
884
|
+
|
885
|
+
if (key && hasExcessParens(key) && precedence(key) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
|
886
|
+
report(key);
|
887
|
+
}
|
888
|
+
}
|
889
|
+
},
|
890
|
+
|
667
891
|
ReturnStatement(node) {
|
668
892
|
const returnToken = sourceCode.getFirstToken(node);
|
669
893
|
|
package/lib/rules/no-octal.js
CHANGED
@@ -28,7 +28,7 @@ module.exports = {
|
|
28
28
|
return {
|
29
29
|
|
30
30
|
Literal(node) {
|
31
|
-
if (typeof node.value === "number" && /^0[0-
|
31
|
+
if (typeof node.value === "number" && /^0[0-9]/u.test(node.raw)) {
|
32
32
|
context.report({ node, message: "Octal literals should not be used." });
|
33
33
|
}
|
34
34
|
}
|
@@ -67,7 +67,8 @@ module.exports = {
|
|
67
67
|
let node = reference.identifier;
|
68
68
|
let parent = node.parent;
|
69
69
|
|
70
|
-
while (parent && !stopNodePattern.test(parent.type)
|
70
|
+
while (parent && (!stopNodePattern.test(parent.type) ||
|
71
|
+
parent.type === "ForInStatement" || parent.type === "ForOfStatement")) {
|
71
72
|
switch (parent.type) {
|
72
73
|
|
73
74
|
// e.g. foo.a = 0;
|
@@ -85,6 +86,16 @@ module.exports = {
|
|
85
86
|
}
|
86
87
|
break;
|
87
88
|
|
89
|
+
// e.g. for (foo.a in b) {}
|
90
|
+
case "ForInStatement":
|
91
|
+
case "ForOfStatement":
|
92
|
+
if (parent.left === node) {
|
93
|
+
return true;
|
94
|
+
}
|
95
|
+
|
96
|
+
// this is a stop node for parent.right and parent.body
|
97
|
+
return false;
|
98
|
+
|
88
99
|
// EXCLUDES: e.g. cache.get(foo.a).b = 0;
|
89
100
|
case "CallExpression":
|
90
101
|
if (parent.callee !== node) {
|