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