You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

487 lines
15 KiB

/**
* @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 /* istanbul ignore else : this will be caught by options validation */ 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);
}
};
};
module.exports.schema = [
{
"oneOf": [
{
"enum": ["tab"]
},
{
"type": "integer"
}
]
},
{
"type": "object",
"properties": {
"indentSwitchCase": {
"type": "boolean"
}
},
"additionalProperties": false
}
];