UNPKG

5.95 kBJavaScriptView Raw
1'use strict';
2
3const declarationValueIndex = require('../../utils/declarationValueIndex');
4const parseCalcExpression = require('../../utils/parseCalcExpression');
5const report = require('../../utils/report');
6const ruleMessages = require('../../utils/ruleMessages');
7const validateOptions = require('../../utils/validateOptions');
8const valueParser = require('postcss-value-parser');
9
10const ruleName = 'function-calc-no-invalid';
11
12const messages = ruleMessages(ruleName, {
13 expectedExpression: () => 'Expected a valid expression',
14 expectedSpaceBeforeOperator: (operator) => `Expected space before "${operator}" operator`,
15 expectedSpaceAfterOperator: (operator) => `Expected space after "${operator}" operator`,
16 rejectedDivisionByZero: () => 'Unexpected division by zero',
17 expectedValidResolvedType: (operator) =>
18 `Expected to be compatible with the left and right argument types of "${operator}" operation.`,
19});
20
21function rule(actual) {
22 return (root, result) => {
23 const validOptions = validateOptions(result, ruleName, { actual });
24
25 if (!validOptions) {
26 return;
27 }
28
29 root.walkDecls((decl) => {
30 const checked = [];
31
32 valueParser(decl.value).walk((node) => {
33 if (node.type !== 'function' || node.value.toLowerCase() !== 'calc') {
34 return;
35 }
36
37 if (checked.includes(node)) {
38 return;
39 }
40
41 checked.push(...getCalcNodes(node));
42
43 checked.push(...node.nodes);
44
45 let ast;
46
47 try {
48 ast = parseCalcExpression(valueParser.stringify(node));
49 } catch (e) {
50 if (e.hash && e.hash.loc) {
51 complain(messages.expectedExpression(), node.sourceIndex + e.hash.loc.range[0]);
52
53 return;
54 }
55
56 throw e;
57 }
58
59 verifyMathExpressions(ast, node);
60 });
61
62 function complain(message, valueIndex) {
63 report({
64 message,
65 node: decl,
66 index: declarationValueIndex(decl) + valueIndex,
67 result,
68 ruleName,
69 });
70 }
71
72 /**
73 * Verify that each operation expression is valid.
74 * Reports when a invalid operation expression is found.
75 * @param {object} expression expression node.
76 * @param {object} node calc function node.
77 * @returns {void}
78 */
79 function verifyMathExpressions(expression, node) {
80 if (expression.type === 'MathExpression') {
81 const { operator, left, right } = expression;
82
83 if (operator === '+' || operator === '-') {
84 if (expression.source.operator.end.index === right.source.start.index) {
85 complain(
86 messages.expectedSpaceAfterOperator(operator),
87 node.sourceIndex + expression.source.operator.end.index,
88 );
89 }
90
91 if (expression.source.operator.start.index === left.source.end.index) {
92 complain(
93 messages.expectedSpaceBeforeOperator(operator),
94 node.sourceIndex + expression.source.operator.start.index,
95 );
96 }
97 } else if (operator === '/') {
98 if (
99 (right.type === 'Value' && right.value === 0) ||
100 (right.type === 'MathExpression' && getNumber(right) === 0)
101 ) {
102 complain(
103 messages.rejectedDivisionByZero(),
104 node.sourceIndex + expression.source.operator.end.index,
105 );
106 }
107 }
108
109 if (getResolvedType(expression) === 'invalid') {
110 complain(
111 messages.expectedValidResolvedType(operator),
112 node.sourceIndex + expression.source.operator.start.index,
113 );
114 }
115
116 verifyMathExpressions(expression.left, node);
117 verifyMathExpressions(expression.right, node);
118 }
119 }
120 });
121 };
122}
123
124function getCalcNodes(node) {
125 if (node.type !== 'function') {
126 return [];
127 }
128
129 const functionName = node.value.toLowerCase();
130 const result = [];
131
132 if (functionName === 'calc') {
133 result.push(node);
134 }
135
136 if (!functionName || functionName === 'calc') {
137 // find nested calc
138 for (const c of node.nodes) {
139 result.push(...getCalcNodes(c));
140 }
141 }
142
143 return result;
144}
145
146function getNumber(mathExpression) {
147 const { left, right } = mathExpression;
148
149 const leftValue =
150 left.type === 'Value' ? left.value : left.type === 'MathExpression' ? getNumber(left) : null;
151 const rightValue =
152 right.type === 'Value'
153 ? right.value
154 : right.type === 'MathExpression'
155 ? getNumber(right)
156 : null;
157
158 // eslint-disable-next-line eqeqeq
159 if (leftValue == null || rightValue == null) {
160 return null;
161 }
162
163 switch (mathExpression.operator) {
164 case '+':
165 return leftValue + rightValue;
166 case '-':
167 return leftValue - rightValue;
168 case '*':
169 return leftValue * rightValue;
170 case '/':
171 return leftValue / rightValue;
172 }
173
174 return null;
175}
176
177function getResolvedType(mathExpression) {
178 const { left: leftExpression, operator, right: rightExpression } = mathExpression;
179 let left =
180 leftExpression.type === 'MathExpression'
181 ? getResolvedType(leftExpression)
182 : leftExpression.type;
183 let right =
184 rightExpression.type === 'MathExpression'
185 ? getResolvedType(rightExpression)
186 : rightExpression.type;
187
188 if (left === 'Function' || left === 'invalid') {
189 left = 'UnknownValue';
190 }
191
192 if (right === 'Function' || right === 'invalid') {
193 right = 'UnknownValue';
194 }
195
196 switch (operator) {
197 case '+':
198 case '-':
199 if (left === 'UnknownValue' || right === 'UnknownValue') {
200 return 'UnknownValue';
201 }
202
203 if (left === right) {
204 return left;
205 }
206
207 if (left === 'Value' || right === 'Value') {
208 return 'invalid';
209 }
210
211 if (left === 'PercentageValue') {
212 return right;
213 }
214
215 if (right === 'PercentageValue') {
216 return left;
217 }
218
219 return 'invalid';
220 case '*':
221 if (left === 'UnknownValue' || right === 'UnknownValue') {
222 return 'UnknownValue';
223 }
224
225 if (left === 'Value') {
226 return right;
227 }
228
229 if (right === 'Value') {
230 return left;
231 }
232
233 return 'invalid';
234 case '/':
235 if (right === 'UnknownValue') {
236 return 'UnknownValue';
237 }
238
239 if (right === 'Value') {
240 return left;
241 }
242
243 return 'invalid';
244 }
245
246 return 'UnknownValue';
247}
248
249rule.ruleName = ruleName;
250rule.messages = messages;
251module.exports = rule;