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