eslint 3.16.0 → 3.18.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 +73 -0
- package/conf/eslint-recommended.js +2 -0
- package/lib/ast-utils.js +3 -67
- package/lib/code-path-analysis/code-path-analyzer.js +2 -7
- package/lib/code-path-analysis/debug-helpers.js +17 -16
- package/lib/config/config-file.js +68 -38
- package/lib/eslint.js +5 -5
- package/lib/formatters/stylish.js +5 -4
- package/lib/ignored-paths.js +6 -0
- package/lib/internal-rules/internal-no-invalid-meta.js +2 -40
- package/lib/rules/array-callback-return.js +15 -5
- package/lib/rules/capitalized-comments.js +2 -1
- package/lib/rules/complexity.js +14 -8
- package/lib/rules/consistent-return.js +17 -10
- package/lib/rules/func-name-matching.js +18 -7
- package/lib/rules/func-names.js +20 -5
- package/lib/rules/keyword-spacing.js +19 -4
- package/lib/rules/line-comment-position.js +15 -5
- package/lib/rules/lines-around-comment.js +19 -0
- package/lib/rules/max-params.js +17 -4
- package/lib/rules/max-statements.js +11 -10
- package/lib/rules/no-compare-neg-zero.js +53 -0
- package/lib/rules/no-else-return.js +13 -1
- package/lib/rules/no-empty-function.js +9 -16
- package/lib/rules/no-extra-parens.js +64 -19
- package/lib/rules/no-extra-semi.js +13 -1
- package/lib/rules/no-global-assign.js +1 -1
- package/lib/rules/no-invalid-regexp.js +2 -1
- package/lib/rules/no-multiple-empty-lines.js +2 -4
- package/lib/rules/no-new-func.js +6 -8
- package/lib/rules/no-new.js +2 -6
- package/lib/rules/no-param-reassign.js +29 -6
- package/lib/rules/no-process-exit.js +2 -10
- package/lib/rules/no-restricted-properties.js +2 -0
- package/lib/rules/no-restricted-syntax.js +6 -22
- package/lib/rules/no-return-await.js +1 -1
- package/lib/rules/no-sync.js +8 -13
- package/lib/rules/no-unused-expressions.js +10 -1
- package/lib/rules/no-unused-vars.js +12 -12
- package/lib/rules/no-use-before-define.js +1 -1
- package/lib/rules/no-useless-escape.js +8 -2
- package/lib/rules/no-useless-return.js +13 -2
- package/lib/rules/nonblock-statement-body-position.js +114 -0
- package/lib/rules/object-shorthand.js +2 -1
- package/lib/rules/operator-assignment.js +1 -1
- package/lib/rules/padded-blocks.js +37 -28
- package/lib/rules/prefer-destructuring.js +1 -1
- package/lib/rules/semi.js +13 -1
- package/lib/rules/sort-vars.js +3 -5
- package/lib/rules/space-unary-ops.js +19 -1
- package/lib/rules/strict.js +8 -2
- package/lib/rules/yoda.js +2 -2
- package/lib/testers/rule-tester.js +44 -13
- package/lib/util/fix-tracker.js +121 -0
- package/lib/util/node-event-generator.js +274 -4
- package/lib/util/source-code-fixer.js +2 -2
- package/lib/util/source-code.js +99 -2
- package/lib/util/traverser.js +16 -25
- package/package.json +8 -8
@@ -0,0 +1,114 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview enforce the location of single-line statements
|
3
|
+
* @author Teddy Katz
|
4
|
+
*/
|
5
|
+
"use strict";
|
6
|
+
|
7
|
+
//------------------------------------------------------------------------------
|
8
|
+
// Rule Definition
|
9
|
+
//------------------------------------------------------------------------------
|
10
|
+
|
11
|
+
const POSITION_SCHEMA = { enum: ["beside", "below", "any"] };
|
12
|
+
|
13
|
+
module.exports = {
|
14
|
+
meta: {
|
15
|
+
docs: {
|
16
|
+
description: "enforce the location of single-line statements",
|
17
|
+
category: "Stylistic Issues",
|
18
|
+
recommended: false
|
19
|
+
},
|
20
|
+
fixable: "whitespace",
|
21
|
+
schema: [
|
22
|
+
POSITION_SCHEMA,
|
23
|
+
{
|
24
|
+
properties: {
|
25
|
+
overrides: {
|
26
|
+
properties: {
|
27
|
+
if: POSITION_SCHEMA,
|
28
|
+
else: POSITION_SCHEMA,
|
29
|
+
while: POSITION_SCHEMA,
|
30
|
+
do: POSITION_SCHEMA,
|
31
|
+
for: POSITION_SCHEMA
|
32
|
+
},
|
33
|
+
additionalProperties: false
|
34
|
+
}
|
35
|
+
},
|
36
|
+
additionalProperties: false
|
37
|
+
}
|
38
|
+
]
|
39
|
+
},
|
40
|
+
|
41
|
+
create(context) {
|
42
|
+
const sourceCode = context.getSourceCode();
|
43
|
+
|
44
|
+
//----------------------------------------------------------------------
|
45
|
+
// Helpers
|
46
|
+
//----------------------------------------------------------------------
|
47
|
+
|
48
|
+
/**
|
49
|
+
* Gets the applicable preference for a particular keyword
|
50
|
+
* @param {string} keywordName The name of a keyword, e.g. 'if'
|
51
|
+
* @returns {string} The applicable option for the keyword, e.g. 'beside'
|
52
|
+
*/
|
53
|
+
function getOption(keywordName) {
|
54
|
+
return context.options[1] && context.options[1].overrides && context.options[1].overrides[keywordName] ||
|
55
|
+
context.options[0] ||
|
56
|
+
"beside";
|
57
|
+
}
|
58
|
+
|
59
|
+
/**
|
60
|
+
* Validates the location of a single-line statement
|
61
|
+
* @param {ASTNode} node The single-line statement
|
62
|
+
* @param {string} keywordName The applicable keyword name for the single-line statement
|
63
|
+
* @returns {void}
|
64
|
+
*/
|
65
|
+
function validateStatement(node, keywordName) {
|
66
|
+
const option = getOption(keywordName);
|
67
|
+
|
68
|
+
if (node.type === "BlockStatement" || option === "any") {
|
69
|
+
return;
|
70
|
+
}
|
71
|
+
|
72
|
+
const tokenBefore = sourceCode.getTokenBefore(node);
|
73
|
+
|
74
|
+
if (tokenBefore.loc.end.line === node.loc.start.line && option === "below") {
|
75
|
+
context.report({
|
76
|
+
node,
|
77
|
+
message: "Expected a linebreak before this statement.",
|
78
|
+
fix: fixer => fixer.insertTextBefore(node, "\n")
|
79
|
+
});
|
80
|
+
} else if (tokenBefore.loc.end.line !== node.loc.start.line && option === "beside") {
|
81
|
+
context.report({
|
82
|
+
node,
|
83
|
+
message: "Expected no linebreak before this statement.",
|
84
|
+
fix(fixer) {
|
85
|
+
if (sourceCode.getText().slice(tokenBefore.range[1], node.range[0]).trim()) {
|
86
|
+
return null;
|
87
|
+
}
|
88
|
+
return fixer.replaceTextRange([tokenBefore.range[1], node.range[0]], " ");
|
89
|
+
}
|
90
|
+
});
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
//----------------------------------------------------------------------
|
95
|
+
// Public
|
96
|
+
//----------------------------------------------------------------------
|
97
|
+
|
98
|
+
return {
|
99
|
+
IfStatement(node) {
|
100
|
+
validateStatement(node.consequent, "if");
|
101
|
+
|
102
|
+
// Check the `else` node, but don't check 'else if' statements.
|
103
|
+
if (node.alternate && node.alternate.type !== "IfStatement") {
|
104
|
+
validateStatement(node.alternate, "else");
|
105
|
+
}
|
106
|
+
},
|
107
|
+
WhileStatement: node => validateStatement(node.body, "while"),
|
108
|
+
DoWhileStatement: node => validateStatement(node.body, "do"),
|
109
|
+
ForStatement: node => validateStatement(node.body, "for"),
|
110
|
+
ForInStatement: node => validateStatement(node.body, "for"),
|
111
|
+
ForOfStatement: node => validateStatement(node.body, "for")
|
112
|
+
};
|
113
|
+
}
|
114
|
+
};
|
@@ -368,11 +368,12 @@ module.exports = {
|
|
368
368
|
// Checks for property/method shorthand.
|
369
369
|
if (isConciseProperty) {
|
370
370
|
if (node.method && (APPLY_NEVER || AVOID_QUOTES && isStringLiteral(node.key))) {
|
371
|
+
const message = APPLY_NEVER ? "Expected longform method syntax." : "Expected longform method syntax for string literal keys.";
|
371
372
|
|
372
373
|
// { x() {} } should be written as { x: function() {} }
|
373
374
|
context.report({
|
374
375
|
node,
|
375
|
-
message
|
376
|
+
message,
|
376
377
|
fix: fixer => makeFunctionLongform(fixer, node)
|
377
378
|
});
|
378
379
|
} else if (APPLY_NEVER) {
|
@@ -135,7 +135,7 @@ module.exports = {
|
|
135
135
|
const equalsToken = getOperatorToken(node);
|
136
136
|
const operatorToken = getOperatorToken(expr);
|
137
137
|
const leftText = sourceCode.getText().slice(node.range[0], equalsToken.range[0]);
|
138
|
-
const rightText = sourceCode.getText().slice(operatorToken.range[1],
|
138
|
+
const rightText = sourceCode.getText().slice(operatorToken.range[1], expr.right.range[1]);
|
139
139
|
|
140
140
|
return fixer.replaceText(node, `${leftText}${expr.operator}=${rightText}`);
|
141
141
|
}
|
@@ -90,23 +90,32 @@ module.exports = {
|
|
90
90
|
return node.type === "Line" || node.type === "Block";
|
91
91
|
}
|
92
92
|
|
93
|
+
/**
|
94
|
+
* Checks if there is padding between two tokens
|
95
|
+
* @param {Token} first The first token
|
96
|
+
* @param {Token} second The second token
|
97
|
+
* @returns {boolean} True if there is at least a line between the tokens
|
98
|
+
*/
|
99
|
+
function isPaddingBetweenTokens(first, second) {
|
100
|
+
return second.loc.start.line - first.loc.end.line >= 2;
|
101
|
+
}
|
102
|
+
|
103
|
+
|
93
104
|
/**
|
94
105
|
* Checks if the given token has a blank line after it.
|
95
106
|
* @param {Token} token The token to check.
|
96
107
|
* @returns {boolean} Whether or not the token is followed by a blank line.
|
97
108
|
*/
|
98
|
-
function
|
99
|
-
|
100
|
-
|
101
|
-
let first = token;
|
109
|
+
function getFirstBlockToken(token) {
|
110
|
+
let prev = token,
|
111
|
+
first = token;
|
102
112
|
|
103
113
|
do {
|
114
|
+
prev = first;
|
104
115
|
first = sourceCode.getTokenAfter(first, { includeComments: true });
|
105
|
-
} while (isComment(first) && first.loc.start.line ===
|
106
|
-
|
107
|
-
const firstLine = first.loc.start.line;
|
116
|
+
} while (isComment(first) && first.loc.start.line === prev.loc.end.line);
|
108
117
|
|
109
|
-
return
|
118
|
+
return first;
|
110
119
|
}
|
111
120
|
|
112
121
|
/**
|
@@ -114,18 +123,16 @@ module.exports = {
|
|
114
123
|
* @param {Token} token The token to check
|
115
124
|
* @returns {boolean} Whether or not the token is preceeded by a blank line
|
116
125
|
*/
|
117
|
-
function
|
118
|
-
|
119
|
-
|
120
|
-
let last = token;
|
126
|
+
function getLastBlockToken(token) {
|
127
|
+
let last = token,
|
128
|
+
next = token;
|
121
129
|
|
122
130
|
do {
|
131
|
+
next = last;
|
123
132
|
last = sourceCode.getTokenBefore(last, { includeComments: true });
|
124
|
-
} while (isComment(last) && last.loc.end.line ===
|
125
|
-
|
126
|
-
const lastLine = last.loc.end.line;
|
133
|
+
} while (isComment(last) && last.loc.end.line === next.loc.start.line);
|
127
134
|
|
128
|
-
return
|
135
|
+
return last;
|
129
136
|
}
|
130
137
|
|
131
138
|
/**
|
@@ -155,17 +162,21 @@ module.exports = {
|
|
155
162
|
*/
|
156
163
|
function checkPadding(node) {
|
157
164
|
const openBrace = getOpenBrace(node),
|
165
|
+
firstBlockToken = getFirstBlockToken(openBrace),
|
166
|
+
tokenBeforeFirst = sourceCode.getTokenBefore(firstBlockToken, { includeComments: true }),
|
158
167
|
closeBrace = sourceCode.getLastToken(node),
|
159
|
-
|
160
|
-
|
168
|
+
lastBlockToken = getLastBlockToken(closeBrace),
|
169
|
+
tokenAfterLast = sourceCode.getTokenAfter(lastBlockToken, { includeComments: true }),
|
170
|
+
blockHasTopPadding = isPaddingBetweenTokens(tokenBeforeFirst, firstBlockToken),
|
171
|
+
blockHasBottomPadding = isPaddingBetweenTokens(lastBlockToken, tokenAfterLast);
|
161
172
|
|
162
173
|
if (requirePaddingFor(node)) {
|
163
174
|
if (!blockHasTopPadding) {
|
164
175
|
context.report({
|
165
176
|
node,
|
166
|
-
loc: { line:
|
177
|
+
loc: { line: tokenBeforeFirst.loc.start.line, column: tokenBeforeFirst.loc.start.column },
|
167
178
|
fix(fixer) {
|
168
|
-
return fixer.insertTextAfter(
|
179
|
+
return fixer.insertTextAfter(tokenBeforeFirst, "\n");
|
169
180
|
},
|
170
181
|
message: ALWAYS_MESSAGE
|
171
182
|
});
|
@@ -173,36 +184,34 @@ module.exports = {
|
|
173
184
|
if (!blockHasBottomPadding) {
|
174
185
|
context.report({
|
175
186
|
node,
|
176
|
-
loc: { line:
|
187
|
+
loc: { line: tokenAfterLast.loc.end.line, column: tokenAfterLast.loc.end.column - 1 },
|
177
188
|
fix(fixer) {
|
178
|
-
return fixer.insertTextBefore(
|
189
|
+
return fixer.insertTextBefore(tokenAfterLast, "\n");
|
179
190
|
},
|
180
191
|
message: ALWAYS_MESSAGE
|
181
192
|
});
|
182
193
|
}
|
183
194
|
} else {
|
184
195
|
if (blockHasTopPadding) {
|
185
|
-
const nextToken = sourceCode.getTokenAfter(openBrace, { includeComments: true });
|
186
196
|
|
187
197
|
context.report({
|
188
198
|
node,
|
189
|
-
loc: { line:
|
199
|
+
loc: { line: tokenBeforeFirst.loc.start.line, column: tokenBeforeFirst.loc.start.column },
|
190
200
|
fix(fixer) {
|
191
|
-
return fixer.replaceTextRange([
|
201
|
+
return fixer.replaceTextRange([tokenBeforeFirst.end, firstBlockToken.start - firstBlockToken.loc.start.column], "\n");
|
192
202
|
},
|
193
203
|
message: NEVER_MESSAGE
|
194
204
|
});
|
195
205
|
}
|
196
206
|
|
197
207
|
if (blockHasBottomPadding) {
|
198
|
-
const previousToken = sourceCode.getTokenBefore(closeBrace, { includeComments: true });
|
199
208
|
|
200
209
|
context.report({
|
201
210
|
node,
|
202
|
-
loc: { line:
|
211
|
+
loc: { line: tokenAfterLast.loc.end.line, column: tokenAfterLast.loc.end.column - 1 },
|
203
212
|
message: NEVER_MESSAGE,
|
204
213
|
fix(fixer) {
|
205
|
-
return fixer.replaceTextRange([
|
214
|
+
return fixer.replaceTextRange([lastBlockToken.end, tokenAfterLast.start - tokenAfterLast.loc.start.column], "\n");
|
206
215
|
}
|
207
216
|
});
|
208
217
|
}
|
@@ -89,7 +89,7 @@ module.exports = {
|
|
89
89
|
* @returns {void}
|
90
90
|
*/
|
91
91
|
function report(reportNode, type) {
|
92
|
-
context.report({ node: reportNode, message:
|
92
|
+
context.report({ node: reportNode, message: "Use {{type}} destructuring.", data: { type } });
|
93
93
|
}
|
94
94
|
|
95
95
|
/**
|
package/lib/rules/semi.js
CHANGED
@@ -4,6 +4,12 @@
|
|
4
4
|
*/
|
5
5
|
"use strict";
|
6
6
|
|
7
|
+
//------------------------------------------------------------------------------
|
8
|
+
// Requirements
|
9
|
+
//------------------------------------------------------------------------------
|
10
|
+
|
11
|
+
const FixTracker = require("../util/fix-tracker");
|
12
|
+
|
7
13
|
//------------------------------------------------------------------------------
|
8
14
|
// Rule Definition
|
9
15
|
//------------------------------------------------------------------------------
|
@@ -85,7 +91,13 @@ module.exports = {
|
|
85
91
|
message = "Extra semicolon.";
|
86
92
|
loc = loc.start;
|
87
93
|
fix = function(fixer) {
|
88
|
-
|
94
|
+
|
95
|
+
// Expand the replacement range to include the surrounding
|
96
|
+
// tokens to avoid conflicting with no-extra-semi.
|
97
|
+
// https://github.com/eslint/eslint/issues/7928
|
98
|
+
return new FixTracker(fixer, sourceCode)
|
99
|
+
.retainSurroundingTokens(lastToken)
|
100
|
+
.remove(lastToken);
|
89
101
|
};
|
90
102
|
}
|
91
103
|
|
package/lib/rules/sort-vars.js
CHANGED
@@ -37,11 +37,9 @@ module.exports = {
|
|
37
37
|
|
38
38
|
return {
|
39
39
|
VariableDeclaration(node) {
|
40
|
-
node.declarations.
|
41
|
-
if (decl.id.type === "ObjectPattern" || decl.id.type === "ArrayPattern") {
|
42
|
-
return memo;
|
43
|
-
}
|
40
|
+
const idDeclarations = node.declarations.filter(decl => decl.id.type === "Identifier");
|
44
41
|
|
42
|
+
idDeclarations.slice(1).reduce((memo, decl) => {
|
45
43
|
let lastVariableName = memo.id.name,
|
46
44
|
currenVariableName = decl.id.name;
|
47
45
|
|
@@ -56,7 +54,7 @@ module.exports = {
|
|
56
54
|
}
|
57
55
|
return decl;
|
58
56
|
|
59
|
-
},
|
57
|
+
}, idDeclarations[0]);
|
60
58
|
}
|
61
59
|
};
|
62
60
|
}
|
@@ -68,6 +68,21 @@ module.exports = {
|
|
68
68
|
return node.argument && node.argument.type && node.argument.type === "ObjectExpression";
|
69
69
|
}
|
70
70
|
|
71
|
+
/**
|
72
|
+
* Check if it is safe to remove the spaces between the two tokens in
|
73
|
+
* the context of a non-word prefix unary operator. For example, `+ +1`
|
74
|
+
* cannot safely be changed to `++1`.
|
75
|
+
* @param {Token} firstToken The operator for a non-word prefix unary operator
|
76
|
+
* @param {Token} secondToken The first token of its operand
|
77
|
+
* @returns {boolean} Whether or not the spacing between the tokens can be removed
|
78
|
+
*/
|
79
|
+
function canRemoveSpacesBetween(firstToken, secondToken) {
|
80
|
+
return !(
|
81
|
+
(firstToken.value === "+" && secondToken.value[0] === "+") ||
|
82
|
+
(firstToken.value === "-" && secondToken.value[0] === "-")
|
83
|
+
);
|
84
|
+
}
|
85
|
+
|
71
86
|
/**
|
72
87
|
* Checks if an override exists for a given operator.
|
73
88
|
* @param {ASTnode} node AST node
|
@@ -244,7 +259,10 @@ module.exports = {
|
|
244
259
|
operator: firstToken.value
|
245
260
|
},
|
246
261
|
fix(fixer) {
|
247
|
-
|
262
|
+
if (canRemoveSpacesBetween(firstToken, secondToken)) {
|
263
|
+
return fixer.removeRange([firstToken.range[1], secondToken.range[0]]);
|
264
|
+
}
|
265
|
+
return null;
|
248
266
|
}
|
249
267
|
});
|
250
268
|
}
|
package/lib/rules/strict.js
CHANGED
@@ -9,6 +9,8 @@
|
|
9
9
|
// Requirements
|
10
10
|
//------------------------------------------------------------------------------
|
11
11
|
|
12
|
+
const astUtils = require("../ast-utils");
|
13
|
+
|
12
14
|
//------------------------------------------------------------------------------
|
13
15
|
// Helpers
|
14
16
|
//------------------------------------------------------------------------------
|
@@ -23,7 +25,7 @@ const messages = {
|
|
23
25
|
implied: "'use strict' is unnecessary when implied strict mode is enabled.",
|
24
26
|
unnecessaryInClasses: "'use strict' is unnecessary inside of classes.",
|
25
27
|
nonSimpleParameterList: "'use strict' directive inside a function with non-simple parameter list throws a syntax error since ES2016.",
|
26
|
-
wrap: "Wrap
|
28
|
+
wrap: "Wrap {{name}} in a function with 'use strict' directive."
|
27
29
|
};
|
28
30
|
|
29
31
|
/**
|
@@ -188,7 +190,11 @@ module.exports = {
|
|
188
190
|
if (isSimpleParameterList(node.params)) {
|
189
191
|
context.report({ node, message: messages.function });
|
190
192
|
} else {
|
191
|
-
context.report({
|
193
|
+
context.report({
|
194
|
+
node,
|
195
|
+
message: messages.wrap,
|
196
|
+
data: { name: astUtils.getFunctionNameWithKind(node) }
|
197
|
+
});
|
192
198
|
}
|
193
199
|
}
|
194
200
|
|
package/lib/rules/yoda.js
CHANGED
@@ -267,8 +267,8 @@ module.exports = {
|
|
267
267
|
const operatorToken = sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
|
268
268
|
const textBeforeOperator = sourceCode.getText().slice(sourceCode.getTokenBefore(operatorToken).range[1], operatorToken.range[0]);
|
269
269
|
const textAfterOperator = sourceCode.getText().slice(operatorToken.range[1], sourceCode.getTokenAfter(operatorToken).range[0]);
|
270
|
-
const leftText = sourceCode.getText().slice(
|
271
|
-
const rightText = sourceCode.getText().slice(sourceCode.getTokenAfter(operatorToken).range[0],
|
270
|
+
const leftText = sourceCode.getText().slice(node.range[0], sourceCode.getTokenBefore(operatorToken).range[1]);
|
271
|
+
const rightText = sourceCode.getText().slice(sourceCode.getTokenAfter(operatorToken).range[0], node.range[1]);
|
272
272
|
|
273
273
|
return rightText + textBeforeOperator + OPERATOR_FLIP_MAP[operatorToken.value] + textAfterOperator + leftText;
|
274
274
|
}
|
@@ -343,12 +343,13 @@ RuleTester.prototype = {
|
|
343
343
|
* running the rule under test.
|
344
344
|
*/
|
345
345
|
eslint.reset();
|
346
|
+
|
346
347
|
eslint.on("Program", node => {
|
347
348
|
beforeAST = cloneDeeplyExcludesParent(node);
|
349
|
+
});
|
348
350
|
|
349
|
-
|
350
|
-
|
351
|
-
});
|
351
|
+
eslint.on("Program:exit", node => {
|
352
|
+
afterAST = node;
|
352
353
|
});
|
353
354
|
|
354
355
|
// Freezes rule-context properties.
|
@@ -385,7 +386,7 @@ RuleTester.prototype = {
|
|
385
386
|
return {
|
386
387
|
messages: eslint.verify(code, config, filename, true),
|
387
388
|
beforeAST,
|
388
|
-
afterAST
|
389
|
+
afterAST: cloneDeeplyExcludesParent(afterAST)
|
389
390
|
};
|
390
391
|
} finally {
|
391
392
|
rules.get = originalGet;
|
@@ -425,6 +426,28 @@ RuleTester.prototype = {
|
|
425
426
|
assertASTDidntChange(result.beforeAST, result.afterAST);
|
426
427
|
}
|
427
428
|
|
429
|
+
/**
|
430
|
+
* Asserts that the message matches its expected value. If the expected
|
431
|
+
* value is a regular expression, it is checked against the actual
|
432
|
+
* value.
|
433
|
+
* @param {string} actual Actual value
|
434
|
+
* @param {string|RegExp} expected Expected value
|
435
|
+
* @returns {void}
|
436
|
+
* @private
|
437
|
+
*/
|
438
|
+
function assertMessageMatches(actual, expected) {
|
439
|
+
if (expected instanceof RegExp) {
|
440
|
+
|
441
|
+
// assert.js doesn't have a built-in RegExp match function
|
442
|
+
assert.ok(
|
443
|
+
expected.test(actual),
|
444
|
+
`Expected '${actual}' to match ${expected}`
|
445
|
+
);
|
446
|
+
} else {
|
447
|
+
assert.equal(actual, expected);
|
448
|
+
}
|
449
|
+
}
|
450
|
+
|
428
451
|
/**
|
429
452
|
* Check if the template is invalid or not
|
430
453
|
* all invalid cases go through this.
|
@@ -454,10 +477,10 @@ RuleTester.prototype = {
|
|
454
477
|
assert.ok(!("fatal" in messages[i]), `A fatal parsing error occurred: ${messages[i].message}`);
|
455
478
|
assert.equal(messages[i].ruleId, ruleName, "Error rule name should be the same as the name of the rule being tested");
|
456
479
|
|
457
|
-
if (typeof item.errors[i] === "string") {
|
480
|
+
if (typeof item.errors[i] === "string" || item.errors[i] instanceof RegExp) {
|
458
481
|
|
459
482
|
// Just an error message.
|
460
|
-
|
483
|
+
assertMessageMatches(messages[i].message, item.errors[i]);
|
461
484
|
} else if (typeof item.errors[i] === "object") {
|
462
485
|
|
463
486
|
/*
|
@@ -466,7 +489,7 @@ RuleTester.prototype = {
|
|
466
489
|
* column.
|
467
490
|
*/
|
468
491
|
if (item.errors[i].message) {
|
469
|
-
|
492
|
+
assertMessageMatches(messages[i].message, item.errors[i].message);
|
470
493
|
}
|
471
494
|
|
472
495
|
if (item.errors[i].type) {
|
@@ -490,16 +513,24 @@ RuleTester.prototype = {
|
|
490
513
|
}
|
491
514
|
} else {
|
492
515
|
|
493
|
-
//
|
494
|
-
assert.fail(messages[i], null, "Error should be a string or
|
516
|
+
// Message was an unexpected type
|
517
|
+
assert.fail(messages[i], null, "Error should be a string, object, or RegExp.");
|
495
518
|
}
|
496
519
|
}
|
497
520
|
}
|
498
521
|
|
499
522
|
if (item.hasOwnProperty("output")) {
|
500
|
-
|
501
|
-
|
502
|
-
|
523
|
+
if (item.output === null) {
|
524
|
+
assert.strictEqual(
|
525
|
+
messages.filter(message => message.fix).length,
|
526
|
+
0,
|
527
|
+
"Expected no autofixes to be suggested"
|
528
|
+
);
|
529
|
+
} else {
|
530
|
+
const fixResult = SourceCodeFixer.applyFixes(eslint.getSourceCode(), messages);
|
531
|
+
|
532
|
+
assert.equal(fixResult.output, item.output, "Output is incorrect.");
|
533
|
+
}
|
503
534
|
}
|
504
535
|
|
505
536
|
assertASTDidntChange(result.beforeAST, result.afterAST);
|
@@ -512,7 +543,7 @@ RuleTester.prototype = {
|
|
512
543
|
RuleTester.describe(ruleName, () => {
|
513
544
|
RuleTester.describe("valid", () => {
|
514
545
|
test.valid.forEach(valid => {
|
515
|
-
RuleTester.it(valid.code
|
546
|
+
RuleTester.it(typeof valid === "object" ? valid.code : valid, () => {
|
516
547
|
eslint.defineRules(this.rules);
|
517
548
|
testValidTemplate(ruleName, valid);
|
518
549
|
});
|
@@ -0,0 +1,121 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview Helper class to aid in constructing fix commands.
|
3
|
+
* @author Alan Pierce
|
4
|
+
*/
|
5
|
+
"use strict";
|
6
|
+
|
7
|
+
//------------------------------------------------------------------------------
|
8
|
+
// Requirements
|
9
|
+
//------------------------------------------------------------------------------
|
10
|
+
|
11
|
+
const astUtils = require("../ast-utils");
|
12
|
+
|
13
|
+
//------------------------------------------------------------------------------
|
14
|
+
// Public Interface
|
15
|
+
//------------------------------------------------------------------------------
|
16
|
+
|
17
|
+
/**
|
18
|
+
* A helper class to combine fix options into a fix command. Currently, it
|
19
|
+
* exposes some "retain" methods that extend the range of the text being
|
20
|
+
* replaced so that other fixes won't touch that region in the same pass.
|
21
|
+
*/
|
22
|
+
class FixTracker {
|
23
|
+
|
24
|
+
/**
|
25
|
+
* Create a new FixTracker.
|
26
|
+
*
|
27
|
+
* @param {ruleFixer} fixer A ruleFixer instance.
|
28
|
+
* @param {SourceCode} sourceCode A SourceCode object for the current code.
|
29
|
+
*/
|
30
|
+
constructor(fixer, sourceCode) {
|
31
|
+
this.fixer = fixer;
|
32
|
+
this.sourceCode = sourceCode;
|
33
|
+
this.retainedRange = null;
|
34
|
+
}
|
35
|
+
|
36
|
+
/**
|
37
|
+
* Mark the given range as "retained", meaning that other fixes may not
|
38
|
+
* may not modify this region in the same pass.
|
39
|
+
*
|
40
|
+
* @param {int[]} range The range to retain.
|
41
|
+
* @returns {FixTracker} The same RuleFixer, for chained calls.
|
42
|
+
*/
|
43
|
+
retainRange(range) {
|
44
|
+
this.retainedRange = range;
|
45
|
+
return this;
|
46
|
+
}
|
47
|
+
|
48
|
+
/**
|
49
|
+
* Given a node, find the function containing it (or the entire program) and
|
50
|
+
* mark it as retained, meaning that other fixes may not modify it in this
|
51
|
+
* pass. This is useful for avoiding conflicts in fixes that modify control
|
52
|
+
* flow.
|
53
|
+
*
|
54
|
+
* @param {ASTNode} node The node to use as a starting point.
|
55
|
+
* @returns {FixTracker} The same RuleFixer, for chained calls.
|
56
|
+
*/
|
57
|
+
retainEnclosingFunction(node) {
|
58
|
+
const functionNode = astUtils.getUpperFunction(node);
|
59
|
+
|
60
|
+
return this.retainRange(
|
61
|
+
functionNode ? functionNode.range : this.sourceCode.ast.range);
|
62
|
+
}
|
63
|
+
|
64
|
+
/**
|
65
|
+
* Given a node or token, find the token before and afterward, and mark that
|
66
|
+
* range as retained, meaning that other fixes may not modify it in this
|
67
|
+
* pass. This is useful for avoiding conflicts in fixes that make a small
|
68
|
+
* change to the code where the AST should not be changed.
|
69
|
+
*
|
70
|
+
* @param {ASTNode|Token} nodeOrToken The node or token to use as a starting
|
71
|
+
* point. The token to the left and right are use in the range.
|
72
|
+
* @returns {FixTracker} The same RuleFixer, for chained calls.
|
73
|
+
*/
|
74
|
+
retainSurroundingTokens(nodeOrToken) {
|
75
|
+
const tokenBefore = this.sourceCode.getTokenBefore(nodeOrToken) || nodeOrToken;
|
76
|
+
const tokenAfter = this.sourceCode.getTokenAfter(nodeOrToken) || nodeOrToken;
|
77
|
+
|
78
|
+
return this.retainRange([tokenBefore.range[0], tokenAfter.range[1]]);
|
79
|
+
}
|
80
|
+
|
81
|
+
/**
|
82
|
+
* Create a fix command that replaces the given range with the given text,
|
83
|
+
* accounting for any retained ranges.
|
84
|
+
*
|
85
|
+
* @param {int[]} range The range to remove in the fix.
|
86
|
+
* @param {string} text The text to insert in place of the range.
|
87
|
+
* @returns {Object} The fix command.
|
88
|
+
*/
|
89
|
+
replaceTextRange(range, text) {
|
90
|
+
let actualRange;
|
91
|
+
|
92
|
+
if (this.retainedRange) {
|
93
|
+
actualRange = [
|
94
|
+
Math.min(this.retainedRange[0], range[0]),
|
95
|
+
Math.max(this.retainedRange[1], range[1])
|
96
|
+
];
|
97
|
+
} else {
|
98
|
+
actualRange = range;
|
99
|
+
}
|
100
|
+
|
101
|
+
return this.fixer.replaceTextRange(
|
102
|
+
actualRange,
|
103
|
+
this.sourceCode.text.slice(actualRange[0], range[0]) +
|
104
|
+
text +
|
105
|
+
this.sourceCode.text.slice(range[1], actualRange[1])
|
106
|
+
);
|
107
|
+
}
|
108
|
+
|
109
|
+
/**
|
110
|
+
* Create a fix command that removes the given node or token, accounting for
|
111
|
+
* any retained ranges.
|
112
|
+
*
|
113
|
+
* @param {ASTNode|Token} nodeOrToken The node or token to remove.
|
114
|
+
* @returns {Object} The fix command.
|
115
|
+
*/
|
116
|
+
remove(nodeOrToken) {
|
117
|
+
return this.replaceTextRange(nodeOrToken.range, "");
|
118
|
+
}
|
119
|
+
}
|
120
|
+
|
121
|
+
module.exports = FixTracker;
|