UNPKG

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