blob: 04f41db9e26128ea51146f26c34b9005874d41a5 [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 Lippe2c891972021-07-29 16:22:50 +01001180 offsets.setDesiredOffset(firstConsequentToken, firstToken, firstConsequentToken.type === "Punctuator" &&
Tim van der Lippe16aca392020-11-13 11:37:13 +00001181 options.offsetTernaryExpressions ? 2 : 1);
Yang Guo4fd355c2019-09-19 10:59:03 +02001182
1183 /*
1184 * The alternate and the consequent should usually have the same indentation.
1185 * If they share part of a line, align the alternate against the first token of the consequent.
1186 * This allows the alternate to be indented correctly in cases like this:
1187 * foo ? (
1188 * bar
1189 * ) : ( // this '(' is aligned with the '(' above, so it's considered to be aligned with `foo`
1190 * baz // as a result, `baz` is offset by 1 rather than 2
1191 * )
1192 */
1193 if (lastConsequentToken.loc.end.line === firstAlternateToken.loc.start.line) {
1194 offsets.setDesiredOffset(firstAlternateToken, firstConsequentToken, 0);
1195 } else {
1196
1197 /**
1198 * If the alternate and consequent do not share part of a line, offset the alternate from the first
1199 * token of the conditional expression. For example:
1200 * foo ? bar
1201 * : baz
1202 *
1203 * If `baz` were aligned with `bar` rather than being offset by 1 from `foo`, `baz` would end up
1204 * having no expected indentation.
1205 */
Tim van der Lippe2c891972021-07-29 16:22:50 +01001206 offsets.setDesiredOffset(firstAlternateToken, firstToken, firstAlternateToken.type === "Punctuator" &&
Tim van der Lippe16aca392020-11-13 11:37:13 +00001207 options.offsetTernaryExpressions ? 2 : 1);
Yang Guo4fd355c2019-09-19 10:59:03 +02001208 }
1209 }
1210 },
1211
1212 "DoWhileStatement, WhileStatement, ForInStatement, ForOfStatement": node => addBlocklessNodeIndent(node.body),
1213
1214 ExportNamedDeclaration(node) {
1215 if (node.declaration === null) {
1216 const closingCurly = sourceCode.getLastToken(node, astUtils.isClosingBraceToken);
1217
1218 // Indent the specifiers in `export {foo, bar, baz}`
1219 addElementListIndent(node.specifiers, sourceCode.getFirstToken(node, { skip: 1 }), closingCurly, 1);
1220
1221 if (node.source) {
1222
1223 // Indent everything after and including the `from` token in `export {foo, bar, baz} from 'qux'`
1224 offsets.setDesiredOffsets([closingCurly.range[1], node.range[1]], sourceCode.getFirstToken(node), 1);
1225 }
1226 }
1227 },
1228
1229 ForStatement(node) {
1230 const forOpeningParen = sourceCode.getFirstToken(node, 1);
1231
1232 if (node.init) {
1233 offsets.setDesiredOffsets(node.init.range, forOpeningParen, 1);
1234 }
1235 if (node.test) {
1236 offsets.setDesiredOffsets(node.test.range, forOpeningParen, 1);
1237 }
1238 if (node.update) {
1239 offsets.setDesiredOffsets(node.update.range, forOpeningParen, 1);
1240 }
1241 addBlocklessNodeIndent(node.body);
1242 },
1243
1244 "FunctionDeclaration, FunctionExpression"(node) {
1245 const closingParen = sourceCode.getTokenBefore(node.body);
1246 const openingParen = sourceCode.getTokenBefore(node.params.length ? node.params[0] : closingParen);
1247
1248 parameterParens.add(openingParen);
1249 parameterParens.add(closingParen);
1250 addElementListIndent(node.params, openingParen, closingParen, options[node.type].parameters);
1251 },
1252
1253 IfStatement(node) {
1254 addBlocklessNodeIndent(node.consequent);
1255 if (node.alternate && node.alternate.type !== "IfStatement") {
1256 addBlocklessNodeIndent(node.alternate);
1257 }
1258 },
1259
1260 ImportDeclaration(node) {
1261 if (node.specifiers.some(specifier => specifier.type === "ImportSpecifier")) {
1262 const openingCurly = sourceCode.getFirstToken(node, astUtils.isOpeningBraceToken);
1263 const closingCurly = sourceCode.getLastToken(node, astUtils.isClosingBraceToken);
1264
1265 addElementListIndent(node.specifiers.filter(specifier => specifier.type === "ImportSpecifier"), openingCurly, closingCurly, options.ImportDeclaration);
1266 }
1267
1268 const fromToken = sourceCode.getLastToken(node, token => token.type === "Identifier" && token.value === "from");
1269 const sourceToken = sourceCode.getLastToken(node, token => token.type === "String");
1270 const semiToken = sourceCode.getLastToken(node, token => token.type === "Punctuator" && token.value === ";");
1271
1272 if (fromToken) {
1273 const end = semiToken && semiToken.range[1] === sourceToken.range[1] ? node.range[1] : sourceToken.range[1];
1274
1275 offsets.setDesiredOffsets([fromToken.range[0], end], sourceCode.getFirstToken(node), 1);
1276 }
1277 },
1278
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +01001279 ImportExpression(node) {
1280 const openingParen = sourceCode.getFirstToken(node, 1);
1281 const closingParen = sourceCode.getLastToken(node);
1282
1283 parameterParens.add(openingParen);
1284 parameterParens.add(closingParen);
1285 offsets.setDesiredOffset(openingParen, sourceCode.getTokenBefore(openingParen), 0);
1286
1287 addElementListIndent([node.source], openingParen, closingParen, options.CallExpression.arguments);
1288 },
1289
Yang Guo4fd355c2019-09-19 10:59:03 +02001290 "MemberExpression, JSXMemberExpression, MetaProperty"(node) {
1291 const object = node.type === "MetaProperty" ? node.meta : node.object;
1292 const firstNonObjectToken = sourceCode.getFirstTokenBetween(object, node.property, astUtils.isNotClosingParenToken);
1293 const secondNonObjectToken = sourceCode.getTokenAfter(firstNonObjectToken);
1294
1295 const objectParenCount = sourceCode.getTokensBetween(object, node.property, { filter: astUtils.isClosingParenToken }).length;
1296 const firstObjectToken = objectParenCount
1297 ? sourceCode.getTokenBefore(object, { skip: objectParenCount - 1 })
1298 : sourceCode.getFirstToken(object);
1299 const lastObjectToken = sourceCode.getTokenBefore(firstNonObjectToken);
1300 const firstPropertyToken = node.computed ? firstNonObjectToken : secondNonObjectToken;
1301
1302 if (node.computed) {
1303
1304 // For computed MemberExpressions, match the closing bracket with the opening bracket.
1305 offsets.setDesiredOffset(sourceCode.getLastToken(node), firstNonObjectToken, 0);
1306 offsets.setDesiredOffsets(node.property.range, firstNonObjectToken, 1);
1307 }
1308
1309 /*
1310 * If the object ends on the same line that the property starts, match against the last token
1311 * of the object, to ensure that the MemberExpression is not indented.
1312 *
1313 * Otherwise, match against the first token of the object, e.g.
1314 * foo
1315 * .bar
1316 * .baz // <-- offset by 1 from `foo`
1317 */
1318 const offsetBase = lastObjectToken.loc.end.line === firstPropertyToken.loc.start.line
1319 ? lastObjectToken
1320 : firstObjectToken;
1321
1322 if (typeof options.MemberExpression === "number") {
1323
1324 // Match the dot (for non-computed properties) or the opening bracket (for computed properties) against the object.
1325 offsets.setDesiredOffset(firstNonObjectToken, offsetBase, options.MemberExpression);
1326
1327 /*
1328 * For computed MemberExpressions, match the first token of the property against the opening bracket.
1329 * Otherwise, match the first token of the property against the object.
1330 */
1331 offsets.setDesiredOffset(secondNonObjectToken, node.computed ? firstNonObjectToken : offsetBase, options.MemberExpression);
1332 } else {
1333
1334 // If the MemberExpression option is off, ignore the dot and the first token of the property.
1335 offsets.ignoreToken(firstNonObjectToken);
1336 offsets.ignoreToken(secondNonObjectToken);
1337
1338 // To ignore the property indentation, ensure that the property tokens depend on the ignored tokens.
1339 offsets.setDesiredOffset(firstNonObjectToken, offsetBase, 0);
1340 offsets.setDesiredOffset(secondNonObjectToken, firstNonObjectToken, 0);
1341 }
1342 },
1343
1344 NewExpression(node) {
1345
1346 // Only indent the arguments if the NewExpression has parens (e.g. `new Foo(bar)` or `new Foo()`, but not `new Foo`
1347 if (node.arguments.length > 0 ||
1348 astUtils.isClosingParenToken(sourceCode.getLastToken(node)) &&
1349 astUtils.isOpeningParenToken(sourceCode.getLastToken(node, 1))) {
1350 addFunctionCallIndent(node);
1351 }
1352 },
1353
1354 Property(node) {
1355 if (!node.shorthand && !node.method && node.kind === "init") {
1356 const colon = sourceCode.getFirstTokenBetween(node.key, node.value, astUtils.isColonToken);
1357
1358 offsets.ignoreToken(sourceCode.getTokenAfter(colon));
1359 }
1360 },
1361
1362 SwitchStatement(node) {
1363 const openingCurly = sourceCode.getTokenAfter(node.discriminant, astUtils.isOpeningBraceToken);
1364 const closingCurly = sourceCode.getLastToken(node);
1365
1366 offsets.setDesiredOffsets([openingCurly.range[1], closingCurly.range[0]], openingCurly, options.SwitchCase);
1367
1368 if (node.cases.length) {
1369 sourceCode.getTokensBetween(
1370 node.cases[node.cases.length - 1],
1371 closingCurly,
1372 { includeComments: true, filter: astUtils.isCommentToken }
1373 ).forEach(token => offsets.ignoreToken(token));
1374 }
1375 },
1376
1377 SwitchCase(node) {
1378 if (!(node.consequent.length === 1 && node.consequent[0].type === "BlockStatement")) {
1379 const caseKeyword = sourceCode.getFirstToken(node);
1380 const tokenAfterCurrentCase = sourceCode.getTokenAfter(node);
1381
1382 offsets.setDesiredOffsets([caseKeyword.range[1], tokenAfterCurrentCase.range[0]], caseKeyword, 1);
1383 }
1384 },
1385
1386 TemplateLiteral(node) {
1387 node.expressions.forEach((expression, index) => {
1388 const previousQuasi = node.quasis[index];
1389 const nextQuasi = node.quasis[index + 1];
1390 const tokenToAlignFrom = previousQuasi.loc.start.line === previousQuasi.loc.end.line
1391 ? sourceCode.getFirstToken(previousQuasi)
1392 : null;
1393
1394 offsets.setDesiredOffsets([previousQuasi.range[1], nextQuasi.range[0]], tokenToAlignFrom, 1);
1395 offsets.setDesiredOffset(sourceCode.getFirstToken(nextQuasi), tokenToAlignFrom, 0);
1396 });
1397 },
1398
1399 VariableDeclaration(node) {
1400 let variableIndent = Object.prototype.hasOwnProperty.call(options.VariableDeclarator, node.kind)
1401 ? options.VariableDeclarator[node.kind]
1402 : DEFAULT_VARIABLE_INDENT;
1403
1404 const firstToken = sourceCode.getFirstToken(node),
1405 lastToken = sourceCode.getLastToken(node);
1406
1407 if (options.VariableDeclarator[node.kind] === "first") {
1408 if (node.declarations.length > 1) {
1409 addElementListIndent(
1410 node.declarations,
1411 firstToken,
1412 lastToken,
1413 "first"
1414 );
1415 return;
1416 }
1417
1418 variableIndent = DEFAULT_VARIABLE_INDENT;
1419 }
1420
1421 if (node.declarations[node.declarations.length - 1].loc.start.line > node.loc.start.line) {
1422
1423 /*
1424 * VariableDeclarator indentation is a bit different from other forms of indentation, in that the
1425 * indentation of an opening bracket sometimes won't match that of a closing bracket. For example,
1426 * the following indentations are correct:
1427 *
1428 * var foo = {
1429 * ok: true
1430 * };
1431 *
1432 * var foo = {
1433 * ok: true,
1434 * },
1435 * bar = 1;
1436 *
1437 * Account for when exiting the AST (after indentations have already been set for the nodes in
1438 * the declaration) by manually increasing the indentation level of the tokens in this declarator
1439 * on the same line as the start of the declaration, provided that there are declarators that
1440 * follow this one.
1441 */
1442 offsets.setDesiredOffsets(node.range, firstToken, variableIndent, true);
1443 } else {
1444 offsets.setDesiredOffsets(node.range, firstToken, variableIndent);
1445 }
1446
1447 if (astUtils.isSemicolonToken(lastToken)) {
1448 offsets.ignoreToken(lastToken);
1449 }
1450 },
1451
1452 VariableDeclarator(node) {
1453 if (node.init) {
1454 const equalOperator = sourceCode.getTokenBefore(node.init, astUtils.isNotOpeningParenToken);
1455 const tokenAfterOperator = sourceCode.getTokenAfter(equalOperator);
1456
1457 offsets.ignoreToken(equalOperator);
1458 offsets.ignoreToken(tokenAfterOperator);
1459 offsets.setDesiredOffsets([tokenAfterOperator.range[0], node.range[1]], equalOperator, 1);
1460 offsets.setDesiredOffset(equalOperator, sourceCode.getLastToken(node.id), 0);
1461 }
1462 },
1463
1464 "JSXAttribute[value]"(node) {
1465 const equalsToken = sourceCode.getFirstTokenBetween(node.name, node.value, token => token.type === "Punctuator" && token.value === "=");
1466
1467 offsets.setDesiredOffsets([equalsToken.range[0], node.value.range[1]], sourceCode.getFirstToken(node.name), 1);
1468 },
1469
1470 JSXElement(node) {
1471 if (node.closingElement) {
1472 addElementListIndent(node.children, sourceCode.getFirstToken(node.openingElement), sourceCode.getFirstToken(node.closingElement), 1);
1473 }
1474 },
1475
1476 JSXOpeningElement(node) {
1477 const firstToken = sourceCode.getFirstToken(node);
1478 let closingToken;
1479
1480 if (node.selfClosing) {
1481 closingToken = sourceCode.getLastToken(node, { skip: 1 });
1482 offsets.setDesiredOffset(sourceCode.getLastToken(node), closingToken, 0);
1483 } else {
1484 closingToken = sourceCode.getLastToken(node);
1485 }
1486 offsets.setDesiredOffsets(node.name.range, sourceCode.getFirstToken(node));
1487 addElementListIndent(node.attributes, firstToken, closingToken, 1);
1488 },
1489
1490 JSXClosingElement(node) {
1491 const firstToken = sourceCode.getFirstToken(node);
1492
1493 offsets.setDesiredOffsets(node.name.range, firstToken, 1);
1494 },
1495
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +01001496 JSXFragment(node) {
1497 const firstOpeningToken = sourceCode.getFirstToken(node.openingFragment);
1498 const firstClosingToken = sourceCode.getFirstToken(node.closingFragment);
1499
1500 addElementListIndent(node.children, firstOpeningToken, firstClosingToken, 1);
1501 },
1502
1503 JSXOpeningFragment(node) {
1504 const firstToken = sourceCode.getFirstToken(node);
1505 const closingToken = sourceCode.getLastToken(node);
1506
1507 offsets.setDesiredOffsets(node.range, firstToken, 1);
1508 offsets.matchOffsetOf(firstToken, closingToken);
1509 },
1510
1511 JSXClosingFragment(node) {
1512 const firstToken = sourceCode.getFirstToken(node);
1513 const slashToken = sourceCode.getLastToken(node, { skip: 1 });
1514 const closingToken = sourceCode.getLastToken(node);
1515 const tokenToMatch = astUtils.isTokenOnSameLine(slashToken, closingToken) ? slashToken : closingToken;
1516
1517 offsets.setDesiredOffsets(node.range, firstToken, 1);
1518 offsets.matchOffsetOf(firstToken, tokenToMatch);
1519 },
1520
Yang Guo4fd355c2019-09-19 10:59:03 +02001521 JSXExpressionContainer(node) {
1522 const openingCurly = sourceCode.getFirstToken(node);
1523 const closingCurly = sourceCode.getLastToken(node);
1524
1525 offsets.setDesiredOffsets(
1526 [openingCurly.range[1], closingCurly.range[0]],
1527 openingCurly,
1528 1
1529 );
1530 },
1531
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +01001532 JSXSpreadAttribute(node) {
1533 const openingCurly = sourceCode.getFirstToken(node);
1534 const closingCurly = sourceCode.getLastToken(node);
1535
1536 offsets.setDesiredOffsets(
1537 [openingCurly.range[1], closingCurly.range[0]],
1538 openingCurly,
1539 1
1540 );
1541 },
1542
Yang Guo4fd355c2019-09-19 10:59:03 +02001543 "*"(node) {
1544 const firstToken = sourceCode.getFirstToken(node);
1545
1546 // Ensure that the children of every node are indented at least as much as the first token.
1547 if (firstToken && !ignoredNodeFirstTokens.has(firstToken)) {
1548 offsets.setDesiredOffsets(node.range, firstToken, 0);
1549 }
1550 }
1551 };
1552
1553 const listenerCallQueue = [];
1554
1555 /*
1556 * To ignore the indentation of a node:
1557 * 1. Don't call the node's listener when entering it (if it has a listener)
1558 * 2. Don't set any offsets against the first token of the node.
1559 * 3. Call `ignoreNode` on the node sometime after exiting it and before validating offsets.
1560 */
Simon Zünd52e20202021-06-16 08:34:28 +02001561 const offsetListeners = {};
1562
1563 for (const [selector, listener] of Object.entries(baseOffsetListeners)) {
Yang Guo4fd355c2019-09-19 10:59:03 +02001564
1565 /*
1566 * Offset listener calls are deferred until traversal is finished, and are called as
1567 * part of the final `Program:exit` listener. This is necessary because a node might
1568 * be matched by multiple selectors.
1569 *
1570 * Example: Suppose there is an offset listener for `Identifier`, and the user has
1571 * specified in configuration that `MemberExpression > Identifier` should be ignored.
1572 * Due to selector specificity rules, the `Identifier` listener will get called first. However,
1573 * if a given Identifier node is supposed to be ignored, then the `Identifier` offset listener
1574 * should not have been called at all. Without doing extra selector matching, we don't know
1575 * whether the Identifier matches the `MemberExpression > Identifier` selector until the
1576 * `MemberExpression > Identifier` listener is called.
1577 *
1578 * To avoid this, the `Identifier` listener isn't called until traversal finishes and all
1579 * ignored nodes are known.
1580 */
Simon Zünd52e20202021-06-16 08:34:28 +02001581 offsetListeners[selector] = node => listenerCallQueue.push({ listener, node });
1582 }
Yang Guo4fd355c2019-09-19 10:59:03 +02001583
1584 // For each ignored node selector, set up a listener to collect it into the `ignoredNodes` set.
1585 const ignoredNodes = new Set();
1586
1587 /**
1588 * Ignores a node
1589 * @param {ASTNode} node The node to ignore
1590 * @returns {void}
1591 */
1592 function addToIgnoredNodes(node) {
1593 ignoredNodes.add(node);
1594 ignoredNodeFirstTokens.add(sourceCode.getFirstToken(node));
1595 }
1596
1597 const ignoredNodeListeners = options.ignoredNodes.reduce(
1598 (listeners, ignoredSelector) => Object.assign(listeners, { [ignoredSelector]: addToIgnoredNodes }),
1599 {}
1600 );
1601
1602 /*
1603 * Join the listeners, and add a listener to verify that all tokens actually have the correct indentation
1604 * at the end.
1605 *
1606 * Using Object.assign will cause some offset listeners to be overwritten if the same selector also appears
1607 * in `ignoredNodeListeners`. This isn't a problem because all of the matching nodes will be ignored,
1608 * so those listeners wouldn't be called anyway.
1609 */
1610 return Object.assign(
1611 offsetListeners,
1612 ignoredNodeListeners,
1613 {
1614 "*:exit"(node) {
1615
1616 // If a node's type is nonstandard, we can't tell how its children should be offset, so ignore it.
1617 if (!KNOWN_NODES.has(node.type)) {
1618 addToIgnoredNodes(node);
1619 }
1620 },
1621 "Program:exit"() {
1622
1623 // If ignoreComments option is enabled, ignore all comment tokens.
1624 if (options.ignoreComments) {
1625 sourceCode.getAllComments()
1626 .forEach(comment => offsets.ignoreToken(comment));
1627 }
1628
1629 // Invoke the queued offset listeners for the nodes that aren't ignored.
1630 listenerCallQueue
1631 .filter(nodeInfo => !ignoredNodes.has(nodeInfo.node))
1632 .forEach(nodeInfo => nodeInfo.listener(nodeInfo.node));
1633
1634 // Update the offsets for ignored nodes to prevent their child tokens from being reported.
1635 ignoredNodes.forEach(ignoreNode);
1636
1637 addParensIndent(sourceCode.ast.tokens);
1638
1639 /*
1640 * Create a Map from (tokenOrComment) => (precedingToken).
1641 * This is necessary because sourceCode.getTokenBefore does not handle a comment as an argument correctly.
1642 */
1643 const precedingTokens = sourceCode.ast.comments.reduce((commentMap, comment) => {
1644 const tokenOrCommentBefore = sourceCode.getTokenBefore(comment, { includeComments: true });
1645
1646 return commentMap.set(comment, commentMap.has(tokenOrCommentBefore) ? commentMap.get(tokenOrCommentBefore) : tokenOrCommentBefore);
1647 }, new WeakMap());
1648
1649 sourceCode.lines.forEach((line, lineIndex) => {
1650 const lineNumber = lineIndex + 1;
1651
1652 if (!tokenInfo.firstTokensByLineNumber.has(lineNumber)) {
1653
1654 // Don't check indentation on blank lines
1655 return;
1656 }
1657
1658 const firstTokenOfLine = tokenInfo.firstTokensByLineNumber.get(lineNumber);
1659
1660 if (firstTokenOfLine.loc.start.line !== lineNumber) {
1661
1662 // Don't check the indentation of multi-line tokens (e.g. template literals or block comments) twice.
1663 return;
1664 }
1665
Yang Guo4fd355c2019-09-19 10:59:03 +02001666 if (astUtils.isCommentToken(firstTokenOfLine)) {
1667 const tokenBefore = precedingTokens.get(firstTokenOfLine);
1668 const tokenAfter = tokenBefore ? sourceCode.getTokenAfter(tokenBefore) : sourceCode.ast.tokens[0];
Yang Guo4fd355c2019-09-19 10:59:03 +02001669 const mayAlignWithBefore = tokenBefore && !hasBlankLinesBetween(tokenBefore, firstTokenOfLine);
1670 const mayAlignWithAfter = tokenAfter && !hasBlankLinesBetween(firstTokenOfLine, tokenAfter);
1671
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +01001672 /*
1673 * If a comment precedes a line that begins with a semicolon token, align to that token, i.e.
1674 *
1675 * let foo
1676 * // comment
1677 * ;(async () => {})()
1678 */
1679 if (tokenAfter && astUtils.isSemicolonToken(tokenAfter) && !astUtils.isTokenOnSameLine(firstTokenOfLine, tokenAfter)) {
1680 offsets.setDesiredOffset(firstTokenOfLine, tokenAfter, 0);
1681 }
1682
Yang Guo4fd355c2019-09-19 10:59:03 +02001683 // If a comment matches the expected indentation of the token immediately before or after, don't report it.
1684 if (
1685 mayAlignWithBefore && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenBefore)) ||
1686 mayAlignWithAfter && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenAfter))
1687 ) {
1688 return;
1689 }
1690 }
1691
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +01001692 // If the token matches the expected indentation, don't report it.
1693 if (validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine))) {
1694 return;
1695 }
1696
Yang Guo4fd355c2019-09-19 10:59:03 +02001697 // Otherwise, report the token/comment.
1698 report(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine));
1699 });
1700 }
1701 }
1702 );
1703 }
1704};