Repository: eslint/css (js)

Issue:
Rule Change: `no-invalid-properties` option to only validate property names with unknown variables
### What rule do you want to change?

no-invalid-properties

### What change do you want to make?

Generate fewer warnings

### How do you think the change should be implemented?

A new option

### Example code

```css
.one {
	ccolor: var(--my-color);
}

.two {
	color: var(--my-color);
}
```

### What does the rule currently do for this code?

1. Can't validate with unknown variable '--my-color'
2. Can't validate with unknown variable '--my-color'

### What will the rule do after it's changed?

1. Unknown property 'ccolor' found
2. (no violation)

### Participation

- [x] I am willing to submit a pull request to implement this change.

### Additional comments

The changes made in 0.9.0 requires us to completely disable `no-invalid-properties` because our design system is based on a huge number of CSS variables that are defined in a single file. The guidance in the docs is not practical:

> If the custom property is defined in another file, it's recommended to create a dummy rule just to ensure proper validation.

This would be a huge lift and would non-trivially increase our CSS size if every variable _usage_ needed a `.dummy { --my-color: blue; }` declaration.

An option to ignore unknown variables and fall back to property name validation would be a great compromise for us.

Pull request:
feat: no-invalid-properties allowUnknownVariables option
<!--
    Thank you for contributing!

    ESLint adheres to the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct).
-->

#### Prerequisites checklist

- [x] I have read the [contributing guidelines](https://github.com/eslint/eslint/blob/HEAD/CONTRIBUTING.md).

<!--
    Please ensure your pull request is ready:

    - Read the pull request guide (https://eslint.org/docs/latest/contribute/pull-requests)
    - Update or create tests
    - If performance-related, include a benchmark
    - Update documentation for this change (if appropriate)
-->

<!--
    The following is required for all pull requests:
-->

#### What is the purpose of this pull request?

Adds an option to `no-invalid-properties` called `allowUnknownVariables`. When true, variable references that can't be traced to a custom property definition are ignored.

#### What changes did you make? (Give an overview)

Added an options schema for `no-invalid-properties` describing `allowUnknownVariables`. When the option is `true`, do the following:
- Ignore variable references that were not found up to this point (skip reporting `unknownVar`)
- Only report `unknownProperty` for errors of type `SyntaxReferenceError`, which indicate that the property is unknown

Also updated documentation for the rule to reflect this option as best I could.

#### Related Issues

Fixes #175

#### Is there anything you'd like reviewers to focus on?

<!-- markdownlint-disable-file MD004 -->


Patch:
diff --git a/docs/rules/no-invalid-properties.md b/docs/rules/no-invalid-properties.md
index a7c1b34..f8c82e8 100644
--- a/docs/rules/no-invalid-properties.md
+++ b/docs/rules/no-invalid-properties.md
@@ -44,11 +44,31 @@ body {
 }
 ```
 
-### Limitations
+## Options
 
-When a variable is used in a property value, such as `var(--my-color)`, the rule can only properly be validated if the parser has already encountered the `--my-color` custom property. For example, this will validate correctly:
+This rule accepts an option which is an object with the following property:
+
+- `allowUnknownVariables` (default: `false`) - Ignore variables that cannot be traced to custom properties in the current file.
+
+When a variable is used in a property value, such as `var(--my-color)`, the rule can only properly be validated if the parser has already encountered the `--my-color` custom property. With `{ allowUnknownVariables: false }`, unknown variables will result in a linting error. With `{ allowUnknownVariables: true }`, the property value will be ignored and only the property name will be validated.
+
+Examples of **incorrect** code with `{ allowUnknownVariables: false }` (the default):
 
 ```css
+/* eslint css/no-invalid-properties: ["error", { allowUnknownVariables: false }] */
+
+a {
+	color: var(--my-color);
+}
+```
+
+This code uses `var(--my-color)` before `--my-color` is defined, or whether it is defined in another CSS file. Therefore, `color: var(--my-color)` cannot be properly validated.
+
+Examples of **correct** code with `{ allowUnknownVariables: false }`:
+
+```css
+/* eslint css/no-invalid-properties: ["error", { allowUnknownVariables: false }] */
+
 :root {
 	--my-color: red;
 }
@@ -60,7 +80,29 @@ a {
 
 This code defines `--my-color` before it is used and therefore the rule can validate the `color` property. If `--my-color` was not defined before `var(--my-color)` was used, it results in a lint error because the validation cannot be completed.
 
-If the custom property is defined in another file, it's recommended to create a dummy rule just to ensure proper validation.
+Examples of **incorrect** code with `{ allowUnknownVariables: true }`:
+
+```css
+/* eslint css/no-invalid-properties: ["error", { allowUnknownVariables: true }] */
+
+a {
+	ccolorr: var(--my-color);
+}
+```
+
+This code uses an unknown property `ccolorr`, which results in a validation error. The unknown reference to `var(--my-color)` is ignored.
+
+Examples of **correct** code with `{ allowUnknownVariables: true }`:
+
+```css
+/* eslint css/no-invalid-properties: ["error", { allowUnknownVariables: true }] */
+
+a {
+	color: var(--my-color);
+}
+```
+
+Even though `var(--my-color)` cannot be traced to a custom property definition, this code passes validation.
 
 ## When Not to Use It
 
diff --git a/src/rules/no-invalid-properties.js b/src/rules/no-invalid-properties.js
index 5b78539..271b0b8 100644
--- a/src/rules/no-invalid-properties.js
+++ b/src/rules/no-invalid-properties.js
@@ -7,7 +7,7 @@
 // Imports
 //-----------------------------------------------------------------------------
 
-import { isSyntaxMatchError } from "../util.js";
+import { isSyntaxMatchError, isSyntaxReferenceError } from "../util.js";
 
 //-----------------------------------------------------------------------------
 // Type Definitions
@@ -17,7 +17,8 @@ import { isSyntaxMatchError } from "../util.js";
  * @import { CSSRuleDefinition } from "../types.js"
  * @import { ValuePlain, FunctionNodePlain, CssLocationRange } from "@eslint/css-tree";
  * @typedef {"invalidPropertyValue" | "unknownProperty" | "unknownVar"} NoInvalidPropertiesMessageIds
- * @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoInvalidPropertiesMessageIds }>} NoInvalidPropertiesRuleDefinition
+ * @typedef {[{allowUnknownVariables?: boolean}]} NoInvalidPropertiesOptions
+ * @typedef {CSSRuleDefinition<{ RuleOptions: NoInvalidPropertiesOptions, MessageIds: NoInvalidPropertiesMessageIds }>} NoInvalidPropertiesRuleDefinition
  */
 
 //-----------------------------------------------------------------------------
@@ -72,6 +73,24 @@ export default {
 			url: "https://github.com/eslint/css/blob/main/docs/rules/no-invalid-properties.md",
 		},
 
+		schema: [
+			{
+				type: "object",
+				properties: {
+					allowUnknownVariables: {
+						type: "boolean",
+					},
+				},
+				additionalProperties: false,
+			},
+		],
+
+		defaultOptions: [
+			{
+				allowUnknownVariables: false,
+			},
+		],
+
 		messages: {
 			invalidPropertyValue:
 				"Invalid value '{{value}}' for property '{{property}}'. Expected {{expected}}.",
@@ -95,6 +114,8 @@ export default {
 		 */
 		const replacements = [];
 
+		const [{ allowUnknownVariables }] = context.options;
+
 		return {
 			"Rule > Block > Declaration"() {
 				replacements.push(new Map());
@@ -153,7 +174,7 @@ export default {
 							offsets.forEach(offset => {
 								varsFoundLocs.set(offset, func.loc);
 							});
-						} else {
+						} else if (!allowUnknownVariables) {
 							context.report({
 								loc: func.children[0].loc,
 								messageId: "unknownVar",
@@ -205,22 +226,27 @@ export default {
 						return;
 					}
 
-					// unknown property
-					context.report({
-						loc: {
-							start: node.loc.start,
-							end: {
-								line: node.loc.start.line,
-								column:
-									node.loc.start.column +
-									node.property.length,
+					if (
+						!allowUnknownVariables ||
+						isSyntaxReferenceError(error)
+					) {
+						// unknown property
+						context.report({
+							loc: {
+								start: node.loc.start,
+								end: {
+									line: node.loc.start.line,
+									column:
+										node.loc.start.column +
+										node.property.length,
+								},
+							},
+							messageId: "unknownProperty",
+							data: {
+								property: node.property,
 							},
-						},
-						messageId: "unknownProperty",
-						data: {
-							property: node.property,
-						},
-					});
+						});
+					}
 				}
 			},
 		};
diff --git a/src/util.js b/src/util.js
index d8f0129..51c03a6 100644
--- a/src/util.js
+++ b/src/util.js
@@ -8,7 +8,7 @@
 //-----------------------------------------------------------------------------
 
 /**
- * @import { SyntaxMatchError } from "@eslint/css-tree"
+ * @import { SyntaxMatchError, SyntaxReferenceError } from "@eslint/css-tree"
  */
 
 //-----------------------------------------------------------------------------
@@ -24,6 +24,15 @@ export function isSyntaxMatchError(error) {
 	return typeof error.syntax === "string";
 }
 
+/**
+ * Determines if an error is a syntax reference error.
+ * @param {Object} error The error object to check.
+ * @returns {error is SyntaxReferenceError} True if the error is a syntax reference error, false if not.
+ */
+export function isSyntaxReferenceError(error) {
+	return typeof error.reference === "string";
+}
+
 /**
  * Finds the line and column offsets for a given offset in a string.
  * @param {string} text The text to search.


Test patch:
diff --git a/tests/rules/no-invalid-properties.test.js b/tests/rules/no-invalid-properties.test.js
index bab0bc4..b35627a 100644
--- a/tests/rules/no-invalid-properties.test.js
+++ b/tests/rules/no-invalid-properties.test.js
@@ -47,6 +47,14 @@ ruleTester.run("no-invalid-properties", rule, {
 				},
 			},
 		},
+		{
+			code: "a { color: var(--my-color); }",
+			options: [{ allowUnknownVariables: true }],
+		},
+		{
+			code: "a { --my-color: red; color: var(--my-color); background-color: var(--unknown-var); }",
+			options: [{ allowUnknownVariables: true }],
+		},
 
 		/*
 		 * CSSTree doesn't currently support custom functions properly, so leaving
@@ -404,5 +412,49 @@ ruleTester.run("no-invalid-properties", rule, {
 				},
 			],
 		},
+		{
+			code: "a { colorr: var(--my-color); }",
+			options: [{ allowUnknownVariables: true }],
+			errors: [
+				{
+					messageId: "unknownProperty",
+					data: {
+						property: "colorr",
+					},
+					line: 1,
+					column: 5,
+					endLine: 1,
+					endColumn: 11,
+				},
+			],
+		},
+		{
+			code: "a { --my-color: 10px; color: var(--my-color); background_color: var(--unknown-var); }",
+			options: [{ allowUnknownVariables: true }],
+			errors: [
+				{
+					messageId: "invalidPropertyValue",
+					data: {
+						property: "color",
+						value: "10px",
+						expected: "<color>",
+					},
+					line: 1,
+					column: 30,
+					endLine: 1,
+					endColumn: 45,
+				},
+				{
+					messageId: "unknownProperty",
+					data: {
+						property: "background_color",
+					},
+					line: 1,
+					column: 47,
+					endLine: 1,
+					endColumn: 63,
+				},
+			],
+		},
 	],
 });