// @ts-nocheck 'use strict'; const declarationValueIndex = require('../../utils/declarationValueIndex'); const isStandardSyntaxMathFunction = require('../../utils/isStandardSyntaxMathFunction'); 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.`, }); function rule(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; } const mathFunction = valueParser.stringify(node); if (!isStandardSyntaxMathFunction(mathFunction)) { return; } if (checked.includes(node)) { return; } checked.push(...getCalcNodes(node)); checked.push(...node.nodes); let ast; try { ast = parseCalcExpression(mathFunction); } catch (e) { if (e.hash && e.hash.loc) { complain(messages.expectedExpression(), node.sourceIndex + e.hash.loc.range[0]); return; } 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 (leftValue == null || rightValue == null) { 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;