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