eslint 6.0.0-rc.0 → 6.2.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 +90 -0
- package/README.md +4 -13
- package/bin/eslint.js +4 -1
- package/conf/config-schema.js +1 -0
- package/conf/environments.js +72 -15
- 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 +8 -2
- package/lib/cli-engine/config-array/extracted-config.js +16 -1
- package/lib/cli-engine/config-array-factory.js +9 -6
- package/lib/cli-engine/file-enumerator.js +5 -13
- package/lib/init/config-initializer.js +19 -9
- package/lib/init/npm-utils.js +2 -2
- package/lib/linter/code-path-analysis/code-path-analyzer.js +1 -0
- package/lib/linter/linter.js +49 -16
- package/lib/rule-tester/rule-tester.js +15 -3
- package/lib/rules/accessor-pairs.js +195 -35
- package/lib/rules/arrow-body-style.js +2 -2
- package/lib/rules/class-methods-use-this.js +10 -3
- package/lib/rules/dot-location.js +21 -17
- package/lib/rules/dot-notation.js +6 -2
- package/lib/rules/func-call-spacing.js +30 -20
- package/lib/rules/func-names.js +4 -0
- package/lib/rules/function-call-argument-newline.js +120 -0
- package/lib/rules/function-paren-newline.js +34 -22
- package/lib/rules/indent.js +13 -2
- package/lib/rules/index.js +1 -0
- package/lib/rules/max-len.js +7 -0
- package/lib/rules/multiline-comment-style.js +2 -1
- package/lib/rules/new-cap.js +2 -1
- package/lib/rules/no-dupe-keys.js +1 -1
- package/lib/rules/no-duplicate-case.js +10 -8
- package/lib/rules/no-else-return.js +127 -0
- package/lib/rules/no-extra-bind.js +1 -0
- package/lib/rules/no-extra-boolean-cast.js +44 -5
- package/lib/rules/no-extra-parens.js +295 -39
- package/lib/rules/no-mixed-operators.js +48 -13
- package/lib/rules/no-param-reassign.js +12 -1
- package/lib/rules/no-restricted-syntax.js +2 -2
- package/lib/rules/no-unused-vars.js +1 -1
- package/lib/rules/no-var.js +14 -1
- package/lib/rules/prefer-const.js +9 -3
- package/lib/rules/prefer-template.js +1 -10
- package/lib/rules/require-atomic-updates.js +63 -84
- package/lib/rules/sort-keys.js +11 -3
- package/lib/rules/utils/ast-utils.js +45 -3
- package/lib/rules/yoda.js +1 -1
- package/lib/{cli-engine → shared}/naming.js +0 -0
- package/lib/shared/types.js +2 -0
- package/messages/extend-config-missing.txt +2 -0
- package/messages/print-config-with-directory-path.txt +2 -0
- package/package.json +27 -30
@@ -78,21 +78,13 @@ module.exports = {
|
|
78
78
|
/**
|
79
79
|
* Check if open space is present in a function name
|
80
80
|
* @param {ASTNode} node node to evaluate
|
81
|
+
* @param {Token} leftToken The last token of the callee. This may be the closing parenthesis that encloses the callee.
|
82
|
+
* @param {Token} rightToken Tha first token of the arguments. this is the opening parenthesis that encloses the arguments.
|
81
83
|
* @returns {void}
|
82
84
|
* @private
|
83
85
|
*/
|
84
|
-
function checkSpacing(node) {
|
85
|
-
const
|
86
|
-
const lastCalleeToken = sourceCode.getLastToken(node.callee);
|
87
|
-
const parenToken = sourceCode.getFirstTokenBetween(lastCalleeToken, lastToken, astUtils.isOpeningParenToken);
|
88
|
-
const prevToken = parenToken && sourceCode.getTokenBefore(parenToken);
|
89
|
-
|
90
|
-
// Parens in NewExpression are optional
|
91
|
-
if (!(parenToken && parenToken.range[1] < node.range[1])) {
|
92
|
-
return;
|
93
|
-
}
|
94
|
-
|
95
|
-
const textBetweenTokens = text.slice(prevToken.range[1], parenToken.range[0]).replace(/\/\*.*?\*\//gu, "");
|
86
|
+
function checkSpacing(node, leftToken, rightToken) {
|
87
|
+
const textBetweenTokens = text.slice(leftToken.range[1], rightToken.range[0]).replace(/\/\*.*?\*\//gu, "");
|
96
88
|
const hasWhitespace = /\s/u.test(textBetweenTokens);
|
97
89
|
const hasNewline = hasWhitespace && astUtils.LINEBREAK_MATCHER.test(textBetweenTokens);
|
98
90
|
|
@@ -123,7 +115,7 @@ module.exports = {
|
|
123
115
|
if (never && hasWhitespace) {
|
124
116
|
context.report({
|
125
117
|
node,
|
126
|
-
loc:
|
118
|
+
loc: leftToken.loc.start,
|
127
119
|
messageId: "unexpected",
|
128
120
|
fix(fixer) {
|
129
121
|
|
@@ -132,7 +124,7 @@ module.exports = {
|
|
132
124
|
* https://github.com/eslint/eslint/issues/7787
|
133
125
|
*/
|
134
126
|
if (!hasNewline) {
|
135
|
-
return fixer.removeRange([
|
127
|
+
return fixer.removeRange([leftToken.range[1], rightToken.range[0]]);
|
136
128
|
}
|
137
129
|
|
138
130
|
return null;
|
@@ -141,27 +133,45 @@ module.exports = {
|
|
141
133
|
} else if (!never && !hasWhitespace) {
|
142
134
|
context.report({
|
143
135
|
node,
|
144
|
-
loc:
|
136
|
+
loc: leftToken.loc.start,
|
145
137
|
messageId: "missing",
|
146
138
|
fix(fixer) {
|
147
|
-
return fixer.insertTextBefore(
|
139
|
+
return fixer.insertTextBefore(rightToken, " ");
|
148
140
|
}
|
149
141
|
});
|
150
142
|
} else if (!never && !allowNewlines && hasNewline) {
|
151
143
|
context.report({
|
152
144
|
node,
|
153
|
-
loc:
|
145
|
+
loc: leftToken.loc.start,
|
154
146
|
messageId: "unexpected",
|
155
147
|
fix(fixer) {
|
156
|
-
return fixer.replaceTextRange([
|
148
|
+
return fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], " ");
|
157
149
|
}
|
158
150
|
});
|
159
151
|
}
|
160
152
|
}
|
161
153
|
|
162
154
|
return {
|
163
|
-
CallExpression
|
164
|
-
|
155
|
+
"CallExpression, NewExpression"(node) {
|
156
|
+
const lastToken = sourceCode.getLastToken(node);
|
157
|
+
const lastCalleeToken = sourceCode.getLastToken(node.callee);
|
158
|
+
const parenToken = sourceCode.getFirstTokenBetween(lastCalleeToken, lastToken, astUtils.isOpeningParenToken);
|
159
|
+
const prevToken = parenToken && sourceCode.getTokenBefore(parenToken);
|
160
|
+
|
161
|
+
// Parens in NewExpression are optional
|
162
|
+
if (!(parenToken && parenToken.range[1] < node.range[1])) {
|
163
|
+
return;
|
164
|
+
}
|
165
|
+
|
166
|
+
checkSpacing(node, prevToken, parenToken);
|
167
|
+
},
|
168
|
+
|
169
|
+
ImportExpression(node) {
|
170
|
+
const leftToken = sourceCode.getFirstToken(node);
|
171
|
+
const rightToken = sourceCode.getTokenAfter(leftToken);
|
172
|
+
|
173
|
+
checkSpacing(node, leftToken, rightToken);
|
174
|
+
}
|
165
175
|
};
|
166
176
|
|
167
177
|
}
|
package/lib/rules/func-names.js
CHANGED
@@ -69,6 +69,8 @@ module.exports = {
|
|
69
69
|
|
70
70
|
create(context) {
|
71
71
|
|
72
|
+
const sourceCode = context.getSourceCode();
|
73
|
+
|
72
74
|
/**
|
73
75
|
* Returns the config option for the given node.
|
74
76
|
* @param {ASTNode} node - A node to get the config for.
|
@@ -130,6 +132,7 @@ module.exports = {
|
|
130
132
|
context.report({
|
131
133
|
node,
|
132
134
|
messageId: "unnamed",
|
135
|
+
loc: astUtils.getFunctionHeadLoc(node, sourceCode),
|
133
136
|
data: { name: astUtils.getFunctionNameWithKind(node) }
|
134
137
|
});
|
135
138
|
}
|
@@ -143,6 +146,7 @@ module.exports = {
|
|
143
146
|
context.report({
|
144
147
|
node,
|
145
148
|
messageId: "named",
|
149
|
+
loc: astUtils.getFunctionHeadLoc(node, sourceCode),
|
146
150
|
data: { name: astUtils.getFunctionNameWithKind(node) }
|
147
151
|
});
|
148
152
|
}
|
@@ -0,0 +1,120 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview Rule to enforce line breaks between arguments of a function call
|
3
|
+
* @author Alexey Gonchar <https://github.com/finico>
|
4
|
+
*/
|
5
|
+
|
6
|
+
"use strict";
|
7
|
+
|
8
|
+
//------------------------------------------------------------------------------
|
9
|
+
// Rule Definition
|
10
|
+
//------------------------------------------------------------------------------
|
11
|
+
|
12
|
+
module.exports = {
|
13
|
+
meta: {
|
14
|
+
type: "layout",
|
15
|
+
|
16
|
+
docs: {
|
17
|
+
description: "enforce line breaks between arguments of a function call",
|
18
|
+
category: "Stylistic Issues",
|
19
|
+
recommended: false,
|
20
|
+
url: "https://eslint.org/docs/rules/function-call-argument-newline"
|
21
|
+
},
|
22
|
+
|
23
|
+
fixable: "whitespace",
|
24
|
+
|
25
|
+
schema: [
|
26
|
+
{
|
27
|
+
enum: ["always", "never", "consistent"]
|
28
|
+
}
|
29
|
+
],
|
30
|
+
|
31
|
+
messages: {
|
32
|
+
unexpectedLineBreak: "There should be no line break here.",
|
33
|
+
missingLineBreak: "There should be a line break after this argument."
|
34
|
+
}
|
35
|
+
},
|
36
|
+
|
37
|
+
create(context) {
|
38
|
+
const sourceCode = context.getSourceCode();
|
39
|
+
|
40
|
+
const checkers = {
|
41
|
+
unexpected: {
|
42
|
+
messageId: "unexpectedLineBreak",
|
43
|
+
check: (prevToken, currentToken) => prevToken.loc.start.line !== currentToken.loc.start.line,
|
44
|
+
createFix: (token, tokenBefore) => fixer =>
|
45
|
+
fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], " ")
|
46
|
+
},
|
47
|
+
missing: {
|
48
|
+
messageId: "missingLineBreak",
|
49
|
+
check: (prevToken, currentToken) => prevToken.loc.start.line === currentToken.loc.start.line,
|
50
|
+
createFix: (token, tokenBefore) => fixer =>
|
51
|
+
fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], "\n")
|
52
|
+
}
|
53
|
+
};
|
54
|
+
|
55
|
+
/**
|
56
|
+
* Check all arguments for line breaks in the CallExpression
|
57
|
+
* @param {CallExpression} node node to evaluate
|
58
|
+
* @param {{ messageId: string, check: Function }} checker selected checker
|
59
|
+
* @returns {void}
|
60
|
+
* @private
|
61
|
+
*/
|
62
|
+
function checkArguments(node, checker) {
|
63
|
+
for (let i = 1; i < node.arguments.length; i++) {
|
64
|
+
const prevArgToken = sourceCode.getFirstToken(node.arguments[i - 1]);
|
65
|
+
const currentArgToken = sourceCode.getFirstToken(node.arguments[i]);
|
66
|
+
|
67
|
+
if (checker.check(prevArgToken, currentArgToken)) {
|
68
|
+
const tokenBefore = sourceCode.getTokenBefore(
|
69
|
+
currentArgToken,
|
70
|
+
{ includeComments: true }
|
71
|
+
);
|
72
|
+
|
73
|
+
context.report({
|
74
|
+
node,
|
75
|
+
loc: {
|
76
|
+
start: tokenBefore.loc.end,
|
77
|
+
end: currentArgToken.loc.start
|
78
|
+
},
|
79
|
+
messageId: checker.messageId,
|
80
|
+
fix: checker.createFix(currentArgToken, tokenBefore)
|
81
|
+
});
|
82
|
+
}
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
/**
|
87
|
+
* Check if open space is present in a function name
|
88
|
+
* @param {CallExpression} node node to evaluate
|
89
|
+
* @returns {void}
|
90
|
+
* @private
|
91
|
+
*/
|
92
|
+
function check(node) {
|
93
|
+
if (node.arguments.length < 2) {
|
94
|
+
return;
|
95
|
+
}
|
96
|
+
|
97
|
+
const option = context.options[0] || "always";
|
98
|
+
|
99
|
+
if (option === "never") {
|
100
|
+
checkArguments(node, checkers.unexpected);
|
101
|
+
} else if (option === "always") {
|
102
|
+
checkArguments(node, checkers.missing);
|
103
|
+
} else if (option === "consistent") {
|
104
|
+
const firstArgToken = sourceCode.getFirstToken(node.arguments[0]);
|
105
|
+
const secondArgToken = sourceCode.getFirstToken(node.arguments[1]);
|
106
|
+
|
107
|
+
if (firstArgToken.loc.start.line === secondArgToken.loc.start.line) {
|
108
|
+
checkArguments(node, checkers.unexpected);
|
109
|
+
} else {
|
110
|
+
checkArguments(node, checkers.missing);
|
111
|
+
}
|
112
|
+
}
|
113
|
+
}
|
114
|
+
|
115
|
+
return {
|
116
|
+
CallExpression: check,
|
117
|
+
NewExpression: check
|
118
|
+
};
|
119
|
+
}
|
120
|
+
};
|
@@ -232,25 +232,15 @@ module.exports = {
|
|
232
232
|
};
|
233
233
|
}
|
234
234
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
}
|
239
|
-
|
240
|
-
/**
|
241
|
-
* Validates the parentheses for a node
|
242
|
-
* @param {ASTNode} node The node with parens
|
243
|
-
* @returns {void}
|
244
|
-
*/
|
245
|
-
function validateNode(node) {
|
246
|
-
const parens = getParenTokens(node);
|
247
|
-
|
248
|
-
if (parens) {
|
249
|
-
validateParens(parens, astUtils.isFunction(node) ? node.params : node.arguments);
|
235
|
+
case "ImportExpression": {
|
236
|
+
const leftParen = sourceCode.getFirstToken(node, 1);
|
237
|
+
const rightParen = sourceCode.getLastToken(node);
|
250
238
|
|
251
|
-
|
252
|
-
validateArguments(parens, astUtils.isFunction(node) ? node.params : node.arguments);
|
239
|
+
return { leftParen, rightParen };
|
253
240
|
}
|
241
|
+
|
242
|
+
default:
|
243
|
+
throw new TypeError(`unexpected node with type ${node.type}`);
|
254
244
|
}
|
255
245
|
}
|
256
246
|
|
@@ -259,11 +249,33 @@ module.exports = {
|
|
259
249
|
//----------------------------------------------------------------------
|
260
250
|
|
261
251
|
return {
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
252
|
+
[[
|
253
|
+
"ArrowFunctionExpression",
|
254
|
+
"CallExpression",
|
255
|
+
"FunctionDeclaration",
|
256
|
+
"FunctionExpression",
|
257
|
+
"ImportExpression",
|
258
|
+
"NewExpression"
|
259
|
+
]](node) {
|
260
|
+
const parens = getParenTokens(node);
|
261
|
+
let params;
|
262
|
+
|
263
|
+
if (node.type === "ImportExpression") {
|
264
|
+
params = [node.source];
|
265
|
+
} else if (astUtils.isFunction(node)) {
|
266
|
+
params = node.params;
|
267
|
+
} else {
|
268
|
+
params = node.arguments;
|
269
|
+
}
|
270
|
+
|
271
|
+
if (parens) {
|
272
|
+
validateParens(parens, params);
|
273
|
+
|
274
|
+
if (multilineArgumentsOption) {
|
275
|
+
validateArguments(parens, params);
|
276
|
+
}
|
277
|
+
}
|
278
|
+
}
|
267
279
|
};
|
268
280
|
}
|
269
281
|
};
|
package/lib/rules/indent.js
CHANGED
@@ -99,7 +99,8 @@ const KNOWN_NODES = new Set([
|
|
99
99
|
"ImportDeclaration",
|
100
100
|
"ImportSpecifier",
|
101
101
|
"ImportDefaultSpecifier",
|
102
|
-
"ImportNamespaceSpecifier"
|
102
|
+
"ImportNamespaceSpecifier",
|
103
|
+
"ImportExpression"
|
103
104
|
]);
|
104
105
|
|
105
106
|
/*
|
@@ -1109,7 +1110,6 @@ module.exports = {
|
|
1109
1110
|
|
1110
1111
|
CallExpression: addFunctionCallIndent,
|
1111
1112
|
|
1112
|
-
|
1113
1113
|
"ClassDeclaration[superClass], ClassExpression[superClass]"(node) {
|
1114
1114
|
const classToken = sourceCode.getFirstToken(node);
|
1115
1115
|
const extendsToken = sourceCode.getTokenBefore(node.superClass, astUtils.isNotOpeningParenToken);
|
@@ -1236,6 +1236,17 @@ module.exports = {
|
|
1236
1236
|
}
|
1237
1237
|
},
|
1238
1238
|
|
1239
|
+
ImportExpression(node) {
|
1240
|
+
const openingParen = sourceCode.getFirstToken(node, 1);
|
1241
|
+
const closingParen = sourceCode.getLastToken(node);
|
1242
|
+
|
1243
|
+
parameterParens.add(openingParen);
|
1244
|
+
parameterParens.add(closingParen);
|
1245
|
+
offsets.setDesiredOffset(openingParen, sourceCode.getTokenBefore(openingParen), 0);
|
1246
|
+
|
1247
|
+
addElementListIndent([node.source], openingParen, closingParen, options.CallExpression.arguments);
|
1248
|
+
},
|
1249
|
+
|
1239
1250
|
"MemberExpression, JSXMemberExpression, MetaProperty"(node) {
|
1240
1251
|
const object = node.type === "MetaProperty" ? node.meta : node.object;
|
1241
1252
|
const firstNonObjectToken = sourceCode.getFirstTokenBetween(object, node.property, astUtils.isNotClosingParenToken);
|
package/lib/rules/index.js
CHANGED
@@ -46,6 +46,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({
|
|
46
46
|
"func-name-matching": () => require("./func-name-matching"),
|
47
47
|
"func-names": () => require("./func-names"),
|
48
48
|
"func-style": () => require("./func-style"),
|
49
|
+
"function-call-argument-newline": () => require("./function-call-argument-newline"),
|
49
50
|
"function-paren-newline": () => require("./function-paren-newline"),
|
50
51
|
"generator-star-spacing": () => require("./generator-star-spacing"),
|
51
52
|
"getter-return": () => require("./getter-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
|
}
|
package/lib/rules/new-cap.js
CHANGED
@@ -33,17 +33,19 @@ module.exports = {
|
|
33
33
|
|
34
34
|
return {
|
35
35
|
SwitchStatement(node) {
|
36
|
-
const
|
36
|
+
const previousKeys = new Set();
|
37
37
|
|
38
|
-
node.cases
|
39
|
-
|
38
|
+
for (const switchCase of node.cases) {
|
39
|
+
if (switchCase.test) {
|
40
|
+
const key = sourceCode.getText(switchCase.test);
|
40
41
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
42
|
+
if (previousKeys.has(key)) {
|
43
|
+
context.report({ node: switchCase, messageId: "unexpected" });
|
44
|
+
} else {
|
45
|
+
previousKeys.add(key);
|
46
|
+
}
|
45
47
|
}
|
46
|
-
}
|
48
|
+
}
|
47
49
|
}
|
48
50
|
};
|
49
51
|
}
|
@@ -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)
|
@@ -98,6 +98,7 @@ module.exports = {
|
|
98
98
|
grandparent.type === "CallExpression" &&
|
99
99
|
grandparent.callee === parent &&
|
100
100
|
grandparent.arguments.length === 1 &&
|
101
|
+
grandparent.arguments[0].type !== "SpreadElement" &&
|
101
102
|
parent.type === "MemberExpression" &&
|
102
103
|
parent.object === node &&
|
103
104
|
astUtils.getStaticPropertyName(parent) === "bind"
|