sass-lint
Version:
All Node Sass linter!
188 lines (174 loc) • 5.44 kB
JavaScript
;
var helpers = require('../helpers'),
selectorHelpers = require('../selector-helpers');
var mergeableNodes = ['atrule', 'include', 'ruleset'],
validAtRules = ['media'],
curLevel = 0,
curSelector = [],
parentSelector = [],
selectorList = [];
/**
* Traverses a block and calls our callback function for each block encountered
*
* @param {object} block - The current node / part of our selector
* @param {object} cb - The callback function we wish to apply to each block
* @returns {undefined}
*/
var traverseBlock = function (block, cb) {
block.forEach(function (contentItem) {
cb(contentItem);
});
};
/**
* Traverses a block and calls our callback function for each block encountered
*
* @param {string} ruleSet - The current selector
* @param {boolean} isAtRule - Whether the ruleSet is an atRule
* @param {string} line - The line that the ruleset starts
* @param {string} col - The column that the ruleset starts
* @returns {undefined}
*/
var updateList = function (ruleSet, isAtRule, line, col) {
parentSelector[curLevel] = ruleSet;
curSelector = {
selector: helpers.stripLastSpace(parentSelector.join('')),
line: line,
column: col
};
if (!isAtRule) {
selectorList.push(curSelector);
}
};
/**
* Checks a rulesets contents for selectors and calls our consstructSelector method
*
* @param {object} ruleNode - The current node / part of our selector
* @returns {undefined}
*/
var checkRuleset = function (ruleNode) {
var ruleSet = '';
ruleNode.forEach(function (ruleNodeItem) {
if (!ruleNodeItem.is('block')) {
if (ruleNodeItem.is('selector')) {
ruleNodeItem.forEach(function (selectorContent) {
ruleSet += selectorHelpers.constructSelector(selectorContent);
});
}
else if (ruleNodeItem.is('delimiter') || ruleNodeItem.is('space')) {
ruleSet += selectorHelpers.constructSelector(ruleNodeItem);
}
}
});
if (ruleSet !== '') {
updateList(ruleSet, false, ruleNode.start.line, ruleNode.start.column);
}
};
/**
* Checks an atRule contents for selectors and calls our consstructSelector method
*
* @param {object} atRule - The current node / atRule part of our selector
* @returns {undefined}
*/
var checkAtRule = function (atRule) {
var test = '';
atRule.forEach(function (atRuleItem) {
if (!atRuleItem.is('block')) {
test += selectorHelpers.constructSelector(atRuleItem);
}
});
updateList(test, true, atRule.start.line, atRule.start.column);
};
/**
* Checks an atRule to see if if it's part of our mergeable at rule list.
*
* @param {object} node - The current node / atRule part of our selector
* @returns {boolean} Whether this atRule should be merged or not
*/
var isMergeableAtRule = function (node) {
var isMergeable = false;
node.forEach(function (item) {
// TODO Check back when Gonzales updates to fix this
// Gonzales has issues with nest levels in media queries :(
if (item.is('atkeyword') && validAtRules.indexOf(item.first('ident').content) !== -1) {
isMergeable = true;
}
});
return isMergeable;
};
/**
* Checks if a node contains a block and if so calls our traverseBlock method. Also
* handles our current level counter.
*
* @param {object} node - The current node / atRule part of our selector
* @param {object} cb - The callback function we wish to pass through
* @returns {undefined}
*/
var checkForBlock = function (node, cb) {
if (node.contains('block')) {
curLevel += 1;
node.forEach('block', function (block) {
traverseBlock(block.content, cb);
});
curLevel -= 1;
parentSelector.pop();
}
};
/**
* Traverses a node and checks for rulesets and at rules and then fires off to the
* respective method for them to be handled
*
* @param {object} node - The current node / atRule part of our selector
* @returns {undefined}
*/
var traverseNode = function (node) {
if (mergeableNodes.indexOf(node.type) !== -1) {
if (node.is('ruleset')) {
checkRuleset(node);
checkForBlock(node, traverseNode);
}
else if (node.is('atrule')) {
if (isMergeableAtRule(node)) {
checkAtRule(node);
checkForBlock(node, traverseNode);
}
}
}
};
/**
* Checks our selector list for mergeable selectors and reports errors where needed
*
* @param {object} parser - The parser object
* @returns {array} Array of result objects
*/
var checkMergeable = function (parser) {
var result = [];
selectorList.forEach(function (item, index, arr) {
var pos = helpers.propertySearch(arr, item.selector, 'selector');
if (pos !== index && parser.options.whitelist.indexOf(item.selector) === -1) {
result = helpers.addUnique(result, {
'ruleId': parser.rule.name,
'line': item.line,
'column': item.column,
'message': 'Rule `' + item.selector + '` should be merged with the rule on line ' + selectorList[pos].line,
'severity': parser.severity
});
}
});
return result;
};
module.exports = {
'name': 'no-mergeable-selectors',
'defaults': {
'whitelist': []
},
'detect': function (ast, parser) {
curLevel = 0;
curSelector = [];
parentSelector = [];
selectorList = [];
ast.traverseByType('stylesheet', function (styleSheet) {
traverseBlock(styleSheet, traverseNode);
});
return checkMergeable(parser);
}
};