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