|
|
|
/**
|
|
|
|
* @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
|
|
|
|
}
|
|
|
|
];
|