UNPKG

69.2 kBJavaScriptView Raw
1/**
2 * @fileoverview This rule sets a specific indentation style and width for your code
3 *
4 * @author Teddy Katz
5 * @author Vitaly Puzrin
6 * @author Gyandeep Singh
7 */
8
9"use strict";
10
11//------------------------------------------------------------------------------
12// Requirements
13//------------------------------------------------------------------------------
14
15const lodash = require("lodash");
16const astUtils = require("./utils/ast-utils");
17const createTree = require("functional-red-black-tree");
18
19//------------------------------------------------------------------------------
20// Rule Definition
21//------------------------------------------------------------------------------
22
23const KNOWN_NODES = new Set([
24 "AssignmentExpression",
25 "AssignmentPattern",
26 "ArrayExpression",
27 "ArrayPattern",
28 "ArrowFunctionExpression",
29 "AwaitExpression",
30 "BlockStatement",
31 "BinaryExpression",
32 "BreakStatement",
33 "CallExpression",
34 "CatchClause",
35 "ClassBody",
36 "ClassDeclaration",
37 "ClassExpression",
38 "ConditionalExpression",
39 "ContinueStatement",
40 "DoWhileStatement",
41 "DebuggerStatement",
42 "EmptyStatement",
43 "ExperimentalRestProperty",
44 "ExperimentalSpreadProperty",
45 "ExpressionStatement",
46 "ForStatement",
47 "ForInStatement",
48 "ForOfStatement",
49 "FunctionDeclaration",
50 "FunctionExpression",
51 "Identifier",
52 "IfStatement",
53 "Literal",
54 "LabeledStatement",
55 "LogicalExpression",
56 "MemberExpression",
57 "MetaProperty",
58 "MethodDefinition",
59 "NewExpression",
60 "ObjectExpression",
61 "ObjectPattern",
62 "Program",
63 "Property",
64 "RestElement",
65 "ReturnStatement",
66 "SequenceExpression",
67 "SpreadElement",
68 "Super",
69 "SwitchCase",
70 "SwitchStatement",
71 "TaggedTemplateExpression",
72 "TemplateElement",
73 "TemplateLiteral",
74 "ThisExpression",
75 "ThrowStatement",
76 "TryStatement",
77 "UnaryExpression",
78 "UpdateExpression",
79 "VariableDeclaration",
80 "VariableDeclarator",
81 "WhileStatement",
82 "WithStatement",
83 "YieldExpression",
84 "JSXIdentifier",
85 "JSXNamespacedName",
86 "JSXMemberExpression",
87 "JSXEmptyExpression",
88 "JSXExpressionContainer",
89 "JSXElement",
90 "JSXClosingElement",
91 "JSXOpeningElement",
92 "JSXAttribute",
93 "JSXSpreadAttribute",
94 "JSXText",
95 "ExportDefaultDeclaration",
96 "ExportNamedDeclaration",
97 "ExportAllDeclaration",
98 "ExportSpecifier",
99 "ImportDeclaration",
100 "ImportSpecifier",
101 "ImportDefaultSpecifier",
102 "ImportNamespaceSpecifier"
103]);
104
105/*
106 * General rule strategy:
107 * 1. An OffsetStorage instance stores a map of desired offsets, where each token has a specified offset from another
108 * specified token or to the first column.
109 * 2. As the AST is traversed, modify the desired offsets of tokens accordingly. For example, when entering a
110 * BlockStatement, offset all of the tokens in the BlockStatement by 1 indent level from the opening curly
111 * brace of the BlockStatement.
112 * 3. After traversing the AST, calculate the expected indentation levels of every token according to the
113 * OffsetStorage container.
114 * 4. For each line, compare the expected indentation of the first token to the actual indentation in the file,
115 * and report the token if the two values are not equal.
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 */
124class 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
181/**
182 * A helper class to get token-based info related to indentation
183 */
184class TokenInfo {
185
186 /**
187 * @param {SourceCode} sourceCode A SourceCode object
188 */
189 constructor(sourceCode) {
190 this.sourceCode = sourceCode;
191 this.firstTokensByLineNumber = sourceCode.tokensAndComments.reduce((map, token) => {
192 if (!map.has(token.loc.start.line)) {
193 map.set(token.loc.start.line, token);
194 }
195 if (!map.has(token.loc.end.line) && sourceCode.text.slice(token.range[1] - token.loc.end.column, token.range[1]).trim()) {
196 map.set(token.loc.end.line, token);
197 }
198 return map;
199 }, new Map());
200 }
201
202 /**
203 * Gets the first token on a given token's line
204 * @param {Token|ASTNode} token a node or token
205 * @returns {Token} The first token on the given line
206 */
207 getFirstTokenOfLine(token) {
208 return this.firstTokensByLineNumber.get(token.loc.start.line);
209 }
210
211 /**
212 * Determines whether a token is the first token in its line
213 * @param {Token} token The token
214 * @returns {boolean} `true` if the token is the first on its line
215 */
216 isFirstTokenOfLine(token) {
217 return this.getFirstTokenOfLine(token) === token;
218 }
219
220 /**
221 * Get the actual indent of a token
222 * @param {Token} token Token to examine. This should be the first token on its line.
223 * @returns {string} The indentation characters that precede the token
224 */
225 getTokenIndent(token) {
226 return this.sourceCode.text.slice(token.range[0] - token.loc.start.column, token.range[0]);
227 }
228}
229
230/**
231 * A class to store information on desired offsets of tokens from each other
232 */
233class OffsetStorage {
234
235 /**
236 * @param {TokenInfo} tokenInfo a TokenInfo instance
237 * @param {number} indentSize The desired size of each indentation level
238 * @param {string} indentType The indentation character
239 */
240 constructor(tokenInfo, indentSize, indentType) {
241 this._tokenInfo = tokenInfo;
242 this._indentSize = indentSize;
243 this._indentType = indentType;
244
245 this._tree = new BinarySearchTree();
246 this._tree.insert(0, { offset: 0, from: null, force: false });
247
248 this._lockedFirstTokens = new WeakMap();
249 this._desiredIndentCache = new WeakMap();
250 this._ignoredTokens = new WeakSet();
251 }
252
253 _getOffsetDescriptor(token) {
254 return this._tree.findLe(token.range[0]).value;
255 }
256
257 /**
258 * Sets the offset column of token B to match the offset column of token A.
259 * **WARNING**: This matches a *column*, even if baseToken is not the first token on its line. In
260 * most cases, `setDesiredOffset` should be used instead.
261 * @param {Token} baseToken The first token
262 * @param {Token} offsetToken The second token, whose offset should be matched to the first token
263 * @returns {void}
264 */
265 matchOffsetOf(baseToken, offsetToken) {
266
267 /*
268 * lockedFirstTokens is a map from a token whose indentation is controlled by the "first" option to
269 * the token that it depends on. For example, with the `ArrayExpression: first` option, the first
270 * token of each element in the array after the first will be mapped to the first token of the first
271 * element. The desired indentation of each of these tokens is computed based on the desired indentation
272 * of the "first" element, rather than through the normal offset mechanism.
273 */
274 this._lockedFirstTokens.set(offsetToken, baseToken);
275 }
276
277 /**
278 * Sets the desired offset of a token.
279 *
280 * This uses a line-based offset collapsing behavior to handle tokens on the same line.
281 * For example, consider the following two cases:
282 *
283 * (
284 * [
285 * bar
286 * ]
287 * )
288 *
289 * ([
290 * bar
291 * ])
292 *
293 * Based on the first case, it's clear that the `bar` token needs to have an offset of 1 indent level (4 spaces) from
294 * the `[` token, and the `[` token has to have an offset of 1 indent level from the `(` token. Since the `(` token is
295 * the first on its line (with an indent of 0 spaces), the `bar` token needs to be offset by 2 indent levels (8 spaces)
296 * from the start of its line.
297 *
298 * However, in the second case `bar` should only be indented by 4 spaces. This is because the offset of 1 indent level
299 * between the `(` and the `[` tokens gets "collapsed" because the two tokens are on the same line. As a result, the
300 * `(` token is mapped to the `[` token with an offset of 0, and the rule correctly decides that `bar` should be indented
301 * by 1 indent level from the start of the line.
302 *
303 * This is useful because rule listeners can usually just call `setDesiredOffset` for all the tokens in the node,
304 * without needing to check which lines those tokens are on.
305 *
306 * Note that since collapsing only occurs when two tokens are on the same line, there are a few cases where non-intuitive
307 * behavior can occur. For example, consider the following cases:
308 *
309 * foo(
310 * ).
311 * bar(
312 * baz
313 * )
314 *
315 * foo(
316 * ).bar(
317 * baz
318 * )
319 *
320 * Based on the first example, it would seem that `bar` should be offset by 1 indent level from `foo`, and `baz`
321 * should be offset by 1 indent level from `bar`. However, this is not correct, because it would result in `baz`
322 * being indented by 2 indent levels in the second case (since `foo`, `bar`, and `baz` are all on separate lines, no
323 * collapsing would occur).
324 *
325 * Instead, the correct way would be to offset `baz` by 1 level from `bar`, offset `bar` by 1 level from the `)`, and
326 * offset the `)` by 0 levels from `foo`. This ensures that the offset between `bar` and the `)` are correctly collapsed
327 * in the second case.
328 *
329 * @param {Token} token The token
330 * @param {Token} fromToken The token that `token` should be offset from
331 * @param {number} offset The desired indent level
332 * @returns {void}
333 */
334 setDesiredOffset(token, fromToken, offset) {
335 return this.setDesiredOffsets(token.range, fromToken, offset);
336 }
337
338 /**
339 * Sets the desired offset of all tokens in a range
340 * It's common for node listeners in this file to need to apply the same offset to a large, contiguous range of tokens.
341 * Moreover, the offset of any given token is usually updated multiple times (roughly once for each node that contains
342 * it). This means that the offset of each token is updated O(AST depth) times.
343 * It would not be performant to store and update the offsets for each token independently, because the rule would end
344 * up having a time complexity of O(number of tokens * AST depth), which is quite slow for large files.
345 *
346 * Instead, the offset tree is represented as a collection of contiguous offset ranges in a file. For example, the following
347 * list could represent the state of the offset tree at a given point:
348 *
349 * * Tokens starting in the interval [0, 15) are aligned with the beginning of the file
350 * * Tokens starting in the interval [15, 30) are offset by 1 indent level from the `bar` token
351 * * Tokens starting in the interval [30, 43) are offset by 1 indent level from the `foo` token
352 * * Tokens starting in the interval [43, 820) are offset by 2 indent levels from the `bar` token
353 * * Tokens starting in the interval [820, ∞) are offset by 1 indent level from the `baz` token
354 *
355 * The `setDesiredOffsets` methods inserts ranges like the ones above. The third line above would be inserted by using:
356 * `setDesiredOffsets([30, 43], fooToken, 1);`
357 *
358 * @param {[number, number]} range A [start, end] pair. All tokens with range[0] <= token.start < range[1] will have the offset applied.
359 * @param {Token} fromToken The token that this is offset from
360 * @param {number} offset The desired indent level
361 * @param {boolean} force `true` if this offset should not use the normal collapsing behavior. This should almost always be false.
362 * @returns {void}
363 */
364 setDesiredOffsets(range, fromToken, offset, force) {
365
366 /*
367 * Offset ranges are stored as a collection of nodes, where each node maps a numeric key to an offset
368 * descriptor. The tree for the example above would have the following nodes:
369 *
370 * * key: 0, value: { offset: 0, from: null }
371 * * key: 15, value: { offset: 1, from: barToken }
372 * * key: 30, value: { offset: 1, from: fooToken }
373 * * key: 43, value: { offset: 2, from: barToken }
374 * * key: 820, value: { offset: 1, from: bazToken }
375 *
376 * To find the offset descriptor for any given token, one needs to find the node with the largest key
377 * which is <= token.start. To make this operation fast, the nodes are stored in a balanced binary
378 * search tree indexed by key.
379 */
380
381 const descriptorToInsert = { offset, from: fromToken, force };
382
383 const descriptorAfterRange = this._tree.findLe(range[1]).value;
384
385 const fromTokenIsInRange = fromToken && fromToken.range[0] >= range[0] && fromToken.range[1] <= range[1];
386 const fromTokenDescriptor = fromTokenIsInRange && this._getOffsetDescriptor(fromToken);
387
388 // First, remove any existing nodes in the range from the tree.
389 this._tree.deleteRange(range[0] + 1, range[1]);
390
391 // Insert a new node into the tree for this range
392 this._tree.insert(range[0], descriptorToInsert);
393
394 /*
395 * To avoid circular offset dependencies, keep the `fromToken` token mapped to whatever it was mapped to previously,
396 * even if it's in the current range.
397 */
398 if (fromTokenIsInRange) {
399 this._tree.insert(fromToken.range[0], fromTokenDescriptor);
400 this._tree.insert(fromToken.range[1], descriptorToInsert);
401 }
402
403 /*
404 * To avoid modifying the offset of tokens after the range, insert another node to keep the offset of the following
405 * tokens the same as it was before.
406 */
407 this._tree.insert(range[1], descriptorAfterRange);
408 }
409
410 /**
411 * Gets the desired indent of a token
412 * @param {Token} token The token
413 * @returns {string} The desired indent of the token
414 */
415 getDesiredIndent(token) {
416 if (!this._desiredIndentCache.has(token)) {
417
418 if (this._ignoredTokens.has(token)) {
419
420 /*
421 * If the token is ignored, use the actual indent of the token as the desired indent.
422 * This ensures that no errors are reported for this token.
423 */
424 this._desiredIndentCache.set(
425 token,
426 this._tokenInfo.getTokenIndent(token)
427 );
428 } else if (this._lockedFirstTokens.has(token)) {
429 const firstToken = this._lockedFirstTokens.get(token);
430
431 this._desiredIndentCache.set(
432 token,
433
434 // (indentation for the first element's line)
435 this.getDesiredIndent(this._tokenInfo.getFirstTokenOfLine(firstToken)) +
436
437 // (space between the start of the first element's line and the first element)
438 this._indentType.repeat(firstToken.loc.start.column - this._tokenInfo.getFirstTokenOfLine(firstToken).loc.start.column)
439 );
440 } else {
441 const offsetInfo = this._getOffsetDescriptor(token);
442 const offset = (
443 offsetInfo.from &&
444 offsetInfo.from.loc.start.line === token.loc.start.line &&
445 !/^\s*?\n/u.test(token.value) &&
446 !offsetInfo.force
447 ) ? 0 : offsetInfo.offset * this._indentSize;
448
449 this._desiredIndentCache.set(
450 token,
451 (offsetInfo.from ? this.getDesiredIndent(offsetInfo.from) : "") + this._indentType.repeat(offset)
452 );
453 }
454 }
455 return this._desiredIndentCache.get(token);
456 }
457
458 /**
459 * Ignores a token, preventing it from being reported.
460 * @param {Token} token The token
461 * @returns {void}
462 */
463 ignoreToken(token) {
464 if (this._tokenInfo.isFirstTokenOfLine(token)) {
465 this._ignoredTokens.add(token);
466 }
467 }
468
469 /**
470 * Gets the first token that the given token's indentation is dependent on
471 * @param {Token} token The token
472 * @returns {Token} The token that the given token depends on, or `null` if the given token is at the top level
473 */
474 getFirstDependency(token) {
475 return this._getOffsetDescriptor(token).from;
476 }
477}
478
479const ELEMENT_LIST_SCHEMA = {
480 oneOf: [
481 {
482 type: "integer",
483 minimum: 0
484 },
485 {
486 enum: ["first", "off"]
487 }
488 ]
489};
490
491module.exports = {
492 meta: {
493 type: "layout",
494
495 docs: {
496 description: "enforce consistent indentation",
497 category: "Stylistic Issues",
498 recommended: false,
499 url: "https://eslint.org/docs/rules/indent"
500 },
501
502 fixable: "whitespace",
503
504 schema: [
505 {
506 oneOf: [
507 {
508 enum: ["tab"]
509 },
510 {
511 type: "integer",
512 minimum: 0
513 }
514 ]
515 },
516 {
517 type: "object",
518 properties: {
519 SwitchCase: {
520 type: "integer",
521 minimum: 0,
522 default: 0
523 },
524 VariableDeclarator: {
525 oneOf: [
526 ELEMENT_LIST_SCHEMA,
527 {
528 type: "object",
529 properties: {
530 var: ELEMENT_LIST_SCHEMA,
531 let: ELEMENT_LIST_SCHEMA,
532 const: ELEMENT_LIST_SCHEMA
533 },
534 additionalProperties: false
535 }
536 ]
537 },
538 outerIIFEBody: {
539 type: "integer",
540 minimum: 0
541 },
542 MemberExpression: {
543 oneOf: [
544 {
545 type: "integer",
546 minimum: 0
547 },
548 {
549 enum: ["off"]
550 }
551 ]
552 },
553 FunctionDeclaration: {
554 type: "object",
555 properties: {
556 parameters: ELEMENT_LIST_SCHEMA,
557 body: {
558 type: "integer",
559 minimum: 0
560 }
561 },
562 additionalProperties: false
563 },
564 FunctionExpression: {
565 type: "object",
566 properties: {
567 parameters: ELEMENT_LIST_SCHEMA,
568 body: {
569 type: "integer",
570 minimum: 0
571 }
572 },
573 additionalProperties: false
574 },
575 CallExpression: {
576 type: "object",
577 properties: {
578 arguments: ELEMENT_LIST_SCHEMA
579 },
580 additionalProperties: false
581 },
582 ArrayExpression: ELEMENT_LIST_SCHEMA,
583 ObjectExpression: ELEMENT_LIST_SCHEMA,
584 ImportDeclaration: ELEMENT_LIST_SCHEMA,
585 flatTernaryExpressions: {
586 type: "boolean",
587 default: false
588 },
589 ignoredNodes: {
590 type: "array",
591 items: {
592 type: "string",
593 not: {
594 pattern: ":exit$"
595 }
596 }
597 },
598 ignoreComments: {
599 type: "boolean",
600 default: false
601 }
602 },
603 additionalProperties: false
604 }
605 ],
606 messages: {
607 wrongIndentation: "Expected indentation of {{expected}} but found {{actual}}."
608 }
609 },
610
611 create(context) {
612 const DEFAULT_VARIABLE_INDENT = 1;
613 const DEFAULT_PARAMETER_INDENT = 1;
614 const DEFAULT_FUNCTION_BODY_INDENT = 1;
615
616 let indentType = "space";
617 let indentSize = 4;
618 const options = {
619 SwitchCase: 0,
620 VariableDeclarator: {
621 var: DEFAULT_VARIABLE_INDENT,
622 let: DEFAULT_VARIABLE_INDENT,
623 const: DEFAULT_VARIABLE_INDENT
624 },
625 outerIIFEBody: 1,
626 FunctionDeclaration: {
627 parameters: DEFAULT_PARAMETER_INDENT,
628 body: DEFAULT_FUNCTION_BODY_INDENT
629 },
630 FunctionExpression: {
631 parameters: DEFAULT_PARAMETER_INDENT,
632 body: DEFAULT_FUNCTION_BODY_INDENT
633 },
634 CallExpression: {
635 arguments: DEFAULT_PARAMETER_INDENT
636 },
637 MemberExpression: 1,
638 ArrayExpression: 1,
639 ObjectExpression: 1,
640 ImportDeclaration: 1,
641 flatTernaryExpressions: false,
642 ignoredNodes: [],
643 ignoreComments: false
644 };
645
646 if (context.options.length) {
647 if (context.options[0] === "tab") {
648 indentSize = 1;
649 indentType = "tab";
650 } else {
651 indentSize = context.options[0];
652 indentType = "space";
653 }
654
655 if (context.options[1]) {
656 Object.assign(options, context.options[1]);
657
658 if (typeof options.VariableDeclarator === "number" || options.VariableDeclarator === "first") {
659 options.VariableDeclarator = {
660 var: options.VariableDeclarator,
661 let: options.VariableDeclarator,
662 const: options.VariableDeclarator
663 };
664 }
665 }
666 }
667
668 const sourceCode = context.getSourceCode();
669 const tokenInfo = new TokenInfo(sourceCode);
670 const offsets = new OffsetStorage(tokenInfo, indentSize, indentType === "space" ? " " : "\t");
671 const parameterParens = new WeakSet();
672
673 /**
674 * Creates an error message for a line, given the expected/actual indentation.
675 * @param {int} expectedAmount The expected amount of indentation characters for this line
676 * @param {int} actualSpaces The actual number of indentation spaces that were found on this line
677 * @param {int} actualTabs The actual number of indentation tabs that were found on this line
678 * @returns {string} An error message for this line
679 */
680 function createErrorMessageData(expectedAmount, actualSpaces, actualTabs) {
681 const expectedStatement = `${expectedAmount} ${indentType}${expectedAmount === 1 ? "" : "s"}`; // e.g. "2 tabs"
682 const foundSpacesWord = `space${actualSpaces === 1 ? "" : "s"}`; // e.g. "space"
683 const foundTabsWord = `tab${actualTabs === 1 ? "" : "s"}`; // e.g. "tabs"
684 let foundStatement;
685
686 if (actualSpaces > 0) {
687
688 /*
689 * Abbreviate the message if the expected indentation is also spaces.
690 * e.g. 'Expected 4 spaces but found 2' rather than 'Expected 4 spaces but found 2 spaces'
691 */
692 foundStatement = indentType === "space" ? actualSpaces : `${actualSpaces} ${foundSpacesWord}`;
693 } else if (actualTabs > 0) {
694 foundStatement = indentType === "tab" ? actualTabs : `${actualTabs} ${foundTabsWord}`;
695 } else {
696 foundStatement = "0";
697 }
698 return {
699 expected: expectedStatement,
700 actual: foundStatement
701 };
702 }
703
704 /**
705 * Reports a given indent violation
706 * @param {Token} token Token violating the indent rule
707 * @param {string} neededIndent Expected indentation string
708 * @returns {void}
709 */
710 function report(token, neededIndent) {
711 const actualIndent = Array.from(tokenInfo.getTokenIndent(token));
712 const numSpaces = actualIndent.filter(char => char === " ").length;
713 const numTabs = actualIndent.filter(char => char === "\t").length;
714
715 context.report({
716 node: token,
717 messageId: "wrongIndentation",
718 data: createErrorMessageData(neededIndent.length, numSpaces, numTabs),
719 loc: {
720 start: { line: token.loc.start.line, column: 0 },
721 end: { line: token.loc.start.line, column: token.loc.start.column }
722 },
723 fix(fixer) {
724 const range = [token.range[0] - token.loc.start.column, token.range[0]];
725 const newText = neededIndent;
726
727 return fixer.replaceTextRange(range, newText);
728 }
729 });
730 }
731
732 /**
733 * Checks if a token's indentation is correct
734 * @param {Token} token Token to examine
735 * @param {string} desiredIndent Desired indentation of the string
736 * @returns {boolean} `true` if the token's indentation is correct
737 */
738 function validateTokenIndent(token, desiredIndent) {
739 const indentation = tokenInfo.getTokenIndent(token);
740
741 return indentation === desiredIndent ||
742
743 // To avoid conflicts with no-mixed-spaces-and-tabs, don't report mixed spaces and tabs.
744 indentation.includes(" ") && indentation.includes("\t");
745 }
746
747 /**
748 * Check to see if the node is a file level IIFE
749 * @param {ASTNode} node The function node to check.
750 * @returns {boolean} True if the node is the outer IIFE
751 */
752 function isOuterIIFE(node) {
753
754 /*
755 * Verify that the node is an IIFE
756 */
757 if (!node.parent || node.parent.type !== "CallExpression" || node.parent.callee !== node) {
758 return false;
759 }
760
761 /*
762 * Navigate legal ancestors to determine whether this IIFE is outer.
763 * A "legal ancestor" is an expression or statement that causes the function to get executed immediately.
764 * For example, `!(function(){})()` is an outer IIFE even though it is preceded by a ! operator.
765 */
766 let statement = node.parent && node.parent.parent;
767
768 while (
769 statement.type === "UnaryExpression" && ["!", "~", "+", "-"].indexOf(statement.operator) > -1 ||
770 statement.type === "AssignmentExpression" ||
771 statement.type === "LogicalExpression" ||
772 statement.type === "SequenceExpression" ||
773 statement.type === "VariableDeclarator"
774 ) {
775 statement = statement.parent;
776 }
777
778 return (statement.type === "ExpressionStatement" || statement.type === "VariableDeclaration") && statement.parent.type === "Program";
779 }
780
781 /**
782 * Counts the number of linebreaks that follow the last non-whitespace character in a string
783 * @param {string} string The string to check
784 * @returns {number} The number of JavaScript linebreaks that follow the last non-whitespace character,
785 * or the total number of linebreaks if the string is all whitespace.
786 */
787 function countTrailingLinebreaks(string) {
788 const trailingWhitespace = string.match(/\s*$/u)[0];
789 const linebreakMatches = trailingWhitespace.match(astUtils.createGlobalLinebreakMatcher());
790
791 return linebreakMatches === null ? 0 : linebreakMatches.length;
792 }
793
794 /**
795 * Check indentation for lists of elements (arrays, objects, function params)
796 * @param {ASTNode[]} elements List of elements that should be offset
797 * @param {Token} startToken The start token of the list that element should be aligned against, e.g. '['
798 * @param {Token} endToken The end token of the list, e.g. ']'
799 * @param {number|string} offset The amount that the elements should be offset
800 * @returns {void}
801 */
802 function addElementListIndent(elements, startToken, endToken, offset) {
803
804 /**
805 * Gets the first token of a given element, including surrounding parentheses.
806 * @param {ASTNode} element A node in the `elements` list
807 * @returns {Token} The first token of this element
808 */
809 function getFirstToken(element) {
810 let token = sourceCode.getTokenBefore(element);
811
812 while (astUtils.isOpeningParenToken(token) && token !== startToken) {
813 token = sourceCode.getTokenBefore(token);
814 }
815 return sourceCode.getTokenAfter(token);
816 }
817
818 // Run through all the tokens in the list, and offset them by one indent level (mainly for comments, other things will end up overridden)
819 offsets.setDesiredOffsets(
820 [startToken.range[1], endToken.range[0]],
821 startToken,
822 typeof offset === "number" ? offset : 1
823 );
824 offsets.setDesiredOffset(endToken, startToken, 0);
825
826 // If the preference is "first" but there is no first element (e.g. sparse arrays w/ empty first slot), fall back to 1 level.
827 if (offset === "first" && elements.length && !elements[0]) {
828 return;
829 }
830 elements.forEach((element, index) => {
831 if (!element) {
832
833 // Skip holes in arrays
834 return;
835 }
836 if (offset === "off") {
837
838 // Ignore the first token of every element if the "off" option is used
839 offsets.ignoreToken(getFirstToken(element));
840 }
841
842 // Offset the following elements correctly relative to the first element
843 if (index === 0) {
844 return;
845 }
846 if (offset === "first" && tokenInfo.isFirstTokenOfLine(getFirstToken(element))) {
847 offsets.matchOffsetOf(getFirstToken(elements[0]), getFirstToken(element));
848 } else {
849 const previousElement = elements[index - 1];
850 const firstTokenOfPreviousElement = previousElement && getFirstToken(previousElement);
851 const previousElementLastToken = previousElement && sourceCode.getLastToken(previousElement);
852
853 if (
854 previousElement &&
855 previousElementLastToken.loc.end.line - countTrailingLinebreaks(previousElementLastToken.value) > startToken.loc.end.line
856 ) {
857 offsets.setDesiredOffsets(
858 [previousElement.range[1], element.range[1]],
859 firstTokenOfPreviousElement,
860 0
861 );
862 }
863 }
864 });
865 }
866
867 /**
868 * Check and decide whether to check for indentation for blockless nodes
869 * Scenarios are for or while statements without braces around them
870 * @param {ASTNode} node node to examine
871 * @returns {void}
872 */
873 function addBlocklessNodeIndent(node) {
874 if (node.type !== "BlockStatement") {
875 const lastParentToken = sourceCode.getTokenBefore(node, astUtils.isNotOpeningParenToken);
876
877 let firstBodyToken = sourceCode.getFirstToken(node);
878 let lastBodyToken = sourceCode.getLastToken(node);
879
880 while (
881 astUtils.isOpeningParenToken(sourceCode.getTokenBefore(firstBodyToken)) &&
882 astUtils.isClosingParenToken(sourceCode.getTokenAfter(lastBodyToken))
883 ) {
884 firstBodyToken = sourceCode.getTokenBefore(firstBodyToken);
885 lastBodyToken = sourceCode.getTokenAfter(lastBodyToken);
886 }
887
888 offsets.setDesiredOffsets([firstBodyToken.range[0], lastBodyToken.range[1]], lastParentToken, 1);
889
890 /*
891 * For blockless nodes with semicolon-first style, don't indent the semicolon.
892 * e.g.
893 * if (foo) bar()
894 * ; [1, 2, 3].map(foo)
895 */
896 const lastToken = sourceCode.getLastToken(node);
897
898 if (node.type !== "EmptyStatement" && astUtils.isSemicolonToken(lastToken)) {
899 offsets.setDesiredOffset(lastToken, lastParentToken, 0);
900 }
901 }
902 }
903
904 /**
905 * Checks the indentation for nodes that are like function calls (`CallExpression` and `NewExpression`)
906 * @param {ASTNode} node A CallExpression or NewExpression node
907 * @returns {void}
908 */
909 function addFunctionCallIndent(node) {
910 let openingParen;
911
912 if (node.arguments.length) {
913 openingParen = sourceCode.getFirstTokenBetween(node.callee, node.arguments[0], astUtils.isOpeningParenToken);
914 } else {
915 openingParen = sourceCode.getLastToken(node, 1);
916 }
917 const closingParen = sourceCode.getLastToken(node);
918
919 parameterParens.add(openingParen);
920 parameterParens.add(closingParen);
921 offsets.setDesiredOffset(openingParen, sourceCode.getTokenBefore(openingParen), 0);
922
923 addElementListIndent(node.arguments, openingParen, closingParen, options.CallExpression.arguments);
924 }
925
926 /**
927 * Checks the indentation of parenthesized values, given a list of tokens in a program
928 * @param {Token[]} tokens A list of tokens
929 * @returns {void}
930 */
931 function addParensIndent(tokens) {
932 const parenStack = [];
933 const parenPairs = [];
934
935 tokens.forEach(nextToken => {
936
937 // Accumulate a list of parenthesis pairs
938 if (astUtils.isOpeningParenToken(nextToken)) {
939 parenStack.push(nextToken);
940 } else if (astUtils.isClosingParenToken(nextToken)) {
941 parenPairs.unshift({ left: parenStack.pop(), right: nextToken });
942 }
943 });
944
945 parenPairs.forEach(pair => {
946 const leftParen = pair.left;
947 const rightParen = pair.right;
948
949 // We only want to handle parens around expressions, so exclude parentheses that are in function parameters and function call arguments.
950 if (!parameterParens.has(leftParen) && !parameterParens.has(rightParen)) {
951 const parenthesizedTokens = new Set(sourceCode.getTokensBetween(leftParen, rightParen));
952
953 parenthesizedTokens.forEach(token => {
954 if (!parenthesizedTokens.has(offsets.getFirstDependency(token))) {
955 offsets.setDesiredOffset(token, leftParen, 1);
956 }
957 });
958 }
959
960 offsets.setDesiredOffset(rightParen, leftParen, 0);
961 });
962 }
963
964 /**
965 * Ignore all tokens within an unknown node whose offset do not depend
966 * on another token's offset within the unknown node
967 * @param {ASTNode} node Unknown Node
968 * @returns {void}
969 */
970 function ignoreNode(node) {
971 const unknownNodeTokens = new Set(sourceCode.getTokens(node, { includeComments: true }));
972
973 unknownNodeTokens.forEach(token => {
974 if (!unknownNodeTokens.has(offsets.getFirstDependency(token))) {
975 const firstTokenOfLine = tokenInfo.getFirstTokenOfLine(token);
976
977 if (token === firstTokenOfLine) {
978 offsets.ignoreToken(token);
979 } else {
980 offsets.setDesiredOffset(token, firstTokenOfLine, 0);
981 }
982 }
983 });
984 }
985
986 /**
987 * Check whether the given token is on the first line of a statement.
988 * @param {Token} token The token to check.
989 * @param {ASTNode} leafNode The expression node that the token belongs directly.
990 * @returns {boolean} `true` if the token is on the first line of a statement.
991 */
992 function isOnFirstLineOfStatement(token, leafNode) {
993 let node = leafNode;
994
995 while (node.parent && !node.parent.type.endsWith("Statement") && !node.parent.type.endsWith("Declaration")) {
996 node = node.parent;
997 }
998 node = node.parent;
999
1000 return !node || node.loc.start.line === token.loc.start.line;
1001 }
1002
1003 /**
1004 * Check whether there are any blank (whitespace-only) lines between
1005 * two tokens on separate lines.
1006 * @param {Token} firstToken The first token.
1007 * @param {Token} secondToken The second token.
1008 * @returns {boolean} `true` if the tokens are on separate lines and
1009 * there exists a blank line between them, `false` otherwise.
1010 */
1011 function hasBlankLinesBetween(firstToken, secondToken) {
1012 const firstTokenLine = firstToken.loc.end.line;
1013 const secondTokenLine = secondToken.loc.start.line;
1014
1015 if (firstTokenLine === secondTokenLine || firstTokenLine === secondTokenLine - 1) {
1016 return false;
1017 }
1018
1019 for (let line = firstTokenLine + 1; line < secondTokenLine; ++line) {
1020 if (!tokenInfo.firstTokensByLineNumber.has(line)) {
1021 return true;
1022 }
1023 }
1024
1025 return false;
1026 }
1027
1028 const ignoredNodeFirstTokens = new Set();
1029
1030 const baseOffsetListeners = {
1031 "ArrayExpression, ArrayPattern"(node) {
1032 const openingBracket = sourceCode.getFirstToken(node);
1033 const closingBracket = sourceCode.getTokenAfter(lodash.findLast(node.elements) || openingBracket, astUtils.isClosingBracketToken);
1034
1035 addElementListIndent(node.elements, openingBracket, closingBracket, options.ArrayExpression);
1036 },
1037
1038 "ObjectExpression, ObjectPattern"(node) {
1039 const openingCurly = sourceCode.getFirstToken(node);
1040 const closingCurly = sourceCode.getTokenAfter(
1041 node.properties.length ? node.properties[node.properties.length - 1] : openingCurly,
1042 astUtils.isClosingBraceToken
1043 );
1044
1045 addElementListIndent(node.properties, openingCurly, closingCurly, options.ObjectExpression);
1046 },
1047
1048 ArrowFunctionExpression(node) {
1049 const firstToken = sourceCode.getFirstToken(node);
1050
1051 if (astUtils.isOpeningParenToken(firstToken)) {
1052 const openingParen = firstToken;
1053 const closingParen = sourceCode.getTokenBefore(node.body, astUtils.isClosingParenToken);
1054
1055 parameterParens.add(openingParen);
1056 parameterParens.add(closingParen);
1057 addElementListIndent(node.params, openingParen, closingParen, options.FunctionExpression.parameters);
1058 }
1059 addBlocklessNodeIndent(node.body);
1060 },
1061
1062 AssignmentExpression(node) {
1063 const operator = sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
1064
1065 offsets.setDesiredOffsets([operator.range[0], node.range[1]], sourceCode.getLastToken(node.left), 1);
1066 offsets.ignoreToken(operator);
1067 offsets.ignoreToken(sourceCode.getTokenAfter(operator));
1068 },
1069
1070 "BinaryExpression, LogicalExpression"(node) {
1071 const operator = sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
1072
1073 /*
1074 * For backwards compatibility, don't check BinaryExpression indents, e.g.
1075 * var foo = bar &&
1076 * baz;
1077 */
1078
1079 const tokenAfterOperator = sourceCode.getTokenAfter(operator);
1080
1081 offsets.ignoreToken(operator);
1082 offsets.ignoreToken(tokenAfterOperator);
1083 offsets.setDesiredOffset(tokenAfterOperator, operator, 0);
1084 },
1085
1086 "BlockStatement, ClassBody"(node) {
1087
1088 let blockIndentLevel;
1089
1090 if (node.parent && isOuterIIFE(node.parent)) {
1091 blockIndentLevel = options.outerIIFEBody;
1092 } else if (node.parent && (node.parent.type === "FunctionExpression" || node.parent.type === "ArrowFunctionExpression")) {
1093 blockIndentLevel = options.FunctionExpression.body;
1094 } else if (node.parent && node.parent.type === "FunctionDeclaration") {
1095 blockIndentLevel = options.FunctionDeclaration.body;
1096 } else {
1097 blockIndentLevel = 1;
1098 }
1099
1100 /*
1101 * For blocks that aren't lone statements, ensure that the opening curly brace
1102 * is aligned with the parent.
1103 */
1104 if (!astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type)) {
1105 offsets.setDesiredOffset(sourceCode.getFirstToken(node), sourceCode.getFirstToken(node.parent), 0);
1106 }
1107 addElementListIndent(node.body, sourceCode.getFirstToken(node), sourceCode.getLastToken(node), blockIndentLevel);
1108 },
1109
1110 CallExpression: addFunctionCallIndent,
1111
1112
1113 "ClassDeclaration[superClass], ClassExpression[superClass]"(node) {
1114 const classToken = sourceCode.getFirstToken(node);
1115 const extendsToken = sourceCode.getTokenBefore(node.superClass, astUtils.isNotOpeningParenToken);
1116
1117 offsets.setDesiredOffsets([extendsToken.range[0], node.body.range[0]], classToken, 1);
1118 },
1119
1120 ConditionalExpression(node) {
1121 const firstToken = sourceCode.getFirstToken(node);
1122
1123 // `flatTernaryExpressions` option is for the following style:
1124 // var a =
1125 // foo > 0 ? bar :
1126 // foo < 0 ? baz :
1127 // /*else*/ qiz ;
1128 if (!options.flatTernaryExpressions ||
1129 !astUtils.isTokenOnSameLine(node.test, node.consequent) ||
1130 isOnFirstLineOfStatement(firstToken, node)
1131 ) {
1132 const questionMarkToken = sourceCode.getFirstTokenBetween(node.test, node.consequent, token => token.type === "Punctuator" && token.value === "?");
1133 const colonToken = sourceCode.getFirstTokenBetween(node.consequent, node.alternate, token => token.type === "Punctuator" && token.value === ":");
1134
1135 const firstConsequentToken = sourceCode.getTokenAfter(questionMarkToken);
1136 const lastConsequentToken = sourceCode.getTokenBefore(colonToken);
1137 const firstAlternateToken = sourceCode.getTokenAfter(colonToken);
1138
1139 offsets.setDesiredOffset(questionMarkToken, firstToken, 1);
1140 offsets.setDesiredOffset(colonToken, firstToken, 1);
1141
1142 offsets.setDesiredOffset(firstConsequentToken, firstToken, 1);
1143
1144 /*
1145 * The alternate and the consequent should usually have the same indentation.
1146 * If they share part of a line, align the alternate against the first token of the consequent.
1147 * This allows the alternate to be indented correctly in cases like this:
1148 * foo ? (
1149 * bar
1150 * ) : ( // this '(' is aligned with the '(' above, so it's considered to be aligned with `foo`
1151 * baz // as a result, `baz` is offset by 1 rather than 2
1152 * )
1153 */
1154 if (lastConsequentToken.loc.end.line === firstAlternateToken.loc.start.line) {
1155 offsets.setDesiredOffset(firstAlternateToken, firstConsequentToken, 0);
1156 } else {
1157
1158 /**
1159 * If the alternate and consequent do not share part of a line, offset the alternate from the first
1160 * token of the conditional expression. For example:
1161 * foo ? bar
1162 * : baz
1163 *
1164 * If `baz` were aligned with `bar` rather than being offset by 1 from `foo`, `baz` would end up
1165 * having no expected indentation.
1166 */
1167 offsets.setDesiredOffset(firstAlternateToken, firstToken, 1);
1168 }
1169 }
1170 },
1171
1172 "DoWhileStatement, WhileStatement, ForInStatement, ForOfStatement": node => addBlocklessNodeIndent(node.body),
1173
1174 ExportNamedDeclaration(node) {
1175 if (node.declaration === null) {
1176 const closingCurly = sourceCode.getLastToken(node, astUtils.isClosingBraceToken);
1177
1178 // Indent the specifiers in `export {foo, bar, baz}`
1179 addElementListIndent(node.specifiers, sourceCode.getFirstToken(node, { skip: 1 }), closingCurly, 1);
1180
1181 if (node.source) {
1182
1183 // Indent everything after and including the `from` token in `export {foo, bar, baz} from 'qux'`
1184 offsets.setDesiredOffsets([closingCurly.range[1], node.range[1]], sourceCode.getFirstToken(node), 1);
1185 }
1186 }
1187 },
1188
1189 ForStatement(node) {
1190 const forOpeningParen = sourceCode.getFirstToken(node, 1);
1191
1192 if (node.init) {
1193 offsets.setDesiredOffsets(node.init.range, forOpeningParen, 1);
1194 }
1195 if (node.test) {
1196 offsets.setDesiredOffsets(node.test.range, forOpeningParen, 1);
1197 }
1198 if (node.update) {
1199 offsets.setDesiredOffsets(node.update.range, forOpeningParen, 1);
1200 }
1201 addBlocklessNodeIndent(node.body);
1202 },
1203
1204 "FunctionDeclaration, FunctionExpression"(node) {
1205 const closingParen = sourceCode.getTokenBefore(node.body);
1206 const openingParen = sourceCode.getTokenBefore(node.params.length ? node.params[0] : closingParen);
1207
1208 parameterParens.add(openingParen);
1209 parameterParens.add(closingParen);
1210 addElementListIndent(node.params, openingParen, closingParen, options[node.type].parameters);
1211 },
1212
1213 IfStatement(node) {
1214 addBlocklessNodeIndent(node.consequent);
1215 if (node.alternate && node.alternate.type !== "IfStatement") {
1216 addBlocklessNodeIndent(node.alternate);
1217 }
1218 },
1219
1220 ImportDeclaration(node) {
1221 if (node.specifiers.some(specifier => specifier.type === "ImportSpecifier")) {
1222 const openingCurly = sourceCode.getFirstToken(node, astUtils.isOpeningBraceToken);
1223 const closingCurly = sourceCode.getLastToken(node, astUtils.isClosingBraceToken);
1224
1225 addElementListIndent(node.specifiers.filter(specifier => specifier.type === "ImportSpecifier"), openingCurly, closingCurly, options.ImportDeclaration);
1226 }
1227
1228 const fromToken = sourceCode.getLastToken(node, token => token.type === "Identifier" && token.value === "from");
1229 const sourceToken = sourceCode.getLastToken(node, token => token.type === "String");
1230 const semiToken = sourceCode.getLastToken(node, token => token.type === "Punctuator" && token.value === ";");
1231
1232 if (fromToken) {
1233 const end = semiToken && semiToken.range[1] === sourceToken.range[1] ? node.range[1] : sourceToken.range[1];
1234
1235 offsets.setDesiredOffsets([fromToken.range[0], end], sourceCode.getFirstToken(node), 1);
1236 }
1237 },
1238
1239 "MemberExpression, JSXMemberExpression, MetaProperty"(node) {
1240 const object = node.type === "MetaProperty" ? node.meta : node.object;
1241 const firstNonObjectToken = sourceCode.getFirstTokenBetween(object, node.property, astUtils.isNotClosingParenToken);
1242 const secondNonObjectToken = sourceCode.getTokenAfter(firstNonObjectToken);
1243
1244 const objectParenCount = sourceCode.getTokensBetween(object, node.property, { filter: astUtils.isClosingParenToken }).length;
1245 const firstObjectToken = objectParenCount
1246 ? sourceCode.getTokenBefore(object, { skip: objectParenCount - 1 })
1247 : sourceCode.getFirstToken(object);
1248 const lastObjectToken = sourceCode.getTokenBefore(firstNonObjectToken);
1249 const firstPropertyToken = node.computed ? firstNonObjectToken : secondNonObjectToken;
1250
1251 if (node.computed) {
1252
1253 // For computed MemberExpressions, match the closing bracket with the opening bracket.
1254 offsets.setDesiredOffset(sourceCode.getLastToken(node), firstNonObjectToken, 0);
1255 offsets.setDesiredOffsets(node.property.range, firstNonObjectToken, 1);
1256 }
1257
1258 /*
1259 * If the object ends on the same line that the property starts, match against the last token
1260 * of the object, to ensure that the MemberExpression is not indented.
1261 *
1262 * Otherwise, match against the first token of the object, e.g.
1263 * foo
1264 * .bar
1265 * .baz // <-- offset by 1 from `foo`
1266 */
1267 const offsetBase = lastObjectToken.loc.end.line === firstPropertyToken.loc.start.line
1268 ? lastObjectToken
1269 : firstObjectToken;
1270
1271 if (typeof options.MemberExpression === "number") {
1272
1273 // Match the dot (for non-computed properties) or the opening bracket (for computed properties) against the object.
1274 offsets.setDesiredOffset(firstNonObjectToken, offsetBase, options.MemberExpression);
1275
1276 /*
1277 * For computed MemberExpressions, match the first token of the property against the opening bracket.
1278 * Otherwise, match the first token of the property against the object.
1279 */
1280 offsets.setDesiredOffset(secondNonObjectToken, node.computed ? firstNonObjectToken : offsetBase, options.MemberExpression);
1281 } else {
1282
1283 // If the MemberExpression option is off, ignore the dot and the first token of the property.
1284 offsets.ignoreToken(firstNonObjectToken);
1285 offsets.ignoreToken(secondNonObjectToken);
1286
1287 // To ignore the property indentation, ensure that the property tokens depend on the ignored tokens.
1288 offsets.setDesiredOffset(firstNonObjectToken, offsetBase, 0);
1289 offsets.setDesiredOffset(secondNonObjectToken, firstNonObjectToken, 0);
1290 }
1291 },
1292
1293 NewExpression(node) {
1294
1295 // Only indent the arguments if the NewExpression has parens (e.g. `new Foo(bar)` or `new Foo()`, but not `new Foo`
1296 if (node.arguments.length > 0 ||
1297 astUtils.isClosingParenToken(sourceCode.getLastToken(node)) &&
1298 astUtils.isOpeningParenToken(sourceCode.getLastToken(node, 1))) {
1299 addFunctionCallIndent(node);
1300 }
1301 },
1302
1303 Property(node) {
1304 if (!node.shorthand && !node.method && node.kind === "init") {
1305 const colon = sourceCode.getFirstTokenBetween(node.key, node.value, astUtils.isColonToken);
1306
1307 offsets.ignoreToken(sourceCode.getTokenAfter(colon));
1308 }
1309 },
1310
1311 SwitchStatement(node) {
1312 const openingCurly = sourceCode.getTokenAfter(node.discriminant, astUtils.isOpeningBraceToken);
1313 const closingCurly = sourceCode.getLastToken(node);
1314
1315 offsets.setDesiredOffsets([openingCurly.range[1], closingCurly.range[0]], openingCurly, options.SwitchCase);
1316
1317 if (node.cases.length) {
1318 sourceCode.getTokensBetween(
1319 node.cases[node.cases.length - 1],
1320 closingCurly,
1321 { includeComments: true, filter: astUtils.isCommentToken }
1322 ).forEach(token => offsets.ignoreToken(token));
1323 }
1324 },
1325
1326 SwitchCase(node) {
1327 if (!(node.consequent.length === 1 && node.consequent[0].type === "BlockStatement")) {
1328 const caseKeyword = sourceCode.getFirstToken(node);
1329 const tokenAfterCurrentCase = sourceCode.getTokenAfter(node);
1330
1331 offsets.setDesiredOffsets([caseKeyword.range[1], tokenAfterCurrentCase.range[0]], caseKeyword, 1);
1332 }
1333 },
1334
1335 TemplateLiteral(node) {
1336 node.expressions.forEach((expression, index) => {
1337 const previousQuasi = node.quasis[index];
1338 const nextQuasi = node.quasis[index + 1];
1339 const tokenToAlignFrom = previousQuasi.loc.start.line === previousQuasi.loc.end.line
1340 ? sourceCode.getFirstToken(previousQuasi)
1341 : null;
1342
1343 offsets.setDesiredOffsets([previousQuasi.range[1], nextQuasi.range[0]], tokenToAlignFrom, 1);
1344 offsets.setDesiredOffset(sourceCode.getFirstToken(nextQuasi), tokenToAlignFrom, 0);
1345 });
1346 },
1347
1348 VariableDeclaration(node) {
1349 let variableIndent = Object.prototype.hasOwnProperty.call(options.VariableDeclarator, node.kind)
1350 ? options.VariableDeclarator[node.kind]
1351 : DEFAULT_VARIABLE_INDENT;
1352
1353 const firstToken = sourceCode.getFirstToken(node),
1354 lastToken = sourceCode.getLastToken(node);
1355
1356 if (options.VariableDeclarator[node.kind] === "first") {
1357 if (node.declarations.length > 1) {
1358 addElementListIndent(
1359 node.declarations,
1360 firstToken,
1361 lastToken,
1362 "first"
1363 );
1364 return;
1365 }
1366
1367 variableIndent = DEFAULT_VARIABLE_INDENT;
1368 }
1369
1370 if (node.declarations[node.declarations.length - 1].loc.start.line > node.loc.start.line) {
1371
1372 /*
1373 * VariableDeclarator indentation is a bit different from other forms of indentation, in that the
1374 * indentation of an opening bracket sometimes won't match that of a closing bracket. For example,
1375 * the following indentations are correct:
1376 *
1377 * var foo = {
1378 * ok: true
1379 * };
1380 *
1381 * var foo = {
1382 * ok: true,
1383 * },
1384 * bar = 1;
1385 *
1386 * Account for when exiting the AST (after indentations have already been set for the nodes in
1387 * the declaration) by manually increasing the indentation level of the tokens in this declarator
1388 * on the same line as the start of the declaration, provided that there are declarators that
1389 * follow this one.
1390 */
1391 offsets.setDesiredOffsets(node.range, firstToken, variableIndent, true);
1392 } else {
1393 offsets.setDesiredOffsets(node.range, firstToken, variableIndent);
1394 }
1395
1396 if (astUtils.isSemicolonToken(lastToken)) {
1397 offsets.ignoreToken(lastToken);
1398 }
1399 },
1400
1401 VariableDeclarator(node) {
1402 if (node.init) {
1403 const equalOperator = sourceCode.getTokenBefore(node.init, astUtils.isNotOpeningParenToken);
1404 const tokenAfterOperator = sourceCode.getTokenAfter(equalOperator);
1405
1406 offsets.ignoreToken(equalOperator);
1407 offsets.ignoreToken(tokenAfterOperator);
1408 offsets.setDesiredOffsets([tokenAfterOperator.range[0], node.range[1]], equalOperator, 1);
1409 offsets.setDesiredOffset(equalOperator, sourceCode.getLastToken(node.id), 0);
1410 }
1411 },
1412
1413 "JSXAttribute[value]"(node) {
1414 const equalsToken = sourceCode.getFirstTokenBetween(node.name, node.value, token => token.type === "Punctuator" && token.value === "=");
1415
1416 offsets.setDesiredOffsets([equalsToken.range[0], node.value.range[1]], sourceCode.getFirstToken(node.name), 1);
1417 },
1418
1419 JSXElement(node) {
1420 if (node.closingElement) {
1421 addElementListIndent(node.children, sourceCode.getFirstToken(node.openingElement), sourceCode.getFirstToken(node.closingElement), 1);
1422 }
1423 },
1424
1425 JSXOpeningElement(node) {
1426 const firstToken = sourceCode.getFirstToken(node);
1427 let closingToken;
1428
1429 if (node.selfClosing) {
1430 closingToken = sourceCode.getLastToken(node, { skip: 1 });
1431 offsets.setDesiredOffset(sourceCode.getLastToken(node), closingToken, 0);
1432 } else {
1433 closingToken = sourceCode.getLastToken(node);
1434 }
1435 offsets.setDesiredOffsets(node.name.range, sourceCode.getFirstToken(node));
1436 addElementListIndent(node.attributes, firstToken, closingToken, 1);
1437 },
1438
1439 JSXClosingElement(node) {
1440 const firstToken = sourceCode.getFirstToken(node);
1441
1442 offsets.setDesiredOffsets(node.name.range, firstToken, 1);
1443 },
1444
1445 JSXExpressionContainer(node) {
1446 const openingCurly = sourceCode.getFirstToken(node);
1447 const closingCurly = sourceCode.getLastToken(node);
1448
1449 offsets.setDesiredOffsets(
1450 [openingCurly.range[1], closingCurly.range[0]],
1451 openingCurly,
1452 1
1453 );
1454 },
1455
1456 "*"(node) {
1457 const firstToken = sourceCode.getFirstToken(node);
1458
1459 // Ensure that the children of every node are indented at least as much as the first token.
1460 if (firstToken && !ignoredNodeFirstTokens.has(firstToken)) {
1461 offsets.setDesiredOffsets(node.range, firstToken, 0);
1462 }
1463 }
1464 };
1465
1466 const listenerCallQueue = [];
1467
1468 /*
1469 * To ignore the indentation of a node:
1470 * 1. Don't call the node's listener when entering it (if it has a listener)
1471 * 2. Don't set any offsets against the first token of the node.
1472 * 3. Call `ignoreNode` on the node sometime after exiting it and before validating offsets.
1473 */
1474 const offsetListeners = lodash.mapValues(
1475 baseOffsetListeners,
1476
1477 /*
1478 * Offset listener calls are deferred until traversal is finished, and are called as
1479 * part of the final `Program:exit` listener. This is necessary because a node might
1480 * be matched by multiple selectors.
1481 *
1482 * Example: Suppose there is an offset listener for `Identifier`, and the user has
1483 * specified in configuration that `MemberExpression > Identifier` should be ignored.
1484 * Due to selector specificity rules, the `Identifier` listener will get called first. However,
1485 * if a given Identifier node is supposed to be ignored, then the `Identifier` offset listener
1486 * should not have been called at all. Without doing extra selector matching, we don't know
1487 * whether the Identifier matches the `MemberExpression > Identifier` selector until the
1488 * `MemberExpression > Identifier` listener is called.
1489 *
1490 * To avoid this, the `Identifier` listener isn't called until traversal finishes and all
1491 * ignored nodes are known.
1492 */
1493 listener =>
1494 node =>
1495 listenerCallQueue.push({ listener, node })
1496 );
1497
1498 // For each ignored node selector, set up a listener to collect it into the `ignoredNodes` set.
1499 const ignoredNodes = new Set();
1500
1501 /**
1502 * Ignores a node
1503 * @param {ASTNode} node The node to ignore
1504 * @returns {void}
1505 */
1506 function addToIgnoredNodes(node) {
1507 ignoredNodes.add(node);
1508 ignoredNodeFirstTokens.add(sourceCode.getFirstToken(node));
1509 }
1510
1511 const ignoredNodeListeners = options.ignoredNodes.reduce(
1512 (listeners, ignoredSelector) => Object.assign(listeners, { [ignoredSelector]: addToIgnoredNodes }),
1513 {}
1514 );
1515
1516 /*
1517 * Join the listeners, and add a listener to verify that all tokens actually have the correct indentation
1518 * at the end.
1519 *
1520 * Using Object.assign will cause some offset listeners to be overwritten if the same selector also appears
1521 * in `ignoredNodeListeners`. This isn't a problem because all of the matching nodes will be ignored,
1522 * so those listeners wouldn't be called anyway.
1523 */
1524 return Object.assign(
1525 offsetListeners,
1526 ignoredNodeListeners,
1527 {
1528 "*:exit"(node) {
1529
1530 // If a node's type is nonstandard, we can't tell how its children should be offset, so ignore it.
1531 if (!KNOWN_NODES.has(node.type)) {
1532 addToIgnoredNodes(node);
1533 }
1534 },
1535 "Program:exit"() {
1536
1537 // If ignoreComments option is enabled, ignore all comment tokens.
1538 if (options.ignoreComments) {
1539 sourceCode.getAllComments()
1540 .forEach(comment => offsets.ignoreToken(comment));
1541 }
1542
1543 // Invoke the queued offset listeners for the nodes that aren't ignored.
1544 listenerCallQueue
1545 .filter(nodeInfo => !ignoredNodes.has(nodeInfo.node))
1546 .forEach(nodeInfo => nodeInfo.listener(nodeInfo.node));
1547
1548 // Update the offsets for ignored nodes to prevent their child tokens from being reported.
1549 ignoredNodes.forEach(ignoreNode);
1550
1551 addParensIndent(sourceCode.ast.tokens);
1552
1553 /*
1554 * Create a Map from (tokenOrComment) => (precedingToken).
1555 * This is necessary because sourceCode.getTokenBefore does not handle a comment as an argument correctly.
1556 */
1557 const precedingTokens = sourceCode.ast.comments.reduce((commentMap, comment) => {
1558 const tokenOrCommentBefore = sourceCode.getTokenBefore(comment, { includeComments: true });
1559
1560 return commentMap.set(comment, commentMap.has(tokenOrCommentBefore) ? commentMap.get(tokenOrCommentBefore) : tokenOrCommentBefore);
1561 }, new WeakMap());
1562
1563 sourceCode.lines.forEach((line, lineIndex) => {
1564 const lineNumber = lineIndex + 1;
1565
1566 if (!tokenInfo.firstTokensByLineNumber.has(lineNumber)) {
1567
1568 // Don't check indentation on blank lines
1569 return;
1570 }
1571
1572 const firstTokenOfLine = tokenInfo.firstTokensByLineNumber.get(lineNumber);
1573
1574 if (firstTokenOfLine.loc.start.line !== lineNumber) {
1575
1576 // Don't check the indentation of multi-line tokens (e.g. template literals or block comments) twice.
1577 return;
1578 }
1579
1580 // If the token matches the expected expected indentation, don't report it.
1581 if (validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine))) {
1582 return;
1583 }
1584
1585 if (astUtils.isCommentToken(firstTokenOfLine)) {
1586 const tokenBefore = precedingTokens.get(firstTokenOfLine);
1587 const tokenAfter = tokenBefore ? sourceCode.getTokenAfter(tokenBefore) : sourceCode.ast.tokens[0];
1588
1589 const mayAlignWithBefore = tokenBefore && !hasBlankLinesBetween(tokenBefore, firstTokenOfLine);
1590 const mayAlignWithAfter = tokenAfter && !hasBlankLinesBetween(firstTokenOfLine, tokenAfter);
1591
1592 // If a comment matches the expected indentation of the token immediately before or after, don't report it.
1593 if (
1594 mayAlignWithBefore && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenBefore)) ||
1595 mayAlignWithAfter && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenAfter))
1596 ) {
1597 return;
1598 }
1599 }
1600
1601 // Otherwise, report the token/comment.
1602 report(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine));
1603 });
1604 }
1605 }
1606 );
1607 }
1608};