You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
259 lines
6.0 KiB
259 lines
6.0 KiB
// @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;
|
|
|