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