UNPKG

@equisoft/tslint-config

Version:
1,104 lines (1,101 loc) 46.2 kB
"use strict"; /** * This rule is a copy of tslint-eslint-rules: * * source file: https://github.com/buzinas/tslint-eslint-rules/blob/master/src/rules/terIndentRule.ts * git commit hash: 3fabc43985fc993aabf799e462d2fa39df858824 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Rule = void 0; /** * This rule is a port of eslint: * * source file: https://github.com/eslint/eslint/blob/master/lib/rules/indent.js * git commit hash: 0643bfeff68979044ca57a2b392d855d18be7d08 * */ const ts = require("typescript"); const Lint = require("tslint"); const RULE_NAME = 'ter-indent'; const DEFAULT_VARIABLE_INDENT = 1; const DEFAULT_PARAMETER_INDENT = null; const DEFAULT_FUNCTION_BODY_INDENT = 1; let indentType = 'space'; let indentSize = 4; let OPTIONS; function assign(target, ...sources) { sources.forEach((source) => { if (source !== undefined && source !== null) { for (const nextKey in source) { if (source.hasOwnProperty(nextKey)) { target[nextKey] = source[nextKey]; } } } }); return target; } function isKind(node, kind) { // @ts-ignore return node.kind === ts.SyntaxKind[kind]; } function isOneOf(node, kinds) { // @ts-ignore return kinds.some(kind => node.kind === ts.SyntaxKind[kind]); } // TODO: Look for `.getFirstToken()!` and try to remove the `!`. class Rule extends Lint.Rules.AbstractRule { apply(sourceFile) { const walker = new IndentWalker(sourceFile, this.getOptions()); return this.applyWithWalker(walker); } } exports.Rule = Rule; Rule.metadata = { ruleName: RULE_NAME, hasFix: true, description: 'enforce consistent indentation', rationale: Lint.Utils.dedent ` Using only one of tabs or spaces for indentation leads to more consistent editor behavior, cleaner diffs in version control, and easier programmatic manipulation. `, optionsDescription: Lint.Utils.dedent ` The string 'tab' or an integer indicating the number of spaces to use per tab. An object may be provided to fine tune the indentation rules: * \`"SwitchCase"\` (default: 0) enforces indentation level for \`case\` clauses in \`switch\` statements * \`"VariableDeclarator"\` (default: 1) enforces indentation level for \`var\` declarators; can also take an object to define separate rules for \`var\`, \`let\` and \`const\` declarations. * \`"outerIIFEBody"\` (default: 1) enforces indentation level for file-level IIFEs. * \`"MemberExpression"\` (off by default) enforces indentation level for multi-line property chains (except in variable declarations and assignments) * \`"FunctionDeclaration"\` takes an object to define rules for function declarations. * \`"parameters"\` (off by default) enforces indentation level for parameters in a function declaration. This can either be a number indicating indentation level, or the string \`"first"\` indicating that all parameters of the declaration must be aligned with the first parameter. * \`"body"\` (default: 1) enforces indentation level for the body of a function expression. * \`"FunctionExpression"\` takes an object to define rules for function declarations. * \`"parameters"\` (off by default) enforces indentation level for parameters in a function declaration. This can either be a number indicating indentation level, or the string \`"first"\` indicating that all parameters of the declaration must be aligned with the first parameter. * \`"body"\` (default: 1) enforces indentation level for the body of a function expression. * \`"CallExpression"\` takes an object to define rules for function call expressions. * \`"arguments"\` (off by default) enforces indentation level for arguments in a call expression. This can either be a number indicating indentation level, or the string \`"first"\` indicating that all arguments of the expression must be aligned with the first argument. `, options: { type: 'array', items: [{ type: 'number', minimum: '0' }, { type: 'string', enum: ['tab'] }, { type: 'object', properties: { SwitchCase: { type: 'number', minimum: 0 }, VariableDeclarator: { type: 'object', properties: { var: { type: 'number', minimum: 0 }, let: { type: 'number', minimum: 0 }, const: { type: 'number', minimum: 0 } } }, outerIIFEBody: { type: 'number' }, FunctionDeclaration: { type: 'object', properties: { parameters: { type: 'number', minimum: 0 }, body: { type: 'number', minimum: 0 } } }, FunctionExpression: { type: 'object', properties: { parameters: { type: 'number', minimum: 0 }, body: { type: 'number', minimum: 0 } } }, MemberExpression: { type: 'number' }, CallExpression: { type: 'object', properties: { arguments: { type: 'number', minimum: 0 } } } }, additionalProperties: false }], minLength: 1, maxLength: 2 }, optionExamples: [ Lint.Utils.dedent ` "${RULE_NAME}": [true, "tab"] `, Lint.Utils.dedent ` "${RULE_NAME}": [true, 2] `, Lint.Utils.dedent ` "${RULE_NAME}": [ true, 2, { "FunctionExpression": { "parameters": 1, "body": 1 } } ] ` ], typescriptOnly: false, type: 'maintainability' }; class IndentWalker extends Lint.RuleWalker { constructor(sourceFile, options) { super(sourceFile, options); this.caseIndentStore = {}; this.varIndentStore = {}; OPTIONS = { SwitchCase: 0, VariableDeclarator: { var: DEFAULT_VARIABLE_INDENT, let: DEFAULT_VARIABLE_INDENT, const: DEFAULT_VARIABLE_INDENT }, outerIIFEBody: null, FunctionDeclaration: { parameters: DEFAULT_PARAMETER_INDENT, body: DEFAULT_FUNCTION_BODY_INDENT }, FunctionExpression: { parameters: DEFAULT_PARAMETER_INDENT, body: DEFAULT_FUNCTION_BODY_INDENT }, CallExpression: { arguments: DEFAULT_PARAMETER_INDENT } }; const firstParam = this.getOptions()[0]; if (firstParam === 'tab') { indentSize = 1; indentType = 'tab'; } else { indentSize = firstParam || 4; indentType = 'space'; } const userOptions = this.getOptions()[1]; if (userOptions) { OPTIONS.SwitchCase = userOptions.SwitchCase || 0; if (typeof userOptions.VariableDeclarator === 'number') { OPTIONS.VariableDeclarator = { var: userOptions.VariableDeclarator, let: userOptions.VariableDeclarator, const: userOptions.VariableDeclarator }; } else if (typeof userOptions.VariableDeclarator === 'object') { assign(OPTIONS.VariableDeclarator, userOptions.VariableDeclarator); } if (typeof userOptions.outerIIFEBody === 'number') { OPTIONS.outerIIFEBody = userOptions.outerIIFEBody; } if (typeof userOptions.MemberExpression === 'number') { OPTIONS.MemberExpression = userOptions.MemberExpression; } if (typeof userOptions.FunctionDeclaration === 'object') { assign(OPTIONS.FunctionDeclaration, userOptions.FunctionDeclaration); } if (typeof userOptions.FunctionExpression === 'object') { assign(OPTIONS.FunctionExpression, userOptions.FunctionExpression); } if (typeof userOptions.CallExpression === 'object') { assign(OPTIONS.CallExpression, userOptions.CallExpression); } } this.srcFile = sourceFile; this.srcText = sourceFile.getFullText(); } getSourceSubstr(start, end) { return this.srcText.substr(start, end - start); } getLineAndCharacter(node, byEndLocation = false) { const index = byEndLocation ? node.getEnd() : node.getStart(); return this.srcFile.getLineAndCharacterOfPosition(index); } getLine(node, byEndLocation = false) { return this.getLineAndCharacter(node, byEndLocation).line; } /** * Creates an error message for a line. * * expectedAmount: The expected amount of indentation characters for this line * actualSpaces: The actual number of indentation spaces that were found on this line * actualTabs: The actual number of indentation tabs that were found on this line */ createErrorMessage(expectedAmount, actualSpaces, actualTabs) { const expectedStatement = `${expectedAmount} ${indentType}${expectedAmount === 1 ? '' : 's'}`; const foundSpacesWord = `space${actualSpaces === 1 ? '' : 's'}`; const foundTabsWord = `tab${actualTabs === 1 ? '' : 's'}`; let foundStatement; if (actualSpaces > 0 && actualTabs > 0) { foundStatement = `${actualSpaces} ${foundSpacesWord} and ${actualTabs} ${foundTabsWord}`; } else if (actualSpaces > 0) { // Abbreviate the message if the expected indentation is also spaces. // e.g. 'Expected 4 spaces but found 2' rather than 'Expected 4 spaces but found 2 spaces' foundStatement = indentType === 'space' ? actualSpaces : `${actualSpaces} ${foundSpacesWord}`; } else if (actualTabs > 0) { foundStatement = indentType === 'tab' ? actualTabs : `${actualTabs} ${foundTabsWord}`; } else { foundStatement = '0'; } return `Expected indentation of ${expectedStatement} but found ${foundStatement}.`; } /** * Reports a given indent violation * node: Node violating the indent rule * needed: Expected indentation character count * gottenSpaces: Indentation space count in the actual node/code * gottenTabs: Indentation tab count in the actual node/code */ report(node, needed, gottenSpaces, gottenTabs, loc) { if (gottenSpaces && gottenTabs) { // Don't report lines that have both spaces and tabs to avoid conflicts with rules that // report a mix of tabs and spaces. return; } const msg = this.createErrorMessage(needed, gottenSpaces, gottenTabs); const width = gottenSpaces + gottenTabs; const start = (loc !== undefined ? loc : node.getStart()) - width; const desiredIndent = (indentType === 'space' ? ' ' : '\t').repeat(needed); const fix = this.createReplacement(start, width, desiredIndent); this.addFailure(this.createFailure(start, width, msg, fix)); } /** * Checks node is the first in its own start line. By default it looks by start line. * [byEndLocation=false]: Lookup based on start position or end */ isNodeFirstInLine(node, byEndLocation = false) { const token = byEndLocation ? node.getLastToken() : node.getFirstToken(); // TODO: How can there be no token? let pos = token.getStart() - 1; while ([' ', '\t'].indexOf(this.srcText.charAt(pos)) !== -1) { pos -= 1; } return this.srcText.charAt(pos) === '\n' || this._firstInLineCommentHelper(node); } /** * Checks to see a leading comment is blocking the start of the node. For instance: * * /* comment *\/ { * * is allowed and in this case `{` would be first in line. */ _firstInLineCommentHelper(node) { let pos; let firstInLine = false; const comments = ts.getLeadingCommentRanges(node.getFullText(), 0); if (comments && comments.length) { const offset = node.getFullStart(); const lastComment = comments[comments.length - 1]; const comment = this.getSourceSubstr(lastComment.pos + offset, lastComment.end + offset); if (comment.indexOf('\n') !== -1) { firstInLine = true; } else { pos = lastComment.pos + offset; while (pos > 0 && this.srcText.charAt(pos) !== '\n') { pos -= 1; } const content = this.getSourceSubstr(pos + 1, lastComment.pos + offset); if (content.trim() === '') { firstInLine = true; } } } return firstInLine; } /** * Returns the node's indent. Contains keys `space` and `tab`, representing the indent of each * character. Also contains keys `goodChar` and `badChar`, where `goodChar` is the amount of the * user's desired indentation character, and `badChar` is the amount of the other indentation * character. */ getNodeIndent(node) { if (node === this.getSourceFile()) { return { contentStart: 0, firstInLine: true, space: 0, tab: 0, goodChar: 0, badChar: 0 }; } if (node.kind === ts.SyntaxKind.SyntaxList && node.parent) { return this.getNodeIndent(node.parent); } const endIndex = node.getStart(this.srcFile); let pos = endIndex - 1; while (pos > 0 && this.srcText.charAt(pos) !== '\n') { pos -= 1; } const str = this.getSourceSubstr(pos + 1, endIndex); const whiteSpace = (str.match(/^\s+/) || [''])[0]; const indentChars = whiteSpace.split(''); const spaces = indentChars.filter(char => char === ' ').length; const tabs = indentChars.filter(char => char === '\t').length; return { contentStart: pos + spaces + tabs + 1, firstInLine: spaces + tabs === str.length || this._firstInLineCommentHelper(node), space: spaces, tab: tabs, goodChar: indentType === 'space' ? spaces : tabs, badChar: indentType === 'space' ? tabs : spaces }; } checkNodeIndent(node, neededIndent) { const actualIndent = this.getNodeIndent(node); if (!isKind(node, 'ArrayLiteralExpression') && !isKind(node, 'ObjectLiteralExpression') && (actualIndent.goodChar !== neededIndent || actualIndent.badChar !== 0) && actualIndent.firstInLine) { this.report(node, neededIndent, actualIndent.space, actualIndent.tab, actualIndent.contentStart); } if (isKind(node, 'IfStatement')) { const elseStatement = node.elseStatement; if (elseStatement) { const elseKeyword = node.getChildren().filter(ch => isKind(ch, 'ElseKeyword')).shift(); this.checkNodeIndent(elseKeyword, neededIndent); if (!this.isNodeFirstInLine(elseStatement)) { this.checkNodeIndent(elseStatement, neededIndent); } } } else if (isKind(node, 'TryStatement')) { const handler = node.catchClause; if (handler) { const catchKeyword = handler.getChildren().filter(ch => isKind(ch, 'CatchKeyword')).shift(); this.checkNodeIndent(catchKeyword, neededIndent); if (!this.isNodeFirstInLine(handler)) { this.checkNodeIndent(handler, neededIndent); } } const finalizer = node.finallyBlock; if (finalizer) { const finallyKeyword = node.getChildren().filter(ch => isKind(ch, 'FinallyKeyword')).shift(); this.checkNodeIndent(finallyKeyword, neededIndent); } } else if (isKind(node, 'DoStatement')) { const whileKeyword = node.getChildren().filter(ch => isKind(ch, 'WhileKeyword')).shift(); this.checkNodeIndent(whileKeyword, neededIndent); } } isSingleLineNode(node) { // Note: all the tests would pass using `node.getFullText()` but we should only use this for // the syntax list nodes, otherwise nodes which are single line may say they are multiline // and this will make us do unnecessary checks. const text = node.kind === ts.SyntaxKind.SyntaxList ? node.getFullText() : node.getText(); return text.indexOf('\n') === -1; } /** * Check indentation for blocks */ blockIndentationCheck(node) { if (this.isSingleLineNode(node)) { return; } const functionLike = [ 'FunctionExpression', 'FunctionDeclaration', 'MethodDeclaration', 'Constructor', 'ArrowFunction' ]; if (node.parent && isOneOf(node.parent, functionLike)) { // TODO: nope, can't figure this last check, shutting it down for now. this.checkIndentInFunctionBlock(node); return; } let indent; let nodesToCheck = []; /* For these statements we should check indent from statement beginning, not from the beginning of the block. */ const statementsWithProperties = [ 'IfStatement', 'WhileStatement', 'ForStatement', 'ForInStatement', 'ForOfStatement', 'DoStatement', 'ClassDeclaration', 'ClassExpression', 'InterfaceDeclaration', 'TypeLiteral', 'TryStatement', 'SourceFile' ]; if (node.parent && isOneOf(node.parent, statementsWithProperties) && this.isNodeBodyBlock(node)) { indent = this.getNodeIndent(node.parent).goodChar; } else if (node.parent && isKind(node.parent, 'CatchClause')) { indent = this.getNodeIndent(node.parent.parent).goodChar; } else { indent = this.getNodeIndent(node).goodChar; } if (isKind(node, 'IfStatement') && !isKind(node.thenStatement, 'Block')) { nodesToCheck = [node.thenStatement]; } else { if (isKind(node, 'Block')) { nodesToCheck = node.getChildren()[1].getChildren(); } else if (node.parent && isOneOf(node.parent, [ 'ClassDeclaration', 'ClassExpression', 'InterfaceDeclaration', 'TypeLiteral' ])) { nodesToCheck = node.getChildren(); } else { // TODO: previously we had it as ts.IterationStatement, need to find out what type of node // we have here. Doing `any` for now. nodesToCheck = [node.statement]; } } this.checkNodeIndent(node, indent); if (nodesToCheck.length > 0) { this.checkNodesIndent(nodesToCheck, indent + indentSize); } if (isKind(node, 'Block')) { this.checkLastNodeLineIndent(node, indent); } else if (node.parent && this.isNodeBodyBlock(node)) { this.checkLastNodeLineIndent(node.parent, indent); } } /** * Check if node is an assignment expression, i.e. the binary expression contains the equal token. */ isAssignment(node) { if (!isKind(node, 'BinaryExpression')) { return false; } return node.operatorToken.getText() === '='; } /** * Check if the node or node body is a BlockStatement or not */ isNodeBodyBlock(node) { const hasBlock = [ 'ClassDeclaration', 'ClassExpression', 'InterfaceDeclaration', 'TypeLiteral' ]; return isKind(node, 'Block') || (isKind(node, 'SyntaxList') && isOneOf(node.parent, hasBlock)); } /** * Check that the start of the node has the correct level of indentation. */ checkFirstNodeLineIndent(node, firstLineIndent) { const startIndent = this.getNodeIndent(node); const firstInLine = startIndent.firstInLine; if (firstInLine && (startIndent.goodChar !== firstLineIndent || startIndent.badChar !== 0)) { this.report(node, firstLineIndent, startIndent.space, startIndent.tab, startIndent.contentStart); } } /** * Check last line of the node has the correct level of indentation. */ checkLastNodeLineIndent(node, lastLineIndent) { const lastToken = node.getLastToken(); const endIndent = this.getNodeIndent(lastToken); const firstInLine = endIndent.firstInLine; if (firstInLine && (endIndent.goodChar !== lastLineIndent || endIndent.badChar !== 0)) { this.report(lastToken, lastLineIndent, endIndent.space, endIndent.tab); } } /** * Check to see if the node function is a file level IIFE */ isOuterIIFE(node) { if (!node.parent) return false; let parent = node.parent; let expressionIsNode = parent.expression !== node; if (isKind(parent, 'ParenthesizedExpression')) { parent = parent.parent; } // Verify that the node is an IIEF if (!isKind(parent, 'CallExpression') || expressionIsNode) { return false; } let stmt = parent; let condition; do { stmt = stmt.parent; condition = (isKind(stmt, 'PrefixUnaryExpression') && (stmt.operator === ts.SyntaxKind.ExclamationToken || stmt.operator === ts.SyntaxKind.TildeToken || stmt.operator === ts.SyntaxKind.PlusToken || stmt.operator === ts.SyntaxKind.MinusToken) || isKind(stmt, 'BinaryExpression') || isKind(stmt, 'SyntaxList') || isKind(stmt, 'VariableDeclaration') || isKind(stmt, 'VariableDeclarationList') || isKind(stmt, 'ParenthesizedExpression')); } while (condition); return ((isKind(stmt, 'ExpressionStatement') || isKind(stmt, 'VariableStatement')) && !!stmt.parent && isKind(stmt.parent, 'SourceFile')); } /** * Check to see if the argument before the callee node is multi-line and * there should only be 1 argument before the callee node */ isArgBeforeCalleeNodeMultiline(node) { const parent = node.parent; if (parent && parent['arguments'].length >= 2 && parent['arguments'][1] === node) { const firstArg = parent['arguments'][0]; return this.getLine(firstArg, true) > this.getLine(firstArg); } return false; } /** * Check indent for function block content */ checkIndentInFunctionBlock(node) { const calleeNode = node.parent; let indent = this.getNodeIndent(calleeNode).goodChar; if (calleeNode.parent && calleeNode.parent.kind === ts.SyntaxKind.CallExpression) { const calleeParent = calleeNode.parent; if (calleeNode.kind !== ts.SyntaxKind.FunctionExpression && calleeNode.kind !== ts.SyntaxKind.ArrowFunction) { if (calleeParent && this.getLine(calleeParent) < this.getLine(node)) { indent = this.getNodeIndent(calleeParent).goodChar; } } else { const callee = calleeParent.expression; if (this.isArgBeforeCalleeNodeMultiline(calleeNode) && this.getLine(callee) === this.getLine(callee, true) && !this.isNodeFirstInLine(calleeNode)) { indent = this.getNodeIndent(calleeParent).goodChar; } } } // function body indent should be indent + indent size, unless this // is a FunctionDeclaration, FunctionExpression, or outer IIFE and the corresponding options are enabled. let functionOffset = indentSize; if (OPTIONS.outerIIFEBody !== null && this.isOuterIIFE(calleeNode)) { functionOffset = OPTIONS.outerIIFEBody * indentSize; } else if (calleeNode.kind === ts.SyntaxKind.FunctionExpression) { functionOffset = OPTIONS.FunctionExpression.body * indentSize; } else if (calleeNode.kind === ts.SyntaxKind.FunctionDeclaration) { functionOffset = OPTIONS.FunctionDeclaration.body * indentSize; } else if (isOneOf(calleeNode, ['MethodDeclaration', 'Constructor'])) { functionOffset = OPTIONS.FunctionExpression.body * indentSize; } indent += functionOffset; // check if the node is inside a variable const parentVarNode = this.getVariableDeclaratorNode(node); if (parentVarNode && this.isNodeInVarOnTop(node, parentVarNode) && parentVarNode.parent) { const varKind = parentVarNode.parent.getFirstToken().getText(); indent += indentSize * OPTIONS.VariableDeclarator[varKind]; } this.checkFirstNodeLineIndent(node, indent - functionOffset); if (node.statements.length) { this.checkNodesIndent(node.statements, indent); } this.checkLastNodeLineIndent(node, indent - functionOffset); } /** * Check indent for nodes list. */ checkNodesIndent(nodes, indent) { nodes.forEach(node => this.checkNodeIndent(node, indent)); } /** * Returns the expected indentation for the case statement. */ expectedCaseIndent(node, switchIndent) { const switchNode = (node.kind === ts.SyntaxKind.SwitchStatement) ? node : node.parent; const line = this.getLine(switchNode); let caseIndent; if (this.caseIndentStore[line]) { return this.caseIndentStore[line]; } else { if (typeof switchIndent === 'undefined') { switchIndent = this.getNodeIndent(switchNode).goodChar; } caseIndent = switchIndent + (indentSize * OPTIONS.SwitchCase); this.caseIndentStore[line] = caseIndent; return caseIndent; } } /** * Returns the expected indentation for the variable declarations. */ expectedVarIndent(node, varIndent) { // VariableStatement -> VariableDeclarationList -> VariableDeclaration const varNode = node.parent; const line = this.getLine(varNode); let indent; if (this.varIndentStore[line]) { return this.varIndentStore[line]; } else { if (typeof varIndent === 'undefined') { varIndent = this.getNodeIndent(varNode).goodChar; } const varKind = varNode.getFirstToken().getText(); indent = varIndent + (indentSize * OPTIONS.VariableDeclarator[varKind]); this.varIndentStore[line] = indent; return indent; } } /** * Returns a parent node of given node based on a specified type * if not present then return null */ getParentNodeByType(node, kind, stopAtList = [ts.SyntaxKind.SourceFile]) { let parent = node.parent; while (parent && parent.kind !== kind && stopAtList.indexOf(parent.kind) === -1 && parent.kind !== ts.SyntaxKind.SourceFile) { parent = parent.parent; } return parent && parent.kind === kind ? parent : null; } /** * Returns the VariableDeclarator based on the current node if not present then return null. */ getVariableDeclaratorNode(node) { return this.getParentNodeByType(node, ts.SyntaxKind.VariableDeclaration); } /** * Returns the BinaryExpression based on the current node if not present then return null. */ getBinaryExpressionNode(node) { return this.getParentNodeByType(node, ts.SyntaxKind.BinaryExpression); } /** * Check indent for array block content or object block content */ checkIndentInArrayOrObjectBlock(node) { if (this.isSingleLineNode(node)) { return; } // @ts-ignore let elements = isKind(node, 'ObjectLiteralExpression') ? node['properties'] : node['elements']; // filter out empty elements, an example would be [ , 2] elements = elements.filter(elem => elem.getText() !== ''); const nodeLine = this.getLine(node); const nodeEndLine = this.getLine(node, true); let nodeIndent; let elementsIndent; let varKind; const parentVarNode = this.getVariableDeclaratorNode(node); if (this.isNodeFirstInLine(node) && node.parent) { const parent = node.parent; nodeIndent = this.getNodeIndent(parent).goodChar; if (parentVarNode && this.getLine(parentVarNode) !== nodeLine) { // TODO: parentVarNode.parent can be a CatchClause and this one may not have a declarations list if (!isKind(parent, 'VariableDeclaration') || parentVarNode === parentVarNode.parent.declarations[0]) { const parentVarLine = this.getLine(parentVarNode); const parentLine = this.getLine(parent); if (isKind(parent, 'VariableDeclaration') && parentVarLine === parentLine && parentVarNode.parent) { varKind = parentVarNode.parent.getFirstToken().getText(); nodeIndent = nodeIndent + (indentSize * OPTIONS.VariableDeclarator[varKind]); } else if (isOneOf(parent, [ 'ObjectLiteralExpression', 'ArrayLiteralExpression', 'CallExpression', 'ArrowFunction', 'NewExpression', 'BinaryExpression' ])) { nodeIndent = nodeIndent + indentSize; } } } else if (!parentVarNode && !this.isFirstArrayElementOnSameLine(parent) && parent.kind !== ts.SyntaxKind.PropertyAccessExpression && parent.kind !== ts.SyntaxKind.ExpressionStatement && parent.kind !== ts.SyntaxKind.PropertyAssignment && !(this.isAssignment(parent))) { nodeIndent = nodeIndent + indentSize; } elementsIndent = nodeIndent + indentSize; this.checkFirstNodeLineIndent(node, nodeIndent); } else { nodeIndent = this.getNodeIndent(node).goodChar; elementsIndent = nodeIndent + indentSize; } /* * Check if the node is a multiple variable declaration; if so, then * make sure indentation takes that into account. */ if (parentVarNode && this.isNodeInVarOnTop(node, parentVarNode) && parentVarNode.parent) { varKind = parentVarNode.parent.getFirstToken().getText(); elementsIndent += indentSize * OPTIONS.VariableDeclarator[varKind]; } this.checkNodesIndent(elements, elementsIndent); if (elements.length > 0) { const lastLine = this.getLine(elements[elements.length - 1], true); // Skip last block line check if last item in same line if (lastLine === nodeEndLine) { return; } } this.checkLastNodeLineIndent(node, elementsIndent - indentSize); } /** * Check to see if the first element inside an array is an object and on the same line as the node */ isFirstArrayElementOnSameLine(node) { if (isKind(node, 'ArrayLiteralExpression')) { const ele = node.elements[0]; if (ele) { return isKind(ele, 'ObjectLiteralExpression') && this.getLine(ele) === this.getLine(node); } } return false; } /** * Check to see if the node is part of the multi-line variable declaration. * Also if its on the same line as the varNode * @param {ASTNode} node node to check * @param {ASTNode} varNode variable declaration node to check against * @returns {boolean} True if all the above condition satisfy */ isNodeInVarOnTop(node, varNode) { const nodeLine = this.getLine(node); const parentLine = this.getLine(varNode.parent); return varNode && parentLine === nodeLine && varNode.parent.declarations.length > 1; } /** * Check and decide whether to check for indentation for blockless nodes * Scenarios are for or while statements without braces around them */ blockLessNodes(node) { if (!isKind(node.statement, 'Block')) { this.blockIndentationCheck(node); } } /** * Check indentation for variable declarations */ checkIndentInVariableDeclarations(node) { const indent = this.expectedVarIndent(node); this.checkNodeIndent(node, indent); } /** * Check indentation for case and default clauses in switch statements. */ visitCase(node) { if (this.isSingleLineNode(node)) { return; } const caseIndent = this.expectedCaseIndent(node); this.checkNodesIndent(node.statements, caseIndent + indentSize); } /** * Check last node line indent this detects, that block closed correctly * This function for more complicated return statement case, where closing parenthesis may be * followed by ';' */ checkLastReturnStatementLineIndent(node, firstLineIndent) { if (!node.expression) { return; } const lastToken = node.expression.getLastToken(); const endIndex = lastToken.getStart(); let pos = endIndex - 1; while (pos > 0 && this.srcText.charAt(pos) !== '\n') { pos -= 1; } const textBeforeClosingParenthesis = this.getSourceSubstr(pos + 1, endIndex); if (textBeforeClosingParenthesis.trim()) { // There are tokens before the closing paren, don't report this case return; } const endIndent = this.getNodeIndent(lastToken); if (endIndent.goodChar !== firstLineIndent) { this.report(node, firstLineIndent, endIndent.space, endIndent.tab, lastToken.getStart()); } } visitClassDeclaration(node) { const len = node.getChildCount(); this.blockIndentationCheck(node.getChildAt(len - 2)); super.visitClassDeclaration(node); } visitClassExpression(node) { const len = node.getChildCount(); this.blockIndentationCheck(node.getChildAt(len - 2)); super.visitClassExpression(node); } visitInterfaceDeclaration(node) { const len = node.getChildCount(); this.blockIndentationCheck(node.getChildAt(len - 2)); super.visitInterfaceDeclaration(node); } visitTypeLiteral(node) { const len = node.getChildCount(); this.blockIndentationCheck(node.getChildAt(len - 2)); super.visitTypeLiteral(node); } visitBlock(node) { this.blockIndentationCheck(node); super.visitBlock(node); } visitIfStatement(node) { const thenLine = this.getLine(node.thenStatement); const line = this.getLine(node); if (!isKind(node.thenStatement, 'Block') && thenLine > line) { // TODO: Check this one very carefully this.blockIndentationCheck(node); } super.visitIfStatement(node); } visitObjectLiteralExpression(node) { this.checkIndentInArrayOrObjectBlock(node); super.visitObjectLiteralExpression(node); } visitArrayLiteralExpression(node) { this.checkIndentInArrayOrObjectBlock(node); super.visitArrayLiteralExpression(node); } visitSwitchStatement(node) { const switchIndent = this.getNodeIndent(node).goodChar; const caseIndent = this.expectedCaseIndent(node, switchIndent); this.checkNodesIndent(node.caseBlock.clauses, caseIndent); this.checkLastNodeLineIndent(node, switchIndent); super.visitSwitchStatement(node); } visitCaseClause(node) { this.visitCase(node); super.visitCaseClause(node); } visitDefaultClause(node) { this.visitCase(node); super.visitDefaultClause(node); } visitWhileStatement(node) { this.blockLessNodes(node); super.visitWhileStatement(node); } visitForStatement(node) { this.blockLessNodes(node); super.visitForStatement(node); } visitForInStatement(node) { this.blockLessNodes(node); super.visitForInStatement(node); } visitDoStatement(node) { this.blockLessNodes(node); super.visitDoStatement(node); } visitVariableDeclaration(node) { this.checkIndentInVariableDeclarations(node); super.visitVariableDeclaration(node); } visitVariableStatement(node) { super.visitVariableStatement(node); // VariableStatement -> VariableDeclarationList -> (VarKeyword, SyntaxList) const list = node.getChildAt(0).getChildAt(1); if (!list) { return; } const len = list.getChildCount(); if (len === 0) { return; } const lastElement = list.getChildAt(len - 1); // TODO: Remove `!` const lastToken = node.getLastToken(); const lastTokenLine = this.getLine(lastToken, true); const lastElementLine = this.getLine(lastElement, true); // Only check the last line if there is any token after the last item if (lastTokenLine <= lastElementLine) { return; } const tokenBeforeLastElement = list.getChildAt(len - 2); if (tokenBeforeLastElement && isKind(tokenBeforeLastElement, 'CommaToken')) { // Special case for comma-first syntax where the semicolon is indented this.checkLastNodeLineIndent(node, this.getNodeIndent(tokenBeforeLastElement).goodChar); } else { const nodeIndent = this.getNodeIndent(node).goodChar; const varKind = node.getFirstToken().getText(); const declaratorIndent = typeof OPTIONS.VariableDeclarator[varKind] === 'number' ? OPTIONS.VariableDeclarator[varKind] : DEFAULT_VARIABLE_INDENT; const elementsIndent = nodeIndent + indentSize * declaratorIndent; this.checkLastNodeLineIndent(node, elementsIndent - indentSize); } } visitFunctionDeclaration(node) { if (this.isSingleLineNode(node)) { return; } if (OPTIONS.FunctionDeclaration.parameters === 'first' && node.parameters.length) { const indent = this.getLineAndCharacter(node.parameters[0]).character; this.checkNodesIndent(node.parameters.slice(1), indent); } else if (OPTIONS.FunctionDeclaration.parameters !== null) { const nodeIndent = this.getNodeIndent(node).goodChar; this.checkNodesIndent(node.parameters, nodeIndent + indentSize * OPTIONS.FunctionDeclaration.parameters); const closingParen = node.getChildAt(node.getChildCount() - 2); this.checkNodeIndent(closingParen, nodeIndent); } super.visitFunctionDeclaration(node); } checkFunctionMethodExpression(node) { if (OPTIONS.FunctionExpression.parameters === 'first' && node.parameters.length) { const indent = this.getLineAndCharacter(node.parameters[0]).character; this.checkNodesIndent(node.parameters.slice(1), indent); } else if (OPTIONS.FunctionExpression.parameters !== null) { const nodeIndent = this.getNodeIndent(node).goodChar; this.checkNodesIndent(node.parameters, nodeIndent + indentSize * OPTIONS.FunctionExpression.parameters); const closingParen = node.getChildAt(node.getChildCount() - 2); this.checkNodeIndent(closingParen, nodeIndent); } } visitFunctionExpression(node) { if (this.isSingleLineNode(node)) { return; } this.checkFunctionMethodExpression(node); super.visitFunctionExpression(node); } visitMethodDeclaration(node) { if (this.isSingleLineNode(node)) { return; } this.checkFunctionMethodExpression(node); super.visitMethodDeclaration(node); } visitConstructorDeclaration(node) { if (this.isSingleLineNode(node)) { return; } this.checkFunctionMethodExpression(node); super.visitConstructorDeclaration(node); } visitCallExpression(node) { if (this.isSingleLineNode(node)) { return; } if (OPTIONS.CallExpression.arguments === 'first' && node.arguments.length) { const indent = this.getLineAndCharacter(node.arguments[0]).character; this.checkNodesIndent(node.arguments.slice(1), indent); } else if (OPTIONS.CallExpression.arguments !== null) { const openParen = node.getChildAt(node.getChildCount(this.srcFile) - 3, this.srcFile); const openParenIndent = this.getNodeIndent(openParen); this.checkNodesIndent(node.arguments, openParenIndent.goodChar + indentSize * OPTIONS.CallExpression.arguments); } super.visitCallExpression(node); } visitPropertyAccessExpression(node) { if (this.isSingleLineNode(node)) { return; } // The typical layout of variable declarations and assignments // alter the expectation of correct indentation. Skip them. // TODO: Add appropriate configuration options for variable // declarations and assignments. const varDec = ts.SyntaxKind.VariableDeclaration; const funcKind = [ts.SyntaxKind.FunctionExpression, ts.SyntaxKind.ArrowFunction]; if (this.getParentNodeByType(node, varDec, funcKind)) { return; } const binExp = ts.SyntaxKind.BinaryExpression; const funcExp = ts.SyntaxKind.FunctionExpression; const binaryNode = this.getParentNodeByType(node, binExp, [funcExp]); if (binaryNode && this.isAssignment(binaryNode)) { return; } super.visitPropertyAccessExpression(node); if (typeof OPTIONS.MemberExpression === 'undefined') { return; } const propertyIndent = this.getNodeIndent(node).goodChar + indentSize * OPTIONS.MemberExpression; // Assuming that the node has three children: [expression, dotToken, name] // In typescript < 2.0 we had access to the dotToken as an attribute of the node. const dotToken = node.getChildAt(1); const checkNodes = [node.name, dotToken]; this.checkNodesIndent(checkNodes, propertyIndent); } visitReturnStatement(node) { if (this.isSingleLineNode(node) || !node.expression) { return; } const firstLineIndent = this.getNodeIndent(node).goodChar; // in case if return statement is wrapped in parenthesis if (isKind(node.expression, 'ParenthesizedExpression')) { this.checkLastReturnStatementLineIndent(node, firstLineIndent); } else { this.checkNodeIndent(node, firstLineIndent); } super.visitReturnStatement(node); } visitSourceFile(node) { // Root nodes should have no indent this.checkNodesIndent(node.statements, 0); super.visitSourceFile(node); } }