diff --git a/lib/rules/function-calc-no-unspaced-operator/README.md b/lib/rules/function-calc-no-unspaced-operator/README.md index 6a68f443cb..d6d00e8f39 100644 --- a/lib/rules/function-calc-no-unspaced-operator/README.md +++ b/lib/rules/function-calc-no-unspaced-operator/README.md @@ -11,6 +11,8 @@ a { top: calc(1px + 2px); } Before the operator, there must be a single whitespace or a newline plus indentation. After the operator, there must be a single whitespace or a newline. +The [`fix` option](../../../docs/user-guide/usage/options.md#fix) can automatically fix all of the problems reported by this rule. + ## Options ### `true` diff --git a/lib/rules/function-calc-no-unspaced-operator/__tests__/index.js b/lib/rules/function-calc-no-unspaced-operator/__tests__/index.js index 27db1656e8..2bf9083171 100644 --- a/lib/rules/function-calc-no-unspaced-operator/__tests__/index.js +++ b/lib/rules/function-calc-no-unspaced-operator/__tests__/index.js @@ -7,6 +7,7 @@ const { messages, ruleName } = require('..'); testRule({ ruleName, config: [true], + fix: true, accept: [ { @@ -155,6 +156,7 @@ testRule({ reject: [ { code: 'a { top: calc(1px +\t-1px)}', + fixed: 'a { top: calc(1px + -1px)}', description: 'tab before sign after operator', message: messages.expectedAfter('+'), line: 1, @@ -162,6 +164,7 @@ testRule({ }, { code: 'a { top: calc(1px + -1px)}', + fixed: 'a { top: calc(1px + -1px)}', description: 'multiple spaces before sign after operator', message: messages.expectedAfter('+'), line: 1, @@ -169,102 +172,119 @@ testRule({ }, { code: 'a { top: calc(1px+ 2px); }', + fixed: 'a { top: calc(1px + 2px); }', message: messages.expectedBefore('+'), line: 1, column: 18, }, { code: 'a { top: cAlC(1px+ 2px); }', + fixed: 'a { top: cAlC(1px + 2px); }', message: messages.expectedBefore('+'), line: 1, column: 18, }, { code: 'a { top: CALC(1px+ 2px); }', + fixed: 'a { top: CALC(1px + 2px); }', message: messages.expectedBefore('+'), line: 1, column: 18, }, { code: 'a { top: calc(1px + 2px); }', + fixed: 'a { top: calc(1px + 2px); }', message: messages.expectedBefore('+'), line: 1, column: 20, }, { code: 'a { top: calc(1px\t+ 2px); }', + fixed: 'a { top: calc(1px + 2px); }', message: messages.expectedBefore('+'), line: 1, column: 19, }, { code: 'a { top: calc(1px + 2px); }', + fixed: 'a { top: calc(1px + 2px); }', message: messages.expectedAfter('+'), line: 1, column: 19, }, { code: 'a { top: calc(1px +\t2px); }', + fixed: 'a { top: calc(1px + 2px); }', message: messages.expectedAfter('+'), line: 1, column: 19, }, { code: 'a { top: calc(1px- 2px); }', + fixed: 'a { top: calc(1px - 2px); }', message: messages.expectedBefore('-'), line: 1, column: 18, }, { code: 'a { top: calc(1px* 2); }', + fixed: 'a { top: calc(1px * 2); }', message: messages.expectedBefore('*'), line: 1, column: 18, }, { code: 'a { top: calc(1px *2); }', + fixed: 'a { top: calc(1px * 2); }', message: messages.expectedAfter('*'), line: 1, column: 19, }, { code: 'a { top: calc(1px/ 2); }', + fixed: 'a { top: calc(1px / 2); }', message: messages.expectedBefore('/'), line: 1, column: 18, }, { code: 'a { top: calc(1px /2); }', + fixed: 'a { top: calc(1px / 2); }', message: messages.expectedAfter('/'), line: 1, column: 19, }, { code: 'a { top: calc(calc(1px* 2px) + 3px); }', + fixed: 'a { top: calc(calc(1px * 2px) + 3px); }', message: messages.expectedBefore('*'), line: 1, column: 23, }, { code: 'a { top: calc(calc(1px + 2px)* 3px); }', + fixed: 'a { top: calc(calc(1px + 2px) * 3px); }', message: messages.expectedBefore('*'), line: 1, column: 30, }, { code: 'a { top: calc(1px +2px); }', + fixed: 'a { top: calc(1px + 2px); }', message: messages.expectedOperatorBeforeSign('+'), line: 1, column: 19, }, { code: 'a { top: calc(1px -2px); }', + fixed: 'a { top: calc(1px - 2px); }', message: messages.expectedOperatorBeforeSign('-'), line: 1, column: 19, }, { code: 'a { padding: 10px calc(1px +\t-1px)}', + fixed: 'a { padding: 10px calc(1px + -1px)}', description: 'tab before sign after operator', message: messages.expectedAfter('+'), line: 1, @@ -272,6 +292,7 @@ testRule({ }, { code: 'a { padding: 10px calc(1px + -1px)}', + fixed: 'a { padding: 10px calc(1px + -1px)}', description: 'multiple spaces before sign after operator', message: messages.expectedAfter('+'), line: 1, @@ -279,88 +300,166 @@ testRule({ }, { code: 'a { padding: 10px calc(1px+ 2px); }', + fixed: 'a { padding: 10px calc(1px + 2px); }', message: messages.expectedBefore('+'), line: 1, column: 27, }, { code: 'a { padding: 10px calc(1px + 2px); }', + fixed: 'a { padding: 10px calc(1px + 2px); }', message: messages.expectedBefore('+'), line: 1, column: 29, }, { code: 'a { padding: 10px calc(1px\t+ 2px); }', + fixed: 'a { padding: 10px calc(1px + 2px); }', message: messages.expectedBefore('+'), line: 1, column: 28, }, { code: 'a { padding: 10px calc(1px + 2px); }', + fixed: 'a { padding: 10px calc(1px + 2px); }', message: messages.expectedAfter('+'), line: 1, column: 28, }, { code: 'a { padding: 10px calc(1px +\t2px); }', + fixed: 'a { padding: 10px calc(1px + 2px); }', message: messages.expectedAfter('+'), line: 1, column: 28, }, { code: 'a { padding: 10px calc(1px- 2px); }', + fixed: 'a { padding: 10px calc(1px - 2px); }', message: messages.expectedBefore('-'), line: 1, column: 27, }, { code: 'a { padding: 10px calc(1px* 2); }', + fixed: 'a { padding: 10px calc(1px * 2); }', message: messages.expectedBefore('*'), line: 1, column: 27, }, { code: 'a { padding: 10px calc(1px *2); }', + fixed: 'a { padding: 10px calc(1px * 2); }', message: messages.expectedAfter('*'), line: 1, column: 28, }, { code: 'a { padding: 10px calc(1px/ 2); }', + fixed: 'a { padding: 10px calc(1px / 2); }', message: messages.expectedBefore('/'), line: 1, column: 27, }, { code: 'a { padding: 10px calc(1px /2); }', + fixed: 'a { padding: 10px calc(1px / 2); }', message: messages.expectedAfter('/'), line: 1, column: 28, }, { code: 'a { padding: 10px calc(calc(1px* 2px) + 3px); }', + fixed: 'a { padding: 10px calc(calc(1px * 2px) + 3px); }', message: messages.expectedBefore('*'), line: 1, column: 32, }, { code: 'a { padding: 10px calc(calc(1px + 2px)* 3px); }', + fixed: 'a { padding: 10px calc(calc(1px + 2px) * 3px); }', message: messages.expectedBefore('*'), line: 1, column: 39, }, { code: 'a { padding: 10px calc(1px +2px); }', + fixed: 'a { padding: 10px calc(1px + 2px); }', message: messages.expectedOperatorBeforeSign('+'), line: 1, column: 28, }, { code: 'a { padding: 10px calc(1px -2px); }', + fixed: 'a { padding: 10px calc(1px - 2px); }', message: messages.expectedOperatorBeforeSign('-'), line: 1, column: 28, }, + { + code: 'a { padding: calc(1rem\t + 1em)}', + fixed: 'a { padding: calc(1rem + 1em)}', + description: 'several whitespace characters before operator starting from space', + message: messages.expectedBefore('+'), + line: 1, + column: 25, + }, + { + code: 'a { padding: calc(1rem \t+ 1em)}', + fixed: 'a { padding: calc(1rem + 1em)}', + description: 'several whitespace characters before operator starting from tab', + message: messages.expectedBefore('+'), + line: 1, + column: 26, + }, + { + code: 'a { padding: calc(1rem\t\f\r\t+ 1em)}', + fixed: 'a { padding: calc(1rem + 1em)}', + description: 'several incorrect whitespace characters before operator', + message: messages.expectedBefore('+'), + line: 1, + column: 27, + }, + { + code: 'a { padding: calc(1rem + \t1em)}', + fixed: 'a { padding: calc(1rem + 1em)}', + description: 'several whitespace characters after operator starting from space', + message: messages.expectedAfter('+'), + line: 1, + column: 24, + }, + { + code: 'a { padding: calc(1rem +\t \t1em)}', + fixed: 'a { padding: calc(1rem + 1em)}', + description: 'several whitespace characters after operator starting from tab', + message: messages.expectedAfter('+'), + line: 1, + column: 24, + }, + { + code: 'a { padding: calc(1rem +\t\r\f\t1em)}', + fixed: 'a { padding: calc(1rem + 1em)}', + description: 'several incorrect whitespace characters after operator', + message: messages.expectedAfter('+'), + line: 1, + column: 24, + }, + { + code: 'a { padding: calc(1rem +\t \t\f\n\f\t1em)}', + fixed: 'a { padding: calc(1rem +\n\f\t1em)}', + description: 'several whitespace characters after operator but before the \\n', + message: messages.expectedAfter('+'), + line: 1, + column: 24, + }, + { + code: 'a { padding: calc(1rem + \t\r\n 1em)}', + fixed: 'a { padding: calc(1rem +\r\n 1em)}', + description: 'several whitespace characters after operator but before the \\r\\n', + message: messages.expectedAfter('+'), + line: 1, + column: 24, + }, ], }); @@ -368,22 +467,26 @@ testRule({ ruleName, config: [true], customSyntax: postcssScss, + fix: true, reject: [ { code: 'a { top: calc(100%- #{$foo}); }', + fixed: 'a { top: calc(100% - #{$foo}); }', message: messages.expectedBefore('-'), line: 1, column: 19, }, { code: 'a { top: calc(100% *#{$foo}); }', + fixed: 'a { top: calc(100% * #{$foo}); }', message: messages.expectedAfter('*'), line: 1, column: 20, }, { code: 'a { top: calc(100% -#{$foo}); }', + fixed: 'a { top: calc(100% - #{$foo}); }', message: messages.expectedOperatorBeforeSign('-'), line: 1, column: 20, diff --git a/lib/rules/function-calc-no-unspaced-operator/index.js b/lib/rules/function-calc-no-unspaced-operator/index.js index abcc9e8f70..e8ec9106e8 100644 --- a/lib/rules/function-calc-no-unspaced-operator/index.js +++ b/lib/rules/function-calc-no-unspaced-operator/index.js @@ -18,7 +18,7 @@ const messages = ruleMessages(ruleName, { expectedOperatorBeforeSign: (operator) => `Expected an operator before sign "${operator}"`, }); -function rule(actual) { +function rule(actual, secondary, context) { return (root, result) => { const validOptions = validateOptions(result, ruleName, { actual }); @@ -31,6 +31,8 @@ function rule(actual) { } root.walkDecls((decl) => { + const symbolsToFix = []; + valueParser(decl.value).walk((node) => { if (node.type !== 'function' || node.value.toLowerCase() !== 'calc') { return; @@ -65,13 +67,14 @@ function rule(actual) { styleSearch(styleSearchOptions, (match) => { const index = match.startIndex; + const symbolIndex = node.sourceIndex + parensMatch.start + index + 1; // Deal with signs. // (@ and $ are considered "digits" here to allow for variable syntaxes // that permit signs in front of variables, e.g. `-$number`) // As is "." to deal with fractional numbers without a leading zero if ((symbol === '+' || symbol === '-') && /[\d@$.]/.test(expression[index + 1])) { - const expressionBeforeSign = expression.substr(0, index); + const expressionBeforeSign = expression.slice(0, index); // Ignore signs that directly follow a opening bracket if (expressionBeforeSign[expressionBeforeSign.length - 1] === '(') { @@ -88,10 +91,16 @@ function rule(actual) { return; } - // And if not, complain - complain(messages.expectedOperatorBeforeSign(symbol), decl, expressionIndex + index); + if (!context.fix) { + // And if not, complain + complain( + messages.expectedOperatorBeforeSign(symbol), + decl, + expressionIndex + index, + ); - return; + return; + } } const beforeOk = @@ -99,24 +108,100 @@ function rule(actual) { newlineBefore(expression, index - 1); if (!beforeOk) { - complain(messages.expectedBefore(symbol), decl, expressionIndex + index); + if (context.fix) { + let step = 1; + + // Remove all whitespace characters before the operator, e.g. `\t` + while (isWhitespace(expression[index - step])) { + symbolsToFix.push({ + index: symbolIndex - step, + insert: false, + }); + + step++; + } + + // Add only one space character + symbolsToFix.push({ + index: symbolIndex, + insert: true, + }); + } else { + complain(messages.expectedBefore(symbol), decl, expressionIndex + index); + } } const afterOk = (expression[index + 1] === ' ' && !isWhitespace(expression[index + 2])) || - expression[index + 1] === '\n' || - expression.substr(index + 1, 2) === '\r\n'; + isNewlineAtIndex(expression, index + 1); if (!afterOk) { - complain(messages.expectedAfter(symbol), decl, expressionIndex + index); + if (context.fix) { + let step = 1; + let spaceNeeded = true; + + // Remove all whitespace characters before the operator or \n, e.g. \t + while (isWhitespace(expression[index + step])) { + if (isNewlineAtIndex(expression, index + step)) { + spaceNeeded = false; + break; + } + + symbolsToFix.push({ + index: symbolIndex + step, + insert: false, + }); + step++; + } + + // Insert one space character if there is no \n + if (spaceNeeded) { + symbolsToFix.push({ + index: symbolIndex + 1, + insert: true, + }); + } + } else { + complain(messages.expectedAfter(symbol), decl, expressionIndex + index); + } } }); } }); + + if (context.fix) { + decl.value = symbolsToFix.reduce((fixedValue, { insert, index }) => { + shiftIndexes(symbolsToFix, index, insert); + + return insert + ? insertCharAtIndex(fixedValue, index, ' ') + : removeCharAtIndex(fixedValue, index); + }, decl.value); + } }); }; } +function isNewlineAtIndex(str, index) { + return str[index] === '\n' || str.slice(index, index + 2) === '\r\n'; +} + +function shiftIndexes(symbolsToFix, index, insert) { + symbolsToFix.forEach((symbol) => { + if (symbol.index > index) { + symbol.index += insert ? 1 : -1; + } + }); +} + +function removeCharAtIndex(str, index) { + return str.slice(0, index) + str.slice(index + 1, str.length); +} + +function insertCharAtIndex(str, index, char) { + return str.slice(0, index) + char + str.slice(index, str.length); +} + function blurVariables(source) { return source.replace(/[$@][^)\s]+|#{.+?}/g, '0'); }