sass-lint
Version:
All Node Sass linter!
327 lines (286 loc) • 8.23 kB
JavaScript
'use strict';
var helpers = require('../helpers');
var operators = ['+', '-', '/', '*', '%', '<', '>', '==', '!=', '<=', '>='];
/**
* Determine a relational operator based on the operator node
*
* @param {Object} node - The operator node
* @returns {string} Returns a relational operator
*/
var getRelationalOperator = function (node) {
if (node.content === '<') {
return '<=';
}
if (node.content === '>') {
return '>=';
}
return false;
};
/**
* Determine if operator is negative number
*
* @param {string} operator - The operator
* @param {Object} next - The next node
* @param {Object} previous - The previous node
* @param {Object} doublePrevious - The double previous node (back two)
* @returns {bool} true / false
*/
var isNegativeNumber = function (operator, next, previous, doublePrevious) {
if (operator === '-') {
// Catch the following:
// $foo: -20;
// $foo: -#{$foo}
// $foo: -($foo * 2)
// $foo: -$foo
if (next) {
if (!previous || (previous.is('space') && doublePrevious && !doublePrevious.is('number'))) {
if (
next.is('number') ||
next.is('interpolation') ||
next.is('variable') ||
next.is('parentheses')
) {
return true;
}
}
}
// Catch the following:
// .foo {
// property: -16px;
// }
if (next && (next.is('dimension') || next.is('percentage'))) {
return true;
}
// Catch the following:
// .foo {
// propery: 2 / -16;
// }
if (doublePrevious && doublePrevious.is('operator')) {
return true;
}
// Catch the following:
// .foo {
// property: 2 /-16px;
// }
if (previous && previous.is('operator')) {
return true;
}
}
return false;
};
/**
* Determine if operator is divider
*
* @param {string} operator - The operator
* @param {Object} next - The next node
* @param {Object} previous - The previous node
* @returns {bool} true / false
*/
var isDivider = function (operator, next, previous) {
if (operator === '/') {
// Catch the following:
// .foo {
// property: calc(100% / 2);
// }
if (previous && next) {
if (previous.is('dimension') && (next.is('dimension') || next.is('number'))) {
return true;
}
}
}
return false;
};
/**
* Determine if operator is part of unicode
*
* @param {string} operator - The operator
* @param {Object} previous - The previous node
* @returns {bool} true / false
*/
var isUnicode = function (operator, previous) {
if (operator === '+') {
// Catch the following:
// @font-face {
// unicode-range: U+26;
// }
if (previous && previous.is('ident') && previous.content === 'U') {
return true;
}
}
return false;
};
/**
* Determine if operator is part of import path
*
* @param {string} operator - The operator
* @param {Object} parent - The parent node
* @returns {bool} true / false
*/
var isImport = function (operator, parent) {
if (operator === '/') {
if (parent && parent.is('atrule') && parent.contains('atkeyword')) {
var keyword = parent.first('atkeyword');
if (keyword.contains('ident')) {
var ident = keyword.first('ident');
if (ident.content === 'import') {
return true;
}
}
}
}
return false;
};
/**
* Determine if operator is part an ident
*
* @param {string} operator - The operator
* @param {Object} next - The next node
* @param {Object} previous - The previous node
* @returns {bool} true / false
*/
var isPartialIdent = function (operator, next, previous) {
if (operator === '-') {
return next && previous && previous.is('interpolation');
}
return false;
};
/**
* Determine if operator is exception
*
* @param {string} operator - The operator
* @param {Object} parent - The parent node
* @param {Object} i - The node index
* @returns {bool} true / false
*/
var isException = function (operator, parent, i) {
var previous = parent.content[i - 1] || false,
doublePrevious = parent.content[i - 2] || false,
next = parent.content[i + 1] || false;
if (isNegativeNumber(operator, next, previous, doublePrevious)) {
return true;
}
if (isDivider(operator, next, previous)) {
return true;
}
if (isUnicode(operator, previous)) {
return true;
}
if (isImport(operator, parent)) {
return true;
}
if (isPartialIdent(operator, next, previous)) {
return true;
}
return false;
};
/**
* Check the spacing around an operator
*
* @param {Object} node - The node to check the spacing around
* @param {int} i - The node's child index of it's parent
* @param {Object} parent - The parent node
* @param {Object} parser - The parser object
* @param {Object} result - The result object
* @returns {bool|null} false if exception
*/
var checkSpacing = function (node, i, parent, parser, result) {
if (node.is('operator') || node.is('unaryOperator')) {
var previous = parent.content[i - 1] || false,
next = parent.content[i + 1] || false,
operator = node.content;
//////////////////////////
// Multi-part operators
//////////////////////////
// If second part of relational operator move on
if (node.content === '=' && previous) {
if (previous.content === '<' || previous.content === '>') {
return false;
}
}
// If first part of relational operator, carry on and build it
if ((node.content === '<' || node.content === '>') && next) {
if (next.content === '=') {
operator = getRelationalOperator(node);
next = parent.content[i + 2] || false;
}
}
//////////////////////////
// Exceptions
//////////////////////////
if (isException(operator, parent, i)) {
return false;
}
// If the operator checks out in our valid operator list
if (operators.indexOf(operator) !== -1) {
if (parser.options.include) {
if (
(previous && !previous.is('space'))
|| (next && !next.is('space'))
) {
result = helpers.addUnique(result, {
'ruleId': parser.rule.name,
'line': node.start.line,
'column': node.start.column,
'message': 'Space expected around operator',
'severity': parser.severity
});
}
else {
if (
(previous && (previous.end.line >= previous.start.line) && (previous.end.column > previous.start.column))
|| (next && (next.end.line >= next.start.line) && (next.end.column > next.start.column))
) {
result = helpers.addUnique(result, {
'ruleId': parser.rule.name,
'line': node.start.line,
'column': node.start.column,
'message': 'Multiple spaces not allowed around operator',
'severity': parser.severity
});
}
}
}
else {
if (
(previous && previous.is('space'))
|| (next && next.is('space'))
) {
result = helpers.addUnique(result, {
'ruleId': parser.rule.name,
'line': node.start.line,
'column': node.start.column,
'message': 'No spaces allowed around operator',
'severity': parser.severity
});
}
}
}
}
return result;
};
module.exports = {
'name': 'space-around-operator',
'defaults': {
'include': true
},
'detect': function (ast, parser) {
var result = [];
ast.traverseByTypes(['condition', 'atrule', 'value'], function (node) {
node.forEach(function (item, i, parent) {
// Perform another loop of the children if we come across a parenthesis
// parent node
if (item.is('parentheses')) {
item.forEach(function (child, j, childParent) {
// Do the spacing checks
checkSpacing(child, j, childParent, parser, result);
});
}
else {
// Do the spacing checks
checkSpacing(item, i, parent, parser, result);
}
});
});
return result;
}
};