echo-fecs
Version:
Front End Code Style Suite
469 lines (387 loc) • 14.3 kB
JavaScript
/**
* @file Rule to flag use of bad indent.
* @author Nodeca Team <https://github.com/nodeca>
* @author chris<wfsr@foxmail.com>
*/
;
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = function (context) {
var MESSAGE = 'Bad indentation ({{gotten}} instead {{needed}}).';
var sourceCode = context.getSourceCode();
var extraColumnStart = 0;
var indentType = (context.options[0] === 'tab') ? 'tab' : 'space';
var indentSize = +(context.options[1] || 4);
var rootIndex = context.options[2] | 0;
var indentSwitchCase = context.options[3] !== false;
var tabSpaces = new Array(indentSize + 1).join(' ');
var tabLineMap;
/**
* Get node indent
*
* @param {ASTNode} node AST node
* @param {boolean} byLastLine get indent of node's last line
* @param {boolean} excludeCommas skip comma on start of line
* @return {number} node indents
*/
var getNodeIndent = function (node, byLastLine, excludeCommas) {
byLastLine = byLastLine || false;
excludeCommas = excludeCommas || false;
var src = context.getSource(node, node.loc.start.column + extraColumnStart);
var lines = src.split('\n');
if (byLastLine) {
src = lines[lines.length - 1];
}
else {
src = lines[0];
}
var skip = excludeCommas ? ',' : '';
var regExp;
var tabReg = /\t/g;
if (indentType === 'space') {
var loc = node.loc[byLastLine ? 'end' : 'start'];
if (tabReg.test(src) && !tabLineMap[loc.line]) {
tabLineMap[loc.line] = true;
context.report(node, loc, 'Expected space but saw tab.');
}
regExp = new RegExp('^[ ' + skip + ']+');
src = src.replace(tabReg, tabSpaces);
}
else {
regExp = new RegExp('^[\t' + skip + ']+');
}
var indent = regExp.exec(src);
return indent ? indent[0].length : 0;
};
/**
* Checks node is the first in its own start line. By default it looks by start line.
*
* @param {ASTNode} node The node to check
* @return {boolean} true if its the first in the its start line
*/
function isNodeFirstInLine(node) {
var firstToken = context.getTokenBefore(node);
var startLine = node.loc.start.line;
var endLine = firstToken ? firstToken.loc.end.line : -1;
return startLine !== endLine;
}
/**
* Check indent for nodes list
*
* @param {Array.<ASTNode>} nodes list of node objects
* @param {number} indent needed indent
* @param {boolean} excludeCommas skip comma on start of line
* @param {?string=} messageSuffix string append to MESSAGE
*/
var checkNodesIndent = function (nodes, indent, excludeCommas, messageSuffix) {
nodes.forEach(function (node) {
var nodeIndent = getNodeIndent(node, false, excludeCommas);
if (nodeIndent !== indent && isNodeFirstInLine(node)) {
context.report(node, MESSAGE + (messageSuffix || ''), {
gotten: nodeIndent,
needed: indent
});
}
});
};
/**
* Check last node line indent this detects, that block closed correctly
*
* @param {ASTNode} node AST node
* @param {number} lastLineIndent needed indent
*/
var checkLastNodeLineIndent = function (node, lastLineIndent) {
var endIndent = getNodeIndent(node, true);
if (endIndent !== lastLineIndent) {
context.report(
node,
{
line: node.loc.end.line,
column: node.loc.end.column
},
MESSAGE,
{
gotten: endIndent,
needed: lastLineIndent
}
);
}
};
/**
* Check indent for function block content
*
* @param {ASTNode} node AST node
*/
var checkIndentInFunctionBlock = function (node) {
// Search first caller in chain.
// Ex.:
//
// Models <- Identifier
// .User
// .find()
// .exec(function () {
// // function body
// });
//
// Looks for 'Models'
var calleeNode = node.parent; // FunctionExpression
var indent;
if (~',Property,ArrayExpression,CallExpression'.indexOf(',' + calleeNode.parent.type)) {
// If function is part of array or object, comma can be put at left
indent = getNodeIndent(calleeNode, false, true);
}
else {
// If function is standalone, simple calculate indent
indent = getNodeIndent(calleeNode);
}
indent += indentSize;
// If function content is not empty
if (node.body.length) {
// Calculate left shift position don't require strict indent
// allow function body allign to (indentSize * X)
while (getNodeIndent(node.body[0]) > indent) {
indent += indentSize;
}
}
checkNodesIndent(node.body, indent);
checkLastNodeLineIndent(node, indent - indentSize);
};
/**
* Check indent for array block content or object block content
*
* @param {ASTNode} node AST node
*/
var checkIndentInArrayOrObjectBlock = function (node) {
// Skip inline
if (node.loc.start.line === node.loc.end.line) {
return;
}
var elements = (node.type === 'ArrayExpression') ? node.elements : node.properties;
// Skip if first element is in same line with this node
if (elements.length > 0 && elements[0].loc.start.line === node.loc.start.line) {
return;
}
var nodeIndent = getNodeIndent(node);
var elementsIndent = nodeIndent + indentSize;
// Elements can have double indent (detected by first item)
if (elements.length > 0 && getNodeIndent(elements[0]) === elementsIndent + indentSize) {
elementsIndent = elementsIndent + indentSize;
}
// Comma can be placed before property name
checkNodesIndent(elements, elementsIndent, true);
if (elements.length > 0) {
// Skip last block line check if last item in same line
if (elements[elements.length - 1].loc.end.line === node.loc.end.line) {
return;
}
}
checkLastNodeLineIndent(node, elementsIndent - indentSize);
};
/**
* Check indent for html strings array
*
* @param {ASTNode} node AST node
* @return {boolean}
*/
var isHTMLStrings = function (node) {
var varNode = node.parent.parent;
if (node.loc.start.line === node.loc.end.line) {
return false;
}
var types = {};
var startColumn = getNodeIndent(node);
var lastColumn = startColumn;
var lastElement;
var messages = [];
var htmlFlag = 0;
var comments = sourceCode.getComments(varNode).leading;
if (comments[0] && /\bhtml\b/.test(comments[0].value)) {
htmlFlag = 3;
}
node.elements.forEach(function (el, i) {
types[el.type] = (types[el.type] | 0) + 1;
if (htmlFlag !== 3 && el.type === 'Literal' && typeof el.value === 'string') {
if (htmlFlag === 0 && ~el.value.indexOf('<')) {
htmlFlag |= 1;
}
else if (htmlFlag === 1 && ~el.value.indexOf('>')) {
htmlFlag |= 2;
}
}
if (
lastElement && lastElement.loc.start.line === el.loc.start.line
|| i === 0 && el.type === 'ArrayExpression' && node.loc.start.line === el.loc.start.line
) {
return;
}
var column = el.loc.start.column;
var needed;
if (column <= startColumn) {
needed = 'bigger then ' + (startColumn + indentSize);
}
else if (column !== lastColumn && Math.abs(column - lastColumn) !== indentSize) {
needed = 'one of [' + [lastColumn - indentSize, lastColumn, lastColumn + indentSize].join(', ') + ']';
}
if (needed) {
messages.push([el, {gotten: column, needed: needed}]);
}
else {
lastColumn = column;
}
lastElement = el;
});
var validTypes = [
'ArrayExpression', 'BinaryExpression',
'CallExpression', 'ConditionalExpression',
'Identifier', 'Literal', 'MemberExpression'
];
var total = validTypes.reduce(
function (total, type) {
total += types[type] | 0;
return total;
},
0
);
if (types.Literal === node.elements.length && htmlFlag !== 3) {
return false;
}
if (htmlFlag === 3 || total === node.elements.length && total > 1) {
messages.forEach(function (message) {
context.report(message[0], MESSAGE, message[1]);
});
return true;
}
return false;
};
var caseIndentStore;
/**
* Returns the expected indentation for the case statement
*
* @param {SwitchCase} node node to examine
* @return {number} indent size
*/
function expectedCaseIndent(node) {
var switchNode = node.parent;
if (caseIndentStore[switchNode.loc.start.line]) {
return caseIndentStore[switchNode.loc.start.line];
}
var switchIndent = getNodeIndent(switchNode);
var caseIndent = switchIndent + (indentSwitchCase ? indentSize : 0);
caseIndentStore[switchNode.loc.start.line] = caseIndent;
return caseIndent;
}
/**
* Check whether in the same line
*
* @param {ASTNode} left left node
* @param {ASTNode} right right node
* @return {boolean}
*/
var isSameLine = function (left, right) {
var target = right.loc.start.line;
return left.loc.start.line === target || left.loc.end.line === target;
};
return {
'Program': function (node) {
tabLineMap = {};
caseIndentStore = {};
// Root nodes should have no indent
checkNodesIndent(node.body, rootIndex);
},
'Program:exit': function () {
tabLineMap = null;
},
'BlockStatement': function (node) {
// Skip inline blocks
if (node.loc.start.line === node.loc.end.line) {
return;
}
if (node.parent && (
node.parent.type === 'FunctionExpression'
|| node.parent.type === 'FunctionDeclaration'
|| node.parent.type === 'ArrowFunctionExpression'
)) {
checkIndentInFunctionBlock(node);
return;
}
var indent;
// For this statements we should check indent from statement begin
// (not from block begin)
var statementsWithProperties = [
'IfStatement', 'WhileStatement', 'ForStatement',
'ForInStatement', 'ForOfStatement', 'DoWhileStatement'
];
if (node.parent && statementsWithProperties.indexOf(node.parent.type) !== -1) {
indent = getNodeIndent(node.parent);
}
else {
indent = getNodeIndent(node);
}
checkNodesIndent(node.body, indent + indentSize);
checkLastNodeLineIndent(node, indent);
},
'ObjectExpression': function (node) {
checkIndentInArrayOrObjectBlock(node);
},
'ArrayExpression': function (node) {
if (!isHTMLStrings(node)) {
checkIndentInArrayOrObjectBlock(node);
}
},
'SwitchStatement': function (node) {
// Switch is not a 'BlockStatement'
var indent = getNodeIndent(node);
checkNodesIndent(node.cases, indent + (indentSwitchCase ? indentSize : 0), false, 'baidu004');
checkLastNodeLineIndent(node, indent);
},
'SwitchCase': function (node) {
// Skip inline cases
if (node.loc.start.line === node.loc.end.line) {
return;
}
var indent = expectedCaseIndent(node);
checkNodesIndent(node.consequent, indent + indentSize);
},
'MemberExpression': function (node) {
if (isSameLine(node.object, node.property)) {
return;
}
var indent = getNodeIndent(node.object);
var propertyIndex = getNodeIndent(node.property);
var needed;
if (node.parent.type === 'CallExpression') {
if (indent !== propertyIndex && propertyIndex - indent !== indentSize) {
needed = indent + ' or ' + (indent + indentSize);
}
}
else if (propertyIndex - indent !== indentSize) {
needed = '' + (indent + indentSize);
}
if (needed) {
context.report(
node.property,
MESSAGE,
{
gotten: propertyIndex,
needed: needed
}
);
}
}
};
};
module.exports.schema = [
{
'enum': ['tab', 'space']
},
{
type: 'integer'
},
{
type: 'integer'
},
{
type: 'boolean'
}
];