sass-lint
Version:
All Node Sass linter!
212 lines (191 loc) • 7.85 kB
JavaScript
;
var helpers = require('../helpers');
module.exports = {
'name': 'indentation',
'defaults': {
'size': 2
},
'detect': function (ast, parser) {
var result = [],
inAtRule = false,
inProps = false,
inBlock = false,
lintSize = parser.options.size,
lintType = 'space',
plural = '',
detected = [lintType];
// Prepare to check for mixed spaces or tabs depending on what the user has specified
if (parser.options.size === 'tab') {
lintSize = 1;
lintType = 'tab';
detected[0] = lintType;
}
var processNode = function (node, level) {
var i,
n,
prevNode,
nextNode,
sassNextNode,
reportNode,
reportCondition,
space,
spaceLength,
newlineLength,
spaceCount,
tabCount,
mixedWarning;
level = level || 0;
if (node.is('braces')) {
return;
}
for (i = 0; i < node.length; i++) {
n = node.get(i);
prevNode = node.get(i - 1);
nextNode = node.get(i + 1) || false;
// Due to the Sass structure in gonzales we sometimes need to check 2 ahead
sassNextNode = node.get(i + 2) || false;
reportNode = null;
if (!n) {
continue;
}
if (n.syntax === 'scss') {
if (n.type === 'space') {
// Test for CRLF first, since it includes LF
space = n.content.lastIndexOf('\r\n');
newlineLength = 2;
if (space === -1) {
// Test for LF
space = n.content.lastIndexOf('\n');
newlineLength = 1;
}
if (space >= 0) {
// Check how many spaces or tabs we have and set our plural character if necessary for
// our lint reporting message
spaceLength = n.content.slice(space + newlineLength).length;
spaceCount = n.content.slice(space + newlineLength).match(/ /g);
tabCount = n.content.slice(space + newlineLength).match(/\t/g);
plural = level > 1 ? 's' : '';
reportNode = nextNode;
reportCondition = i !== node.length - 1;
}
}
}
else if (n.syntax === 'sass') {
if (n.is('declarationDelimiter') || (helpers.isNewLine(n))) {
// Due to the way gonzales handles line endings in Sass we don't care if it's CRLF or just LF
if (nextNode && nextNode.is('space') && nextNode.content.indexOf('\n') === -1) {
spaceLength = nextNode.content.length;
spaceCount = nextNode.content.match(/ /g);
tabCount = nextNode.content.match(/\t/g);
plural = level > 1 ? 's' : '';
// if we're at the end of a block we want to drop the level here for Sass
if (!node.get(i + 2)) {
level--;
}
reportNode = sassNextNode;
reportCondition = true;
}
}
// Check all the spaces in Sass that aren't newlines
else if (helpers.isSpace(n)) {
// This is a special condition for the first property in a block with Sass as it usually
// doesn't have a previous node before the space appears so we need to check this is
// valid and then we can rely on the declarationDelimiter check above.
if (inBlock && (!prevNode || prevNode.is('space'))) {
inBlock = false;
spaceLength = n.content.length;
spaceCount = n.content.match(/ /g);
tabCount = n.content.match(/\t/g);
plural = level > 1 ? 's' : '';
reportNode = nextNode;
reportCondition = true;
}
// A extra check for tabs when using spaces as single tab characters aren't highlighted
// as mixed spaces and tabs without this. Spaces on the other hand are fine. Gonzales
// reports them a little differently.
else if (n.type === 'space' && lintType === 'space') {
tabCount = n.content.match(/\t/g);
reportNode = nextNode;
// we dont want to check the lint levels here as it could be a tab between a prop and
// value, totally unrealistic I know but we still want to report it.
reportCondition = false;
}
}
}
if (reportNode) {
// if we've encountered a space check if we have before if not save a reference
if (spaceCount !== null && detected.indexOf('space') === -1) {
detected.push('space');
}
// if we've encountered a tab check if we have before if not save a reference
if (tabCount !== null && detected.indexOf('tab') === -1) {
detected.push('tab');
}
if (detected.length > 1) {
// Indicates we've told the user about mixed tabs and spaces in their file
mixedWarning = true;
// Remove the last detected type from our detected array,
// if we encounter a mix again we'll output again but all the while keep a reference
// to the first space character (tab or space) that we encountered so as to be
// consistent with our warnings
detected.pop();
result = helpers.addUnique(result, {
'ruleId': parser.rule.name,
'line': reportNode.start.line,
'column': reportNode.start.column,
'message': 'Mixed tabs and spaces',
'severity': parser.severity
});
}
if (reportCondition && !mixedWarning && spaceLength / lintSize !== level) {
// Check if expected indentation matches what it should be
result = helpers.addUnique(result, {
'ruleId': parser.rule.name,
'line': reportNode.start.line,
'column': reportNode.start.column,
'message': 'Expected indentation of ' + level * lintSize + ' ' + lintType + plural + ' but found ' + spaceLength + '.',
'severity': parser.severity
});
}
mixedWarning = false;
}
// if we're in an atrule make we need to possibly handle multiline arguments
if (n.is('atrule') && n.contains('block')) {
inAtRule = true;
inBlock = false;
}
// if a delimeter is encountered we check if it's directly after a parenthesis node
// if it is we know next node will be the same level of indentation
if (n.is('operator')) {
if (n.content === ',' && prevNode.is('parentheses') && helpers.isNewLine(nextNode)) {
if (inAtRule && !inProps) {
level++;
inProps = true;
}
else if (!inProps) {
level--;
}
}
}
// if a block node is encountered we first check to see if it's within an include/function
// by checking if the node also contains arguments, if it does we skip the block as we add a level
// for arguments anyway. If not the the block is a usual ruleset block and should be treated accordingly
// The other checks are kept from 1.0 and work for their respective types.
if ((n.is('block') && !node.contains('arguments'))
|| n.is('arguments')
|| (n.is('parentheses') && !node.is('atrule'))
) {
level++;
}
if (n.is('block')) {
inAtRule = false;
inProps = false;
inBlock = true;
}
processNode(n, level);
}
};
processNode(ast);
return result;
}
};