| "use strict"; |
| |
| const _ = require("lodash"); |
| const declarationValueIndex = require("../../utils/declarationValueIndex"); |
| const parseCalcExpression = require("../../utils/parseCalcExpression"); |
| const report = require("../../utils/report"); |
| const ruleMessages = require("../../utils/ruleMessages"); |
| const validateOptions = require("../../utils/validateOptions"); |
| const valueParser = require("postcss-value-parser"); |
| |
| const ruleName = "function-calc-no-invalid"; |
| |
| const messages = ruleMessages(ruleName, { |
| expectedExpression: () => "Expected a valid expression", |
| expectedSpaceBeforeOperator: operator => |
| `Expected space before "${operator}" operator`, |
| expectedSpaceAfterOperator: operator => |
| `Expected space after "${operator}" operator`, |
| rejectedDivisionByZero: () => "Unexpected division by zero", |
| expectedValidResolvedType: operator => |
| `Expected to be compatible with the left and right argument types of "${operator}" operation.` |
| }); |
| |
| const rule = function(actual) { |
| return (root, result) => { |
| const validOptions = validateOptions(result, ruleName, { actual }); |
| |
| if (!validOptions) { |
| return; |
| } |
| |
| root.walkDecls(decl => { |
| const checked = []; |
| |
| valueParser(decl.value).walk(node => { |
| if (node.type !== "function" || node.value.toLowerCase() !== "calc") { |
| return; |
| } |
| |
| if (checked.indexOf(node) >= 0) { |
| return; |
| } |
| |
| checked.push(...getCalcNodes(node)); |
| |
| checked.push(...node.nodes); |
| |
| let ast; |
| |
| try { |
| ast = parseCalcExpression(valueParser.stringify(node)); |
| } catch (e) { |
| if (e.hash && e.hash.loc) { |
| complain( |
| messages.expectedExpression(), |
| node.sourceIndex + e.hash.loc.range[0] |
| ); |
| |
| return; |
| } else { |
| throw e; |
| } |
| } |
| |
| verifyMathExpressions(ast, node); |
| }); |
| |
| function complain(message, valueIndex) { |
| report({ |
| message, |
| node: decl, |
| index: declarationValueIndex(decl) + valueIndex, |
| result, |
| ruleName |
| }); |
| } |
| |
| /** |
| * Verify that each operation expression is valid. |
| * Reports when a invalid operation expression is found. |
| * @param {object} expression expression node. |
| * @param {object} node calc function node. |
| * @returns {void} |
| */ |
| function verifyMathExpressions(expression, node) { |
| if (expression.type === "MathExpression") { |
| const { operator, left, right } = expression; |
| |
| if (operator === "+" || operator === "-") { |
| if ( |
| expression.source.operator.end.index === right.source.start.index |
| ) { |
| complain( |
| messages.expectedSpaceAfterOperator(operator), |
| node.sourceIndex + expression.source.operator.end.index |
| ); |
| } |
| |
| if ( |
| expression.source.operator.start.index === left.source.end.index |
| ) { |
| complain( |
| messages.expectedSpaceBeforeOperator(operator), |
| node.sourceIndex + expression.source.operator.start.index |
| ); |
| } |
| } else if (operator === "/") { |
| if ( |
| (right.type === "Value" && right.value === 0) || |
| (right.type === "MathExpression" && getNumber(right) === 0) |
| ) { |
| complain( |
| messages.rejectedDivisionByZero(), |
| node.sourceIndex + expression.source.operator.end.index |
| ); |
| } |
| } |
| |
| if (getResolvedType(expression) === "invalid") { |
| complain( |
| messages.expectedValidResolvedType(operator), |
| node.sourceIndex + expression.source.operator.start.index |
| ); |
| } |
| |
| verifyMathExpressions(expression.left, node); |
| verifyMathExpressions(expression.right, node); |
| } |
| } |
| }); |
| }; |
| }; |
| |
| function getCalcNodes(node) { |
| if (node.type !== "function") { |
| return []; |
| } |
| |
| const functionName = node.value.toLowerCase(); |
| const result = []; |
| |
| if (functionName === "calc") { |
| result.push(node); |
| } |
| |
| if (!functionName || functionName === "calc") { |
| // find nested calc |
| for (const c of node.nodes) { |
| result.push(...getCalcNodes(c)); |
| } |
| } |
| |
| return result; |
| } |
| |
| function getNumber(mathExpression) { |
| const { left, right } = mathExpression; |
| |
| const leftValue = |
| left.type === "Value" |
| ? left.value |
| : left.type === "MathExpression" |
| ? getNumber(left) |
| : null; |
| const rightValue = |
| right.type === "Value" |
| ? right.value |
| : right.type === "MathExpression" |
| ? getNumber(right) |
| : null; |
| |
| if (_.isNil(leftValue) || _.isNil(rightValue)) { |
| return null; |
| } |
| |
| switch (mathExpression.operator) { |
| case "+": |
| return leftValue + rightValue; |
| case "-": |
| return leftValue - rightValue; |
| case "*": |
| return leftValue * rightValue; |
| case "/": |
| return leftValue / rightValue; |
| } |
| |
| return null; |
| } |
| |
| function getResolvedType(mathExpression) { |
| const { |
| left: leftExpression, |
| operator, |
| right: rightExpression |
| } = mathExpression; |
| let left = |
| leftExpression.type === "MathExpression" |
| ? getResolvedType(leftExpression) |
| : leftExpression.type; |
| let right = |
| rightExpression.type === "MathExpression" |
| ? getResolvedType(rightExpression) |
| : rightExpression.type; |
| |
| if (left === "Function" || left === "invalid") { |
| left = "UnknownValue"; |
| } |
| |
| if (right === "Function" || right === "invalid") { |
| right = "UnknownValue"; |
| } |
| |
| switch (operator) { |
| case "+": |
| case "-": |
| if (left === "UnknownValue" || right === "UnknownValue") { |
| return "UnknownValue"; |
| } |
| |
| if (left === right) { |
| return left; |
| } |
| |
| if (left === "Value" || right === "Value") { |
| return "invalid"; |
| } |
| |
| if (left === "PercentageValue") { |
| return right; |
| } |
| |
| if (right === "PercentageValue") { |
| return left; |
| } |
| |
| return "invalid"; |
| case "*": |
| if (left === "UnknownValue" || right === "UnknownValue") { |
| return "UnknownValue"; |
| } |
| |
| if (left === "Value") { |
| return right; |
| } |
| |
| if (right === "Value") { |
| return left; |
| } |
| |
| return "invalid"; |
| case "/": |
| if (right === "UnknownValue") { |
| return "UnknownValue"; |
| } |
| |
| if (right === "Value") { |
| return left; |
| } |
| |
| return "invalid"; |
| } |
| |
| return "UnknownValue"; |
| } |
| |
| rule.ruleName = ruleName; |
| rule.messages = messages; |
| module.exports = rule; |