Skip to content

fix: var() with fallback value in no-invalid-properties #184

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 165 additions & 59 deletions src/rules/no-invalid-properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,36 +26,44 @@ import { isSyntaxMatchError, isSyntaxReferenceError } from "../util.js";
//-----------------------------------------------------------------------------

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

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

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

/*
* We need the offset of the replacement after other replacements have
* been made, so we push the current length of the result before appending
* the replacement value.
*/
offsets.push(result.length);
result += replaceValue;
lastIndex = match.index + match[0].length;
const prop = match[1].trim();
const fallback = match[2]?.trim();

list.push(prop);

if (!fallback) {
break;
}

// If fallback is not another var(), we're done
if (!fallback.includes("var(")) {
list.push(fallback);
break;
}

// Continue parsing from fallback
currentValue = fallback;
}

result += text.slice(lastIndex);
return { text: result, offsets };
return list;
}

//-----------------------------------------------------------------------------
Expand Down Expand Up @@ -146,53 +154,151 @@ export default {

const varsFound = replacements.pop();

/** @type {Map<number,CssLocationRange>} */
const varsFoundLocs = new Map();
/** @type {Map<string,CssLocationRange>} */
const valuesWithVarLocs = new Map();
const usingVars = varsFound?.size > 0;
let value = node.value;

if (usingVars) {
// need to use a text version of the value here
value = sourceCode.getText(node.value);
let offsets;

// replace any custom properties with their values
for (const [name, func] of varsFound) {
const varValue = vars.get(name);

if (varValue) {
({ text: value, offsets } = replaceWithOffsets(
value,
name,
sourceCode.getText(varValue).trim(),
));

/*
* Store the offsets of the replacements so we can
* report the correct location of any validation error.
*/
offsets.forEach(offset => {
varsFoundLocs.set(offset, func.loc);
});
} else if (!allowUnknownVariables) {
context.report({
loc: func.children[0].loc,
messageId: "unknownVar",
data: {
var: name,
},
});

return;
const valueList = [];
const valueNodes = node.value.children;

// When `var()` is used, we store all the values to `valueList` with the replacement of `var()` with there values or fallback values
for (const child of valueNodes) {
// If value is a function starts with `var()`
if (child.type === "Function" && child.name === "var") {
const varValue = vars.get(child.children[0].name);

// If the variable is found, use its value, otherwise check for fallback values
if (varValue) {
const varValueText = sourceCode
.getText(varValue)
.trim();

valueList.push(varValueText);
valuesWithVarLocs.set(varValueText, child.loc);
} else {
// If the variable is not found and doesn't have a fallback value, report it
if (child.children.length === 1) {
if (!allowUnknownVariables) {
context.report({
loc: child.children[0].loc,
messageId: "unknownVar",
data: {
var: child.children[0].name,
},
});

return;
}
} else {
// If it has a fallback value, use that
if (child.children[2].type === "Raw") {
const fallbackVarList =
getVarFallbackList(
child.children[2].value.trim(),
);
if (fallbackVarList.length > 0) {
let gotFallbackVarValue = false;

for (const fallbackVar of fallbackVarList) {
if (
fallbackVar.startsWith("--")
) {
const fallbackVarValue =
vars.get(fallbackVar);

if (!fallbackVarValue) {
continue; // Try the next fallback
}

valueList.push(
sourceCode
.getText(
fallbackVarValue,
)
.trim(),
);
valuesWithVarLocs.set(
sourceCode
.getText(
fallbackVarValue,
)
.trim(),
child.loc,
);
gotFallbackVarValue = true;
break; // Stop after finding the first valid variable
} else {
const fallbackValue =
fallbackVar.trim();
valueList.push(
fallbackValue,
);
valuesWithVarLocs.set(
fallbackValue,
child.loc,
);
gotFallbackVarValue = true;
break; // Stop after finding the first non-variable fallback
}
}

// If none of the fallback value is defined then report an error
if (
!allowUnknownVariables &&
!gotFallbackVarValue
) {
context.report({
loc: child.children[0].loc,
messageId: "unknownVar",
data: {
var: child.children[0]
.name,
},
});

return;
}
} else {
// if it has a fallback value, use that
const fallbackValue =
child.children[2].value.trim();
valueList.push(fallbackValue);
valuesWithVarLocs.set(
fallbackValue,
child.loc,
);
}
}
}
}
} else {
// If the child is not a `var()` function, just add its text to the `valueList`
const valueText = sourceCode.getText(child).trim();
valueList.push(valueText);
valuesWithVarLocs.set(valueText, child.loc);
}
}

value =
valueList.length > 0
? valueList.join(" ")
: sourceCode.getText(node.value);
}

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

if (error) {
// validation failure
if (isSyntaxMatchError(error)) {
const errorValue =
usingVars &&
value.slice(
error.mismatchOffset,
error.mismatchOffset + error.mismatchLength,
);

context.report({
/*
* When using variables, check to see if the error
Expand All @@ -201,7 +307,7 @@ export default {
* reported location.
*/
loc: usingVars
? (varsFoundLocs.get(error.mismatchOffset) ??
? (valuesWithVarLocs.get(errorValue) ??
node.value.loc)
: error.loc,
messageId: "invalidPropertyValue",
Expand Down
Loading
Loading