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