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