blob: b1af2a73b33be262230525b0a1f5e18ce9b48d5c [file] [log] [blame]
Yang Guo4fd355c2019-09-19 10:59:03 +02001/**
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
Yang Guo4fd355c2019-09-19 10:59:03 +020015const createTree = require("functional-red-black-tree");
16
Simon Zünd52e20202021-06-16 08:34:28 +020017const astUtils = require("./utils/ast-utils");
18
Yang Guo4fd355c2019-09-19 10:59:03 +020019//------------------------------------------------------------------------------
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",
Tim van der Lippe16aca392020-11-13 11:37:13 +000035 "ChainExpression",
Yang Guo4fd355c2019-09-19 10:59:03 +020036 "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",
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010085 "JSXFragment",
86 "JSXOpeningFragment",
87 "JSXClosingFragment",
Yang Guo4fd355c2019-09-19 10:59:03 +020088 "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",
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100106 "ImportNamespaceSpecifier",
107 "ImportExpression"
Yang Guo4fd355c2019-09-19 10:59:03 +0200108]);
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
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100191 // eslint-disable-next-line jsdoc/require-description
Yang Guo4fd355c2019-09-19 10:59:03 +0200192 /**
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
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100241 // eslint-disable-next-line jsdoc/require-description
Yang Guo4fd355c2019-09-19 10:59:03 +0200242 /**
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.
Yang Guo4fd355c2019-09-19 10:59:03 +0200335 * @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);`
Yang Guo4fd355c2019-09-19 10:59:03 +0200363 * @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: {
Tim van der Lippe16aca392020-11-13 11:37:13 +0000544 oneOf: [
545 {
546 type: "integer",
547 minimum: 0
548 },
549 {
550 enum: ["off"]
551 }
552 ]
Yang Guo4fd355c2019-09-19 10:59:03 +0200553 },
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 },
Tim van der Lippe16aca392020-11-13 11:37:13 +0000601 offsetTernaryExpressions: {
602 type: "boolean",
603 default: false
604 },
Yang Guo4fd355c2019-09-19 10:59:03 +0200605 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);
Tim van der Lippe16aca392020-11-13 11:37:13 +0000937
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
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);
Yang Guo4fd355c2019-09-19 10:59:03 +0200960
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);
Simon Zünd52e20202021-06-16 08:34:28 +02001071 const closingBracket = sourceCode.getTokenAfter([...node.elements].reverse().find(_ => _) || openingBracket, astUtils.isClosingBracketToken);
Yang Guo4fd355c2019-09-19 10:59:03 +02001072
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) {
Tim van der Lippe16aca392020-11-13 11:37:13 +00001087 const maybeOpeningParen = sourceCode.getFirstToken(node, { skip: node.async ? 1 : 0 });
Yang Guo4fd355c2019-09-19 10:59:03 +02001088
Tim van der Lippe16aca392020-11-13 11:37:13 +00001089 if (astUtils.isOpeningParenToken(maybeOpeningParen)) {
1090 const openingParen = maybeOpeningParen;
Yang Guo4fd355c2019-09-19 10:59:03 +02001091 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 }
Tim van der Lippe16aca392020-11-13 11:37:13 +00001097
Yang Guo4fd355c2019-09-19 10:59:03 +02001098 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) {
Yang Guo4fd355c2019-09-19 10:59:03 +02001126 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 }
Tim van der Lippe16aca392020-11-13 11:37:13 +00001145
Yang Guo4fd355c2019-09-19 10:59:03 +02001146 addElementListIndent(node.body, sourceCode.getFirstToken(node), sourceCode.getLastToken(node), blockIndentLevel);
1147 },
1148
1149 CallExpression: addFunctionCallIndent,
1150
Yang Guo4fd355c2019-09-19 10:59:03 +02001151 "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
Tim van der Lippe16aca392020-11-13 11:37:13 +00001180 offsets.setDesiredOffset(firstConsequentToken, firstToken,
Tim van der Lippeb97da6b2021-02-12 14:32:53 +00001181 firstConsequentToken.type === "Punctuator" &&
Tim van der Lippe16aca392020-11-13 11:37:13 +00001182 options.offsetTernaryExpressions ? 2 : 1);
Yang Guo4fd355c2019-09-19 10:59:03 +02001183
1184 /*
1185 * The alternate and the consequent should usually have the same indentation.
1186 * If they share part of a line, align the alternate against the first token of the consequent.
1187 * This allows the alternate to be indented correctly in cases like this:
1188 * foo ? (
1189 * bar
1190 * ) : ( // this '(' is aligned with the '(' above, so it's considered to be aligned with `foo`
1191 * baz // as a result, `baz` is offset by 1 rather than 2
1192 * )
1193 */
1194 if (lastConsequentToken.loc.end.line === firstAlternateToken.loc.start.line) {
1195 offsets.setDesiredOffset(firstAlternateToken, firstConsequentToken, 0);
1196 } else {
1197
1198 /**
1199 * If the alternate and consequent do not share part of a line, offset the alternate from the first
1200 * token of the conditional expression. For example:
1201 * foo ? bar
1202 * : baz
1203 *
1204 * If `baz` were aligned with `bar` rather than being offset by 1 from `foo`, `baz` would end up
1205 * having no expected indentation.
1206 */
Tim van der Lippe16aca392020-11-13 11:37:13 +00001207 offsets.setDesiredOffset(firstAlternateToken, firstToken,
1208 firstAlternateToken.type === "Punctuator" &&
1209 options.offsetTernaryExpressions ? 2 : 1);
Yang Guo4fd355c2019-09-19 10:59:03 +02001210 }
1211 }
1212 },
1213
1214 "DoWhileStatement, WhileStatement, ForInStatement, ForOfStatement": node => addBlocklessNodeIndent(node.body),
1215
1216 ExportNamedDeclaration(node) {
1217 if (node.declaration === null) {
1218 const closingCurly = sourceCode.getLastToken(node, astUtils.isClosingBraceToken);
1219
1220 // Indent the specifiers in `export {foo, bar, baz}`
1221 addElementListIndent(node.specifiers, sourceCode.getFirstToken(node, { skip: 1 }), closingCurly, 1);
1222
1223 if (node.source) {
1224
1225 // Indent everything after and including the `from` token in `export {foo, bar, baz} from 'qux'`
1226 offsets.setDesiredOffsets([closingCurly.range[1], node.range[1]], sourceCode.getFirstToken(node), 1);
1227 }
1228 }
1229 },
1230
1231 ForStatement(node) {
1232 const forOpeningParen = sourceCode.getFirstToken(node, 1);
1233
1234 if (node.init) {
1235 offsets.setDesiredOffsets(node.init.range, forOpeningParen, 1);
1236 }
1237 if (node.test) {
1238 offsets.setDesiredOffsets(node.test.range, forOpeningParen, 1);
1239 }
1240 if (node.update) {
1241 offsets.setDesiredOffsets(node.update.range, forOpeningParen, 1);
1242 }
1243 addBlocklessNodeIndent(node.body);
1244 },
1245
1246 "FunctionDeclaration, FunctionExpression"(node) {
1247 const closingParen = sourceCode.getTokenBefore(node.body);
1248 const openingParen = sourceCode.getTokenBefore(node.params.length ? node.params[0] : closingParen);
1249
1250 parameterParens.add(openingParen);
1251 parameterParens.add(closingParen);
1252 addElementListIndent(node.params, openingParen, closingParen, options[node.type].parameters);
1253 },
1254
1255 IfStatement(node) {
1256 addBlocklessNodeIndent(node.consequent);
1257 if (node.alternate && node.alternate.type !== "IfStatement") {
1258 addBlocklessNodeIndent(node.alternate);
1259 }
1260 },
1261
1262 ImportDeclaration(node) {
1263 if (node.specifiers.some(specifier => specifier.type === "ImportSpecifier")) {
1264 const openingCurly = sourceCode.getFirstToken(node, astUtils.isOpeningBraceToken);
1265 const closingCurly = sourceCode.getLastToken(node, astUtils.isClosingBraceToken);
1266
1267 addElementListIndent(node.specifiers.filter(specifier => specifier.type === "ImportSpecifier"), openingCurly, closingCurly, options.ImportDeclaration);
1268 }
1269
1270 const fromToken = sourceCode.getLastToken(node, token => token.type === "Identifier" && token.value === "from");
1271 const sourceToken = sourceCode.getLastToken(node, token => token.type === "String");
1272 const semiToken = sourceCode.getLastToken(node, token => token.type === "Punctuator" && token.value === ";");
1273
1274 if (fromToken) {
1275 const end = semiToken && semiToken.range[1] === sourceToken.range[1] ? node.range[1] : sourceToken.range[1];
1276
1277 offsets.setDesiredOffsets([fromToken.range[0], end], sourceCode.getFirstToken(node), 1);
1278 }
1279 },
1280
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +01001281 ImportExpression(node) {
1282 const openingParen = sourceCode.getFirstToken(node, 1);
1283 const closingParen = sourceCode.getLastToken(node);
1284
1285 parameterParens.add(openingParen);
1286 parameterParens.add(closingParen);
1287 offsets.setDesiredOffset(openingParen, sourceCode.getTokenBefore(openingParen), 0);
1288
1289 addElementListIndent([node.source], openingParen, closingParen, options.CallExpression.arguments);
1290 },
1291
Yang Guo4fd355c2019-09-19 10:59:03 +02001292 "MemberExpression, JSXMemberExpression, MetaProperty"(node) {
1293 const object = node.type === "MetaProperty" ? node.meta : node.object;
1294 const firstNonObjectToken = sourceCode.getFirstTokenBetween(object, node.property, astUtils.isNotClosingParenToken);
1295 const secondNonObjectToken = sourceCode.getTokenAfter(firstNonObjectToken);
1296
1297 const objectParenCount = sourceCode.getTokensBetween(object, node.property, { filter: astUtils.isClosingParenToken }).length;
1298 const firstObjectToken = objectParenCount
1299 ? sourceCode.getTokenBefore(object, { skip: objectParenCount - 1 })
1300 : sourceCode.getFirstToken(object);
1301 const lastObjectToken = sourceCode.getTokenBefore(firstNonObjectToken);
1302 const firstPropertyToken = node.computed ? firstNonObjectToken : secondNonObjectToken;
1303
1304 if (node.computed) {
1305
1306 // For computed MemberExpressions, match the closing bracket with the opening bracket.
1307 offsets.setDesiredOffset(sourceCode.getLastToken(node), firstNonObjectToken, 0);
1308 offsets.setDesiredOffsets(node.property.range, firstNonObjectToken, 1);
1309 }
1310
1311 /*
1312 * If the object ends on the same line that the property starts, match against the last token
1313 * of the object, to ensure that the MemberExpression is not indented.
1314 *
1315 * Otherwise, match against the first token of the object, e.g.
1316 * foo
1317 * .bar
1318 * .baz // <-- offset by 1 from `foo`
1319 */
1320 const offsetBase = lastObjectToken.loc.end.line === firstPropertyToken.loc.start.line
1321 ? lastObjectToken
1322 : firstObjectToken;
1323
1324 if (typeof options.MemberExpression === "number") {
1325
1326 // Match the dot (for non-computed properties) or the opening bracket (for computed properties) against the object.
1327 offsets.setDesiredOffset(firstNonObjectToken, offsetBase, options.MemberExpression);
1328
1329 /*
1330 * For computed MemberExpressions, match the first token of the property against the opening bracket.
1331 * Otherwise, match the first token of the property against the object.
1332 */
1333 offsets.setDesiredOffset(secondNonObjectToken, node.computed ? firstNonObjectToken : offsetBase, options.MemberExpression);
1334 } else {
1335
1336 // If the MemberExpression option is off, ignore the dot and the first token of the property.
1337 offsets.ignoreToken(firstNonObjectToken);
1338 offsets.ignoreToken(secondNonObjectToken);
1339
1340 // To ignore the property indentation, ensure that the property tokens depend on the ignored tokens.
1341 offsets.setDesiredOffset(firstNonObjectToken, offsetBase, 0);
1342 offsets.setDesiredOffset(secondNonObjectToken, firstNonObjectToken, 0);
1343 }
1344 },
1345
1346 NewExpression(node) {
1347
1348 // Only indent the arguments if the NewExpression has parens (e.g. `new Foo(bar)` or `new Foo()`, but not `new Foo`
1349 if (node.arguments.length > 0 ||
1350 astUtils.isClosingParenToken(sourceCode.getLastToken(node)) &&
1351 astUtils.isOpeningParenToken(sourceCode.getLastToken(node, 1))) {
1352 addFunctionCallIndent(node);
1353 }
1354 },
1355
1356 Property(node) {
1357 if (!node.shorthand && !node.method && node.kind === "init") {
1358 const colon = sourceCode.getFirstTokenBetween(node.key, node.value, astUtils.isColonToken);
1359
1360 offsets.ignoreToken(sourceCode.getTokenAfter(colon));
1361 }
1362 },
1363
1364 SwitchStatement(node) {
1365 const openingCurly = sourceCode.getTokenAfter(node.discriminant, astUtils.isOpeningBraceToken);
1366 const closingCurly = sourceCode.getLastToken(node);
1367
1368 offsets.setDesiredOffsets([openingCurly.range[1], closingCurly.range[0]], openingCurly, options.SwitchCase);
1369
1370 if (node.cases.length) {
1371 sourceCode.getTokensBetween(
1372 node.cases[node.cases.length - 1],
1373 closingCurly,
1374 { includeComments: true, filter: astUtils.isCommentToken }
1375 ).forEach(token => offsets.ignoreToken(token));
1376 }
1377 },
1378
1379 SwitchCase(node) {
1380 if (!(node.consequent.length === 1 && node.consequent[0].type === "BlockStatement")) {
1381 const caseKeyword = sourceCode.getFirstToken(node);
1382 const tokenAfterCurrentCase = sourceCode.getTokenAfter(node);
1383
1384 offsets.setDesiredOffsets([caseKeyword.range[1], tokenAfterCurrentCase.range[0]], caseKeyword, 1);
1385 }
1386 },
1387
1388 TemplateLiteral(node) {
1389 node.expressions.forEach((expression, index) => {
1390 const previousQuasi = node.quasis[index];
1391 const nextQuasi = node.quasis[index + 1];
1392 const tokenToAlignFrom = previousQuasi.loc.start.line === previousQuasi.loc.end.line
1393 ? sourceCode.getFirstToken(previousQuasi)
1394 : null;
1395
1396 offsets.setDesiredOffsets([previousQuasi.range[1], nextQuasi.range[0]], tokenToAlignFrom, 1);
1397 offsets.setDesiredOffset(sourceCode.getFirstToken(nextQuasi), tokenToAlignFrom, 0);
1398 });
1399 },
1400
1401 VariableDeclaration(node) {
1402 let variableIndent = Object.prototype.hasOwnProperty.call(options.VariableDeclarator, node.kind)
1403 ? options.VariableDeclarator[node.kind]
1404 : DEFAULT_VARIABLE_INDENT;
1405
1406 const firstToken = sourceCode.getFirstToken(node),
1407 lastToken = sourceCode.getLastToken(node);
1408
1409 if (options.VariableDeclarator[node.kind] === "first") {
1410 if (node.declarations.length > 1) {
1411 addElementListIndent(
1412 node.declarations,
1413 firstToken,
1414 lastToken,
1415 "first"
1416 );
1417 return;
1418 }
1419
1420 variableIndent = DEFAULT_VARIABLE_INDENT;
1421 }
1422
1423 if (node.declarations[node.declarations.length - 1].loc.start.line > node.loc.start.line) {
1424
1425 /*
1426 * VariableDeclarator indentation is a bit different from other forms of indentation, in that the
1427 * indentation of an opening bracket sometimes won't match that of a closing bracket. For example,
1428 * the following indentations are correct:
1429 *
1430 * var foo = {
1431 * ok: true
1432 * };
1433 *
1434 * var foo = {
1435 * ok: true,
1436 * },
1437 * bar = 1;
1438 *
1439 * Account for when exiting the AST (after indentations have already been set for the nodes in
1440 * the declaration) by manually increasing the indentation level of the tokens in this declarator
1441 * on the same line as the start of the declaration, provided that there are declarators that
1442 * follow this one.
1443 */
1444 offsets.setDesiredOffsets(node.range, firstToken, variableIndent, true);
1445 } else {
1446 offsets.setDesiredOffsets(node.range, firstToken, variableIndent);
1447 }
1448
1449 if (astUtils.isSemicolonToken(lastToken)) {
1450 offsets.ignoreToken(lastToken);
1451 }
1452 },
1453
1454 VariableDeclarator(node) {
1455 if (node.init) {
1456 const equalOperator = sourceCode.getTokenBefore(node.init, astUtils.isNotOpeningParenToken);
1457 const tokenAfterOperator = sourceCode.getTokenAfter(equalOperator);
1458
1459 offsets.ignoreToken(equalOperator);
1460 offsets.ignoreToken(tokenAfterOperator);
1461 offsets.setDesiredOffsets([tokenAfterOperator.range[0], node.range[1]], equalOperator, 1);
1462 offsets.setDesiredOffset(equalOperator, sourceCode.getLastToken(node.id), 0);
1463 }
1464 },
1465
1466 "JSXAttribute[value]"(node) {
1467 const equalsToken = sourceCode.getFirstTokenBetween(node.name, node.value, token => token.type === "Punctuator" && token.value === "=");
1468
1469 offsets.setDesiredOffsets([equalsToken.range[0], node.value.range[1]], sourceCode.getFirstToken(node.name), 1);
1470 },
1471
1472 JSXElement(node) {
1473 if (node.closingElement) {
1474 addElementListIndent(node.children, sourceCode.getFirstToken(node.openingElement), sourceCode.getFirstToken(node.closingElement), 1);
1475 }
1476 },
1477
1478 JSXOpeningElement(node) {
1479 const firstToken = sourceCode.getFirstToken(node);
1480 let closingToken;
1481
1482 if (node.selfClosing) {
1483 closingToken = sourceCode.getLastToken(node, { skip: 1 });
1484 offsets.setDesiredOffset(sourceCode.getLastToken(node), closingToken, 0);
1485 } else {
1486 closingToken = sourceCode.getLastToken(node);
1487 }
1488 offsets.setDesiredOffsets(node.name.range, sourceCode.getFirstToken(node));
1489 addElementListIndent(node.attributes, firstToken, closingToken, 1);
1490 },
1491
1492 JSXClosingElement(node) {
1493 const firstToken = sourceCode.getFirstToken(node);
1494
1495 offsets.setDesiredOffsets(node.name.range, firstToken, 1);
1496 },
1497
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +01001498 JSXFragment(node) {
1499 const firstOpeningToken = sourceCode.getFirstToken(node.openingFragment);
1500 const firstClosingToken = sourceCode.getFirstToken(node.closingFragment);
1501
1502 addElementListIndent(node.children, firstOpeningToken, firstClosingToken, 1);
1503 },
1504
1505 JSXOpeningFragment(node) {
1506 const firstToken = sourceCode.getFirstToken(node);
1507 const closingToken = sourceCode.getLastToken(node);
1508
1509 offsets.setDesiredOffsets(node.range, firstToken, 1);
1510 offsets.matchOffsetOf(firstToken, closingToken);
1511 },
1512
1513 JSXClosingFragment(node) {
1514 const firstToken = sourceCode.getFirstToken(node);
1515 const slashToken = sourceCode.getLastToken(node, { skip: 1 });
1516 const closingToken = sourceCode.getLastToken(node);
1517 const tokenToMatch = astUtils.isTokenOnSameLine(slashToken, closingToken) ? slashToken : closingToken;
1518
1519 offsets.setDesiredOffsets(node.range, firstToken, 1);
1520 offsets.matchOffsetOf(firstToken, tokenToMatch);
1521 },
1522
Yang Guo4fd355c2019-09-19 10:59:03 +02001523 JSXExpressionContainer(node) {
1524 const openingCurly = sourceCode.getFirstToken(node);
1525 const closingCurly = sourceCode.getLastToken(node);
1526
1527 offsets.setDesiredOffsets(
1528 [openingCurly.range[1], closingCurly.range[0]],
1529 openingCurly,
1530 1
1531 );
1532 },
1533
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +01001534 JSXSpreadAttribute(node) {
1535 const openingCurly = sourceCode.getFirstToken(node);
1536 const closingCurly = sourceCode.getLastToken(node);
1537
1538 offsets.setDesiredOffsets(
1539 [openingCurly.range[1], closingCurly.range[0]],
1540 openingCurly,
1541 1
1542 );
1543 },
1544
Yang Guo4fd355c2019-09-19 10:59:03 +02001545 "*"(node) {
1546 const firstToken = sourceCode.getFirstToken(node);
1547
1548 // Ensure that the children of every node are indented at least as much as the first token.
1549 if (firstToken && !ignoredNodeFirstTokens.has(firstToken)) {
1550 offsets.setDesiredOffsets(node.range, firstToken, 0);
1551 }
1552 }
1553 };
1554
1555 const listenerCallQueue = [];
1556
1557 /*
1558 * To ignore the indentation of a node:
1559 * 1. Don't call the node's listener when entering it (if it has a listener)
1560 * 2. Don't set any offsets against the first token of the node.
1561 * 3. Call `ignoreNode` on the node sometime after exiting it and before validating offsets.
1562 */
Simon Zünd52e20202021-06-16 08:34:28 +02001563 const offsetListeners = {};
1564
1565 for (const [selector, listener] of Object.entries(baseOffsetListeners)) {
Yang Guo4fd355c2019-09-19 10:59:03 +02001566
1567 /*
1568 * Offset listener calls are deferred until traversal is finished, and are called as
1569 * part of the final `Program:exit` listener. This is necessary because a node might
1570 * be matched by multiple selectors.
1571 *
1572 * Example: Suppose there is an offset listener for `Identifier`, and the user has
1573 * specified in configuration that `MemberExpression > Identifier` should be ignored.
1574 * Due to selector specificity rules, the `Identifier` listener will get called first. However,
1575 * if a given Identifier node is supposed to be ignored, then the `Identifier` offset listener
1576 * should not have been called at all. Without doing extra selector matching, we don't know
1577 * whether the Identifier matches the `MemberExpression > Identifier` selector until the
1578 * `MemberExpression > Identifier` listener is called.
1579 *
1580 * To avoid this, the `Identifier` listener isn't called until traversal finishes and all
1581 * ignored nodes are known.
1582 */
Simon Zünd52e20202021-06-16 08:34:28 +02001583 offsetListeners[selector] = node => listenerCallQueue.push({ listener, node });
1584 }
Yang Guo4fd355c2019-09-19 10:59:03 +02001585
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
Yang Guo4fd355c2019-09-19 10:59:03 +02001668 if (astUtils.isCommentToken(firstTokenOfLine)) {
1669 const tokenBefore = precedingTokens.get(firstTokenOfLine);
1670 const tokenAfter = tokenBefore ? sourceCode.getTokenAfter(tokenBefore) : sourceCode.ast.tokens[0];
Yang Guo4fd355c2019-09-19 10:59:03 +02001671 const mayAlignWithBefore = tokenBefore && !hasBlankLinesBetween(tokenBefore, firstTokenOfLine);
1672 const mayAlignWithAfter = tokenAfter && !hasBlankLinesBetween(firstTokenOfLine, tokenAfter);
1673
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +01001674 /*
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
Yang Guo4fd355c2019-09-19 10:59:03 +02001685 // 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
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +01001694 // If the token matches the expected indentation, don't report it.
1695 if (validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine))) {
1696 return;
1697 }
1698
Yang Guo4fd355c2019-09-19 10:59:03 +02001699 // Otherwise, report the token/comment.
1700 report(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine));
1701 });
1702 }
1703 }
1704 );
1705 }
1706};