eslint 4.7.1 → 4.10.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 (65) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/README.md +34 -19
  3. package/conf/default-cli-options.js +2 -1
  4. package/conf/eslint-recommended.js +2 -0
  5. package/lib/ast-utils.js +2 -1
  6. package/lib/cli-engine.js +26 -5
  7. package/lib/cli.js +17 -9
  8. package/lib/code-path-analysis/code-path-segment.js +39 -39
  9. package/lib/code-path-analysis/code-path-state.js +3 -0
  10. package/lib/formatters/html-template-message.html +1 -1
  11. package/lib/formatters/html-template-page.html +3 -1
  12. package/lib/formatters/html.js +2 -1
  13. package/lib/ignored-paths.js +1 -1
  14. package/lib/linter.js +43 -71
  15. package/lib/logging.js +2 -2
  16. package/lib/options.js +12 -0
  17. package/lib/rules/array-bracket-newline.js +19 -5
  18. package/lib/rules/block-spacing.js +1 -1
  19. package/lib/rules/callback-return.js +2 -1
  20. package/lib/rules/capitalized-comments.js +2 -1
  21. package/lib/rules/comma-style.js +3 -1
  22. package/lib/rules/dot-notation.js +56 -35
  23. package/lib/rules/generator-star-spacing.js +3 -3
  24. package/lib/rules/indent-legacy.js +5 -2
  25. package/lib/rules/indent.js +25 -19
  26. package/lib/rules/lines-around-comment.js +33 -4
  27. package/lib/rules/lines-between-class-members.js +91 -0
  28. package/lib/rules/max-len.js +2 -3
  29. package/lib/rules/multiline-comment-style.js +294 -0
  30. package/lib/rules/new-cap.js +2 -1
  31. package/lib/rules/newline-before-return.js +4 -2
  32. package/lib/rules/no-alert.js +7 -15
  33. package/lib/rules/no-catch-shadow.js +1 -1
  34. package/lib/rules/no-constant-condition.js +2 -2
  35. package/lib/rules/no-control-regex.js +2 -1
  36. package/lib/rules/no-else-return.js +43 -8
  37. package/lib/rules/no-extra-parens.js +6 -3
  38. package/lib/rules/no-lonely-if.js +2 -1
  39. package/lib/rules/no-loop-func.js +2 -3
  40. package/lib/rules/no-mixed-requires.js +8 -4
  41. package/lib/rules/no-restricted-imports.js +86 -17
  42. package/lib/rules/no-restricted-modules.js +84 -15
  43. package/lib/rules/no-trailing-spaces.js +1 -1
  44. package/lib/rules/no-unneeded-ternary.js +3 -1
  45. package/lib/rules/no-unused-labels.js +2 -1
  46. package/lib/rules/no-useless-computed-key.js +2 -1
  47. package/lib/rules/no-useless-escape.js +8 -1
  48. package/lib/rules/no-var.js +11 -0
  49. package/lib/rules/object-shorthand.js +6 -2
  50. package/lib/rules/operator-linebreak.js +3 -1
  51. package/lib/rules/padding-line-between-statements.js +2 -2
  52. package/lib/rules/require-jsdoc.js +11 -18
  53. package/lib/rules/sort-imports.js +6 -3
  54. package/lib/rules/space-unary-ops.js +6 -8
  55. package/lib/rules/valid-jsdoc.js +39 -33
  56. package/lib/testers/rule-tester.js +20 -6
  57. package/lib/util/apply-disable-directives.js +56 -27
  58. package/lib/util/node-event-generator.js +6 -20
  59. package/lib/util/safe-emitter.js +54 -0
  60. package/lib/util/source-code.js +73 -67
  61. package/messages/no-config-found.txt +1 -1
  62. package/package.json +3 -4
  63. package/lib/internal-rules/.eslintrc.yml +0 -3
  64. package/lib/internal-rules/internal-consistent-docs-description.js +0 -130
  65. package/lib/internal-rules/internal-no-invalid-meta.js +0 -188
@@ -15,6 +15,15 @@ const astUtils = require("../ast-utils");
15
15
  // Helpers
16
16
  //------------------------------------------------------------------------------
17
17
 
18
+ /**
19
+ * Check whether a given variable is a global variable or not.
20
+ * @param {eslint-scope.Variable} variable The variable to check.
21
+ * @returns {boolean} `true` if the variable is a global variable.
22
+ */
23
+ function isGlobal(variable) {
24
+ return Boolean(variable.scope) && variable.scope.type === "global";
25
+ }
26
+
18
27
  /**
19
28
  * Finds the nearest function scope or global scope walking up the scope
20
29
  * hierarchy.
@@ -203,6 +212,7 @@ module.exports = {
203
212
  * Checks whether it can fix a given variable declaration or not.
204
213
  * It cannot fix if the following cases:
205
214
  *
215
+ * - A variable is a global variable.
206
216
  * - A variable is declared on a SwitchCase node.
207
217
  * - A variable is redeclared.
208
218
  * - A variable is used from outside the scope.
@@ -256,6 +266,7 @@ module.exports = {
256
266
 
257
267
  if (node.parent.type === "SwitchCase" ||
258
268
  node.declarations.some(hasSelfReferenceInTDZ) ||
269
+ variables.some(isGlobal) ||
259
270
  variables.some(isRedeclared) ||
260
271
  variables.some(isUsedFromOutsideOf(scopeNode))
261
272
  ) {
@@ -215,8 +215,12 @@ module.exports = {
215
215
  * @returns {Object} A fix for this node
216
216
  */
217
217
  function makeFunctionShorthand(fixer, node) {
218
- const firstKeyToken = node.computed ? sourceCode.getFirstToken(node, astUtils.isOpeningBracketToken) : sourceCode.getFirstToken(node.key);
219
- const lastKeyToken = node.computed ? sourceCode.getFirstTokenBetween(node.key, node.value, astUtils.isClosingBracketToken) : sourceCode.getLastToken(node.key);
218
+ const firstKeyToken = node.computed
219
+ ? sourceCode.getFirstToken(node, astUtils.isOpeningBracketToken)
220
+ : sourceCode.getFirstToken(node.key);
221
+ const lastKeyToken = node.computed
222
+ ? sourceCode.getFirstTokenBetween(node.key, node.value, astUtils.isClosingBracketToken)
223
+ : sourceCode.getLastToken(node.key);
220
224
  const keyText = sourceCode.text.slice(firstKeyToken.range[0], lastKeyToken.range[1]);
221
225
  let keyPrefix = "";
222
226
 
@@ -87,7 +87,9 @@ module.exports = {
87
87
  if (hasLinebreakBefore !== hasLinebreakAfter && desiredStyle !== "none") {
88
88
 
89
89
  // If there is a comment before and after the operator, don't do a fix.
90
- if (sourceCode.getTokenBefore(operatorToken, { includeComments: true }) !== tokenBefore && sourceCode.getTokenAfter(operatorToken, { includeComments: true }) !== tokenAfter) {
90
+ if (sourceCode.getTokenBefore(operatorToken, { includeComments: true }) !== tokenBefore &&
91
+ sourceCode.getTokenAfter(operatorToken, { includeComments: true }) !== tokenAfter) {
92
+
91
93
  return null;
92
94
  }
93
95
 
@@ -205,14 +205,14 @@ function verifyForAny() {
205
205
  * blank lines automatically.
206
206
  *
207
207
  * @param {RuleContext} context The rule context to report.
208
- * @param {ASTNode} prevNode The previous node to check.
208
+ * @param {ASTNode} _ Unused. The previous node to check.
209
209
  * @param {ASTNode} nextNode The next node to check.
210
210
  * @param {Array<Token[]>} paddingLines The array of token pairs that blank
211
211
  * lines exist between the pair.
212
212
  * @returns {void}
213
213
  * @private
214
214
  */
215
- function verifyForNever(context, prevNode, nextNode, paddingLines) {
215
+ function verifyForNever(context, _, nextNode, paddingLines) {
216
216
  if (paddingLines.length === 0) {
217
217
  return;
218
218
  }
@@ -30,6 +30,9 @@ module.exports = {
30
30
  },
31
31
  ArrowFunctionExpression: {
32
32
  type: "boolean"
33
+ },
34
+ FunctionExpression: {
35
+ type: "boolean"
33
36
  }
34
37
  },
35
38
  additionalProperties: false
@@ -45,7 +48,9 @@ module.exports = {
45
48
  const DEFAULT_OPTIONS = {
46
49
  FunctionDeclaration: true,
47
50
  MethodDefinition: false,
48
- ClassDeclaration: false
51
+ ClassDeclaration: false,
52
+ ArrowFunctionExpression: false,
53
+ FunctionExpression: false
49
54
  };
50
55
  const options = Object.assign(DEFAULT_OPTIONS, context.options[0] && context.options[0].require || {});
51
56
 
@@ -58,21 +63,6 @@ module.exports = {
58
63
  context.report({ node, message: "Missing JSDoc comment." });
59
64
  }
60
65
 
61
- /**
62
- * Check if the jsdoc comment is present for class methods
63
- * @param {ASTNode} node node to examine
64
- * @returns {void}
65
- */
66
- function checkClassMethodJsDoc(node) {
67
- if (node.parent.type === "MethodDefinition") {
68
- const jsdocComment = source.getJSDocComment(node);
69
-
70
- if (!jsdocComment) {
71
- report(node);
72
- }
73
- }
74
- }
75
-
76
66
  /**
77
67
  * Check if the jsdoc comment is present or not.
78
68
  * @param {ASTNode} node node to examine
@@ -93,8 +83,11 @@ module.exports = {
93
83
  }
94
84
  },
95
85
  FunctionExpression(node) {
96
- if (options.MethodDefinition) {
97
- checkClassMethodJsDoc(node);
86
+ if (
87
+ (options.MethodDefinition && node.parent.type === "MethodDefinition") ||
88
+ (options.FunctionExpression && (node.parent.type === "VariableDeclarator" || (node.parent.type === "Property" && node === node.parent.value)))
89
+ ) {
90
+ checkJsDoc(node);
98
91
  }
99
92
  },
100
93
  ClassDeclaration(node) {
@@ -67,9 +67,11 @@ module.exports = {
67
67
  function usedMemberSyntax(node) {
68
68
  if (node.specifiers.length === 0) {
69
69
  return "none";
70
- } else if (node.specifiers[0].type === "ImportNamespaceSpecifier") {
70
+ }
71
+ if (node.specifiers[0].type === "ImportNamespaceSpecifier") {
71
72
  return "all";
72
- } else if (node.specifiers.length === 1) {
73
+ }
74
+ if (node.specifiers.length === 1) {
73
75
  return "single";
74
76
  }
75
77
  return "multiple";
@@ -149,7 +151,8 @@ module.exports = {
149
151
  message: "Member '{{memberName}}' of the import declaration should be sorted alphabetically.",
150
152
  data: { memberName: importSpecifiers[firstUnsortedIndex].local.name },
151
153
  fix(fixer) {
152
- if (importSpecifiers.some(specifier => sourceCode.getCommentsBefore(specifier).length || sourceCode.getCommentsAfter(specifier).length)) {
154
+ if (importSpecifiers.some(specifier =>
155
+ sourceCode.getCommentsBefore(specifier).length || sourceCode.getCommentsAfter(specifier).length)) {
153
156
 
154
157
  // If there are comments in the ImportSpecifier list, don't rearrange the specifiers.
155
158
  return null;
@@ -76,21 +76,19 @@ module.exports = {
76
76
 
77
77
  /**
78
78
  * Checks if an override exists for a given operator.
79
- * @param {ASTnode} node AST node
80
79
  * @param {string} operator Operator
81
80
  * @returns {boolean} Whether or not an override has been provided for the operator
82
81
  */
83
- function overrideExistsForOperator(node, operator) {
82
+ function overrideExistsForOperator(operator) {
84
83
  return options.overrides && options.overrides.hasOwnProperty(operator);
85
84
  }
86
85
 
87
86
  /**
88
87
  * Gets the value that the override was set to for this operator
89
- * @param {ASTnode} node AST node
90
88
  * @param {string} operator Operator
91
89
  * @returns {boolean} Whether or not an override enforces a space with this operator
92
90
  */
93
- function overrideEnforcesSpaces(node, operator) {
91
+ function overrideEnforcesSpaces(operator) {
94
92
  return options.overrides[operator];
95
93
  }
96
94
 
@@ -153,8 +151,8 @@ module.exports = {
153
151
  function checkUnaryWordOperatorForSpaces(node, firstToken, secondToken, word) {
154
152
  word = word || firstToken.value;
155
153
 
156
- if (overrideExistsForOperator(node, word)) {
157
- if (overrideEnforcesSpaces(node, word)) {
154
+ if (overrideExistsForOperator(word)) {
155
+ if (overrideEnforcesSpaces(word)) {
158
156
  verifyWordHasSpaces(node, firstToken, secondToken, word);
159
157
  } else {
160
158
  verifyWordDoesntHaveSpaces(node, firstToken, secondToken, word);
@@ -292,8 +290,8 @@ module.exports = {
292
290
 
293
291
  const operator = node.prefix ? tokens[0].value : tokens[1].value;
294
292
 
295
- if (overrideExistsForOperator(node, operator)) {
296
- if (overrideEnforcesSpaces(node, operator)) {
293
+ if (overrideExistsForOperator(operator)) {
294
+ if (overrideEnforcesSpaces(operator)) {
297
295
  verifyNonWordsHaveSpaces(node, firstToken, secondToken);
298
296
  } else {
299
297
  verifyNonWordsDontHaveSpaces(node, firstToken, secondToken);
@@ -226,8 +226,10 @@ module.exports = {
226
226
  function checkJSDoc(node) {
227
227
  const jsdocNode = sourceCode.getJSDocComment(node),
228
228
  functionData = fns.pop(),
229
- params = Object.create(null);
229
+ params = Object.create(null),
230
+ paramsTags = [];
230
231
  let hasReturns = false,
232
+ returnsTag,
231
233
  hasConstructor = false,
232
234
  isInterface = false,
233
235
  isOverride = false,
@@ -261,43 +263,13 @@ module.exports = {
261
263
  case "param":
262
264
  case "arg":
263
265
  case "argument":
264
- if (!tag.type) {
265
- context.report({ node: jsdocNode, message: "Missing JSDoc parameter type for '{{name}}'.", data: { name: tag.name } });
266
- }
267
-
268
- if (!tag.description && requireParamDescription) {
269
- context.report({ node: jsdocNode, message: "Missing JSDoc parameter description for '{{name}}'.", data: { name: tag.name } });
270
- }
271
-
272
- if (params[tag.name]) {
273
- context.report({ node: jsdocNode, message: "Duplicate JSDoc parameter '{{name}}'.", data: { name: tag.name } });
274
- } else if (tag.name.indexOf(".") === -1) {
275
- params[tag.name] = 1;
276
- }
266
+ paramsTags.push(tag);
277
267
  break;
278
268
 
279
269
  case "return":
280
270
  case "returns":
281
271
  hasReturns = true;
282
-
283
- if (!requireReturn && !functionData.returnPresent && (tag.type === null || !isValidReturnType(tag)) && !isAbstract) {
284
- context.report({
285
- node: jsdocNode,
286
- message: "Unexpected @{{title}} tag; function has no return statement.",
287
- data: {
288
- title: tag.title
289
- }
290
- });
291
- } else {
292
- if (requireReturnType && !tag.type) {
293
- context.report({ node: jsdocNode, message: "Missing JSDoc return type." });
294
- }
295
-
296
- if (!isValidReturnType(tag) && !tag.description && requireReturnDescription) {
297
- context.report({ node: jsdocNode, message: "Missing JSDoc return description." });
298
- }
299
- }
300
-
272
+ returnsTag = tag;
301
273
  break;
302
274
 
303
275
  case "constructor":
@@ -333,6 +305,40 @@ module.exports = {
333
305
  }
334
306
  });
335
307
 
308
+ paramsTags.forEach(param => {
309
+ if (!param.type) {
310
+ context.report({ node: jsdocNode, message: "Missing JSDoc parameter type for '{{name}}'.", data: { name: param.name } });
311
+ }
312
+ if (!param.description && requireParamDescription) {
313
+ context.report({ node: jsdocNode, message: "Missing JSDoc parameter description for '{{name}}'.", data: { name: param.name } });
314
+ }
315
+ if (params[param.name]) {
316
+ context.report({ node: jsdocNode, message: "Duplicate JSDoc parameter '{{name}}'.", data: { name: param.name } });
317
+ } else if (param.name.indexOf(".") === -1) {
318
+ params[param.name] = 1;
319
+ }
320
+ });
321
+
322
+ if (hasReturns) {
323
+ if (!requireReturn && !functionData.returnPresent && (returnsTag.type === null || !isValidReturnType(returnsTag)) && !isAbstract) {
324
+ context.report({
325
+ node: jsdocNode,
326
+ message: "Unexpected @{{title}} tag; function has no return statement.",
327
+ data: {
328
+ title: returnsTag.title
329
+ }
330
+ });
331
+ } else {
332
+ if (requireReturnType && !returnsTag.type) {
333
+ context.report({ node: jsdocNode, message: "Missing JSDoc return type." });
334
+ }
335
+
336
+ if (!isValidReturnType(returnsTag) && !returnsTag.description && requireReturnDescription) {
337
+ context.report({ node: jsdocNode, message: "Missing JSDoc return description." });
338
+ }
339
+ }
340
+ }
341
+
336
342
  // check for functions missing @returns
337
343
  if (!isOverride && !hasReturns && !hasConstructor && !isInterface &&
338
344
  node.parent.kind !== "get" && node.parent.kind !== "constructor" &&
@@ -178,7 +178,7 @@ class RuleTester {
178
178
  */
179
179
  static setDefaultConfig(config) {
180
180
  if (typeof config !== "object") {
181
- throw new Error("RuleTester.setDefaultConfig: config must be an object");
181
+ throw new TypeError("RuleTester.setDefaultConfig: config must be an object");
182
182
  }
183
183
  defaultConfig = config;
184
184
 
@@ -254,7 +254,7 @@ class RuleTester {
254
254
  linter = this.linter;
255
255
 
256
256
  if (lodash.isNil(test) || typeof test !== "object") {
257
- throw new Error(`Test Scenarios for rule ${ruleName} : Could not find test scenario object`);
257
+ throw new TypeError(`Test Scenarios for rule ${ruleName} : Could not find test scenario object`);
258
258
  }
259
259
 
260
260
  requiredScenarios.forEach(scenarioType => {
@@ -369,6 +369,7 @@ class RuleTester {
369
369
  if (!lodash.isEqual(beforeAST, afterAST)) {
370
370
 
371
371
  // Not using directly to avoid performance problem in node 6.1.0. See #6111
372
+ // eslint-disable-next-line no-restricted-properties
372
373
  assert.deepEqual(beforeAST, afterAST, "Rule should not modify AST.");
373
374
  }
374
375
  }
@@ -384,7 +385,7 @@ class RuleTester {
384
385
  const result = runRuleForItem(item);
385
386
  const messages = result.messages;
386
387
 
387
- assert.equal(messages.length, 0, util.format("Should have no errors but had %d: %s",
388
+ assert.strictEqual(messages.length, 0, util.format("Should have no errors but had %d: %s",
388
389
  messages.length, util.inspect(messages)));
389
390
 
390
391
  assertASTDidntChange(result.beforeAST, result.afterAST);
@@ -408,7 +409,7 @@ class RuleTester {
408
409
  `Expected '${actual}' to match ${expected}`
409
410
  );
410
411
  } else {
411
- assert.equal(actual, expected);
412
+ assert.strictEqual(actual, expected);
412
413
  }
413
414
  }
414
415
 
@@ -428,10 +429,10 @@ class RuleTester {
428
429
 
429
430
 
430
431
  if (typeof item.errors === "number") {
431
- assert.equal(messages.length, item.errors, util.format("Should have %d error%s but had %d: %s",
432
+ assert.strictEqual(messages.length, item.errors, util.format("Should have %d error%s but had %d: %s",
432
433
  item.errors, item.errors === 1 ? "" : "s", messages.length, util.inspect(messages)));
433
434
  } else {
434
- assert.equal(
435
+ assert.strictEqual(
435
436
  messages.length, item.errors.length,
436
437
  util.format(
437
438
  "Should have %d error%s but had %d: %s",
@@ -460,23 +461,35 @@ class RuleTester {
460
461
  assertMessageMatches(messages[i].message, item.errors[i].message);
461
462
  }
462
463
 
464
+ // The following checks use loose equality assertions for backwards compatibility.
465
+
463
466
  if (item.errors[i].type) {
467
+
468
+ // eslint-disable-next-line no-restricted-properties
464
469
  assert.equal(messages[i].nodeType, item.errors[i].type, `Error type should be ${item.errors[i].type}, found ${messages[i].nodeType}`);
465
470
  }
466
471
 
467
472
  if (item.errors[i].hasOwnProperty("line")) {
473
+
474
+ // eslint-disable-next-line no-restricted-properties
468
475
  assert.equal(messages[i].line, item.errors[i].line, `Error line should be ${item.errors[i].line}`);
469
476
  }
470
477
 
471
478
  if (item.errors[i].hasOwnProperty("column")) {
479
+
480
+ // eslint-disable-next-line no-restricted-properties
472
481
  assert.equal(messages[i].column, item.errors[i].column, `Error column should be ${item.errors[i].column}`);
473
482
  }
474
483
 
475
484
  if (item.errors[i].hasOwnProperty("endLine")) {
485
+
486
+ // eslint-disable-next-line no-restricted-properties
476
487
  assert.equal(messages[i].endLine, item.errors[i].endLine, `Error endLine should be ${item.errors[i].endLine}`);
477
488
  }
478
489
 
479
490
  if (item.errors[i].hasOwnProperty("endColumn")) {
491
+
492
+ // eslint-disable-next-line no-restricted-properties
480
493
  assert.equal(messages[i].endColumn, item.errors[i].endColumn, `Error endColumn should be ${item.errors[i].endColumn}`);
481
494
  }
482
495
  } else {
@@ -497,6 +510,7 @@ class RuleTester {
497
510
  } else {
498
511
  const fixResult = SourceCodeFixer.applyFixes(item.code, messages);
499
512
 
513
+ // eslint-disable-next-line no-restricted-properties
500
514
  assert.equal(fixResult.output, item.output, "Output is incorrect.");
501
515
  }
502
516
  }
@@ -19,20 +19,24 @@ function compareLocations(itemA, itemB) {
19
19
  }
20
20
 
21
21
  /**
22
- * This is the same as the exported function, except that it doesn't handle disable-line and disable-next-line directives.
23
- * @param {Object} options options (see the exported function)
24
- * @returns {Problem[]} Filtered problems (see the exported function)
22
+ * This is the same as the exported function, except that it
23
+ * doesn't handle disable-line and disable-next-line directives, and it always reports unused
24
+ * disable directives.
25
+ * @param {Object} options options for applying directives. This is the same as the options
26
+ * for the exported function, except that `reportUnusedDisableDirectives` is not supported
27
+ * (this function always reports unused disable directives).
28
+ * @returns {{problems: Problem[], unusedDisableDirectives: Problem[]}} An object with a list
29
+ * of filtered problems and unused eslint-disable directives
25
30
  */
26
31
  function applyDirectives(options) {
27
32
  const problems = [];
28
33
  let nextDirectiveIndex = 0;
29
- let globalDisableActive = false;
34
+ let currentGlobalDisableDirective = null;
35
+ const disabledRuleMap = new Map();
30
36
 
31
- // disabledRules is only used when there is no active global /* eslint-disable */ comment.
32
- const disabledRules = new Set();
33
-
34
- // enabledRules is only used when there is an active global /* eslint-disable */ comment.
37
+ // enabledRules is only used when there is a current global disable directive.
35
38
  const enabledRules = new Set();
39
+ const usedDisableDirectives = new Set();
36
40
 
37
41
  for (const problem of options.problems) {
38
42
  while (
@@ -44,23 +48,26 @@ function applyDirectives(options) {
44
48
  switch (directive.type) {
45
49
  case "disable":
46
50
  if (directive.ruleId === null) {
47
- globalDisableActive = true;
51
+ currentGlobalDisableDirective = directive;
52
+ disabledRuleMap.clear();
48
53
  enabledRules.clear();
49
- } else if (globalDisableActive) {
54
+ } else if (currentGlobalDisableDirective) {
50
55
  enabledRules.delete(directive.ruleId);
56
+ disabledRuleMap.set(directive.ruleId, directive);
51
57
  } else {
52
- disabledRules.add(directive.ruleId);
58
+ disabledRuleMap.set(directive.ruleId, directive);
53
59
  }
54
60
  break;
55
61
 
56
62
  case "enable":
57
63
  if (directive.ruleId === null) {
58
- globalDisableActive = false;
59
- disabledRules.clear();
60
- } else if (globalDisableActive) {
64
+ currentGlobalDisableDirective = null;
65
+ disabledRuleMap.clear();
66
+ } else if (currentGlobalDisableDirective) {
61
67
  enabledRules.add(directive.ruleId);
68
+ disabledRuleMap.delete(directive.ruleId);
62
69
  } else {
63
- disabledRules.delete(directive.ruleId);
70
+ disabledRuleMap.delete(directive.ruleId);
64
71
  }
65
72
  break;
66
73
 
@@ -68,15 +75,30 @@ function applyDirectives(options) {
68
75
  }
69
76
  }
70
77
 
71
- if (
72
- globalDisableActive && enabledRules.has(problem.ruleId) ||
73
- !globalDisableActive && !disabledRules.has(problem.ruleId)
74
- ) {
78
+ if (disabledRuleMap.has(problem.ruleId)) {
79
+ usedDisableDirectives.add(disabledRuleMap.get(problem.ruleId));
80
+ } else if (currentGlobalDisableDirective && !enabledRules.has(problem.ruleId)) {
81
+ usedDisableDirectives.add(currentGlobalDisableDirective);
82
+ } else {
75
83
  problems.push(problem);
76
84
  }
77
85
  }
78
86
 
79
- return problems;
87
+ const unusedDisableDirectives = options.directives
88
+ .filter(directive => directive.type === "disable" && !usedDisableDirectives.has(directive))
89
+ .map(directive => ({
90
+ ruleId: null,
91
+ message: directive.ruleId
92
+ ? `Unused eslint-disable directive (no problems were reported from '${directive.ruleId}').`
93
+ : "Unused eslint-disable directive (no problems were reported).",
94
+ line: directive.unprocessedDirective.line,
95
+ column: directive.unprocessedDirective.column,
96
+ severity: 2,
97
+ source: null,
98
+ nodeType: null
99
+ }));
100
+
101
+ return { problems, unusedDisableDirectives };
80
102
  }
81
103
 
82
104
  /**
@@ -93,12 +115,14 @@ function applyDirectives(options) {
93
115
  * comment for two different rules is represented as two directives).
94
116
  * @param {{ruleId: (string|null), line: number, column: number}[]} options.problems
95
117
  * A list of problems reported by rules, sorted by increasing location in the file, with one-based columns.
118
+ * @param {boolean} options.reportUnusedDisableDirectives If `true`, adds additional problems for unused directives
96
119
  * @returns {{ruleId: (string|null), line: number, column: number}[]}
97
120
  * A list of reported problems that were not disabled by the directive comments.
98
121
  */
99
122
  module.exports = options => {
100
123
  const blockDirectives = options.directives
101
124
  .filter(directive => directive.type === "disable" || directive.type === "enable")
125
+ .map(directive => Object.assign({}, directive, { unprocessedDirective: directive }))
102
126
  .sort(compareLocations);
103
127
 
104
128
  const lineDirectives = lodash.flatMap(options.directives, directive => {
@@ -109,14 +133,14 @@ module.exports = options => {
109
133
 
110
134
  case "disable-line":
111
135
  return [
112
- { type: "disable", line: directive.line, column: 1, ruleId: directive.ruleId },
113
- { type: "enable", line: directive.line + 1, column: 0, ruleId: directive.ruleId }
136
+ { type: "disable", line: directive.line, column: 1, ruleId: directive.ruleId, unprocessedDirective: directive },
137
+ { type: "enable", line: directive.line + 1, column: 0, ruleId: directive.ruleId, unprocessedDirective: directive }
114
138
  ];
115
139
 
116
140
  case "disable-next-line":
117
141
  return [
118
- { type: "disable", line: directive.line + 1, column: 1, ruleId: directive.ruleId },
119
- { type: "enable", line: directive.line + 2, column: 0, ruleId: directive.ruleId }
142
+ { type: "disable", line: directive.line + 1, column: 1, ruleId: directive.ruleId, unprocessedDirective: directive },
143
+ { type: "enable", line: directive.line + 2, column: 0, ruleId: directive.ruleId, unprocessedDirective: directive }
120
144
  ];
121
145
 
122
146
  default:
@@ -124,8 +148,13 @@ module.exports = options => {
124
148
  }
125
149
  }).sort(compareLocations);
126
150
 
127
- const problemsAfterBlockDirectives = applyDirectives({ problems: options.problems, directives: blockDirectives });
128
- const problemsAfterLineDirectives = applyDirectives({ problems: problemsAfterBlockDirectives, directives: lineDirectives });
151
+ const blockDirectivesResult = applyDirectives({ problems: options.problems, directives: blockDirectives });
152
+ const lineDirectivesResult = applyDirectives({ problems: blockDirectivesResult.problems, directives: lineDirectives });
129
153
 
130
- return problemsAfterLineDirectives.sort(compareLocations);
154
+ return options.reportUnusedDisableDirectives
155
+ ? lineDirectivesResult.problems
156
+ .concat(blockDirectivesResult.unusedDisableDirectives)
157
+ .concat(lineDirectivesResult.unusedDisableDirectives)
158
+ .sort(compareLocations)
159
+ : lineDirectivesResult.problems;
131
160
  };
@@ -160,7 +160,7 @@ function tryParseSelector(rawSelector) {
160
160
  return esquery.parse(rawSelector.replace(/:exit$/, ""));
161
161
  } catch (err) {
162
162
  if (typeof err.offset === "number") {
163
- throw new Error(`Syntax error in selector "${rawSelector}" at position ${err.offset}: ${err.message}`);
163
+ throw new SyntaxError(`Syntax error in selector "${rawSelector}" at position ${err.offset}: ${err.message}`);
164
164
  }
165
165
  throw err;
166
166
  }
@@ -194,7 +194,7 @@ const parseSelector = lodash.memoize(rawSelector => {
194
194
  *
195
195
  * ```ts
196
196
  * interface EventGenerator {
197
- * emitter: EventEmitter;
197
+ * emitter: SafeEmitter;
198
198
  * enterNode(node: ASTNode): void;
199
199
  * leaveNode(node: ASTNode): void;
200
200
  * }
@@ -203,8 +203,10 @@ const parseSelector = lodash.memoize(rawSelector => {
203
203
  class NodeEventGenerator {
204
204
 
205
205
  /**
206
- * @param {EventEmitter} emitter - An event emitter which is the destination of events. This emitter must already
206
+ * @param {SafeEmitter} emitter
207
+ * An SafeEmitter which is the destination of events. This emitter must already
207
208
  * have registered listeners for all of the events that it needs to listen for.
209
+ * (See lib/util/safe-emitter.js for more details on `SafeEmitter`.)
208
210
  * @returns {NodeEventGenerator} new instance
209
211
  */
210
212
  constructor(emitter) {
@@ -215,23 +217,7 @@ class NodeEventGenerator {
215
217
  this.anyTypeEnterSelectors = [];
216
218
  this.anyTypeExitSelectors = [];
217
219
 
218
- const eventNames = typeof emitter.eventNames === "function"
219
-
220
- // Use the built-in eventNames() function if available (Node 6+)
221
- ? emitter.eventNames()
222
-
223
- /*
224
- * Otherwise, use the private _events property.
225
- * Using a private property isn't ideal here, but this seems to
226
- * be the best way to get a list of event names without overriding
227
- * addEventListener, which would hurt performance. This property
228
- * is widely used and unlikely to be removed in a future version
229
- * (see https://github.com/nodejs/node/issues/1817). Also, future
230
- * node versions will have eventNames() anyway.
231
- */
232
- : Object.keys(emitter._events); // eslint-disable-line no-underscore-dangle
233
-
234
- eventNames.forEach(rawSelector => {
220
+ emitter.eventNames().forEach(rawSelector => {
235
221
  const selector = parseSelector(rawSelector);
236
222
 
237
223
  if (selector.listenerTypes) {