This repository has been archived on 2020-11-02. You can view files and clone it, but cannot push or open issues or pull requests.
2020-11-01 22:46:04 +00:00

260 lines
6.0 KiB
JavaScript

// @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;