eslint 4.2.0 → 4.5.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 (47) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/bin/eslint.js +5 -4
  3. package/conf/category-list.json +2 -2
  4. package/conf/config-schema.js +3 -1
  5. package/conf/eslint-recommended.js +11 -14
  6. package/lib/config/config-file.js +5 -5
  7. package/lib/config/config-initializer.js +123 -14
  8. package/lib/config/config-validator.js +25 -1
  9. package/lib/config/plugins.js +13 -1
  10. package/lib/formatters/junit.js +2 -8
  11. package/lib/formatters/stylish.js +2 -1
  12. package/lib/linter.js +21 -13
  13. package/lib/rule-context.js +53 -41
  14. package/lib/rules/arrow-parens.js +1 -1
  15. package/lib/rules/curly.js +1 -1
  16. package/lib/rules/getter-return.js +34 -9
  17. package/lib/rules/id-blacklist.js +7 -3
  18. package/lib/rules/id-match.js +8 -4
  19. package/lib/rules/indent-legacy.js +2 -2
  20. package/lib/rules/indent.js +451 -380
  21. package/lib/rules/key-spacing.js +2 -2
  22. package/lib/rules/no-cond-assign.js +7 -3
  23. package/lib/rules/no-constant-condition.js +62 -6
  24. package/lib/rules/no-else-return.js +1 -1
  25. package/lib/rules/no-extra-parens.js +3 -1
  26. package/lib/rules/no-inner-declarations.js +8 -4
  27. package/lib/rules/no-multi-spaces.js +53 -115
  28. package/lib/rules/no-regex-spaces.js +2 -2
  29. package/lib/rules/no-restricted-globals.js +50 -9
  30. package/lib/rules/no-restricted-properties.js +19 -11
  31. package/lib/rules/no-tabs.js +8 -4
  32. package/lib/rules/no-underscore-dangle.js +28 -1
  33. package/lib/rules/object-curly-newline.js +18 -0
  34. package/lib/rules/object-curly-spacing.js +1 -1
  35. package/lib/rules/padded-blocks.js +2 -2
  36. package/lib/rules/padding-line-between-statements.js +1 -1
  37. package/lib/rules/prefer-destructuring.js +70 -32
  38. package/lib/rules/prefer-numeric-literals.js +36 -7
  39. package/lib/rules/prefer-reflect.js +8 -4
  40. package/lib/rules/prefer-template.js +2 -2
  41. package/lib/rules/space-infix-ops.js +1 -1
  42. package/lib/rules/spaced-comment.js +2 -2
  43. package/lib/rules/valid-jsdoc.js +15 -7
  44. package/lib/testers/rule-tester.js +13 -21
  45. package/lib/testers/test-parser.js +48 -0
  46. package/lib/util/npm-util.js +9 -8
  47. package/package.json +11 -6
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @fileoverview This option sets a specific tab width for your code
3
3
  *
4
- * This rule has been ported and modified from nodeca.
4
+ * @author Teddy Katz
5
5
  * @author Vitaly Puzrin
6
6
  * @author Gyandeep Singh
7
7
  */
@@ -14,6 +14,7 @@
14
14
 
15
15
  const lodash = require("lodash");
16
16
  const astUtils = require("../ast-utils");
17
+ const createTree = require("functional-red-black-tree");
17
18
 
18
19
  //------------------------------------------------------------------------------
19
20
  // Rule Definition
@@ -114,6 +115,69 @@ const KNOWN_NODES = new Set([
114
115
  * and report the token if the two values are not equal.
115
116
  */
116
117
 
118
+
119
+ /**
120
+ * A mutable balanced binary search tree that stores (key, value) pairs. The keys are numeric, and must be unique.
121
+ * This is intended to be a generic wrapper around a balanced binary search tree library, so that the underlying implementation
122
+ * can easily be swapped out.
123
+ */
124
+ class BinarySearchTree {
125
+
126
+ /**
127
+ * Creates an empty tree
128
+ */
129
+ constructor() {
130
+ this._rbTree = createTree();
131
+ }
132
+
133
+ /**
134
+ * Inserts an entry into the tree.
135
+ * @param {number} key The entry's key
136
+ * @param {*} value The entry's value
137
+ * @returns {void}
138
+ */
139
+ insert(key, value) {
140
+ const iterator = this._rbTree.find(key);
141
+
142
+ if (iterator.valid) {
143
+ this._rbTree = iterator.update(value);
144
+ } else {
145
+ this._rbTree = this._rbTree.insert(key, value);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Finds the entry with the largest key less than or equal to the provided key
151
+ * @param {number} key The provided key
152
+ * @returns {{key: number, value: *}|null} The found entry, or null if no such entry exists.
153
+ */
154
+ findLe(key) {
155
+ const iterator = this._rbTree.le(key);
156
+
157
+ return iterator && { key: iterator.key, value: iterator.value };
158
+ }
159
+
160
+ /**
161
+ * Deletes all of the keys in the interval [start, end)
162
+ * @param {number} start The start of the range
163
+ * @param {number} end The end of the range
164
+ * @returns {void}
165
+ */
166
+ deleteRange(start, end) {
167
+
168
+ // Exit without traversing the tree if the range has zero size.
169
+ if (start === end) {
170
+ return;
171
+ }
172
+ const iterator = this._rbTree.ge(start);
173
+
174
+ while (iterator.valid && iterator.key < end) {
175
+ this._rbTree = this._rbTree.remove(iterator.key);
176
+ iterator.next();
177
+ }
178
+ }
179
+ }
180
+
117
181
  /**
118
182
  * A helper class to get token-based info related to indentation
119
183
  */
@@ -135,14 +199,6 @@ class TokenInfo {
135
199
  }, new Map());
136
200
  }
137
201
 
138
- /**
139
- * Gets all tokens and comments
140
- * @returns {Token[]} A list of all tokens and comments
141
- */
142
- getAllTokens() {
143
- return this.sourceCode.tokensAndComments;
144
- }
145
-
146
202
  /**
147
203
  * Gets the first token on a given token's line
148
204
  * @param {Token|ASTNode} token a node or token
@@ -178,50 +234,28 @@ class OffsetStorage {
178
234
 
179
235
  /**
180
236
  * @param {TokenInfo} tokenInfo a TokenInfo instance
181
- * @param {string} indentType The desired type of indentation (either "space" or "tab")
182
237
  * @param {number} indentSize The desired size of each indentation level
183
238
  */
184
- constructor(tokenInfo, indentType, indentSize) {
185
- this.tokenInfo = tokenInfo;
186
- this.indentType = indentType;
187
- this.indentSize = indentSize;
239
+ constructor(tokenInfo, indentSize) {
240
+ this._tokenInfo = tokenInfo;
241
+ this._indentSize = indentSize;
188
242
 
189
- /*
190
- * desiredOffsets, lockedFirstTokens, and desiredIndentCache conceptually map tokens to something else.
191
- * However, they're implemented as objects with range indices as keys because this improves performance as of Node 7.
192
- * This uses the assumption that no two tokens start at the same index in the program.
193
- *
194
- * The values of the desiredOffsets map are objects with the schema { offset: number, from: Token|null }.
195
- * These objects should not be mutated or exposed outside of OffsetStorage.
196
- */
197
- const NO_OFFSET = { offset: 0, from: null };
198
-
199
- this.desiredOffsets = tokenInfo.getAllTokens().reduce((desiredOffsets, token) => {
200
- desiredOffsets[token.range[0]] = NO_OFFSET;
243
+ this._tree = new BinarySearchTree();
244
+ this._tree.insert(0, { offset: 0, from: null, force: false });
201
245
 
202
- return desiredOffsets;
203
- }, Object.create(null));
204
- this.lockedFirstTokens = Object.create(null);
205
- this.desiredIndentCache = Object.create(null);
206
- this.ignoredTokens = new WeakSet();
246
+ this._lockedFirstTokens = new WeakMap();
247
+ this._desiredIndentCache = new WeakMap();
248
+ this._ignoredTokens = new WeakSet();
207
249
  }
208
250
 
209
- /**
210
- * Sets the indent of one token to match the indent of another.
211
- * @param {Token} baseToken The first token
212
- * @param {Token} offsetToken The second token, whose indent should be matched to the first token
213
- * @returns {void}
214
- */
215
- matchIndentOf(baseToken, offsetToken) {
216
- if (baseToken !== offsetToken) {
217
- this.desiredOffsets[offsetToken.range[0]] = { offset: 0, from: baseToken };
218
- }
251
+ _getOffsetDescriptor(token) {
252
+ return this._tree.findLe(token.range[0]).value;
219
253
  }
220
254
 
221
255
  /**
222
256
  * Sets the offset column of token B to match the offset column of token A.
223
- * **WARNING**: This is different from matchIndentOf because it matches a *column*, even if baseToken is not
224
- * the first token on its line. In most cases, `matchIndentOf` should be used instead.
257
+ * **WARNING**: This matches a *column*, even if baseToken is not the first token on its line. In
258
+ * most cases, `setDesiredOffset` should be used instead.
225
259
  * @param {Token} baseToken The first token
226
260
  * @param {Token} offsetToken The second token, whose offset should be matched to the first token
227
261
  * @returns {void}
@@ -235,7 +269,7 @@ class OffsetStorage {
235
269
  * element. The desired indentation of each of these tokens is computed based on the desired indentation
236
270
  * of the "first" element, rather than through the normal offset mechanism.
237
271
  */
238
- this.lockedFirstTokens[offsetToken.range[0]] = baseToken;
272
+ this._lockedFirstTokens.set(offsetToken, baseToken);
239
273
  }
240
274
 
241
275
  /**
@@ -291,48 +325,84 @@ class OffsetStorage {
291
325
  * in the second case.
292
326
  *
293
327
  * @param {Token} token The token
294
- * @param {Token} offsetFrom The token that `token` should be offset from
328
+ * @param {Token} fromToken The token that `token` should be offset from
295
329
  * @param {number} offset The desired indent level
296
330
  * @returns {void}
297
331
  */
298
- setDesiredOffset(token, offsetFrom, offset) {
299
- if (offsetFrom && token.loc.start.line === offsetFrom.loc.start.line) {
300
- this.matchIndentOf(offsetFrom, token);
301
- } else {
302
- this.desiredOffsets[token.range[0]] = { offset, from: offsetFrom };
303
- }
332
+ setDesiredOffset(token, fromToken, offset) {
333
+ return this.setDesiredOffsets(token.range, fromToken, offset);
304
334
  }
305
335
 
306
336
  /**
307
- * Sets the desired offset of a token, ignoring the usual collapsing behavior.
308
- * **WARNING**: This is usually not what you want to use. See `setDesiredOffset` instead.
309
- * @param {Token} token The token
310
- * @param {Token} offsetFrom The token that `token` should be offset from
311
- * @param {number} offset The desired indent level
312
- * @returns {void}
313
- */
314
- forceSetDesiredOffset(token, offsetFrom, offset) {
315
- this.desiredOffsets[token.range[0]] = { offset, from: offsetFrom };
316
- }
317
-
318
- /**
319
- * Sets the desired offset of multiple tokens
320
- * @param {Token[]} tokens A list of tokens. These tokens should be consecutive.
321
- * @param {Token} offsetFrom The token that this is offset from
337
+ * Sets the desired offset of all tokens in a range
338
+ * It's common for node listeners in this file to need to apply the same offset to a large, contiguous range of tokens.
339
+ * Moreover, the offset of any given token is usually updated multiple times (roughly once for each node that contains
340
+ * it). This means that the offset of each token is updated O(AST depth) times.
341
+ * It would not be performant to store and update the offsets for each token independently, because the rule would end
342
+ * up having a time complexity of O(number of tokens * AST depth), which is quite slow for large files.
343
+ *
344
+ * Instead, the offset tree is represented as a collection of contiguous offset ranges in a file. For example, the following
345
+ * list could represent the state of the offset tree at a given point:
346
+ *
347
+ * * Tokens starting in the interval [0, 15) are aligned with the beginning of the file
348
+ * * Tokens starting in the interval [15, 30) are offset by 1 indent level from the `bar` token
349
+ * * Tokens starting in the interval [30, 43) are offset by 1 indent level from the `foo` token
350
+ * * Tokens starting in the interval [43, 820) are offset by 2 indent levels from the `bar` token
351
+ * * Tokens starting in the interval [820, ∞) are offset by 1 indent level from the `baz` token
352
+ *
353
+ * The `setDesiredOffsets` methods inserts ranges like the ones above. The third line above would be inserted by using:
354
+ * `setDesiredOffsets([30, 43], fooToken, 1);`
355
+ *
356
+ * @param {[number, number]} range A [start, end] pair. All tokens with range[0] <= token.start < range[1] will have the offset applied.
357
+ * @param {Token} fromToken The token that this is offset from
322
358
  * @param {number} offset The desired indent level
359
+ * @param {boolean} force `true` if this offset should not use the normal collapsing behavior. This should almost always be false.
323
360
  * @returns {void}
324
361
  */
325
- setDesiredOffsets(tokens, offsetFrom, offset) {
362
+ setDesiredOffsets(range, fromToken, offset, force) {
326
363
 
327
364
  /*
328
- * TODO: (not-an-aardvark) This function is the main performance holdup for this rule. It works
329
- * by setting the desired offset of each token to the given amount relative to the parent, but it's
330
- * frequently called with a large list of tokens, and it takes time to set the offset for each token
331
- * individually. Since the tokens are always consecutive, it might be possible to improve performance
332
- * here by changing the data structure used to store offsets (e.g. allowing a *range* of tokens to
333
- * be offset rather than offsetting each token individually).
365
+ * Offset ranges are stored as a collection of nodes, where each node maps a numeric key to an offset
366
+ * descriptor. The tree for the example above would have the following nodes:
367
+ *
368
+ * * key: 0, value: { offset: 0, from: null }
369
+ * * key: 15, value: { offset: 1, from: barToken }
370
+ * * key: 30, value: { offset: 1, from: fooToken }
371
+ * * key: 43, value: { offset: 2, from: barToken }
372
+ * * key: 820, value: { offset: 1, from: bazToken }
373
+ *
374
+ * To find the offset descriptor for any given token, one needs to find the node with the largest key
375
+ * which is <= token.start. To make this operation fast, the nodes are stored in a balanced binary
376
+ * search tree indexed by key.
334
377
  */
335
- tokens.forEach(token => this.setDesiredOffset(token, offsetFrom, offset));
378
+
379
+ const descriptorToInsert = { offset, from: fromToken, force };
380
+
381
+ const descriptorAfterRange = this._tree.findLe(range[1]).value;
382
+
383
+ const fromTokenIsInRange = fromToken && fromToken.range[0] >= range[0] && fromToken.range[1] <= range[1];
384
+ const fromTokenDescriptor = fromTokenIsInRange && this._getOffsetDescriptor(fromToken);
385
+
386
+ // First, remove any existing nodes in the range from the tree.
387
+ this._tree.deleteRange(range[0] + 1, range[1]);
388
+
389
+ // Insert a new node into the tree for this range
390
+ this._tree.insert(range[0], descriptorToInsert);
391
+
392
+ /*
393
+ * To avoid circular offset dependencies, keep the `fromToken` token mapped to whatever it was mapped to previously,
394
+ * even if it's in the current range.
395
+ */
396
+ if (fromTokenIsInRange) {
397
+ this._tree.insert(fromToken.range[0], fromTokenDescriptor);
398
+ this._tree.insert(fromToken.range[1], descriptorToInsert);
399
+ }
400
+
401
+ /*
402
+ * To avoid modifying the offset of tokens after the range, insert another node to keep the offset of the following
403
+ * tokens the same as it was before.
404
+ */
405
+ this._tree.insert(range[1], descriptorAfterRange);
336
406
  }
337
407
 
338
408
  /**
@@ -341,30 +411,37 @@ class OffsetStorage {
341
411
  * @returns {number} The desired indent of the token
342
412
  */
343
413
  getDesiredIndent(token) {
344
- if (!(token.range[0] in this.desiredIndentCache)) {
414
+ if (!this._desiredIndentCache.has(token)) {
345
415
 
346
- if (this.ignoredTokens.has(token)) {
416
+ if (this._ignoredTokens.has(token)) {
347
417
 
348
418
  // If the token is ignored, use the actual indent of the token as the desired indent.
349
419
  // This ensures that no errors are reported for this token.
350
- this.desiredIndentCache[token.range[0]] = this.tokenInfo.getTokenIndent(token).length / this.indentSize;
351
- } else if (token.range[0] in this.lockedFirstTokens) {
352
- const firstToken = this.lockedFirstTokens[token.range[0]];
420
+ this._desiredIndentCache.set(token, this._tokenInfo.getTokenIndent(token).length / this._indentSize);
421
+ } else if (this._lockedFirstTokens.has(token)) {
422
+ const firstToken = this._lockedFirstTokens.get(token);
353
423
 
354
- this.desiredIndentCache[token.range[0]] =
424
+ this._desiredIndentCache.set(
425
+ token,
355
426
 
356
427
  // (indentation for the first element's line)
357
- this.getDesiredIndent(this.tokenInfo.getFirstTokenOfLine(firstToken)) +
428
+ this.getDesiredIndent(this._tokenInfo.getFirstTokenOfLine(firstToken)) +
358
429
 
359
430
  // (space between the start of the first element's line and the first element)
360
- (firstToken.loc.start.column - this.tokenInfo.getFirstTokenOfLine(firstToken).loc.start.column) / this.indentSize;
431
+ (firstToken.loc.start.column - this._tokenInfo.getFirstTokenOfLine(firstToken).loc.start.column) / this._indentSize
432
+ );
361
433
  } else {
362
- const offsetInfo = this.desiredOffsets[token.range[0]];
363
-
364
- this.desiredIndentCache[token.range[0]] = offsetInfo.offset + (offsetInfo.from ? this.getDesiredIndent(offsetInfo.from) : 0);
434
+ const offsetInfo = this._getOffsetDescriptor(token);
435
+ const offset = (
436
+ offsetInfo.from &&
437
+ offsetInfo.from.loc.start.line === token.loc.start.line &&
438
+ !offsetInfo.force
439
+ ) ? 0 : offsetInfo.offset;
440
+
441
+ this._desiredIndentCache.set(token, offset + (offsetInfo.from ? this.getDesiredIndent(offsetInfo.from) : 0));
365
442
  }
366
443
  }
367
- return this.desiredIndentCache[token.range[0]];
444
+ return this._desiredIndentCache.get(token);
368
445
  }
369
446
 
370
447
  /**
@@ -373,8 +450,8 @@ class OffsetStorage {
373
450
  * @returns {void}
374
451
  */
375
452
  ignoreToken(token) {
376
- if (this.tokenInfo.isFirstTokenOfLine(token)) {
377
- this.ignoredTokens.add(token);
453
+ if (this._tokenInfo.isFirstTokenOfLine(token)) {
454
+ this._ignoredTokens.add(token);
378
455
  }
379
456
  }
380
457
 
@@ -384,7 +461,7 @@ class OffsetStorage {
384
461
  * @returns {Token} The token that the given token depends on, or `null` if the given token is at the top level
385
462
  */
386
463
  getFirstDependency(token) {
387
- return this.desiredOffsets[token.range[0]].from;
464
+ return this._getOffsetDescriptor(token).from;
388
465
  }
389
466
  }
390
467
 
@@ -501,8 +578,18 @@ module.exports = {
501
578
  },
502
579
  ArrayExpression: ELEMENT_LIST_SCHEMA,
503
580
  ObjectExpression: ELEMENT_LIST_SCHEMA,
581
+ ImportDeclaration: ELEMENT_LIST_SCHEMA,
504
582
  flatTernaryExpressions: {
505
583
  type: "boolean"
584
+ },
585
+ ignoredNodes: {
586
+ type: "array",
587
+ items: {
588
+ type: "string",
589
+ not: {
590
+ pattern: ":exit$"
591
+ }
592
+ }
506
593
  }
507
594
  },
508
595
  additionalProperties: false
@@ -539,16 +626,16 @@ module.exports = {
539
626
  MemberExpression: 1,
540
627
  ArrayExpression: 1,
541
628
  ObjectExpression: 1,
542
- ArrayPattern: 1,
543
- ObjectPattern: 1,
544
- flatTernaryExpressions: false
629
+ ImportDeclaration: 1,
630
+ flatTernaryExpressions: false,
631
+ ignoredNodes: []
545
632
  };
546
633
 
547
634
  if (context.options.length) {
548
635
  if (context.options[0] === "tab") {
549
636
  indentSize = 1;
550
637
  indentType = "tab";
551
- } else if (typeof context.options[0] === "number") {
638
+ } else {
552
639
  indentSize = context.options[0];
553
640
  indentType = "space";
554
641
  }
@@ -568,7 +655,7 @@ module.exports = {
568
655
 
569
656
  const sourceCode = context.getSourceCode();
570
657
  const tokenInfo = new TokenInfo(sourceCode);
571
- const offsets = new OffsetStorage(tokenInfo, indentType, indentSize);
658
+ const offsets = new OffsetStorage(tokenInfo, indentSize);
572
659
  const parameterParens = new WeakSet();
573
660
 
574
661
  /**
@@ -600,10 +687,10 @@ module.exports = {
600
687
 
601
688
  /**
602
689
  * Reports a given indent violation
603
- * @param {Token} token Node violating the indent rule
690
+ * @param {Token} token Token violating the indent rule
604
691
  * @param {int} neededIndentLevel Expected indentation level
605
- * @param {int} gottenSpaces Indentation space count in the actual node/code
606
- * @param {int} gottenTabs Indentation tab count in the actual node/code
692
+ * @param {int} gottenSpaces Actual number of indentation spaces for the token
693
+ * @param {int} gottenTabs Actual number of indentation tabs for the token
607
694
  * @returns {void}
608
695
  */
609
696
  function report(token, neededIndentLevel) {
@@ -678,15 +765,6 @@ module.exports = {
678
765
  return (statement.type === "ExpressionStatement" || statement.type === "VariableDeclaration") && statement.parent.type === "Program";
679
766
  }
680
767
 
681
- /**
682
- * Gets all tokens and comments for a node
683
- * @param {ASTNode} node The node
684
- * @returns {Token[]} A list of tokens and comments
685
- */
686
- function getTokensAndComments(node) {
687
- return sourceCode.getTokens(node, { includeComments: true });
688
- }
689
-
690
768
  /**
691
769
  * Check indentation for lists of elements (arrays, objects, function params)
692
770
  * @param {ASTNode[]} elements List of elements that should be offset
@@ -712,13 +790,12 @@ module.exports = {
712
790
  }
713
791
 
714
792
  // Run through all the tokens in the list, and offset them by one indent level (mainly for comments, other things will end up overridden)
715
- // FIXME: (not-an-aardvark) This isn't performant at all.
716
793
  offsets.setDesiredOffsets(
717
- sourceCode.getTokensBetween(startToken, endToken, { includeComments: true }),
794
+ [startToken.range[1], endToken.range[0]],
718
795
  startToken,
719
- offset === "first" ? 1 : offset
796
+ typeof offset === "number" ? offset : 1
720
797
  );
721
- offsets.matchIndentOf(startToken, endToken);
798
+ offsets.setDesiredOffset(endToken, startToken, 0);
722
799
 
723
800
  // If the preference is "first" but there is no first element (e.g. sparse arrays w/ empty first slot), fall back to 1 level.
724
801
  if (offset === "first" && elements.length && !elements[0]) {
@@ -738,52 +815,12 @@ module.exports = {
738
815
  const firstTokenOfPreviousElement = previousElement && getFirstToken(previousElement);
739
816
 
740
817
  if (previousElement && sourceCode.getLastToken(previousElement).loc.start.line > startToken.loc.end.line) {
741
- offsets.setDesiredOffsets(getTokensAndComments(element), firstTokenOfPreviousElement, 0);
818
+ offsets.setDesiredOffsets(element.range, firstTokenOfPreviousElement, 0);
742
819
  }
743
820
  }
744
821
  });
745
822
  }
746
823
 
747
- /**
748
- * Check indentation for blocks and class bodies
749
- * @param {ASTNode} node The BlockStatement or ClassBody node to indent
750
- * @returns {void}
751
- */
752
- function addBlockIndent(node) {
753
-
754
- let blockIndentLevel;
755
-
756
- if (node.parent && isOuterIIFE(node.parent)) {
757
- blockIndentLevel = options.outerIIFEBody;
758
- } else if (node.parent && (node.parent.type === "FunctionExpression" || node.parent.type === "ArrowFunctionExpression")) {
759
- blockIndentLevel = options.FunctionExpression.body;
760
- } else if (node.parent && node.parent.type === "FunctionDeclaration") {
761
- blockIndentLevel = options.FunctionDeclaration.body;
762
- } else {
763
- blockIndentLevel = 1;
764
- }
765
-
766
- /*
767
- * For blocks that aren't lone statements, ensure that the opening curly brace
768
- * is aligned with the parent.
769
- */
770
- if (!astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type)) {
771
- offsets.matchIndentOf(sourceCode.getFirstToken(node.parent), sourceCode.getFirstToken(node));
772
- }
773
- addElementListIndent(node.body, sourceCode.getFirstToken(node), sourceCode.getLastToken(node), blockIndentLevel);
774
- }
775
-
776
- /**
777
- * Check indent for array block content or object block content
778
- * @param {ASTNode} node node to examine
779
- * @returns {void}
780
- */
781
- function addArrayOrObjectIndent(node) {
782
- const tokens = getTokensAndComments(node);
783
-
784
- addElementListIndent(node.elements || node.properties, tokens[0], tokens[tokens.length - 1], options[node.type]);
785
- }
786
-
787
824
  /**
788
825
  * Check and decide whether to check for indentation for blockless nodes
789
826
  * Scenarios are for or while statements without braces around them
@@ -794,16 +831,18 @@ module.exports = {
794
831
  if (node.type !== "BlockStatement") {
795
832
  const lastParentToken = sourceCode.getTokenBefore(node, astUtils.isNotOpeningParenToken);
796
833
 
797
- let bodyTokens = getTokensAndComments(node);
834
+ let firstBodyToken = sourceCode.getFirstToken(node);
835
+ let lastBodyToken = sourceCode.getLastToken(node);
798
836
 
799
837
  while (
800
- astUtils.isOpeningParenToken(sourceCode.getTokenBefore(bodyTokens[0])) &&
801
- astUtils.isClosingParenToken(sourceCode.getTokenAfter(bodyTokens[bodyTokens.length - 1]))
838
+ astUtils.isOpeningParenToken(sourceCode.getTokenBefore(firstBodyToken)) &&
839
+ astUtils.isClosingParenToken(sourceCode.getTokenAfter(lastBodyToken))
802
840
  ) {
803
- bodyTokens = [sourceCode.getTokenBefore(bodyTokens[0])].concat(bodyTokens).concat(sourceCode.getTokenAfter(bodyTokens[bodyTokens.length - 1]));
841
+ firstBodyToken = sourceCode.getTokenBefore(firstBodyToken);
842
+ lastBodyToken = sourceCode.getTokenAfter(lastBodyToken);
804
843
  }
805
844
 
806
- offsets.setDesiredOffsets(bodyTokens, lastParentToken, 1);
845
+ offsets.setDesiredOffsets([firstBodyToken.range[0], lastBodyToken.range[1]], lastParentToken, 1);
807
846
 
808
847
  /*
809
848
  * For blockless nodes with semicolon-first style, don't indent the semicolon.
@@ -814,59 +853,11 @@ module.exports = {
814
853
  const lastToken = sourceCode.getLastToken(node);
815
854
 
816
855
  if (node.type !== "EmptyStatement" && astUtils.isSemicolonToken(lastToken)) {
817
- offsets.matchIndentOf(lastParentToken, lastToken);
856
+ offsets.setDesiredOffset(lastToken, lastParentToken, 0);
818
857
  }
819
858
  }
820
859
  }
821
860
 
822
- /**
823
- * Checks the indentation of a function's parameters
824
- * @param {ASTNode} node The node
825
- * @param {number} paramsIndent The indentation level option for the parameters
826
- * @returns {void}
827
- */
828
- function addFunctionParamsIndent(node, paramsIndent) {
829
- const openingParen = node.params.length ? sourceCode.getTokenBefore(node.params[0]) : sourceCode.getTokenBefore(node.body, 1);
830
-
831
- if (!openingParen) {
832
-
833
- // If there is no opening paren (e.g. for an arrow function with a single parameter), don't indent anything.
834
- return;
835
- }
836
-
837
- const closingParen = sourceCode.getTokenBefore(node.body);
838
- const nodeTokens = getTokensAndComments(node);
839
- const openingParenIndex = lodash.sortedIndexBy(nodeTokens, openingParen, token => token.range[0]);
840
- const closingParenIndex = lodash.sortedIndexBy(nodeTokens, closingParen, token => token.range[0]);
841
-
842
- parameterParens.add(nodeTokens[openingParenIndex]);
843
- parameterParens.add(nodeTokens[closingParenIndex]);
844
-
845
- addElementListIndent(node.params, nodeTokens[openingParenIndex], nodeTokens[closingParenIndex], paramsIndent);
846
- }
847
-
848
- /**
849
- * Adds indentation for the right-hand side of binary/logical expressions.
850
- * @param {ASTNode} node A BinaryExpression or LogicalExpression node
851
- * @returns {void}
852
- */
853
- function addBinaryOrLogicalExpressionIndent(node) {
854
- const tokens = getTokensAndComments(node);
855
- const operator = sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
856
- const firstTokenAfterOperator = sourceCode.getTokenAfter(operator);
857
- const tokensAfterOperator = tokens.slice(lodash.sortedIndexBy(tokens, firstTokenAfterOperator, token => token.range[0]));
858
-
859
- /*
860
- * For backwards compatibility, don't check BinaryExpression indents, e.g.
861
- * var foo = bar &&
862
- * baz;
863
- */
864
-
865
- offsets.ignoreToken(operator);
866
- offsets.ignoreToken(tokensAfterOperator[0]);
867
- offsets.setDesiredOffsets(tokensAfterOperator, tokensAfterOperator[0], 1);
868
- }
869
-
870
861
  /**
871
862
  * Checks the indentation for nodes that are like function calls (`CallExpression` and `NewExpression`)
872
863
  * @param {ASTNode} node A CallExpression or NewExpression node
@@ -884,26 +875,11 @@ module.exports = {
884
875
 
885
876
  parameterParens.add(openingParen);
886
877
  parameterParens.add(closingParen);
887
- offsets.matchIndentOf(sourceCode.getTokenBefore(openingParen), openingParen);
878
+ offsets.setDesiredOffset(openingParen, sourceCode.getTokenBefore(openingParen), 0);
888
879
 
889
880
  addElementListIndent(node.arguments, openingParen, closingParen, options.CallExpression.arguments);
890
881
  }
891
882
 
892
- /**
893
- * Checks the indentation of ClassDeclarations and ClassExpressions
894
- * @param {ASTNode} node A ClassDeclaration or ClassExpression node
895
- * @returns {void}
896
- */
897
- function addClassIndent(node) {
898
- if (node.superClass) {
899
- const classToken = sourceCode.getFirstToken(node);
900
- const extendsToken = sourceCode.getTokenBefore(node.superClass, astUtils.isNotOpeningParenToken);
901
-
902
- offsets.setDesiredOffset(extendsToken, classToken, 1);
903
- offsets.setDesiredOffsets(sourceCode.getTokensBetween(extendsToken, node.body, { includeComments: true }), classToken, 1);
904
- }
905
- }
906
-
907
883
  /**
908
884
  * Checks the indentation of parenthesized values, given a list of tokens in a program
909
885
  * @param {Token[]} tokens A list of tokens
@@ -936,10 +912,9 @@ module.exports = {
936
912
  offsets.setDesiredOffset(token, leftParen, 1);
937
913
  }
938
914
  });
939
- offsets.setDesiredOffset(sourceCode.getTokenAfter(leftParen), leftParen, 1);
940
915
  }
941
916
 
942
- offsets.matchIndentOf(leftParen, rightParen);
917
+ offsets.setDesiredOffset(rightParen, leftParen, 0);
943
918
  });
944
919
  }
945
920
 
@@ -949,8 +924,8 @@ module.exports = {
949
924
  * @param {ASTNode} node Unknown Node
950
925
  * @returns {void}
951
926
  */
952
- function ignoreUnknownNode(node) {
953
- const unknownNodeTokens = new Set(getTokensAndComments(node));
927
+ function ignoreNode(node) {
928
+ const unknownNodeTokens = new Set(sourceCode.getTokens(node, { includeComments: true }));
954
929
 
955
930
  unknownNodeTokens.forEach(token => {
956
931
  if (!unknownNodeTokens.has(offsets.getFirstDependency(token))) {
@@ -959,25 +934,12 @@ module.exports = {
959
934
  if (token === firstTokenOfLine) {
960
935
  offsets.ignoreToken(token);
961
936
  } else {
962
- offsets.matchIndentOf(firstTokenOfLine, token);
937
+ offsets.setDesiredOffset(token, firstTokenOfLine, 0);
963
938
  }
964
939
  }
965
940
  });
966
941
  }
967
942
 
968
- /**
969
- * Ignore node if it is unknown
970
- * @param {ASTNode} node Node
971
- * @returns {void}
972
- */
973
- function checkForUnknownNode(node) {
974
- const isNodeUnknown = !(KNOWN_NODES.has(node.type));
975
-
976
- if (isNodeUnknown) {
977
- ignoreUnknownNode(node);
978
- }
979
- }
980
-
981
943
  /**
982
944
  * Check whether the given token is the first token of a statement.
983
945
  * @param {Token} token The token to check.
@@ -995,12 +957,35 @@ module.exports = {
995
957
  return !node || node.range[0] === token.range[0];
996
958
  }
997
959
 
998
- return {
999
- ArrayExpression: addArrayOrObjectIndent,
1000
- ArrayPattern: addArrayOrObjectIndent,
960
+ const baseOffsetListeners = {
961
+ "ArrayExpression, ArrayPattern"(node) {
962
+ const openingBracket = sourceCode.getFirstToken(node);
963
+ const closingBracket = sourceCode.getTokenAfter(lodash.findLast(node.elements) || openingBracket, astUtils.isClosingBracketToken);
964
+
965
+ addElementListIndent(node.elements, openingBracket, closingBracket, options.ArrayExpression);
966
+ },
967
+
968
+ "ObjectExpression, ObjectPattern"(node) {
969
+ const openingCurly = sourceCode.getFirstToken(node);
970
+ const closingCurly = sourceCode.getTokenAfter(
971
+ node.properties.length ? node.properties[node.properties.length - 1] : openingCurly,
972
+ astUtils.isClosingBraceToken
973
+ );
974
+
975
+ addElementListIndent(node.properties, openingCurly, closingCurly, options.ObjectExpression);
976
+ },
1001
977
 
1002
978
  ArrowFunctionExpression(node) {
1003
- addFunctionParamsIndent(node, options.FunctionExpression.parameters);
979
+ const firstToken = sourceCode.getFirstToken(node);
980
+
981
+ if (astUtils.isOpeningParenToken(firstToken)) {
982
+ const openingParen = firstToken;
983
+ const closingParen = sourceCode.getTokenBefore(node.body, astUtils.isClosingParenToken);
984
+
985
+ parameterParens.add(openingParen);
986
+ parameterParens.add(closingParen);
987
+ addElementListIndent(node.params, openingParen, closingParen, options.FunctionExpression.parameters);
988
+ }
1004
989
  addBlocklessNodeIndent(node.body);
1005
990
 
1006
991
  let arrowToken;
@@ -1010,30 +995,67 @@ module.exports = {
1010
995
  } else {
1011
996
  arrowToken = sourceCode.getFirstToken(node, astUtils.isArrowToken);
1012
997
  }
1013
- offsets.matchIndentOf(sourceCode.getFirstToken(node), arrowToken);
998
+ offsets.setDesiredOffset(arrowToken, sourceCode.getFirstToken(node), 0);
1014
999
  },
1015
1000
 
1016
1001
  AssignmentExpression(node) {
1017
1002
  const operator = sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
1018
- const nodeTokens = getTokensAndComments(node);
1019
- const tokensFromOperator = nodeTokens.slice(lodash.sortedIndexBy(nodeTokens, operator, token => token.range[0]));
1020
1003
 
1021
- offsets.setDesiredOffsets(tokensFromOperator, sourceCode.getLastToken(node.left), 1);
1022
- offsets.ignoreToken(tokensFromOperator[0]);
1023
- offsets.ignoreToken(tokensFromOperator[1]);
1004
+ offsets.setDesiredOffsets([operator.range[0], node.range[1]], sourceCode.getLastToken(node.left), 1);
1005
+ offsets.ignoreToken(operator);
1006
+ offsets.ignoreToken(sourceCode.getTokenAfter(operator));
1007
+ },
1008
+
1009
+ "BinaryExpression, LogicalExpression"(node) {
1010
+ const operator = sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
1011
+
1012
+ /*
1013
+ * For backwards compatibility, don't check BinaryExpression indents, e.g.
1014
+ * var foo = bar &&
1015
+ * baz;
1016
+ */
1017
+
1018
+ const tokenAfterOperator = sourceCode.getTokenAfter(operator);
1019
+
1020
+ offsets.ignoreToken(operator);
1021
+ offsets.ignoreToken(tokenAfterOperator);
1022
+ offsets.setDesiredOffset(tokenAfterOperator, operator, 0);
1023
+ offsets.setDesiredOffsets([tokenAfterOperator.range[1], node.range[1]], tokenAfterOperator, 1);
1024
1024
  },
1025
1025
 
1026
- BinaryExpression: addBinaryOrLogicalExpressionIndent,
1026
+ "BlockStatement, ClassBody"(node) {
1027
1027
 
1028
- BlockStatement: addBlockIndent,
1028
+ let blockIndentLevel;
1029
+
1030
+ if (node.parent && isOuterIIFE(node.parent)) {
1031
+ blockIndentLevel = options.outerIIFEBody;
1032
+ } else if (node.parent && (node.parent.type === "FunctionExpression" || node.parent.type === "ArrowFunctionExpression")) {
1033
+ blockIndentLevel = options.FunctionExpression.body;
1034
+ } else if (node.parent && node.parent.type === "FunctionDeclaration") {
1035
+ blockIndentLevel = options.FunctionDeclaration.body;
1036
+ } else {
1037
+ blockIndentLevel = 1;
1038
+ }
1039
+
1040
+ /*
1041
+ * For blocks that aren't lone statements, ensure that the opening curly brace
1042
+ * is aligned with the parent.
1043
+ */
1044
+ if (!astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type)) {
1045
+ offsets.setDesiredOffset(sourceCode.getFirstToken(node), sourceCode.getFirstToken(node.parent), 0);
1046
+ }
1047
+ addElementListIndent(node.body, sourceCode.getFirstToken(node), sourceCode.getLastToken(node), blockIndentLevel);
1048
+ },
1029
1049
 
1030
1050
  CallExpression: addFunctionCallIndent,
1031
1051
 
1032
- ClassBody: addBlockIndent,
1033
1052
 
1034
- ClassDeclaration: addClassIndent,
1053
+ "ClassDeclaration[superClass], ClassExpression[superClass]"(node) {
1054
+ const classToken = sourceCode.getFirstToken(node);
1055
+ const extendsToken = sourceCode.getTokenBefore(node.superClass, astUtils.isNotOpeningParenToken);
1035
1056
 
1036
- ClassExpression: addClassIndent,
1057
+ offsets.setDesiredOffsets([extendsToken.range[0], node.body.range[0]], classToken, 1);
1058
+ },
1037
1059
 
1038
1060
  ConditionalExpression(node) {
1039
1061
  const firstToken = sourceCode.getFirstToken(node);
@@ -1050,13 +1072,14 @@ module.exports = {
1050
1072
  const questionMarkToken = sourceCode.getFirstTokenBetween(node.test, node.consequent, token => token.type === "Punctuator" && token.value === "?");
1051
1073
  const colonToken = sourceCode.getFirstTokenBetween(node.consequent, node.alternate, token => token.type === "Punctuator" && token.value === ":");
1052
1074
 
1053
- const consequentTokens = sourceCode.getTokensBetween(questionMarkToken, colonToken, { includeComments: true });
1054
- const alternateTokens = sourceCode.getTokensAfter(colonToken, token => token.range[1] <= node.range[1]);
1075
+ const firstConsequentToken = sourceCode.getTokenAfter(questionMarkToken, { includeComments: true });
1076
+ const lastConsequentToken = sourceCode.getTokenBefore(colonToken, { includeComments: true });
1077
+ const firstAlternateToken = sourceCode.getTokenAfter(colonToken);
1055
1078
 
1056
1079
  offsets.setDesiredOffset(questionMarkToken, firstToken, 1);
1057
1080
  offsets.setDesiredOffset(colonToken, firstToken, 1);
1058
1081
 
1059
- offsets.setDesiredOffset(consequentTokens[0], firstToken, 1);
1082
+ offsets.setDesiredOffset(firstConsequentToken, firstToken, 1);
1060
1083
 
1061
1084
  /*
1062
1085
  * The alternate and the consequent should usually have the same indentation.
@@ -1068,8 +1091,8 @@ module.exports = {
1068
1091
  * baz // as a result, `baz` is offset by 1 rather than 2
1069
1092
  * )
1070
1093
  */
1071
- if (consequentTokens[consequentTokens.length - 1].loc.end.line === alternateTokens[0].loc.start.line) {
1072
- offsets.matchIndentOf(consequentTokens[0], alternateTokens[0]);
1094
+ if (lastConsequentToken.loc.end.line === firstAlternateToken.loc.start.line) {
1095
+ offsets.setDesiredOffset(firstAlternateToken, firstConsequentToken, 0);
1073
1096
  } else {
1074
1097
 
1075
1098
  /**
@@ -1081,21 +1104,19 @@ module.exports = {
1081
1104
  * If `baz` were aligned with `bar` rather than being offset by 1 from `foo`, `baz` would end up
1082
1105
  * having no expected indentation.
1083
1106
  */
1084
- offsets.setDesiredOffset(alternateTokens[0], firstToken, 1);
1107
+ offsets.setDesiredOffset(firstAlternateToken, firstToken, 1);
1085
1108
  }
1086
1109
 
1087
- offsets.setDesiredOffsets(consequentTokens, consequentTokens[0], 0);
1088
- offsets.setDesiredOffsets(alternateTokens, alternateTokens[0], 0);
1110
+ offsets.setDesiredOffsets([questionMarkToken.range[1], colonToken.range[0]], firstConsequentToken, 0);
1111
+ offsets.setDesiredOffsets([colonToken.range[1], node.range[1]], firstAlternateToken, 0);
1089
1112
  }
1090
1113
  },
1091
1114
 
1092
- DoWhileStatement: node => addBlocklessNodeIndent(node.body),
1115
+ "DoWhileStatement, WhileStatement, ForInStatement, ForOfStatement": node => addBlocklessNodeIndent(node.body),
1093
1116
 
1094
1117
  ExportNamedDeclaration(node) {
1095
1118
  if (node.declaration === null) {
1096
- const tokensInNode = getTokensAndComments(node);
1097
1119
  const closingCurly = sourceCode.getLastToken(node, astUtils.isClosingBraceToken);
1098
- const closingCurlyIndex = lodash.sortedIndexBy(tokensInNode, closingCurly, token => token.range[0]);
1099
1120
 
1100
1121
  // Indent the specifiers in `export {foo, bar, baz}`
1101
1122
  addElementListIndent(node.specifiers, sourceCode.getFirstToken(node, { skip: 1 }), closingCurly, 1);
@@ -1103,36 +1124,33 @@ module.exports = {
1103
1124
  if (node.source) {
1104
1125
 
1105
1126
  // Indent everything after and including the `from` token in `export {foo, bar, baz} from 'qux'`
1106
- offsets.setDesiredOffsets(tokensInNode.slice(closingCurlyIndex + 1), sourceCode.getFirstToken(node), 1);
1127
+ offsets.setDesiredOffsets([closingCurly.range[1], node.range[1]], sourceCode.getFirstToken(node), 1);
1107
1128
  }
1108
1129
  }
1109
1130
  },
1110
1131
 
1111
- ForInStatement: node => addBlocklessNodeIndent(node.body),
1112
-
1113
- ForOfStatement: node => addBlocklessNodeIndent(node.body),
1114
-
1115
1132
  ForStatement(node) {
1116
1133
  const forOpeningParen = sourceCode.getFirstToken(node, 1);
1117
1134
 
1118
1135
  if (node.init) {
1119
- offsets.setDesiredOffsets(getTokensAndComments(node.init), forOpeningParen, 1);
1136
+ offsets.setDesiredOffsets(node.init.range, forOpeningParen, 1);
1120
1137
  }
1121
1138
  if (node.test) {
1122
- offsets.setDesiredOffsets(getTokensAndComments(node.test), forOpeningParen, 1);
1139
+ offsets.setDesiredOffsets(node.test.range, forOpeningParen, 1);
1123
1140
  }
1124
1141
  if (node.update) {
1125
- offsets.setDesiredOffsets(getTokensAndComments(node.update), forOpeningParen, 1);
1142
+ offsets.setDesiredOffsets(node.update.range, forOpeningParen, 1);
1126
1143
  }
1127
1144
  addBlocklessNodeIndent(node.body);
1128
1145
  },
1129
1146
 
1130
- FunctionDeclaration(node) {
1131
- addFunctionParamsIndent(node, options.FunctionDeclaration.parameters);
1132
- },
1147
+ "FunctionDeclaration, FunctionExpression"(node) {
1148
+ const closingParen = sourceCode.getTokenBefore(node.body);
1149
+ const openingParen = sourceCode.getTokenBefore(node.params.length ? node.params[0] : closingParen);
1133
1150
 
1134
- FunctionExpression(node) {
1135
- addFunctionParamsIndent(node, options.FunctionExpression.parameters);
1151
+ parameterParens.add(openingParen);
1152
+ parameterParens.add(closingParen);
1153
+ addElementListIndent(node.params, openingParen, closingParen, options[node.type].parameters);
1136
1154
  },
1137
1155
 
1138
1156
  IfStatement(node) {
@@ -1147,34 +1165,33 @@ module.exports = {
1147
1165
  const openingCurly = sourceCode.getFirstToken(node, astUtils.isOpeningBraceToken);
1148
1166
  const closingCurly = sourceCode.getLastToken(node, astUtils.isClosingBraceToken);
1149
1167
 
1150
- addElementListIndent(node.specifiers.filter(specifier => specifier.type === "ImportSpecifier"), openingCurly, closingCurly, 1);
1168
+ addElementListIndent(node.specifiers.filter(specifier => specifier.type === "ImportSpecifier"), openingCurly, closingCurly, options.ImportDeclaration);
1151
1169
  }
1152
1170
 
1153
1171
  const fromToken = sourceCode.getLastToken(node, token => token.type === "Identifier" && token.value === "from");
1154
1172
 
1155
1173
  if (fromToken) {
1156
- const tokensToOffset = sourceCode.getTokensBetween(fromToken, sourceCode.getLastToken(node), 1);
1157
-
1158
- offsets.setDesiredOffsets(tokensToOffset, sourceCode.getFirstToken(node), 1);
1174
+ offsets.setDesiredOffsets([fromToken.range[0], node.range[1]], sourceCode.getFirstToken(node), 1);
1159
1175
  }
1160
1176
  },
1161
1177
 
1162
- LogicalExpression: addBinaryOrLogicalExpressionIndent,
1163
-
1164
- "MemberExpression, JSXMemberExpression"(node) {
1165
- const firstNonObjectToken = sourceCode.getFirstTokenBetween(node.object, node.property, astUtils.isNotClosingParenToken);
1178
+ "MemberExpression, JSXMemberExpression, MetaProperty"(node) {
1179
+ const object = node.type === "MetaProperty" ? node.meta : node.object;
1180
+ const firstNonObjectToken = sourceCode.getFirstTokenBetween(object, node.property, astUtils.isNotClosingParenToken);
1166
1181
  const secondNonObjectToken = sourceCode.getTokenAfter(firstNonObjectToken);
1167
1182
 
1168
- const tokenBeforeObject = sourceCode.getTokenBefore(node.object, token => astUtils.isNotOpeningParenToken(token) || parameterParens.has(token));
1169
- const firstObjectToken = tokenBeforeObject ? sourceCode.getTokenAfter(tokenBeforeObject) : sourceCode.ast.tokens[0];
1183
+ const objectParenCount = sourceCode.getTokensBetween(object, node.property, { filter: astUtils.isClosingParenToken }).length;
1184
+ const firstObjectToken = objectParenCount
1185
+ ? sourceCode.getTokenBefore(object, { skip: objectParenCount - 1 })
1186
+ : sourceCode.getFirstToken(object);
1170
1187
  const lastObjectToken = sourceCode.getTokenBefore(firstNonObjectToken);
1171
1188
  const firstPropertyToken = node.computed ? firstNonObjectToken : secondNonObjectToken;
1172
1189
 
1173
1190
  if (node.computed) {
1174
1191
 
1175
1192
  // For computed MemberExpressions, match the closing bracket with the opening bracket.
1176
- offsets.matchIndentOf(firstNonObjectToken, sourceCode.getLastToken(node));
1177
- offsets.setDesiredOffsets(getTokensAndComments(node.property), firstNonObjectToken, 1);
1193
+ offsets.setDesiredOffset(sourceCode.getLastToken(node), firstNonObjectToken, 0);
1194
+ offsets.setDesiredOffsets(node.property.range, firstNonObjectToken, 1);
1178
1195
  }
1179
1196
 
1180
1197
  /*
@@ -1207,8 +1224,8 @@ module.exports = {
1207
1224
  offsets.ignoreToken(secondNonObjectToken);
1208
1225
 
1209
1226
  // To ignore the property indentation, ensure that the property tokens depend on the ignored tokens.
1210
- offsets.matchIndentOf(offsetBase, firstNonObjectToken);
1211
- offsets.matchIndentOf(firstNonObjectToken, secondNonObjectToken);
1227
+ offsets.setDesiredOffset(firstNonObjectToken, offsetBase, 0);
1228
+ offsets.setDesiredOffset(secondNonObjectToken, firstNonObjectToken, 0);
1212
1229
  }
1213
1230
  },
1214
1231
 
@@ -1220,11 +1237,8 @@ module.exports = {
1220
1237
  }
1221
1238
  },
1222
1239
 
1223
- ObjectExpression: addArrayOrObjectIndent,
1224
- ObjectPattern: addArrayOrObjectIndent,
1225
-
1226
1240
  Property(node) {
1227
- if (!node.computed && !node.shorthand && !node.method && node.kind === "init") {
1241
+ if (!node.shorthand && !node.method && node.kind === "init") {
1228
1242
  const colon = sourceCode.getFirstTokenBetween(node.key, node.value, astUtils.isColonToken);
1229
1243
 
1230
1244
  offsets.ignoreToken(sourceCode.getTokenAfter(colon));
@@ -1232,41 +1246,38 @@ module.exports = {
1232
1246
  },
1233
1247
 
1234
1248
  SwitchStatement(node) {
1235
- const tokens = getTokensAndComments(node);
1236
- const openingCurlyIndex = tokens.findIndex(token => token.range[0] >= node.discriminant.range[1] && astUtils.isOpeningBraceToken(token));
1249
+ const openingCurly = sourceCode.getTokenAfter(node.discriminant, astUtils.isOpeningBraceToken);
1250
+ const closingCurly = sourceCode.getLastToken(node);
1251
+ const caseKeywords = node.cases.map(switchCase => sourceCode.getFirstToken(switchCase));
1237
1252
 
1238
- offsets.setDesiredOffsets(tokens.slice(openingCurlyIndex + 1, -1), tokens[openingCurlyIndex], options.SwitchCase);
1253
+ offsets.setDesiredOffsets([openingCurly.range[1], closingCurly.range[0]], openingCurly, options.SwitchCase);
1239
1254
 
1240
- const caseKeywords = new WeakSet(node.cases.map(switchCase => sourceCode.getFirstToken(switchCase)));
1241
- const lastCaseKeyword = node.cases.length && sourceCode.getFirstToken(node.cases[node.cases.length - 1]);
1242
- const casesWithBlocks = new WeakSet(
1243
- node.cases
1244
- .filter(switchCase => switchCase.consequent.length === 1 && switchCase.consequent[0].type === "BlockStatement")
1245
- .map(switchCase => sourceCode.getFirstToken(switchCase))
1246
- );
1247
- let lastAnchor = tokens[openingCurlyIndex];
1255
+ node.cases.forEach((switchCase, index) => {
1256
+ const caseKeyword = caseKeywords[index];
1248
1257
 
1249
- tokens.slice(openingCurlyIndex + 1, -1).forEach(token => {
1250
- if (caseKeywords.has(token)) {
1251
- lastAnchor = token;
1252
- } else if (lastAnchor === lastCaseKeyword && (token.type === "Line" || token.type === "Block")) {
1253
- offsets.ignoreToken(token);
1254
- } else if (!casesWithBlocks.has(lastAnchor)) {
1255
- offsets.setDesiredOffset(token, lastAnchor, 1);
1258
+ if (!(switchCase.consequent.length === 1 && switchCase.consequent[0].type === "BlockStatement")) {
1259
+ const tokenAfterCurrentCase = index === node.cases.length - 1 ? closingCurly : caseKeywords[index + 1];
1260
+
1261
+ offsets.setDesiredOffsets([caseKeyword.range[1], tokenAfterCurrentCase.range[0]], caseKeyword, 1);
1256
1262
  }
1257
1263
  });
1264
+
1265
+ if (node.cases.length) {
1266
+ sourceCode.getTokensBetween(
1267
+ node.cases[node.cases.length - 1],
1268
+ closingCurly,
1269
+ { includeComments: true, filter: astUtils.isCommentToken }
1270
+ ).forEach(token => offsets.ignoreToken(token));
1271
+ }
1258
1272
  },
1259
1273
 
1260
1274
  TemplateLiteral(node) {
1261
- const tokens = getTokensAndComments(node);
1262
-
1263
- offsets.setDesiredOffsets(getTokensAndComments(node.quasis[0]), tokens[0], 0);
1264
1275
  node.expressions.forEach((expression, index) => {
1265
1276
  const previousQuasi = node.quasis[index];
1266
1277
  const nextQuasi = node.quasis[index + 1];
1267
1278
  const tokenToAlignFrom = previousQuasi.loc.start.line === previousQuasi.loc.end.line ? sourceCode.getFirstToken(previousQuasi) : null;
1268
1279
 
1269
- offsets.setDesiredOffsets(sourceCode.getTokensBetween(previousQuasi, nextQuasi), tokenToAlignFrom, 1);
1280
+ offsets.setDesiredOffsets([previousQuasi.range[1], nextQuasi.range[0]], tokenToAlignFrom, 1);
1270
1281
  offsets.setDesiredOffset(sourceCode.getFirstToken(nextQuasi), tokenToAlignFrom, 0);
1271
1282
  });
1272
1283
  },
@@ -1295,13 +1306,11 @@ module.exports = {
1295
1306
  * on the same line as the start of the declaration, provided that there are declarators that
1296
1307
  * follow this one.
1297
1308
  */
1298
- getTokensAndComments(node).forEach((token, index, tokens) => {
1299
- if (index !== 0) {
1300
- offsets.forceSetDesiredOffset(token, tokens[0], variableIndent);
1301
- }
1302
- });
1309
+ const firstToken = sourceCode.getFirstToken(node);
1310
+
1311
+ offsets.setDesiredOffsets(node.range, firstToken, variableIndent, true);
1303
1312
  } else {
1304
- offsets.setDesiredOffsets(getTokensAndComments(node), sourceCode.getFirstToken(node), variableIndent);
1313
+ offsets.setDesiredOffsets(node.range, sourceCode.getFirstToken(node), variableIndent);
1305
1314
  }
1306
1315
  const lastToken = sourceCode.getLastToken(node);
1307
1316
 
@@ -1313,25 +1322,19 @@ module.exports = {
1313
1322
  VariableDeclarator(node) {
1314
1323
  if (node.init) {
1315
1324
  const equalOperator = sourceCode.getTokenBefore(node.init, astUtils.isNotOpeningParenToken);
1316
- const tokensAfterOperator = sourceCode.getTokensAfter(equalOperator, token => token.range[1] <= node.range[1]);
1325
+ const tokenAfterOperator = sourceCode.getTokenAfter(equalOperator);
1317
1326
 
1318
1327
  offsets.ignoreToken(equalOperator);
1319
- offsets.ignoreToken(tokensAfterOperator[0]);
1320
- offsets.setDesiredOffsets(tokensAfterOperator, equalOperator, 1);
1321
- offsets.matchIndentOf(sourceCode.getLastToken(node.id), equalOperator);
1328
+ offsets.ignoreToken(tokenAfterOperator);
1329
+ offsets.setDesiredOffsets([tokenAfterOperator.range[0], node.range[1]], equalOperator, 1);
1330
+ offsets.setDesiredOffset(equalOperator, sourceCode.getLastToken(node.id), 0);
1322
1331
  }
1323
1332
  },
1324
1333
 
1325
- WhileStatement: node => addBlocklessNodeIndent(node.body),
1326
-
1327
- "*:exit": checkForUnknownNode,
1328
-
1329
1334
  "JSXAttribute[value]"(node) {
1330
1335
  const equalsToken = sourceCode.getFirstTokenBetween(node.name, node.value, token => token.type === "Punctuator" && token.value === "=");
1331
- const firstNameToken = sourceCode.getFirstToken(node.name);
1332
1336
 
1333
- offsets.setDesiredOffset(equalsToken, firstNameToken, 1);
1334
- offsets.setDesiredOffset(sourceCode.getFirstToken(node.value), firstNameToken, 1);
1337
+ offsets.setDesiredOffsets([equalsToken.range[0], node.value.range[1]], sourceCode.getFirstToken(node.name), 1);
1335
1338
  },
1336
1339
 
1337
1340
  JSXElement(node) {
@@ -1346,19 +1349,19 @@ module.exports = {
1346
1349
 
1347
1350
  if (node.selfClosing) {
1348
1351
  closingToken = sourceCode.getLastToken(node, { skip: 1 });
1349
- offsets.matchIndentOf(closingToken, sourceCode.getLastToken(node));
1352
+ offsets.setDesiredOffset(sourceCode.getLastToken(node), closingToken, 0);
1350
1353
  } else {
1351
1354
  closingToken = sourceCode.getLastToken(node);
1352
1355
  }
1353
- offsets.setDesiredOffsets(getTokensAndComments(node.name), sourceCode.getFirstToken(node));
1356
+ offsets.setDesiredOffsets(node.name.range, sourceCode.getFirstToken(node));
1354
1357
  addElementListIndent(node.attributes, firstToken, closingToken, 1);
1355
1358
  },
1356
1359
 
1357
1360
  JSXClosingElement(node) {
1358
1361
  const firstToken = sourceCode.getFirstToken(node);
1359
1362
 
1360
- offsets.setDesiredOffsets(getTokensAndComments(node.name), firstToken, 1);
1361
- offsets.matchIndentOf(firstToken, sourceCode.getLastToken(node));
1363
+ offsets.setDesiredOffsets(node.name.range, firstToken, 1);
1364
+ offsets.setDesiredOffset(sourceCode.getLastToken(node), firstToken, 0);
1362
1365
  },
1363
1366
 
1364
1367
  JSXExpressionContainer(node) {
@@ -1366,67 +1369,135 @@ module.exports = {
1366
1369
  const closingCurly = sourceCode.getLastToken(node);
1367
1370
 
1368
1371
  offsets.setDesiredOffsets(
1369
- sourceCode.getTokensBetween(openingCurly, closingCurly, { includeComments: true }),
1372
+ [openingCurly.range[1], closingCurly.range[0]],
1370
1373
  openingCurly,
1371
1374
  1
1372
1375
  );
1373
- offsets.matchIndentOf(openingCurly, closingCurly);
1374
- },
1376
+ offsets.setDesiredOffset(closingCurly, openingCurly, 0);
1377
+ }
1378
+ };
1375
1379
 
1376
- "Program:exit"() {
1377
- addParensIndent(sourceCode.ast.tokens);
1380
+ const listenerCallQueue = [];
1378
1381
 
1379
- /*
1380
- * Create a Map from (tokenOrComment) => (precedingToken).
1381
- * This is necessary because sourceCode.getTokenBefore does not handle a comment as an argument correctly.
1382
- */
1383
- const precedingTokens = sourceCode.ast.comments.reduce((commentMap, comment) => {
1384
- const tokenOrCommentBefore = sourceCode.getTokenBefore(comment, { includeComments: true });
1382
+ /*
1383
+ * To ignore the indentation of a node:
1384
+ * 1. Don't call the node's listener when entering it (if it has a listener)
1385
+ * 2. Call `ignoreNode` on the node sometime after exiting it and before validating offsets.
1386
+ */
1387
+ const offsetListeners = lodash.mapValues(
1388
+ baseOffsetListeners,
1389
+
1390
+ /*
1391
+ * Offset listener calls are deferred until traversal is finished, and are called as
1392
+ * part of the final `Program:exit` listener. This is necessary because a node might
1393
+ * be matched by multiple selectors.
1394
+ *
1395
+ * Example: Suppose there is an offset listener for `Identifier`, and the user has
1396
+ * specified in configuration that `MemberExpression > Identifier` should be ignored.
1397
+ * Due to selector specificity rules, the `Identifier` listener will get called first. However,
1398
+ * if a given Identifier node is supposed to be ignored, then the `Identifier` offset listener
1399
+ * should not have been called at all. Without doing extra selector matching, we don't know
1400
+ * whether the Identifier matches the `MemberExpression > Identifier` selector until the
1401
+ * `MemberExpression > Identifier` listener is called.
1402
+ *
1403
+ * To avoid this, the `Identifier` listener isn't called until traversal finishes and all
1404
+ * ignored nodes are known.
1405
+ */
1406
+ listener =>
1407
+ node =>
1408
+ listenerCallQueue.push({ listener, node })
1409
+ );
1385
1410
 
1386
- return commentMap.set(comment, commentMap.has(tokenOrCommentBefore) ? commentMap.get(tokenOrCommentBefore) : tokenOrCommentBefore);
1387
- }, new WeakMap());
1411
+ // For each ignored node selector, set up a listener to collect it into the `ignoredNodes` set.
1412
+ const ignoredNodes = new Set();
1413
+ const addToIgnoredNodes = ignoredNodes.add.bind(ignoredNodes);
1388
1414
 
1389
- sourceCode.lines.forEach((line, lineIndex) => {
1390
- const lineNumber = lineIndex + 1;
1415
+ const ignoredNodeListeners = options.ignoredNodes.reduce(
1416
+ (listeners, ignoredSelector) => Object.assign(listeners, { [ignoredSelector]: addToIgnoredNodes }),
1417
+ {}
1418
+ );
1391
1419
 
1392
- if (!tokenInfo.firstTokensByLineNumber.has(lineNumber)) {
1420
+ /*
1421
+ * Join the listeners, and add a listener to verify that all tokens actually have the correct indentation
1422
+ * at the end.
1423
+ *
1424
+ * Using Object.assign will cause some offset listeners to be overwritten if the same selector also appears
1425
+ * in `ignoredNodeListeners`. This isn't a problem because all of the matching nodes will be ignored,
1426
+ * so those listeners wouldn't be called anyway.
1427
+ */
1428
+ return Object.assign(
1429
+ offsetListeners,
1430
+ ignoredNodeListeners,
1431
+ {
1432
+ "*:exit"(node) {
1393
1433
 
1394
- // Don't check indentation on blank lines
1395
- return;
1434
+ // If a node's type is nonstandard, we can't tell how its children should be offset, so ignore it.
1435
+ if (!KNOWN_NODES.has(node.type)) {
1436
+ ignoredNodes.add(node);
1396
1437
  }
1438
+ },
1439
+ "Program:exit"() {
1397
1440
 
1398
- const firstTokenOfLine = tokenInfo.firstTokensByLineNumber.get(lineNumber);
1441
+ // Invoke the queued offset listeners for the nodes that aren't ignored.
1442
+ listenerCallQueue
1443
+ .filter(nodeInfo => !ignoredNodes.has(nodeInfo.node))
1444
+ .forEach(nodeInfo => nodeInfo.listener(nodeInfo.node));
1399
1445
 
1400
- if (firstTokenOfLine.loc.start.line !== lineNumber) {
1446
+ // Update the offsets for ignored nodes to prevent their child tokens from being reported.
1447
+ ignoredNodes.forEach(ignoreNode);
1401
1448
 
1402
- // Don't check the indentation of multi-line tokens (e.g. template literals or block comments) twice.
1403
- return;
1404
- }
1449
+ addParensIndent(sourceCode.ast.tokens);
1405
1450
 
1406
- // If the token matches the expected expected indentation, don't report it.
1407
- if (validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine))) {
1408
- return;
1409
- }
1451
+ /*
1452
+ * Create a Map from (tokenOrComment) => (precedingToken).
1453
+ * This is necessary because sourceCode.getTokenBefore does not handle a comment as an argument correctly.
1454
+ */
1455
+ const precedingTokens = sourceCode.ast.comments.reduce((commentMap, comment) => {
1456
+ const tokenOrCommentBefore = sourceCode.getTokenBefore(comment, { includeComments: true });
1457
+
1458
+ return commentMap.set(comment, commentMap.has(tokenOrCommentBefore) ? commentMap.get(tokenOrCommentBefore) : tokenOrCommentBefore);
1459
+ }, new WeakMap());
1410
1460
 
1411
- if (astUtils.isCommentToken(firstTokenOfLine)) {
1412
- const tokenBefore = precedingTokens.get(firstTokenOfLine);
1413
- const tokenAfter = tokenBefore ? sourceCode.getTokenAfter(tokenBefore) : sourceCode.ast.tokens[0];
1461
+ sourceCode.lines.forEach((line, lineIndex) => {
1462
+ const lineNumber = lineIndex + 1;
1414
1463
 
1415
- // If a comment matches the expected indentation of the token immediately before or after, don't report it.
1416
- if (
1417
- tokenBefore && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenBefore)) ||
1418
- tokenAfter && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenAfter))
1419
- ) {
1464
+ if (!tokenInfo.firstTokensByLineNumber.has(lineNumber)) {
1465
+
1466
+ // Don't check indentation on blank lines
1420
1467
  return;
1421
1468
  }
1422
- }
1423
1469
 
1424
- // Otherwise, report the token/comment.
1425
- report(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine));
1426
- });
1427
- }
1470
+ const firstTokenOfLine = tokenInfo.firstTokensByLineNumber.get(lineNumber);
1428
1471
 
1429
- };
1472
+ if (firstTokenOfLine.loc.start.line !== lineNumber) {
1430
1473
 
1474
+ // Don't check the indentation of multi-line tokens (e.g. template literals or block comments) twice.
1475
+ return;
1476
+ }
1477
+
1478
+ // If the token matches the expected expected indentation, don't report it.
1479
+ if (validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine))) {
1480
+ return;
1481
+ }
1482
+
1483
+ if (astUtils.isCommentToken(firstTokenOfLine)) {
1484
+ const tokenBefore = precedingTokens.get(firstTokenOfLine);
1485
+ const tokenAfter = tokenBefore ? sourceCode.getTokenAfter(tokenBefore) : sourceCode.ast.tokens[0];
1486
+
1487
+ // If a comment matches the expected indentation of the token immediately before or after, don't report it.
1488
+ if (
1489
+ tokenBefore && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenBefore)) ||
1490
+ tokenAfter && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenAfter))
1491
+ ) {
1492
+ return;
1493
+ }
1494
+ }
1495
+
1496
+ // Otherwise, report the token/comment.
1497
+ report(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine));
1498
+ });
1499
+ }
1500
+ }
1501
+ );
1431
1502
  }
1432
1503
  };