eslint 3.16.1 → 3.19.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 (86) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +1 -0
  3. package/conf/eslint-recommended.js +2 -0
  4. package/lib/ast-utils.js +3 -67
  5. package/lib/code-path-analysis/code-path-analyzer.js +2 -7
  6. package/lib/code-path-analysis/debug-helpers.js +17 -16
  7. package/lib/config/config-file.js +68 -38
  8. package/lib/config/config-rule.js +14 -10
  9. package/lib/config/plugins.js +19 -8
  10. package/lib/eslint.js +11 -10
  11. package/lib/formatters/codeframe.js +4 -9
  12. package/lib/formatters/stylish.js +5 -4
  13. package/lib/ignored-paths.js +6 -0
  14. package/lib/internal-rules/internal-no-invalid-meta.js +2 -40
  15. package/lib/rules/array-callback-return.js +15 -5
  16. package/lib/rules/arrow-body-style.js +2 -2
  17. package/lib/rules/arrow-parens.js +9 -3
  18. package/lib/rules/capitalized-comments.js +2 -1
  19. package/lib/rules/comma-dangle.js +3 -2
  20. package/lib/rules/comma-spacing.js +4 -14
  21. package/lib/rules/comma-style.js +8 -14
  22. package/lib/rules/complexity.js +14 -8
  23. package/lib/rules/consistent-return.js +17 -10
  24. package/lib/rules/curly.js +2 -2
  25. package/lib/rules/dot-notation.js +12 -6
  26. package/lib/rules/func-name-matching.js +18 -7
  27. package/lib/rules/func-names.js +20 -5
  28. package/lib/rules/keyword-spacing.js +19 -4
  29. package/lib/rules/line-comment-position.js +15 -5
  30. package/lib/rules/lines-around-comment.js +19 -0
  31. package/lib/rules/lines-around-directive.js +1 -1
  32. package/lib/rules/max-params.js +17 -4
  33. package/lib/rules/max-statements.js +11 -10
  34. package/lib/rules/new-parens.js +7 -21
  35. package/lib/rules/no-compare-neg-zero.js +53 -0
  36. package/lib/rules/no-cond-assign.js +4 -17
  37. package/lib/rules/no-else-return.js +19 -4
  38. package/lib/rules/no-empty-function.js +9 -16
  39. package/lib/rules/no-extra-parens.js +110 -121
  40. package/lib/rules/no-extra-semi.js +16 -3
  41. package/lib/rules/no-global-assign.js +1 -1
  42. package/lib/rules/no-implicit-coercion.js +21 -8
  43. package/lib/rules/no-invalid-regexp.js +2 -1
  44. package/lib/rules/no-multiple-empty-lines.js +2 -4
  45. package/lib/rules/no-native-reassign.js +1 -1
  46. package/lib/rules/no-negated-in-lhs.js +1 -1
  47. package/lib/rules/no-new-func.js +6 -8
  48. package/lib/rules/no-new.js +2 -6
  49. package/lib/rules/no-param-reassign.js +37 -7
  50. package/lib/rules/no-process-exit.js +2 -10
  51. package/lib/rules/no-restricted-properties.js +2 -0
  52. package/lib/rules/no-restricted-syntax.js +32 -21
  53. package/lib/rules/no-return-await.js +1 -1
  54. package/lib/rules/no-sequences.js +2 -2
  55. package/lib/rules/no-sync.js +8 -13
  56. package/lib/rules/no-unsafe-negation.js +1 -1
  57. package/lib/rules/no-unused-expressions.js +10 -1
  58. package/lib/rules/no-unused-vars.js +12 -12
  59. package/lib/rules/no-use-before-define.js +1 -1
  60. package/lib/rules/no-useless-computed-key.js +12 -1
  61. package/lib/rules/no-useless-escape.js +8 -2
  62. package/lib/rules/no-useless-return.js +13 -2
  63. package/lib/rules/nonblock-statement-body-position.js +114 -0
  64. package/lib/rules/object-curly-spacing.js +2 -2
  65. package/lib/rules/object-shorthand.js +10 -3
  66. package/lib/rules/operator-assignment.js +20 -3
  67. package/lib/rules/padded-blocks.js +37 -31
  68. package/lib/rules/prefer-const.js +1 -1
  69. package/lib/rules/prefer-destructuring.js +1 -1
  70. package/lib/rules/quotes.js +1 -0
  71. package/lib/rules/semi-spacing.js +2 -15
  72. package/lib/rules/semi.js +17 -13
  73. package/lib/rules/sort-vars.js +3 -5
  74. package/lib/rules/space-before-function-paren.js +53 -77
  75. package/lib/rules/space-in-parens.js +4 -8
  76. package/lib/rules/space-unary-ops.js +19 -1
  77. package/lib/rules/strict.js +8 -2
  78. package/lib/rules/yoda.js +2 -2
  79. package/lib/testers/rule-tester.js +44 -13
  80. package/lib/util/fix-tracker.js +121 -0
  81. package/lib/util/glob-util.js +1 -1
  82. package/lib/util/node-event-generator.js +274 -4
  83. package/lib/util/source-code-fixer.js +3 -9
  84. package/lib/util/source-code.js +99 -2
  85. package/lib/util/traverser.js +16 -25
  86. package/package.json +34 -34
@@ -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;
@@ -86,7 +86,7 @@ function resolveFileGlobPatterns(patterns, options) {
86
86
 
87
87
  const processPathExtensions = processPath(options);
88
88
 
89
- return patterns.map(processPathExtensions);
89
+ return patterns.filter(p => p.length).map(processPathExtensions);
90
90
  }
91
91
 
92
92
  /**
@@ -5,6 +5,185 @@
5
5
 
6
6
  "use strict";
7
7
 
8
+ //------------------------------------------------------------------------------
9
+ // Requirements
10
+ //------------------------------------------------------------------------------
11
+
12
+ const esquery = require("esquery");
13
+ const lodash = require("lodash");
14
+
15
+ //------------------------------------------------------------------------------
16
+ // Typedefs
17
+ //------------------------------------------------------------------------------
18
+
19
+ /**
20
+ * An object describing an AST selector
21
+ * @typedef {Object} ASTSelector
22
+ * @property {string} rawSelector The string that was parsed into this selector
23
+ * @property {boolean} isExit `true` if this should be emitted when exiting the node rather than when entering
24
+ * @property {Object} parsedSelector An object (from esquery) describing the matching behavior of the selector
25
+ * @property {string[]|null} listenerTypes A list of node types that could possibly cause the selector to match,
26
+ * or `null` if all node types could cause a match
27
+ * @property {number} attributeCount The total number of classes, pseudo-classes, and attribute queries in this selector
28
+ * @property {number} identifierCount The total number of identifier queries in this selector
29
+ */
30
+
31
+ //------------------------------------------------------------------------------
32
+ // Helpers
33
+ //------------------------------------------------------------------------------
34
+
35
+ /**
36
+ * Gets the possible types of a selector
37
+ * @param {Object} parsedSelector An object (from esquery) describing the matching behavior of the selector
38
+ * @returns {string[]|null} The node types that could possibly trigger this selector, or `null` if all node types could trigger it
39
+ */
40
+ function getPossibleTypes(parsedSelector) {
41
+ switch (parsedSelector.type) {
42
+ case "identifier":
43
+ return [parsedSelector.value];
44
+
45
+ case "matches": {
46
+ const typesForComponents = parsedSelector.selectors.map(getPossibleTypes);
47
+
48
+ if (typesForComponents.every(typesForComponent => typesForComponent)) {
49
+ return lodash.union.apply(null, typesForComponents);
50
+ }
51
+ return null;
52
+ }
53
+
54
+ case "compound": {
55
+ const typesForComponents = parsedSelector.selectors.map(getPossibleTypes).filter(typesForComponent => typesForComponent);
56
+
57
+ // If all of the components could match any type, then the compound could also match any type.
58
+ if (!typesForComponents.length) {
59
+ return null;
60
+ }
61
+
62
+ /*
63
+ * If at least one of the components could only match a particular type, the compound could only match
64
+ * the intersection of those types.
65
+ */
66
+ return lodash.intersection.apply(null, typesForComponents);
67
+ }
68
+
69
+ case "child":
70
+ case "descendant":
71
+ case "sibling":
72
+ case "adjacent":
73
+ return getPossibleTypes(parsedSelector.right);
74
+
75
+ default:
76
+ return null;
77
+
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Counts the number of class, pseudo-class, and attribute queries in this selector
83
+ * @param {Object} parsedSelector An object (from esquery) describing the selector's matching behavior
84
+ * @returns {number} The number of class, pseudo-class, and attribute queries in this selector
85
+ */
86
+ function countClassAttributes(parsedSelector) {
87
+ switch (parsedSelector.type) {
88
+ case "child":
89
+ case "descendant":
90
+ case "sibling":
91
+ case "adjacent":
92
+ return countClassAttributes(parsedSelector.left) + countClassAttributes(parsedSelector.right);
93
+
94
+ case "compound":
95
+ case "not":
96
+ case "matches":
97
+ return parsedSelector.selectors.reduce((sum, childSelector) => sum + countClassAttributes(childSelector), 0);
98
+
99
+ case "attribute":
100
+ case "field":
101
+ case "nth-child":
102
+ case "nth-last-child":
103
+ return 1;
104
+
105
+ default:
106
+ return 0;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Counts the number of identifier queries in this selector
112
+ * @param {Object} parsedSelector An object (from esquery) describing the selector's matching behavior
113
+ * @returns {number} The number of identifier queries
114
+ */
115
+ function countIdentifiers(parsedSelector) {
116
+ switch (parsedSelector.type) {
117
+ case "child":
118
+ case "descendant":
119
+ case "sibling":
120
+ case "adjacent":
121
+ return countIdentifiers(parsedSelector.left) + countIdentifiers(parsedSelector.right);
122
+
123
+ case "compound":
124
+ case "not":
125
+ case "matches":
126
+ return parsedSelector.selectors.reduce((sum, childSelector) => sum + countIdentifiers(childSelector), 0);
127
+
128
+ case "identifier":
129
+ return 1;
130
+
131
+ default:
132
+ return 0;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Compares the specificity of two selector objects, with CSS-like rules.
138
+ * @param {ASTSelector} selectorA An AST selector descriptor
139
+ * @param {ASTSelector} selectorB Another AST selector descriptor
140
+ * @returns {number}
141
+ * a value less than 0 if selectorA is less specific than selectorB
142
+ * a value greater than 0 if selectorA is more specific than selectorB
143
+ * a value less than 0 if selectorA and selectorB have the same specificity, and selectorA <= selectorB alphabetically
144
+ * a value greater than 0 if selectorA and selectorB have the same specificity, and selectorA > selectorB alphabetically
145
+ */
146
+ function compareSpecificity(selectorA, selectorB) {
147
+ return selectorA.attributeCount - selectorB.attributeCount ||
148
+ selectorA.identifierCount - selectorB.identifierCount ||
149
+ (selectorA.rawSelector <= selectorB.rawSelector ? -1 : 1);
150
+ }
151
+
152
+ /**
153
+ * Parses a raw selector string, and throws a useful error if parsing fails.
154
+ * @param {string} rawSelector A raw AST selector
155
+ * @returns {Object} An object (from esquery) describing the matching behavior of this selector
156
+ * @throws {Error} An error if the selector is invalid
157
+ */
158
+ function tryParseSelector(rawSelector) {
159
+ try {
160
+ return esquery.parse(rawSelector.replace(/:exit$/, ""));
161
+ } catch (err) {
162
+ if (typeof err.offset === "number") {
163
+ throw new Error(`Syntax error in selector "${rawSelector}" at position ${err.offset}: ${err.message}`);
164
+ }
165
+ throw err;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Parses a raw selector string, and returns the parsed selector along with specificity and type information.
171
+ * @param {string} rawSelector A raw AST selector
172
+ * @returns {ASTSelector} A selector descriptor
173
+ */
174
+ const parseSelector = lodash.memoize(rawSelector => {
175
+ const parsedSelector = tryParseSelector(rawSelector);
176
+
177
+ return {
178
+ rawSelector,
179
+ isExit: rawSelector.endsWith(":exit"),
180
+ parsedSelector,
181
+ listenerTypes: getPossibleTypes(parsedSelector),
182
+ attributeCount: countClassAttributes(parsedSelector),
183
+ identifierCount: countIdentifiers(parsedSelector)
184
+ };
185
+ });
186
+
8
187
  //------------------------------------------------------------------------------
9
188
  // Public Interface
10
189
  //------------------------------------------------------------------------------
@@ -24,10 +203,97 @@
24
203
  class NodeEventGenerator {
25
204
 
26
205
  /**
27
- * @param {EventEmitter} emitter - An event emitter which is the destination of events.
28
- */
206
+ * @param {EventEmitter} emitter - An event emitter which is the destination of events. This emitter must already
207
+ * have registered listeners for all of the events that it needs to listen for.
208
+ * @returns {NodeEventGenerator} new instance
209
+ */
29
210
  constructor(emitter) {
30
211
  this.emitter = emitter;
212
+ this.currentAncestry = [];
213
+ this.enterSelectorsByNodeType = new Map();
214
+ this.exitSelectorsByNodeType = new Map();
215
+ this.anyTypeEnterSelectors = [];
216
+ this.anyTypeExitSelectors = [];
217
+
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 => {
235
+ const selector = parseSelector(rawSelector);
236
+
237
+ if (selector.listenerTypes) {
238
+ selector.listenerTypes.forEach(nodeType => {
239
+ const typeMap = selector.isExit ? this.exitSelectorsByNodeType : this.enterSelectorsByNodeType;
240
+
241
+ if (!typeMap.has(nodeType)) {
242
+ typeMap.set(nodeType, []);
243
+ }
244
+ typeMap.get(nodeType).push(selector);
245
+ });
246
+ } else {
247
+ (selector.isExit ? this.anyTypeExitSelectors : this.anyTypeEnterSelectors).push(selector);
248
+ }
249
+ });
250
+
251
+ this.anyTypeEnterSelectors.sort(compareSpecificity);
252
+ this.anyTypeExitSelectors.sort(compareSpecificity);
253
+ this.enterSelectorsByNodeType.forEach(selectorList => selectorList.sort(compareSpecificity));
254
+ this.exitSelectorsByNodeType.forEach(selectorList => selectorList.sort(compareSpecificity));
255
+ }
256
+
257
+ /**
258
+ * Checks a selector against a node, and emits it if it matches
259
+ * @param {ASTNode} node The node to check
260
+ * @param {ASTSelector} selector An AST selector descriptor
261
+ * @returns {void}
262
+ */
263
+ applySelector(node, selector) {
264
+ if (esquery.matches(node, selector.parsedSelector, this.currentAncestry)) {
265
+ this.emitter.emit(selector.rawSelector, node);
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Applies all appropriate selectors to a node, in specificity order
271
+ * @param {ASTNode} node The node to check
272
+ * @param {boolean} isExit `false` if the node is currently being entered, `true` if it's currently being exited
273
+ * @returns {void}
274
+ */
275
+ applySelectors(node, isExit) {
276
+ const selectorsByNodeType = (isExit ? this.exitSelectorsByNodeType : this.enterSelectorsByNodeType).get(node.type) || [];
277
+ const anyTypeSelectors = isExit ? this.anyTypeExitSelectors : this.anyTypeEnterSelectors;
278
+
279
+ /*
280
+ * selectorsByNodeType and anyTypeSelectors were already sorted by specificity in the constructor.
281
+ * Iterate through each of them, applying selectors in the right order.
282
+ */
283
+ let selectorsByTypeIndex = 0;
284
+ let anyTypeSelectorsIndex = 0;
285
+
286
+ while (selectorsByTypeIndex < selectorsByNodeType.length || anyTypeSelectorsIndex < anyTypeSelectors.length) {
287
+ if (
288
+ selectorsByTypeIndex >= selectorsByNodeType.length ||
289
+ anyTypeSelectorsIndex < anyTypeSelectors.length &&
290
+ compareSpecificity(anyTypeSelectors[anyTypeSelectorsIndex], selectorsByNodeType[selectorsByTypeIndex]) < 0
291
+ ) {
292
+ this.applySelector(node, anyTypeSelectors[anyTypeSelectorsIndex++]);
293
+ } else {
294
+ this.applySelector(node, selectorsByNodeType[selectorsByTypeIndex++]);
295
+ }
296
+ }
31
297
  }
32
298
 
33
299
  /**
@@ -36,7 +302,10 @@ class NodeEventGenerator {
36
302
  * @returns {void}
37
303
  */
38
304
  enterNode(node) {
39
- this.emitter.emit(node.type, node);
305
+ if (node.parent) {
306
+ this.currentAncestry.unshift(node.parent);
307
+ }
308
+ this.applySelectors(node, false);
40
309
  }
41
310
 
42
311
  /**
@@ -45,7 +314,8 @@ class NodeEventGenerator {
45
314
  * @returns {void}
46
315
  */
47
316
  leaveNode(node) {
48
- this.emitter.emit(`${node.type}:exit`, node);
317
+ this.applySelectors(node, true);
318
+ this.currentAncestry.shift();
49
319
  }
50
320
  }
51
321
 
@@ -94,8 +94,8 @@ SourceCodeFixer.applyFixes = function(sourceCode, messages) {
94
94
  const start = fix.range[0];
95
95
  const end = fix.range[1];
96
96
 
97
- // Remain it as a problem if it's overlapped.
98
- if (lastPos >= start) {
97
+ // Remain it as a problem if it's overlapped or it's a negative range
98
+ if (lastPos >= start || start > end) {
99
99
  remainingMessages.push(problem);
100
100
  continue;
101
101
  }
@@ -108,13 +108,7 @@ SourceCodeFixer.applyFixes = function(sourceCode, messages) {
108
108
  // Make output to this fix.
109
109
  output += text.slice(Math.max(0, lastPos), Math.max(0, start));
110
110
  output += fix.text;
111
-
112
- /*
113
- * If the start of the range is larger than the end for some reason, make sure
114
- * the text between the end and the start doesn't get duplicated.
115
- * https://github.com/eslint/eslint/issues/8116
116
- */
117
- lastPos = Math.max(start, end);
111
+ lastPos = end;
118
112
  }
119
113
  output += text.slice(Math.max(0, lastPos));
120
114