Skip to content

Commit 1db0b1a

Browse files
fix: var() with fallback value in no-invalid-properties (eslint#184)
* fix: property value with fallback * update code to fix location and multiple var() * check fallback list in condition --------- Co-authored-by: Nitin Kumar <snitin315@gmail.com>
1 parent 777fa49 commit 1db0b1a

File tree

2 files changed

+302
-60
lines changed

2 files changed

+302
-60
lines changed

src/rules/no-invalid-properties.js

Lines changed: 165 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -26,36 +26,44 @@ import { isSyntaxMatchError, isSyntaxReferenceError } from "../util.js";
2626
//-----------------------------------------------------------------------------
2727

2828
/**
29-
* Replaces all instances of a regex pattern with a replacement and tracks the offsets
30-
* @param {string} text The text to perform replacements on
31-
* @param {string} varName The regex pattern string to search for
32-
* @param {string} replaceValue The string to replace with
33-
* @returns {{text: string, offsets: Array<number>}} The updated text and array of offsets
34-
* where replacements occurred
29+
* Extracts the list of fallback value or variable name used in a `var()` that is used as fallback function.
30+
* For example, for `var(--my-color, var(--fallback-color, red));` it will return `["--fallback-color", "red"]`.
31+
* @param {string} value The fallback value that is used in `var()`.
32+
* @return {Array<string>} The list of variable names of fallback value.
3533
*/
36-
function replaceWithOffsets(text, varName, replaceValue) {
37-
const offsets = [];
38-
let result = "";
39-
let lastIndex = 0;
34+
function getVarFallbackList(value) {
35+
const list = [];
36+
let currentValue = value;
4037

41-
const regex = new RegExp(`var\\(\\s*${varName}\\s*\\)`, "gu");
42-
let match;
38+
while (true) {
39+
const match = currentValue.match(
40+
/var\(\s*(--[^,\s)]+)\s*(?:,\s*(.+))?\)/u,
41+
);
4342

44-
while ((match = regex.exec(text)) !== null) {
45-
result += text.slice(lastIndex, match.index);
43+
if (!match) {
44+
break;
45+
}
4646

47-
/*
48-
* We need the offset of the replacement after other replacements have
49-
* been made, so we push the current length of the result before appending
50-
* the replacement value.
51-
*/
52-
offsets.push(result.length);
53-
result += replaceValue;
54-
lastIndex = match.index + match[0].length;
47+
const prop = match[1].trim();
48+
const fallback = match[2]?.trim();
49+
50+
list.push(prop);
51+
52+
if (!fallback) {
53+
break;
54+
}
55+
56+
// If fallback is not another var(), we're done
57+
if (!fallback.includes("var(")) {
58+
list.push(fallback);
59+
break;
60+
}
61+
62+
// Continue parsing from fallback
63+
currentValue = fallback;
5564
}
5665

57-
result += text.slice(lastIndex);
58-
return { text: result, offsets };
66+
return list;
5967
}
6068

6169
//-----------------------------------------------------------------------------
@@ -146,53 +154,151 @@ export default {
146154

147155
const varsFound = replacements.pop();
148156

149-
/** @type {Map<number,CssLocationRange>} */
150-
const varsFoundLocs = new Map();
157+
/** @type {Map<string,CssLocationRange>} */
158+
const valuesWithVarLocs = new Map();
151159
const usingVars = varsFound?.size > 0;
152160
let value = node.value;
153161

154162
if (usingVars) {
155-
// need to use a text version of the value here
156-
value = sourceCode.getText(node.value);
157-
let offsets;
158-
159-
// replace any custom properties with their values
160-
for (const [name, func] of varsFound) {
161-
const varValue = vars.get(name);
162-
163-
if (varValue) {
164-
({ text: value, offsets } = replaceWithOffsets(
165-
value,
166-
name,
167-
sourceCode.getText(varValue).trim(),
168-
));
169-
170-
/*
171-
* Store the offsets of the replacements so we can
172-
* report the correct location of any validation error.
173-
*/
174-
offsets.forEach(offset => {
175-
varsFoundLocs.set(offset, func.loc);
176-
});
177-
} else if (!allowUnknownVariables) {
178-
context.report({
179-
loc: func.children[0].loc,
180-
messageId: "unknownVar",
181-
data: {
182-
var: name,
183-
},
184-
});
185-
186-
return;
163+
const valueList = [];
164+
const valueNodes = node.value.children;
165+
166+
// When `var()` is used, we store all the values to `valueList` with the replacement of `var()` with there values or fallback values
167+
for (const child of valueNodes) {
168+
// If value is a function starts with `var()`
169+
if (child.type === "Function" && child.name === "var") {
170+
const varValue = vars.get(child.children[0].name);
171+
172+
// If the variable is found, use its value, otherwise check for fallback values
173+
if (varValue) {
174+
const varValueText = sourceCode
175+
.getText(varValue)
176+
.trim();
177+
178+
valueList.push(varValueText);
179+
valuesWithVarLocs.set(varValueText, child.loc);
180+
} else {
181+
// If the variable is not found and doesn't have a fallback value, report it
182+
if (child.children.length === 1) {
183+
if (!allowUnknownVariables) {
184+
context.report({
185+
loc: child.children[0].loc,
186+
messageId: "unknownVar",
187+
data: {
188+
var: child.children[0].name,
189+
},
190+
});
191+
192+
return;
193+
}
194+
} else {
195+
// If it has a fallback value, use that
196+
if (child.children[2].type === "Raw") {
197+
const fallbackVarList =
198+
getVarFallbackList(
199+
child.children[2].value.trim(),
200+
);
201+
if (fallbackVarList.length > 0) {
202+
let gotFallbackVarValue = false;
203+
204+
for (const fallbackVar of fallbackVarList) {
205+
if (
206+
fallbackVar.startsWith("--")
207+
) {
208+
const fallbackVarValue =
209+
vars.get(fallbackVar);
210+
211+
if (!fallbackVarValue) {
212+
continue; // Try the next fallback
213+
}
214+
215+
valueList.push(
216+
sourceCode
217+
.getText(
218+
fallbackVarValue,
219+
)
220+
.trim(),
221+
);
222+
valuesWithVarLocs.set(
223+
sourceCode
224+
.getText(
225+
fallbackVarValue,
226+
)
227+
.trim(),
228+
child.loc,
229+
);
230+
gotFallbackVarValue = true;
231+
break; // Stop after finding the first valid variable
232+
} else {
233+
const fallbackValue =
234+
fallbackVar.trim();
235+
valueList.push(
236+
fallbackValue,
237+
);
238+
valuesWithVarLocs.set(
239+
fallbackValue,
240+
child.loc,
241+
);
242+
gotFallbackVarValue = true;
243+
break; // Stop after finding the first non-variable fallback
244+
}
245+
}
246+
247+
// If none of the fallback value is defined then report an error
248+
if (
249+
!allowUnknownVariables &&
250+
!gotFallbackVarValue
251+
) {
252+
context.report({
253+
loc: child.children[0].loc,
254+
messageId: "unknownVar",
255+
data: {
256+
var: child.children[0]
257+
.name,
258+
},
259+
});
260+
261+
return;
262+
}
263+
} else {
264+
// if it has a fallback value, use that
265+
const fallbackValue =
266+
child.children[2].value.trim();
267+
valueList.push(fallbackValue);
268+
valuesWithVarLocs.set(
269+
fallbackValue,
270+
child.loc,
271+
);
272+
}
273+
}
274+
}
275+
}
276+
} else {
277+
// If the child is not a `var()` function, just add its text to the `valueList`
278+
const valueText = sourceCode.getText(child).trim();
279+
valueList.push(valueText);
280+
valuesWithVarLocs.set(valueText, child.loc);
187281
}
188282
}
283+
284+
value =
285+
valueList.length > 0
286+
? valueList.join(" ")
287+
: sourceCode.getText(node.value);
189288
}
190289

191290
const { error } = lexer.matchProperty(node.property, value);
192291

193292
if (error) {
194293
// validation failure
195294
if (isSyntaxMatchError(error)) {
295+
const errorValue =
296+
usingVars &&
297+
value.slice(
298+
error.mismatchOffset,
299+
error.mismatchOffset + error.mismatchLength,
300+
);
301+
196302
context.report({
197303
/*
198304
* When using variables, check to see if the error
@@ -201,7 +307,7 @@ export default {
201307
* reported location.
202308
*/
203309
loc: usingVars
204-
? (varsFoundLocs.get(error.mismatchOffset) ??
310+
? (valuesWithVarLocs.get(errorValue) ??
205311
node.value.loc)
206312
: error.loc,
207313
messageId: "invalidPropertyValue",

0 commit comments

Comments
 (0)