1/** 2 * @fileoverview This option sets a specific tab width for your code 3 * 4 * This rule has been ported and modified from nodeca. 5 * @author Vitaly Puzrin 6 * @author Gyandeep Singh 7 */ 8 9"use strict"; 10 11//------------------------------------------------------------------------------ 12// Requirements 13//------------------------------------------------------------------------------ 14 15const astUtils = require("./utils/ast-utils"); 16 17//------------------------------------------------------------------------------ 18// Rule Definition 19//------------------------------------------------------------------------------ 20 21/* istanbul ignore next: this rule has known coverage issues, but it's deprecated and shouldn't be updated in the future anyway. */ 22module.exports = { 23 meta: { 24 type: "layout", 25 26 docs: { 27 description: "enforce consistent indentation", 28 category: "Stylistic Issues", 29 recommended: false, 30 url: "https://eslint.org/docs/rules/indent-legacy" 31 }, 32 33 deprecated: true, 34 35 replacedBy: ["indent"], 36 37 fixable: "whitespace", 38 39 schema: [ 40 { 41 oneOf: [ 42 { 43 enum: ["tab"] 44 }, 45 { 46 type: "integer", 47 minimum: 0 48 } 49 ] 50 }, 51 { 52 type: "object", 53 properties: { 54 SwitchCase: { 55 type: "integer", 56 minimum: 0 57 }, 58 VariableDeclarator: { 59 oneOf: [ 60 { 61 type: "integer", 62 minimum: 0 63 }, 64 { 65 type: "object", 66 properties: { 67 var: { 68 type: "integer", 69 minimum: 0 70 }, 71 let: { 72 type: "integer", 73 minimum: 0 74 }, 75 const: { 76 type: "integer", 77 minimum: 0 78 } 79 } 80 } 81 ] 82 }, 83 outerIIFEBody: { 84 type: "integer", 85 minimum: 0 86 }, 87 MemberExpression: { 88 type: "integer", 89 minimum: 0 90 }, 91 FunctionDeclaration: { 92 type: "object", 93 properties: { 94 parameters: { 95 oneOf: [ 96 { 97 type: "integer", 98 minimum: 0 99 }, 100 { 101 enum: ["first"] 102 } 103 ] 104 }, 105 body: { 106 type: "integer", 107 minimum: 0 108 } 109 } 110 }, 111 FunctionExpression: { 112 type: "object", 113 properties: { 114 parameters: { 115 oneOf: [ 116 { 117 type: "integer", 118 minimum: 0 119 }, 120 { 121 enum: ["first"] 122 } 123 ] 124 }, 125 body: { 126 type: "integer", 127 minimum: 0 128 } 129 } 130 }, 131 CallExpression: { 132 type: "object", 133 properties: { 134 parameters: { 135 oneOf: [ 136 { 137 type: "integer", 138 minimum: 0 139 }, 140 { 141 enum: ["first"] 142 } 143 ] 144 } 145 } 146 }, 147 ArrayExpression: { 148 oneOf: [ 149 { 150 type: "integer", 151 minimum: 0 152 }, 153 { 154 enum: ["first"] 155 } 156 ] 157 }, 158 ObjectExpression: { 159 oneOf: [ 160 { 161 type: "integer", 162 minimum: 0 163 }, 164 { 165 enum: ["first"] 166 } 167 ] 168 } 169 }, 170 additionalProperties: false 171 } 172 ], 173 messages: { 174 expected: "Expected indentation of {{expected}} but found {{actual}}." 175 } 176 }, 177 178 create(context) { 179 const DEFAULT_VARIABLE_INDENT = 1; 180 const DEFAULT_PARAMETER_INDENT = null; // For backwards compatibility, don't check parameter indentation unless specified in the config 181 const DEFAULT_FUNCTION_BODY_INDENT = 1; 182 183 let indentType = "space"; 184 let indentSize = 4; 185 const options = { 186 SwitchCase: 0, 187 VariableDeclarator: { 188 var: DEFAULT_VARIABLE_INDENT, 189 let: DEFAULT_VARIABLE_INDENT, 190 const: DEFAULT_VARIABLE_INDENT 191 }, 192 outerIIFEBody: null, 193 FunctionDeclaration: { 194 parameters: DEFAULT_PARAMETER_INDENT, 195 body: DEFAULT_FUNCTION_BODY_INDENT 196 }, 197 FunctionExpression: { 198 parameters: DEFAULT_PARAMETER_INDENT, 199 body: DEFAULT_FUNCTION_BODY_INDENT 200 }, 201 CallExpression: { 202 arguments: DEFAULT_PARAMETER_INDENT 203 }, 204 ArrayExpression: 1, 205 ObjectExpression: 1 206 }; 207 208 const sourceCode = context.getSourceCode(); 209 210 if (context.options.length) { 211 if (context.options[0] === "tab") { 212 indentSize = 1; 213 indentType = "tab"; 214 } else /* istanbul ignore else : this will be caught by options validation */ if (typeof context.options[0] === "number") { 215 indentSize = context.options[0]; 216 indentType = "space"; 217 } 218 219 if (context.options[1]) { 220 const opts = context.options[1]; 221 222 options.SwitchCase = opts.SwitchCase || 0; 223 const variableDeclaratorRules = opts.VariableDeclarator; 224 225 if (typeof variableDeclaratorRules === "number") { 226 options.VariableDeclarator = { 227 var: variableDeclaratorRules, 228 let: variableDeclaratorRules, 229 const: variableDeclaratorRules 230 }; 231 } else if (typeof variableDeclaratorRules === "object") { 232 Object.assign(options.VariableDeclarator, variableDeclaratorRules); 233 } 234 235 if (typeof opts.outerIIFEBody === "number") { 236 options.outerIIFEBody = opts.outerIIFEBody; 237 } 238 239 if (typeof opts.MemberExpression === "number") { 240 options.MemberExpression = opts.MemberExpression; 241 } 242 243 if (typeof opts.FunctionDeclaration === "object") { 244 Object.assign(options.FunctionDeclaration, opts.FunctionDeclaration); 245 } 246 247 if (typeof opts.FunctionExpression === "object") { 248 Object.assign(options.FunctionExpression, opts.FunctionExpression); 249 } 250 251 if (typeof opts.CallExpression === "object") { 252 Object.assign(options.CallExpression, opts.CallExpression); 253 } 254 255 if (typeof opts.ArrayExpression === "number" || typeof opts.ArrayExpression === "string") { 256 options.ArrayExpression = opts.ArrayExpression; 257 } 258 259 if (typeof opts.ObjectExpression === "number" || typeof opts.ObjectExpression === "string") { 260 options.ObjectExpression = opts.ObjectExpression; 261 } 262 } 263 } 264 265 const caseIndentStore = {}; 266 267 /** 268 * Creates an error message for a line, given the expected/actual indentation. 269 * @param {int} expectedAmount The expected amount of indentation characters for this line 270 * @param {int} actualSpaces The actual number of indentation spaces that were found on this line 271 * @param {int} actualTabs The actual number of indentation tabs that were found on this line 272 * @returns {string} An error message for this line 273 */ 274 function createErrorMessageData(expectedAmount, actualSpaces, actualTabs) { 275 const expectedStatement = `${expectedAmount} ${indentType}${expectedAmount === 1 ? "" : "s"}`; // e.g. "2 tabs" 276 const foundSpacesWord = `space${actualSpaces === 1 ? "" : "s"}`; // e.g. "space" 277 const foundTabsWord = `tab${actualTabs === 1 ? "" : "s"}`; // e.g. "tabs" 278 let foundStatement; 279 280 if (actualSpaces > 0 && actualTabs > 0) { 281 foundStatement = `${actualSpaces} ${foundSpacesWord} and ${actualTabs} ${foundTabsWord}`; // e.g. "1 space and 2 tabs" 282 } else if (actualSpaces > 0) { 283 284 /* 285 * Abbreviate the message if the expected indentation is also spaces. 286 * e.g. 'Expected 4 spaces but found 2' rather than 'Expected 4 spaces but found 2 spaces' 287 */ 288 foundStatement = indentType === "space" ? actualSpaces : `${actualSpaces} ${foundSpacesWord}`; 289 } else if (actualTabs > 0) { 290 foundStatement = indentType === "tab" ? actualTabs : `${actualTabs} ${foundTabsWord}`; 291 } else { 292 foundStatement = "0"; 293 } 294 return { 295 expected: expectedStatement, 296 actual: foundStatement 297 }; 298 } 299 300 /** 301 * Reports a given indent violation 302 * @param {ASTNode} node Node violating the indent rule 303 * @param {int} needed Expected indentation character count 304 * @param {int} gottenSpaces Indentation space count in the actual node/code 305 * @param {int} gottenTabs Indentation tab count in the actual node/code 306 * @param {Object} [loc] Error line and column location 307 * @param {boolean} isLastNodeCheck Is the error for last node check 308 * @returns {void} 309 */ 310 function report(node, needed, gottenSpaces, gottenTabs, loc, isLastNodeCheck) { 311 if (gottenSpaces && gottenTabs) { 312 313 // To avoid conflicts with `no-mixed-spaces-and-tabs`, don't report lines that have both spaces and tabs. 314 return; 315 } 316 317 const desiredIndent = (indentType === "space" ? " " : "\t").repeat(needed); 318 319 const textRange = isLastNodeCheck 320 ? [node.range[1] - node.loc.end.column, node.range[1] - node.loc.end.column + gottenSpaces + gottenTabs] 321 : [node.range[0] - node.loc.start.column, node.range[0] - node.loc.start.column + gottenSpaces + gottenTabs]; 322 323 context.report({ 324 node, 325 loc, 326 messageId: "expected", 327 data: createErrorMessageData(needed, gottenSpaces, gottenTabs), 328 fix: fixer => fixer.replaceTextRange(textRange, desiredIndent) 329 }); 330 } 331 332 /** 333 * Get the actual indent of node 334 * @param {ASTNode|Token} node Node to examine 335 * @param {boolean} [byLastLine=false] get indent of node's last line 336 * @returns {Object} The node's indent. Contains keys `space` and `tab`, representing the indent of each character. Also 337 * contains keys `goodChar` and `badChar`, where `goodChar` is the amount of the user's desired indentation character, and 338 * `badChar` is the amount of the other indentation character. 339 */ 340 function getNodeIndent(node, byLastLine) { 341 const token = byLastLine ? sourceCode.getLastToken(node) : sourceCode.getFirstToken(node); 342 const srcCharsBeforeNode = sourceCode.getText(token, token.loc.start.column).split(""); 343 const indentChars = srcCharsBeforeNode.slice(0, srcCharsBeforeNode.findIndex(char => char !== " " && char !== "\t")); 344 const spaces = indentChars.filter(char => char === " ").length; 345 const tabs = indentChars.filter(char => char === "\t").length; 346 347 return { 348 space: spaces, 349 tab: tabs, 350 goodChar: indentType === "space" ? spaces : tabs, 351 badChar: indentType === "space" ? tabs : spaces 352 }; 353 } 354 355 /** 356 * Checks node is the first in its own start line. By default it looks by start line. 357 * @param {ASTNode} node The node to check 358 * @param {boolean} [byEndLocation=false] Lookup based on start position or end 359 * @returns {boolean} true if its the first in the its start line 360 */ 361 function isNodeFirstInLine(node, byEndLocation) { 362 const firstToken = byEndLocation === true ? sourceCode.getLastToken(node, 1) : sourceCode.getTokenBefore(node), 363 startLine = byEndLocation === true ? node.loc.end.line : node.loc.start.line, 364 endLine = firstToken ? firstToken.loc.end.line : -1; 365 366 return startLine !== endLine; 367 } 368 369 /** 370 * Check indent for node 371 * @param {ASTNode} node Node to check 372 * @param {int} neededIndent needed indent 373 * @returns {void} 374 */ 375 function checkNodeIndent(node, neededIndent) { 376 const actualIndent = getNodeIndent(node, false); 377 378 if ( 379 node.type !== "ArrayExpression" && 380 node.type !== "ObjectExpression" && 381 (actualIndent.goodChar !== neededIndent || actualIndent.badChar !== 0) && 382 isNodeFirstInLine(node) 383 ) { 384 report(node, neededIndent, actualIndent.space, actualIndent.tab); 385 } 386 387 if (node.type === "IfStatement" && node.alternate) { 388 const elseToken = sourceCode.getTokenBefore(node.alternate); 389 390 checkNodeIndent(elseToken, neededIndent); 391 392 if (!isNodeFirstInLine(node.alternate)) { 393 checkNodeIndent(node.alternate, neededIndent); 394 } 395 } 396 397 if (node.type === "TryStatement" && node.handler) { 398 const catchToken = sourceCode.getFirstToken(node.handler); 399 400 checkNodeIndent(catchToken, neededIndent); 401 } 402 403 if (node.type === "TryStatement" && node.finalizer) { 404 const finallyToken = sourceCode.getTokenBefore(node.finalizer); 405 406 checkNodeIndent(finallyToken, neededIndent); 407 } 408 409 if (node.type === "DoWhileStatement") { 410 const whileToken = sourceCode.getTokenAfter(node.body); 411 412 checkNodeIndent(whileToken, neededIndent); 413 } 414 } 415 416 /** 417 * Check indent for nodes list 418 * @param {ASTNode[]} nodes list of node objects 419 * @param {int} indent needed indent 420 * @returns {void} 421 */ 422 function checkNodesIndent(nodes, indent) { 423 nodes.forEach(node => checkNodeIndent(node, indent)); 424 } 425 426 /** 427 * Check last node line indent this detects, that block closed correctly 428 * @param {ASTNode} node Node to examine 429 * @param {int} lastLineIndent needed indent 430 * @returns {void} 431 */ 432 function checkLastNodeLineIndent(node, lastLineIndent) { 433 const lastToken = sourceCode.getLastToken(node); 434 const endIndent = getNodeIndent(lastToken, true); 435 436 if ((endIndent.goodChar !== lastLineIndent || endIndent.badChar !== 0) && isNodeFirstInLine(node, true)) { 437 report( 438 node, 439 lastLineIndent, 440 endIndent.space, 441 endIndent.tab, 442 { line: lastToken.loc.start.line, column: lastToken.loc.start.column }, 443 true 444 ); 445 } 446 } 447 448 /** 449 * Check last node line indent this detects, that block closed correctly 450 * This function for more complicated return statement case, where closing parenthesis may be followed by ';' 451 * @param {ASTNode} node Node to examine 452 * @param {int} firstLineIndent first line needed indent 453 * @returns {void} 454 */ 455 function checkLastReturnStatementLineIndent(node, firstLineIndent) { 456 457 /* 458 * in case if return statement ends with ');' we have traverse back to ')' 459 * otherwise we'll measure indent for ';' and replace ')' 460 */ 461 const lastToken = sourceCode.getLastToken(node, astUtils.isClosingParenToken); 462 const textBeforeClosingParenthesis = sourceCode.getText(lastToken, lastToken.loc.start.column).slice(0, -1); 463 464 if (textBeforeClosingParenthesis.trim()) { 465 466 // There are tokens before the closing paren, don't report this case 467 return; 468 } 469 470 const endIndent = getNodeIndent(lastToken, true); 471 472 if (endIndent.goodChar !== firstLineIndent) { 473 report( 474 node, 475 firstLineIndent, 476 endIndent.space, 477 endIndent.tab, 478 { line: lastToken.loc.start.line, column: lastToken.loc.start.column }, 479 true 480 ); 481 } 482 } 483 484 /** 485 * Check first node line indent is correct 486 * @param {ASTNode} node Node to examine 487 * @param {int} firstLineIndent needed indent 488 * @returns {void} 489 */ 490 function checkFirstNodeLineIndent(node, firstLineIndent) { 491 const startIndent = getNodeIndent(node, false); 492 493 if ((startIndent.goodChar !== firstLineIndent || startIndent.badChar !== 0) && isNodeFirstInLine(node)) { 494 report( 495 node, 496 firstLineIndent, 497 startIndent.space, 498 startIndent.tab, 499 { line: node.loc.start.line, column: node.loc.start.column } 500 ); 501 } 502 } 503 504 /** 505 * Returns a parent node of given node based on a specified type 506 * if not present then return null 507 * @param {ASTNode} node node to examine 508 * @param {string} type type that is being looked for 509 * @param {string} stopAtList end points for the evaluating code 510 * @returns {ASTNode|void} if found then node otherwise null 511 */ 512 function getParentNodeByType(node, type, stopAtList) { 513 let parent = node.parent; 514 const stopAtSet = new Set(stopAtList || ["Program"]); 515 516 while (parent.type !== type && !stopAtSet.has(parent.type) && parent.type !== "Program") { 517 parent = parent.parent; 518 } 519 520 return parent.type === type ? parent : null; 521 } 522 523 /** 524 * Returns the VariableDeclarator based on the current node 525 * if not present then return null 526 * @param {ASTNode} node node to examine 527 * @returns {ASTNode|void} if found then node otherwise null 528 */ 529 function getVariableDeclaratorNode(node) { 530 return getParentNodeByType(node, "VariableDeclarator"); 531 } 532 533 /** 534 * Check to see if the node is part of the multi-line variable declaration. 535 * Also if its on the same line as the varNode 536 * @param {ASTNode} node node to check 537 * @param {ASTNode} varNode variable declaration node to check against 538 * @returns {boolean} True if all the above condition satisfy 539 */ 540 function isNodeInVarOnTop(node, varNode) { 541 return varNode && 542 varNode.parent.loc.start.line === node.loc.start.line && 543 varNode.parent.declarations.length > 1; 544 } 545 546 /** 547 * Check to see if the argument before the callee node is multi-line and 548 * there should only be 1 argument before the callee node 549 * @param {ASTNode} node node to check 550 * @returns {boolean} True if arguments are multi-line 551 */ 552 function isArgBeforeCalleeNodeMultiline(node) { 553 const parent = node.parent; 554 555 if (parent.arguments.length >= 2 && parent.arguments[1] === node) { 556 return parent.arguments[0].loc.end.line > parent.arguments[0].loc.start.line; 557 } 558 559 return false; 560 } 561 562 /** 563 * Check to see if the node is a file level IIFE 564 * @param {ASTNode} node The function node to check. 565 * @returns {boolean} True if the node is the outer IIFE 566 */ 567 function isOuterIIFE(node) { 568 const parent = node.parent; 569 let stmt = parent.parent; 570 571 /* 572 * Verify that the node is an IIEF 573 */ 574 if ( 575 parent.type !== "CallExpression" || 576 parent.callee !== node) { 577 578 return false; 579 } 580 581 /* 582 * Navigate legal ancestors to determine whether this IIEF is outer 583 */ 584 while ( 585 stmt.type === "UnaryExpression" && ( 586 stmt.operator === "!" || 587 stmt.operator === "~" || 588 stmt.operator === "+" || 589 stmt.operator === "-") || 590 stmt.type === "AssignmentExpression" || 591 stmt.type === "LogicalExpression" || 592 stmt.type === "SequenceExpression" || 593 stmt.type === "VariableDeclarator") { 594 595 stmt = stmt.parent; 596 } 597 598 return (( 599 stmt.type === "ExpressionStatement" || 600 stmt.type === "VariableDeclaration") && 601 stmt.parent && stmt.parent.type === "Program" 602 ); 603 } 604 605 /** 606 * Check indent for function block content 607 * @param {ASTNode} node A BlockStatement node that is inside of a function. 608 * @returns {void} 609 */ 610 function checkIndentInFunctionBlock(node) { 611 612 /* 613 * Search first caller in chain. 614 * Ex.: 615 * 616 * Models <- Identifier 617 * .User 618 * .find() 619 * .exec(function() { 620 * // function body 621 * }); 622 * 623 * Looks for 'Models' 624 */ 625 const calleeNode = node.parent; // FunctionExpression 626 let indent; 627 628 if (calleeNode.parent && 629 (calleeNode.parent.type === "Property" || 630 calleeNode.parent.type === "ArrayExpression")) { 631 632 // If function is part of array or object, comma can be put at left 633 indent = getNodeIndent(calleeNode, false).goodChar; 634 } else { 635 636 // If function is standalone, simple calculate indent 637 indent = getNodeIndent(calleeNode).goodChar; 638 } 639 640 if (calleeNode.parent.type === "CallExpression") { 641 const calleeParent = calleeNode.parent; 642 643 if (calleeNode.type !== "FunctionExpression" && calleeNode.type !== "ArrowFunctionExpression") { 644 if (calleeParent && calleeParent.loc.start.line < node.loc.start.line) { 645 indent = getNodeIndent(calleeParent).goodChar; 646 } 647 } else { 648 if (isArgBeforeCalleeNodeMultiline(calleeNode) && 649 calleeParent.callee.loc.start.line === calleeParent.callee.loc.end.line && 650 !isNodeFirstInLine(calleeNode)) { 651 indent = getNodeIndent(calleeParent).goodChar; 652 } 653 } 654 } 655 656 /* 657 * function body indent should be indent + indent size, unless this 658 * is a FunctionDeclaration, FunctionExpression, or outer IIFE and the corresponding options are enabled. 659 */ 660 let functionOffset = indentSize; 661 662 if (options.outerIIFEBody !== null && isOuterIIFE(calleeNode)) { 663 functionOffset = options.outerIIFEBody * indentSize; 664 } else if (calleeNode.type === "FunctionExpression") { 665 functionOffset = options.FunctionExpression.body * indentSize; 666 } else if (calleeNode.type === "FunctionDeclaration") { 667 functionOffset = options.FunctionDeclaration.body * indentSize; 668 } 669 indent += functionOffset; 670 671 // check if the node is inside a variable 672 const parentVarNode = getVariableDeclaratorNode(node); 673 674 if (parentVarNode && isNodeInVarOnTop(node, parentVarNode)) { 675 indent += indentSize * options.VariableDeclarator[parentVarNode.parent.kind]; 676 } 677 678 if (node.body.length > 0) { 679 checkNodesIndent(node.body, indent); 680 } 681 682 checkLastNodeLineIndent(node, indent - functionOffset); 683 } 684 685 686 /** 687 * Checks if the given node starts and ends on the same line 688 * @param {ASTNode} node The node to check 689 * @returns {boolean} Whether or not the block starts and ends on the same line. 690 */ 691 function isSingleLineNode(node) { 692 const lastToken = sourceCode.getLastToken(node), 693 startLine = node.loc.start.line, 694 endLine = lastToken.loc.end.line; 695 696 return startLine === endLine; 697 } 698 699 /** 700 * Check indent for array block content or object block content 701 * @param {ASTNode} node node to examine 702 * @returns {void} 703 */ 704 function checkIndentInArrayOrObjectBlock(node) { 705 706 // Skip inline 707 if (isSingleLineNode(node)) { 708 return; 709 } 710 711 let elements = (node.type === "ArrayExpression") ? node.elements : node.properties; 712 713 // filter out empty elements example would be [ , 2] so remove first element as espree considers it as null 714 elements = elements.filter(elem => elem !== null); 715 716 let nodeIndent; 717 let elementsIndent; 718 const parentVarNode = getVariableDeclaratorNode(node); 719 720 // TODO - come up with a better strategy in future 721 if (isNodeFirstInLine(node)) { 722 const parent = node.parent; 723 724 nodeIndent = getNodeIndent(parent).goodChar; 725 if (!parentVarNode || parentVarNode.loc.start.line !== node.loc.start.line) { 726 if (parent.type !== "VariableDeclarator" || parentVarNode === parentVarNode.parent.declarations[0]) { 727 if (parent.type === "VariableDeclarator" && parentVarNode.loc.start.line === parent.loc.start.line) { 728 nodeIndent += (indentSize * options.VariableDeclarator[parentVarNode.parent.kind]); 729 } else if (parent.type === "ObjectExpression" || parent.type === "ArrayExpression") { 730 const parentElements = node.parent.type === "ObjectExpression" ? node.parent.properties : node.parent.elements; 731 732 if (parentElements[0] && 733 parentElements[0].loc.start.line === parent.loc.start.line && 734 parentElements[0].loc.end.line !== parent.loc.start.line) { 735 736 /* 737 * If the first element of the array spans multiple lines, don't increase the expected indentation of the rest. 738 * e.g. [{ 739 * foo: 1 740 * }, 741 * { 742 * bar: 1 743 * }] 744 * the second object is not indented. 745 */ 746 } else if (typeof options[parent.type] === "number") { 747 nodeIndent += options[parent.type] * indentSize; 748 } else { 749 nodeIndent = parentElements[0].loc.start.column; 750 } 751 } else if (parent.type === "CallExpression" || parent.type === "NewExpression") { 752 if (typeof options.CallExpression.arguments === "number") { 753 nodeIndent += options.CallExpression.arguments * indentSize; 754 } else if (options.CallExpression.arguments === "first") { 755 if (parent.arguments.indexOf(node) !== -1) { 756 nodeIndent = parent.arguments[0].loc.start.column; 757 } 758 } else { 759 nodeIndent += indentSize; 760 } 761 } else if (parent.type === "LogicalExpression" || parent.type === "ArrowFunctionExpression") { 762 nodeIndent += indentSize; 763 } 764 } 765 } 766 767 checkFirstNodeLineIndent(node, nodeIndent); 768 } else { 769 nodeIndent = getNodeIndent(node).goodChar; 770 } 771 772 if (options[node.type] === "first") { 773 elementsIndent = elements.length ? elements[0].loc.start.column : 0; // If there are no elements, elementsIndent doesn't matter. 774 } else { 775 elementsIndent = nodeIndent + indentSize * options[node.type]; 776 } 777 778 /* 779 * Check if the node is a multiple variable declaration; if so, then 780 * make sure indentation takes that into account. 781 */ 782 if (isNodeInVarOnTop(node, parentVarNode)) { 783 elementsIndent += indentSize * options.VariableDeclarator[parentVarNode.parent.kind]; 784 } 785 786 checkNodesIndent(elements, elementsIndent); 787 788 if (elements.length > 0) { 789 790 // Skip last block line check if last item in same line 791 if (elements[elements.length - 1].loc.end.line === node.loc.end.line) { 792 return; 793 } 794 } 795 796 checkLastNodeLineIndent(node, nodeIndent + 797 (isNodeInVarOnTop(node, parentVarNode) ? options.VariableDeclarator[parentVarNode.parent.kind] * indentSize : 0)); 798 } 799 800 /** 801 * Check if the node or node body is a BlockStatement or not 802 * @param {ASTNode} node node to test 803 * @returns {boolean} True if it or its body is a block statement 804 */ 805 function isNodeBodyBlock(node) { 806 return node.type === "BlockStatement" || node.type === "ClassBody" || (node.body && node.body.type === "BlockStatement") || 807 (node.consequent && node.consequent.type === "BlockStatement"); 808 } 809 810 /** 811 * Check indentation for blocks 812 * @param {ASTNode} node node to check 813 * @returns {void} 814 */ 815 function blockIndentationCheck(node) { 816 817 // Skip inline blocks 818 if (isSingleLineNode(node)) { 819 return; 820 } 821 822 if (node.parent && ( 823 node.parent.type === "FunctionExpression" || 824 node.parent.type === "FunctionDeclaration" || 825 node.parent.type === "ArrowFunctionExpression") 826 ) { 827 checkIndentInFunctionBlock(node); 828 return; 829 } 830 831 let indent; 832 let nodesToCheck = []; 833 834 /* 835 * For this statements we should check indent from statement beginning, 836 * not from the beginning of the block. 837 */ 838 const statementsWithProperties = [ 839 "IfStatement", "WhileStatement", "ForStatement", "ForInStatement", "ForOfStatement", "DoWhileStatement", "ClassDeclaration", "TryStatement" 840 ]; 841 842 if (node.parent && statementsWithProperties.indexOf(node.parent.type) !== -1 && isNodeBodyBlock(node)) { 843 indent = getNodeIndent(node.parent).goodChar; 844 } else if (node.parent && node.parent.type === "CatchClause") { 845 indent = getNodeIndent(node.parent.parent).goodChar; 846 } else { 847 indent = getNodeIndent(node).goodChar; 848 } 849 850 if (node.type === "IfStatement" && node.consequent.type !== "BlockStatement") { 851 nodesToCheck = [node.consequent]; 852 } else if (Array.isArray(node.body)) { 853 nodesToCheck = node.body; 854 } else { 855 nodesToCheck = [node.body]; 856 } 857 858 if (nodesToCheck.length > 0) { 859 checkNodesIndent(nodesToCheck, indent + indentSize); 860 } 861 862 if (node.type === "BlockStatement") { 863 checkLastNodeLineIndent(node, indent); 864 } 865 } 866 867 /** 868 * Filter out the elements which are on the same line of each other or the node. 869 * basically have only 1 elements from each line except the variable declaration line. 870 * @param {ASTNode} node Variable declaration node 871 * @returns {ASTNode[]} Filtered elements 872 */ 873 function filterOutSameLineVars(node) { 874 return node.declarations.reduce((finalCollection, elem) => { 875 const lastElem = finalCollection[finalCollection.length - 1]; 876 877 if ((elem.loc.start.line !== node.loc.start.line && !lastElem) || 878 (lastElem && lastElem.loc.start.line !== elem.loc.start.line)) { 879 finalCollection.push(elem); 880 } 881 882 return finalCollection; 883 }, []); 884 } 885 886 /** 887 * Check indentation for variable declarations 888 * @param {ASTNode} node node to examine 889 * @returns {void} 890 */ 891 function checkIndentInVariableDeclarations(node) { 892 const elements = filterOutSameLineVars(node); 893 const nodeIndent = getNodeIndent(node).goodChar; 894 const lastElement = elements[elements.length - 1]; 895 896 const elementsIndent = nodeIndent + indentSize * options.VariableDeclarator[node.kind]; 897 898 checkNodesIndent(elements, elementsIndent); 899 900 // Only check the last line if there is any token after the last item 901 if (sourceCode.getLastToken(node).loc.end.line <= lastElement.loc.end.line) { 902 return; 903 } 904 905 const tokenBeforeLastElement = sourceCode.getTokenBefore(lastElement); 906 907 if (tokenBeforeLastElement.value === ",") { 908 909 // Special case for comma-first syntax where the semicolon is indented 910 checkLastNodeLineIndent(node, getNodeIndent(tokenBeforeLastElement).goodChar); 911 } else { 912 checkLastNodeLineIndent(node, elementsIndent - indentSize); 913 } 914 } 915 916 /** 917 * Check and decide whether to check for indentation for blockless nodes 918 * Scenarios are for or while statements without braces around them 919 * @param {ASTNode} node node to examine 920 * @returns {void} 921 */ 922 function blockLessNodes(node) { 923 if (node.body.type !== "BlockStatement") { 924 blockIndentationCheck(node); 925 } 926 } 927 928 /** 929 * Returns the expected indentation for the case statement 930 * @param {ASTNode} node node to examine 931 * @param {int} [providedSwitchIndent] indent for switch statement 932 * @returns {int} indent size 933 */ 934 function expectedCaseIndent(node, providedSwitchIndent) { 935 const switchNode = (node.type === "SwitchStatement") ? node : node.parent; 936 const switchIndent = typeof providedSwitchIndent === "undefined" 937 ? getNodeIndent(switchNode).goodChar 938 : providedSwitchIndent; 939 let caseIndent; 940 941 if (caseIndentStore[switchNode.loc.start.line]) { 942 return caseIndentStore[switchNode.loc.start.line]; 943 } 944 945 if (switchNode.cases.length > 0 && options.SwitchCase === 0) { 946 caseIndent = switchIndent; 947 } else { 948 caseIndent = switchIndent + (indentSize * options.SwitchCase); 949 } 950 951 caseIndentStore[switchNode.loc.start.line] = caseIndent; 952 return caseIndent; 953 954 } 955 956 /** 957 * Checks wether a return statement is wrapped in () 958 * @param {ASTNode} node node to examine 959 * @returns {boolean} the result 960 */ 961 function isWrappedInParenthesis(node) { 962 const regex = /^return\s*?\(\s*?\);*?/u; 963 964 const statementWithoutArgument = sourceCode.getText(node).replace( 965 sourceCode.getText(node.argument), "" 966 ); 967 968 return regex.test(statementWithoutArgument); 969 } 970 971 return { 972 Program(node) { 973 if (node.body.length > 0) { 974 975 // Root nodes should have no indent 976 checkNodesIndent(node.body, getNodeIndent(node).goodChar); 977 } 978 }, 979 980 ClassBody: blockIndentationCheck, 981 982 BlockStatement: blockIndentationCheck, 983 984 WhileStatement: blockLessNodes, 985 986 ForStatement: blockLessNodes, 987 988 ForInStatement: blockLessNodes, 989 990 ForOfStatement: blockLessNodes, 991 992 DoWhileStatement: blockLessNodes, 993 994 IfStatement(node) { 995 if (node.consequent.type !== "BlockStatement" && node.consequent.loc.start.line > node.loc.start.line) { 996 blockIndentationCheck(node); 997 } 998 }, 999 1000 VariableDeclaration(node) { 1001 if (node.declarations[node.declarations.length - 1].loc.start.line > node.declarations[0].loc.start.line) { 1002 checkIndentInVariableDeclarations(node); 1003 } 1004 }, 1005 1006 ObjectExpression(node) { 1007 checkIndentInArrayOrObjectBlock(node); 1008 }, 1009 1010 ArrayExpression(node) { 1011 checkIndentInArrayOrObjectBlock(node); 1012 }, 1013 1014 MemberExpression(node) { 1015 1016 if (typeof options.MemberExpression === "undefined") { 1017 return; 1018 } 1019 1020 if (isSingleLineNode(node)) { 1021 return; 1022 } 1023 1024 /* 1025 * The typical layout of variable declarations and assignments 1026 * alter the expectation of correct indentation. Skip them. 1027 * TODO: Add appropriate configuration options for variable 1028 * declarations and assignments. 1029 */ 1030 if (getParentNodeByType(node, "VariableDeclarator", ["FunctionExpression", "ArrowFunctionExpression"])) { 1031 return; 1032 } 1033 1034 if (getParentNodeByType(node, "AssignmentExpression", ["FunctionExpression"])) { 1035 return; 1036 } 1037 1038 const propertyIndent = getNodeIndent(node).goodChar + indentSize * options.MemberExpression; 1039 1040 const checkNodes = [node.property]; 1041 1042 const dot = sourceCode.getTokenBefore(node.property); 1043 1044 if (dot.type === "Punctuator" && dot.value === ".") { 1045 checkNodes.push(dot); 1046 } 1047 1048 checkNodesIndent(checkNodes, propertyIndent); 1049 }, 1050 1051 SwitchStatement(node) { 1052 1053 // Switch is not a 'BlockStatement' 1054 const switchIndent = getNodeIndent(node).goodChar; 1055 const caseIndent = expectedCaseIndent(node, switchIndent); 1056 1057 checkNodesIndent(node.cases, caseIndent); 1058 1059 1060 checkLastNodeLineIndent(node, switchIndent); 1061 }, 1062 1063 SwitchCase(node) { 1064 1065 // Skip inline cases 1066 if (isSingleLineNode(node)) { 1067 return; 1068 } 1069 const caseIndent = expectedCaseIndent(node); 1070 1071 checkNodesIndent(node.consequent, caseIndent + indentSize); 1072 }, 1073 1074 FunctionDeclaration(node) { 1075 if (isSingleLineNode(node)) { 1076 return; 1077 } 1078 if (options.FunctionDeclaration.parameters === "first" && node.params.length) { 1079 checkNodesIndent(node.params.slice(1), node.params[0].loc.start.column); 1080 } else if (options.FunctionDeclaration.parameters !== null) { 1081 checkNodesIndent(node.params, getNodeIndent(node).goodChar + indentSize * options.FunctionDeclaration.parameters); 1082 } 1083 }, 1084 1085 FunctionExpression(node) { 1086 if (isSingleLineNode(node)) { 1087 return; 1088 } 1089 if (options.FunctionExpression.parameters === "first" && node.params.length) { 1090 checkNodesIndent(node.params.slice(1), node.params[0].loc.start.column); 1091 } else if (options.FunctionExpression.parameters !== null) { 1092 checkNodesIndent(node.params, getNodeIndent(node).goodChar + indentSize * options.FunctionExpression.parameters); 1093 } 1094 }, 1095 1096 ReturnStatement(node) { 1097 if (isSingleLineNode(node)) { 1098 return; 1099 } 1100 1101 const firstLineIndent = getNodeIndent(node).goodChar; 1102 1103 // in case if return statement is wrapped in parenthesis 1104 if (isWrappedInParenthesis(node)) { 1105 checkLastReturnStatementLineIndent(node, firstLineIndent); 1106 } else { 1107 checkNodeIndent(node, firstLineIndent); 1108 } 1109 }, 1110 1111 CallExpression(node) { 1112 if (isSingleLineNode(node)) { 1113 return; 1114 } 1115 if (options.CallExpression.arguments === "first" && node.arguments.length) { 1116 checkNodesIndent(node.arguments.slice(1), node.arguments[0].loc.start.column); 1117 } else if (options.CallExpression.arguments !== null) { 1118 checkNodesIndent(node.arguments, getNodeIndent(node).goodChar + indentSize * options.CallExpression.arguments); 1119 } 1120 } 1121 1122 }; 1123 1124 } 1125}; 1126