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.
Files changed (59) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/conf/eslint-recommended.js +2 -0
  3. package/lib/ast-utils.js +3 -67
  4. package/lib/code-path-analysis/code-path-analyzer.js +2 -7
  5. package/lib/code-path-analysis/debug-helpers.js +17 -16
  6. package/lib/config/config-file.js +68 -38
  7. package/lib/eslint.js +5 -5
  8. package/lib/formatters/stylish.js +5 -4
  9. package/lib/ignored-paths.js +6 -0
  10. package/lib/internal-rules/internal-no-invalid-meta.js +2 -40
  11. package/lib/rules/array-callback-return.js +15 -5
  12. package/lib/rules/capitalized-comments.js +2 -1
  13. package/lib/rules/complexity.js +14 -8
  14. package/lib/rules/consistent-return.js +17 -10
  15. package/lib/rules/func-name-matching.js +18 -7
  16. package/lib/rules/func-names.js +20 -5
  17. package/lib/rules/keyword-spacing.js +19 -4
  18. package/lib/rules/line-comment-position.js +15 -5
  19. package/lib/rules/lines-around-comment.js +19 -0
  20. package/lib/rules/max-params.js +17 -4
  21. package/lib/rules/max-statements.js +11 -10
  22. package/lib/rules/no-compare-neg-zero.js +53 -0
  23. package/lib/rules/no-else-return.js +13 -1
  24. package/lib/rules/no-empty-function.js +9 -16
  25. package/lib/rules/no-extra-parens.js +64 -19
  26. package/lib/rules/no-extra-semi.js +13 -1
  27. package/lib/rules/no-global-assign.js +1 -1
  28. package/lib/rules/no-invalid-regexp.js +2 -1
  29. package/lib/rules/no-multiple-empty-lines.js +2 -4
  30. package/lib/rules/no-new-func.js +6 -8
  31. package/lib/rules/no-new.js +2 -6
  32. package/lib/rules/no-param-reassign.js +29 -6
  33. package/lib/rules/no-process-exit.js +2 -10
  34. package/lib/rules/no-restricted-properties.js +2 -0
  35. package/lib/rules/no-restricted-syntax.js +6 -22
  36. package/lib/rules/no-return-await.js +1 -1
  37. package/lib/rules/no-sync.js +8 -13
  38. package/lib/rules/no-unused-expressions.js +10 -1
  39. package/lib/rules/no-unused-vars.js +12 -12
  40. package/lib/rules/no-use-before-define.js +1 -1
  41. package/lib/rules/no-useless-escape.js +8 -2
  42. package/lib/rules/no-useless-return.js +13 -2
  43. package/lib/rules/nonblock-statement-body-position.js +114 -0
  44. package/lib/rules/object-shorthand.js +2 -1
  45. package/lib/rules/operator-assignment.js +1 -1
  46. package/lib/rules/padded-blocks.js +37 -28
  47. package/lib/rules/prefer-destructuring.js +1 -1
  48. package/lib/rules/semi.js +13 -1
  49. package/lib/rules/sort-vars.js +3 -5
  50. package/lib/rules/space-unary-ops.js +19 -1
  51. package/lib/rules/strict.js +8 -2
  52. package/lib/rules/yoda.js +2 -2
  53. package/lib/testers/rule-tester.js +44 -13
  54. package/lib/util/fix-tracker.js +121 -0
  55. package/lib/util/node-event-generator.js +274 -4
  56. package/lib/util/source-code-fixer.js +2 -2
  57. package/lib/util/source-code.js +99 -2
  58. package/lib/util/traverser.js +16 -25
  59. 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: `Expected longform method syntax${APPLY_NEVER ? "" : " for string literal keys"}.`,
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], node.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 isTokenTopPadded(token) {
99
- const tokenStartLine = token.loc.start.line,
100
- expectedFirstLine = tokenStartLine + 2;
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 === tokenStartLine);
106
-
107
- const firstLine = first.loc.start.line;
116
+ } while (isComment(first) && first.loc.start.line === prev.loc.end.line);
108
117
 
109
- return expectedFirstLine <= firstLine;
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 isTokenBottomPadded(token) {
118
- const blockEnd = token.loc.end.line,
119
- expectedLastLine = blockEnd - 2;
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 === blockEnd);
125
-
126
- const lastLine = last.loc.end.line;
133
+ } while (isComment(last) && last.loc.end.line === next.loc.start.line);
127
134
 
128
- return lastLine <= expectedLastLine;
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
- blockHasTopPadding = isTokenTopPadded(openBrace),
160
- blockHasBottomPadding = isTokenBottomPadded(closeBrace);
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: openBrace.loc.start.line, column: openBrace.loc.start.column },
177
+ loc: { line: tokenBeforeFirst.loc.start.line, column: tokenBeforeFirst.loc.start.column },
167
178
  fix(fixer) {
168
- return fixer.insertTextAfter(openBrace, "\n");
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: closeBrace.loc.end.line, column: closeBrace.loc.end.column - 1 },
187
+ loc: { line: tokenAfterLast.loc.end.line, column: tokenAfterLast.loc.end.column - 1 },
177
188
  fix(fixer) {
178
- return fixer.insertTextBefore(closeBrace, "\n");
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: openBrace.loc.start.line, column: openBrace.loc.start.column },
199
+ loc: { line: tokenBeforeFirst.loc.start.line, column: tokenBeforeFirst.loc.start.column },
190
200
  fix(fixer) {
191
- return fixer.replaceTextRange([openBrace.end, nextToken.start - nextToken.loc.start.column], "\n");
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: closeBrace.loc.end.line, column: closeBrace.loc.end.column - 1 },
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([previousToken.end, closeBrace.start - closeBrace.loc.start.column], "\n");
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: `Use ${type} destructuring` });
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
- return fixer.remove(lastToken);
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
 
@@ -37,11 +37,9 @@ module.exports = {
37
37
 
38
38
  return {
39
39
  VariableDeclaration(node) {
40
- node.declarations.reduce((memo, decl) => {
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
- }, node.declarations[0]);
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
- return fixer.removeRange([firstToken.range[1], secondToken.range[0]]);
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
  }
@@ -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 this function in a function with 'use strict' directive."
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({ node, message: messages.wrap });
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(sourceCode.getFirstToken(node).range[0], sourceCode.getTokenBefore(operatorToken).range[1]);
271
- const rightText = sourceCode.getText().slice(sourceCode.getTokenAfter(operatorToken).range[0], sourceCode.getLastToken(node).range[1]);
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
- eslint.on("Program:exit", node => {
350
- afterAST = cloneDeeplyExcludesParent(node);
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
- assert.equal(messages[i].message, item.errors[i]);
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
- assert.equal(messages[i].message, item.errors[i].message);
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
- // Only string or object errors are valid.
494
- assert.fail(messages[i], null, "Error should be a string or object.");
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
- const fixResult = SourceCodeFixer.applyFixes(eslint.getSourceCode(), messages);
501
-
502
- assert.equal(fixResult.output, item.output, "Output is incorrect.");
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 || valid, () => {
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;