diff options
Diffstat (limited to 'tools/eslint/lib/rules/indent.js')
-rw-r--r-- | tools/eslint/lib/rules/indent.js | 464 |
1 files changed, 464 insertions, 0 deletions
diff --git a/tools/eslint/lib/rules/indent.js b/tools/eslint/lib/rules/indent.js new file mode 100644 index 0000000000..b0e838e74b --- /dev/null +++ b/tools/eslint/lib/rules/indent.js @@ -0,0 +1,464 @@ +/** + * @fileoverview This option sets a specific tab width for your code + * This rule has been ported and modified from JSCS. + * @author Dmitriy Shekhovtsov + * @copyright 2015 Dmitriy Shekhovtsov. All rights reserved. + * @copyright 2013 Dulin Marat and other contributors. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +/*eslint no-use-before-define:[2, "nofunc"]*/ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function (context) { + // indentation defaults: 4 spaces + var indentChar = " "; + var indentSize = 4; + var options = {indentSwitchCase: false}; + + var lines = null; + var indentStack = [0]; + var linesToCheck = null; + var breakIndents = null; + + if (context.options.length) { + if (context.options[0] === "tab") { + indentChar = "\t"; + indentSize = 1; + } else if (typeof context.options[0] === "number") { + indentSize = context.options[0]; + } + + if (context.options[1]) { + var opts = context.options[1]; + options.indentSwitchCase = opts.indentSwitchCase === true; + } + } + + var blockParents = [ + "IfStatement", + "WhileStatement", + "DoWhileStatement", + "ForStatement", + "ForInStatement", + "ForOfStatement", + "FunctionDeclaration", + "FunctionExpression", + "ArrowExpression", + "CatchClause", + "WithStatement" + ]; + + var indentableNodes = { + BlockStatement: "body", + Program: "body", + ObjectExpression: "properties", + ArrayExpression: "elements", + SwitchStatement: "cases" + }; + + if (options.indentSwitchCase) { + indentableNodes.SwitchCase = "consequent"; + } + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Mark line to be checked + * @param {Number} line - line number + * @returns {void} + */ + function markCheckLine(line) { + linesToCheck[line].check = true; + } + + /** + * Mark line with targeted node to be checked + * @param {ASTNode} checkNode - targeted node + * @returns {void} + */ + function markCheck(checkNode) { + markCheckLine(checkNode.loc.start.line - 1); + } + + /** + * Sets pushing indent of current node + * @param {ASTNode} node - targeted node + * @param {Number} indents - indents count to push + * @returns {void} + */ + function markPush(node, indents) { + linesToCheck[node.loc.start.line - 1].push.push(indents); + } + + /** + * Marks line as outdent, end of block statement for example + * @param {ASTNode} node - targeted node + * @param {Number} outdents - count of outedents in targeted line + * @returns {void} + */ + function markPop(node, outdents) { + linesToCheck[node.loc.end.line - 1].pop.push(outdents); + } + + /** + * Set alt push for current node + * @param {ASTNode} node - targeted node + * @returns {void} + */ + function markPushAlt(node) { + linesToCheck[node.loc.start.line - 1].pushAltLine.push(node.loc.end.line - 1); + } + + /** + * Marks end of node block to be checked + * and marks targeted node as indent pushing + * @param {ASTNode} pushNode - targeted node + * @param {Number} indents - indent count to push + * @returns {void} + */ + function markPushAndEndCheck(pushNode, indents) { + markPush(pushNode, indents); + markCheckLine(pushNode.loc.end.line - 1); + } + + /** + * Mark node as switch case statement + * and set push\pop indentation changes + * @param {ASTNode} caseNode - targeted node + * @param {ASTNode[]} children - consequent child nodes of case node + * @returns {void} + */ + function markCase(caseNode, children) { + var outdentNode = getCaseOutdent(children); + + if (outdentNode) { + // If a case statement has a `break` as a direct child and it is the + // first one encountered, use it as the example for all future case indentation + if (breakIndents === null) { + breakIndents = (caseNode.loc.start.column === outdentNode.loc.start.column) ? 1 : 0; + } + markPop(outdentNode, breakIndents); + } else { + markPop(caseNode, 0); + } + } + + /** + * Mark child nodes to be checked later of targeted node, + * only if child node not in same line as targeted one + * (if child and parent nodes wrote in single line) + * @param {ASTNode} node - targeted node + * @returns {void} + */ + function markChildren(node) { + getChildren(node).forEach(function(childNode) { + if (childNode.loc.start.line !== node.loc.start.line || node.type === "Program") { + markCheck(childNode); + } + }); + } + + /** + * Mark child block as scope pushing and mark to check + * @param {ASTNode} node - target node + * @param {String} property - target node property containing child + * @returns {void} + */ + function markAlternateBlockStatement(node, property) { + var child = node[property]; + if (child && child.type === "BlockStatement") { + markCheck(child); + } + } + + /** + * Checks whether node is multiline or single line + * @param {ASTNode} node - target node + * @returns {boolean} - is multiline node + */ + function isMultiline(node) { + return node.loc.start.line !== node.loc.end.line; + } + + /** + * Get switch case statement outdent node if any + * @param {ASTNode[]} caseChildren - case statement childs + * @returns {ASTNode} - outdent node + */ + function getCaseOutdent(caseChildren) { + var outdentNode; + caseChildren.some(function(node) { + if (node.type === "BreakStatement") { + outdentNode = node; + return true; + } + }); + + return outdentNode; + } + + /** + * Returns block containing node + * @param {ASTNode} node - targeted node + * @returns {ASTNode} - block node + */ + function getBlockNodeToMark(node) { + var parent = node.parent; + + // The parent of an else is the entire if/else block. To avoid over indenting + // in the case of a non-block if with a block else, mark push where the else starts, + // not where the if starts! + if (parent.type === "IfStatement" && parent.alternate === node) { + return node; + } + + // The end line to check of a do while statement needs to be the location of the + // closing curly brace, not the while statement, to avoid marking the last line of + // a multiline while as a line to check. + if (parent.type === "DoWhileStatement") { + return node; + } + + // Detect bare blocks: a block whose parent doesn"t expect blocks in its syntax specifically. + if (blockParents.indexOf(parent.type) === -1) { + return node; + } + + return parent; + } + + /** + * Get node's children + * @param {ASTNode} node - current node + * @returns {ASTNode[]} - children + */ + function getChildren(node) { + var childrenProperty = indentableNodes[node.type]; + return node[childrenProperty]; + } + + /** + * Gets indentation in line `i` + * @param {Number} i - number of line to get indentation + * @returns {Number} - count of indentation symbols + */ + function getIndentationFromLine(i) { + var rNotIndentChar = new RegExp("[^" + indentChar + "]"); + var firstContent = lines[i].search(rNotIndentChar); + if (firstContent === -1) { + firstContent = lines[i].length; + } + return firstContent; + } + + /** + * Compares expected and actual indentation + * and reports any violations + * @param {ASTNode} node - node used only for reporting + * @returns {void} + */ + function checkIndentations(node) { + linesToCheck.forEach(function(line, i) { + var actualIndentation = getIndentationFromLine(i); + var expectedIndentation = getExpectedIndentation(line, actualIndentation); + + if (line.check) { + + if (actualIndentation !== expectedIndentation) { + context.report(node, + {line: i + 1, column: expectedIndentation}, + "Expected indentation of " + expectedIndentation + " characters."); + // correct the indentation so that future lines + // can be validated appropriately + actualIndentation = expectedIndentation; + } + } + + if (line.push.length) { + pushExpectedIndentations(line, actualIndentation); + } + }); + } + + /** + * Counts expected indentation for given line number + * @param {Number} line - line number + * @param {Number} actual - actual indentation + * @returns {number} - expected indentation + */ + function getExpectedIndentation(line, actual) { + var outdent = indentSize * Math.max.apply(null, line.pop); + + var idx = indentStack.length - 1; + var expected = indentStack[idx]; + + if (!Array.isArray(expected)) { + expected = [expected]; + } + + expected = expected.map(function(value) { + if (line.pop.length) { + value -= outdent; + } + + return value; + }).reduce(function(previous, current) { + // when the expected is an array, resolve the value + // back into a Number by checking both values are the actual indentation + return actual === current ? current : previous; + }); + + indentStack[idx] = expected; + + line.pop.forEach(function() { + indentStack.pop(); + }); + + return expected; + } + + /** + * Store in stack expected indentations + * @param {Number} line - current line + * @param {Number} actualIndentation - actual indentation at current line + * @returns {void} + */ + function pushExpectedIndentations(line, actualIndentation) { + var indents = Math.max.apply(null, line.push); + var expected = actualIndentation + (indentSize * indents); + + // when a line has alternate indentations, push an array of possible values + // on the stack, to be resolved when checked against an actual indentation + if (line.pushAltLine.length) { + expected = [expected]; + line.pushAltLine.forEach(function(altLine) { + expected.push(getIndentationFromLine(altLine) + (indentSize * indents)); + }); + } + + line.push.forEach(function() { + indentStack.push(expected); + }); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + "Program": function (node) { + lines = context.getSourceLines(); + linesToCheck = lines.map(function () { + return { + push: [], + pushAltLine: [], + pop: [], + check: false + }; + }); + + if (!isMultiline(node)) { + return; + } + + markChildren(node); + }, + "Program:exit": function (node) { + checkIndentations(node); + }, + + "BlockStatement": function (node) { + if (!isMultiline(node)) { + return; + } + + markChildren(node); + markPop(node, 1); + + markPushAndEndCheck(getBlockNodeToMark(node), 1); + }, + + "IfStatement": function (node) { + markAlternateBlockStatement(node, "alternate"); + }, + + "TryStatement": function (node) { + markAlternateBlockStatement(node, "handler"); + markAlternateBlockStatement(node, "finalizer"); + }, + + "SwitchStatement": function (node) { + if (!isMultiline(node)) { + return; + } + + var indents = 1; + var children = getChildren(node); + + if (children.length && node.loc.start.column === children[0].loc.start.column) { + indents = 0; + } + + markChildren(node); + markPop(node, indents); + markPushAndEndCheck(node, indents); + }, + + "SwitchCase": function (node) { + if (!options.indentSwitchCase) { + return; + } + + if (!isMultiline(node)) { + return; + } + + var children = getChildren(node); + + if (children.length === 1 && children[0].type === "BlockStatement") { + return; + } + + markPush(node, 1); + markCheck(node); + markChildren(node); + + markCase(node, children); + }, + + // indentations inside of function expressions can be offset from + // either the start of the function or the end of the function, therefore + // mark all starting lines of functions as potential indentations + "FunctionDeclaration": function (node) { + markPushAlt(node); + }, + "FunctionExpression": function (node) { + markPushAlt(node); + } + }; + +}; |