diff --git a/src/rules/no-invalid-properties.js b/src/rules/no-invalid-properties.js index 104d236b..22456b60 100644 --- a/src/rules/no-invalid-properties.js +++ b/src/rules/no-invalid-properties.js @@ -124,6 +124,127 @@ export default { const [{ allowUnknownVariables }] = context.options; + /** + * Process a var function node and add its resolved value to the value list + * @param {Object} varNode The var function node + * @param {string[]} valueList Array to collect processed values + * @param {Map} valuesWithVarLocs Map of values to their locations + * @returns {boolean} Whether processing was successful + */ + function processVarFunction(varNode, valueList, valuesWithVarLocs) { + const varValue = vars.get(varNode.children[0].name); + + if (varValue) { + const varValueText = sourceCode.getText(varValue).trim(); + valueList.push(varValueText); + valuesWithVarLocs.set(varValueText, varNode.loc); + return true; + } + + // If the variable is not found and doesn't have a fallback value, report it + if (varNode.children.length === 1) { + if (!allowUnknownVariables) { + context.report({ + loc: varNode.children[0].loc, + messageId: "unknownVar", + data: { var: varNode.children[0].name }, + }); + return false; + } + return true; + } + + // Handle fallback values + if (varNode.children[2].type !== "Raw") { + return true; + } + + const fallbackVarList = getVarFallbackList( + varNode.children[2].value.trim(), + ); + + if (fallbackVarList.length === 0) { + const fallbackValue = varNode.children[2].value.trim(); + valueList.push(fallbackValue); + valuesWithVarLocs.set(fallbackValue, varNode.loc); + return true; + } + + // Process fallback variables + for (const fallbackVar of fallbackVarList) { + if (fallbackVar.startsWith("--")) { + const fallbackVarValue = vars.get(fallbackVar); + if (fallbackVarValue) { + const fallbackValue = sourceCode + .getText(fallbackVarValue) + .trim(); + valueList.push(fallbackValue); + valuesWithVarLocs.set(fallbackValue, varNode.loc); + return true; + } + } else { + valueList.push(fallbackVar.trim()); + valuesWithVarLocs.set(fallbackVar.trim(), varNode.loc); + return true; + } + } + + // No valid fallback found + if (!allowUnknownVariables) { + context.report({ + loc: varNode.children[0].loc, + messageId: "unknownVar", + data: { var: varNode.children[0].name }, + }); + return false; + } + + return true; + } + + /** + * Process a nested function by recursively handling its children + * @param {FunctionNodePlain} funcNode The function node + * @param {Map} valuesWithVarLocs Map of values to their locations + * @returns {string|null} The processed function string or null if processing failed + */ + function processNestedFunction(funcNode, valuesWithVarLocs) { + const nestedValueList = []; + + for (const nestedChild of funcNode.children) { + if ( + nestedChild.type === "Function" && + nestedChild.name === "var" + ) { + if ( + !processVarFunction( + nestedChild, + nestedValueList, + valuesWithVarLocs, + ) + ) { + return null; + } + } else if (nestedChild.type === "Function") { + // Recursively process nested functions + const processedNestedFunction = processNestedFunction( + nestedChild, + valuesWithVarLocs, + ); + if (!processedNestedFunction) { + return null; + } + nestedValueList.push(processedNestedFunction); + } else { + nestedValueList.push( + sourceCode.getText(nestedChild).trim(), + ); + } + } + + return `${funcNode.name}(${nestedValueList.join(" ")})`; + } + return { "Rule > Block > Declaration"() { replacements.push(new Map()); @@ -167,112 +288,24 @@ export default { 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, - ); - } - } - } + if ( + !processVarFunction( + child, + valueList, + valuesWithVarLocs, + ) + ) { + return; + } + } else if (child.type === "Function") { + const processedFunction = processNestedFunction( + child, + valuesWithVarLocs, + ); + if (!processedFunction) { + return; } + valueList.push(processedFunction); } else { // If the child is not a `var()` function, just add its text to the `valueList` const valueText = sourceCode.getText(child).trim(); diff --git a/tests/rules/no-invalid-properties.test.js b/tests/rules/no-invalid-properties.test.js index d8768749..101da59b 100644 --- a/tests/rules/no-invalid-properties.test.js +++ b/tests/rules/no-invalid-properties.test.js @@ -55,6 +55,36 @@ ruleTester.run("no-invalid-properties", rule, { ":root { --my-color: red; }\na { color: var(--my-color, var(--fallback-color, var(--foo, var(--bar)))) }", ":root { --my-color: red; }\na { color: var(--my-color, var(--fallback-color, var(--foo, var(--bar, blue)))) }", ":root { --color: red }\na { border-top: 1px var(--style, var(--fallback, solid)) var(--color, blue); }", + "a { width: calc(var(--my-width, 100px)) }", + ":root { --my-heading: 3rem; }\na { width: calc(var(--my-width, 100px)) }", + ":root { --my-heading: 3rem; --foo: 100px }\na { width: calc(var(--my-width, var(--foo, 200px))) }", + ":root { --my-heading: 3rem; }\na { width: calc(var(--my-width, var(--foo, 200px))) }", + "a { width: calc(var(--my-width, var(--foo, var(--bar, 200px)))) }", + ":root { --my-width: 100px; }\na { width: calc(var(--my-width, 200px)) }", + ":root { --my-fallback: 100px; }\na { width: calc(var(--my-width, var(--my-fallback))) }", + ":root { --my-fallback: 100px; }\na { width: calc(var(--my-width, var(--my-fallback, 200px))) }", + ":root { --foo: 100px; }\na { width: calc(var(--my-width, var(--my-fallback, var(--foo)))) }", + "a { width: calc(var(--my-width, var(--my-fallback, var(--foo, 200px)))) }", + ":root { --my-width: 100px; }\na { width: calc(var(--my-width, var(--fallback-width))) }", + ":root { --my-width: 100px; --fallback-width: 200px; }\na { width: calc(var(--my-width, var(--fallback-width))) }", + ":root { --my-width: 100px; }\na { width: calc(var(--my-width, var(--fallback-width, 200px))) }", + ":root { --my-width: 100px; }\na { width: calc(var(--my-width, var(--fallback-width, var(--foo)))) }", + ":root { --my-width: 100px; }\na { width: calc(var(--my-width, var(--fallback-width, var(--foo, 200px)))) }", + ":root { --my-width: 100px; }\na { width: calc(var(--my-width, var(--fallback-width, var(--foo, var(--bar))))) }", + ":root { --my-width: 100px; }\na { width: calc(var(--my-width, var(--fallback-width, var(--foo, var(--bar, 200px))))) }", + ":root { --width: 1px; }\na { border-top: calc(var(--width, 2px)) var(--style, var(--fallback, solid)) red; }", + ":root { --width: 100px; }\na { width: calc(calc(100% - var(--width))); }", + ":root { --width: 100px; }\na { width: calc(calc(var(--width) + 50px) - 25px); }", + ":root { --color: red; }\na { background: linear-gradient(to right, var(--color), blue); }", + ":root { --color: red; --offset: 10px; }\na { transform: translateX(calc(var(--offset) + 20px)); }", + ":root { --width: 100px; }\na { width: clamp(50px, var(--width), 200px); }", + ":root { --width: 100px; }\na { width: min(var(--width), 150px); }", + ":root { --width: 100px; }\na { width: max(var(--width), 50px); }", + ":root { --width: 100px; }\na { width: calc(min(var(--width), 150px) + 10px); }", + ":root { --width: 100px; }\na { width: calc(max(var(--width), 50px) - 5px); }", + ":root { --width: 100px; }\na { width: calc(clamp(50px, var(--width), 200px) / 2); }", + ":root { --color: red; }\na { filter: drop-shadow(0 0 10px var(--color)); }", + ":root { --color: red; }\na { background: radial-gradient(circle, var(--color), transparent); }", { code: "a { my-custom-color: red; }", languageOptions: { @@ -69,6 +99,10 @@ ruleTester.run("no-invalid-properties", rule, { code: "a { color: var(--my-color); }", options: [{ allowUnknownVariables: true }], }, + { + code: "a { width: calc(var(--width)); }", + options: [{ allowUnknownVariables: true }], + }, { code: "a { --my-color: red; color: var(--my-color); background-color: var(--unknown-var); }", options: [{ allowUnknownVariables: true }], @@ -592,5 +626,95 @@ ruleTester.run("no-invalid-properties", rule, { }, ], }, + { + code: "a { padding: calc(var(--padding-top, 1px) + 1px) 2px calc(var(--padding-bottom) + 1px); }", + errors: [ + { + messageId: "unknownVar", + data: { + var: "--padding-bottom", + }, + line: 1, + column: 63, + endLine: 1, + endColumn: 79, + }, + ], + }, + { + code: "a { padding: calc(var(--padding-top, var(--fallback))) 2px calc(var(--padding-bottom)); }", + errors: [ + { + messageId: "unknownVar", + data: { + var: "--padding-top", + }, + line: 1, + column: 23, + endLine: 1, + endColumn: 36, + }, + ], + }, + { + code: ":root { --width: 100px }\na { widthh: calc(var(--width, 200px)); }", + errors: [ + { + messageId: "unknownProperty", + data: { + property: "widthh", + }, + line: 2, + column: 5, + endLine: 2, + endColumn: 11, + }, + ], + }, + { + code: "a { padding: calc(max(var(--padding-top, var(--fallback))), 1px) 2px calc(var(--padding-bottom)); }", + errors: [ + { + messageId: "unknownVar", + data: { + var: "--padding-top", + }, + line: 1, + column: 27, + endLine: 1, + endColumn: 40, + }, + ], + }, + { + code: "a { color: rgba(calc(var(--red, 255) + var(--green)), 0, calc(var(--blue)), 1); }", + errors: [ + { + messageId: "unknownVar", + data: { + var: "--green", + }, + line: 1, + column: 44, + endLine: 1, + endColumn: 51, + }, + ], + }, + { + code: "a { transform: translateX(calc(var(--offset-x, min(var(--default-offset, 5px), 10px))) rotate(var(--rotation))); }", + errors: [ + { + messageId: "unknownVar", + data: { + var: "--rotation", + }, + line: 1, + column: 99, + endLine: 1, + endColumn: 109, + }, + ], + }, ], });