Repository: eslint/css (js)

Issue:
New Rule: no-important
### Rule details

Flag the use of !important

### What type of rule is this?

Warns about a potential problem

### Example code

```css
.really-red {
    color: red !important;
}
```

### Participation

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

### Additional comments

Needing `!important` indicates there may be a larger underlying issue.

Pull request description:
feat: add no-important rule
<!--
    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?

This pull request introduces a new rule, `no-important`, which disallows the use of the !important flag in declarations.

#### What changes did you make? (Give an overview)

- Implemented the no-important rule to detect and warn when !important is used.
- Added documentation for the rule
- Added tests

#### Related Issues

Closes #20, Closes #23

<!-- include tags like "fixes #123" or "refs #123" -->

#### Is there anything you'd like reviewers to focus on?

<!-- markdownlint-disable-file MD004 -->


Patch:
diff --git a/README.md b/README.md
index 5dd875f..dbbf7b5 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,7 @@ export default defineConfig([
 | :----------------------------------------------------------------------- | :------------------------------------ | :-------------: |
 | [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md)           | Disallow duplicate @import rules      |       yes       |
 | [`no-empty-blocks`](./docs/rules/no-empty-blocks.md)                     | Disallow empty blocks                 |       yes       |
+| [`no-important`](./docs/rules/no-important.md)                           | Disallow !important flags             |       yes       |
 | [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md)             | Disallow invalid at-rules             |       yes       |
 | [`no-invalid-properties`](./docs/rules/no-invalid-properties.md)         | Disallow invalid properties           |       yes       |
 | [`prefer-logical-properties`](./docs/rules/prefer-logical-properties.md) | Enforce the use of logical properties |       no        |
diff --git a/docs/rules/no-important.md b/docs/rules/no-important.md
new file mode 100644
index 0000000..33bce70
--- /dev/null
+++ b/docs/rules/no-important.md
@@ -0,0 +1,45 @@
+# no-important
+
+Disallow `!important` flags.
+
+## Background
+
+The `!important` flag is a CSS declaration modifier that overrides normal cascade behavior, forcing a declaration to take precedence over competing rules—regardless of specificity or source order. While it can be necessary in rare cases (e.g., overriding third-party styles or enforcing accessibility fixes), it’s widely considered an anti-pattern because:
+
+- It breaks the natural cascade of CSS
+- It makes debugging more difficult
+- It can lead to specificity wars where developers keep adding more `!important` declarations to override each other
+- It makes the code harder to maintain
+
+## Rule Details
+
+This rule warns when it detects the `!important` flag in declarations.
+
+Examples of incorrect code:
+
+```css
+.foo {
+	color: red !important;
+}
+```
+
+Examples of correct code:
+
+```css
+.foo {
+	color: red;
+}
+```
+
+## When Not to Use It
+
+You may disable this rule if you are using `!important` in these specific cases:
+
+- Overriding third-party styles where you lack control over the source CSS
+- Fixing accessibility issues (e.g., enforcing focus states or color contrast)
+- Working with legacy code where refactoring isn’t feasible
+
+## Prior Art
+
+- [`important`](https://github.com/CSSLint/csslint/wiki/Disallow-%21important)
+- [`declaration-no-important`](https://stylelint.io/user-guide/rules/declaration-no-important/)
diff --git a/package.json b/package.json
index ddd509b..7ee4ccc 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,14 @@
         "types": "./dist/esm/syntax/index.d.ts",
         "default": "./dist/esm/syntax/index.js"
       }
+    },
+    "./types": {
+      "require": {
+        "types": "./dist/cjs/types.cts"
+      },
+      "import": {
+        "types": "./dist/esm/types.d.ts"
+      }
     }
   },
   "files": [
@@ -85,9 +93,9 @@
   ],
   "license": "Apache-2.0",
   "dependencies": {
-    "@eslint/core": "^0.13.0",
+    "@eslint/core": "^0.14.0",
     "@eslint/css-tree": "^3.3.3",
-    "@eslint/plugin-kit": "^0.2.5"
+    "@eslint/plugin-kit": "^0.3.1"
   },
   "devDependencies": {
     "@eslint/json": "^0.5.0",
diff --git a/src/index.js b/src/index.js
index ae62b36..af96538 100644
--- a/src/index.js
+++ b/src/index.js
@@ -11,6 +11,7 @@ import { CSSLanguage } from "./languages/css-language.js";
 import { CSSSourceCode } from "./languages/css-source-code.js";
 import noEmptyBlocks from "./rules/no-empty-blocks.js";
 import noDuplicateImports from "./rules/no-duplicate-imports.js";
+import noImportant from "./rules/no-important.js";
 import noInvalidProperties from "./rules/no-invalid-properties.js";
 import noInvalidAtRules from "./rules/no-invalid-at-rules.js";
 import preferLogicalProperties from "./rules/prefer-logical-properties.js";
@@ -32,6 +33,7 @@ const plugin = {
 	rules: {
 		"no-empty-blocks": noEmptyBlocks,
 		"no-duplicate-imports": noDuplicateImports,
+		"no-important": noImportant,
 		"no-invalid-at-rules": noInvalidAtRules,
 		"no-invalid-properties": noInvalidProperties,
 		"prefer-logical-properties": preferLogicalProperties,
@@ -44,6 +46,7 @@ const plugin = {
 			rules: /** @type {const} */ ({
 				"css/no-empty-blocks": "error",
 				"css/no-duplicate-imports": "error",
+				"css/no-important": "error",
 				"css/no-invalid-at-rules": "error",
 				"css/no-invalid-properties": "error",
 				"css/use-baseline": "warn",
diff --git a/src/languages/css-source-code.js b/src/languages/css-source-code.js
index da6a43e..babece8 100644
--- a/src/languages/css-source-code.js
+++ b/src/languages/css-source-code.js
@@ -21,7 +21,7 @@ import { visitorKeys } from "./css-visitor-keys.js";
 
 /**
  * @import { CssNode, CssNodePlain, Comment, Lexer, StyleSheetPlain } from "@eslint/css-tree"
- * @import { SourceRange, SourceLocation, FileProblem, DirectiveType, RulesConfig, TextSourceCode } from "@eslint/core"
+ * @import { SourceRange, SourceLocation, FileProblem, DirectiveType, RulesConfig } from "@eslint/core"
  * @import { CSSSyntaxElement } from "../types.js"
  * @import { CSSLanguageOptions } from "./css-language.js"
  */
@@ -65,7 +65,7 @@ class CSSTraversalStep extends VisitNodeStep {
 
 /**
  * CSS Source Code Object.
- * @implements {TextSourceCode<{LangOptions: CSSLanguageOptions, RootNode: StyleSheetPlain, SyntaxElementWithLoc: CSSSyntaxElement, ConfigNode: Comment}>}
+ * @extends {TextSourceCodeBase<{LangOptions: CSSLanguageOptions, RootNode: StyleSheetPlain, SyntaxElementWithLoc: CSSSyntaxElement, ConfigNode: Comment}>}
  */
 export class CSSSourceCode extends TextSourceCodeBase {
 	/**
diff --git a/src/rules/no-important.js b/src/rules/no-important.js
new file mode 100644
index 0000000..42071dc
--- /dev/null
+++ b/src/rules/no-important.js
@@ -0,0 +1,94 @@
+/**
+ * @fileoverview Rule to disallow `!important` flags.
+ * @author thecalamiity
+ * @author Yann Bertrand
+ */
+
+//-----------------------------------------------------------------------------
+// Imports
+//-----------------------------------------------------------------------------
+
+import { findOffsets } from "../util.js";
+
+//-----------------------------------------------------------------------------
+// Type Definitions
+//-----------------------------------------------------------------------------
+
+/**
+ * @import { CSSRuleDefinition } from "../types.js"
+ * @typedef {"unexpectedImportant"} NoImportantMessageIds
+ * @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoImportantMessageIds }>} NoImportantRuleDefinition
+ */
+
+//-----------------------------------------------------------------------------
+// Rule Definition
+//-----------------------------------------------------------------------------
+
+/** @type {NoImportantRuleDefinition} */
+export default {
+	meta: {
+		type: "problem",
+
+		docs: {
+			description: "Disallow !important flags",
+			recommended: true,
+			url: "https://github.com/eslint/css/blob/main/docs/rules/no-important.md",
+		},
+
+		messages: {
+			unexpectedImportant: "Unexpected !important flag found.",
+		},
+	},
+
+	create(context) {
+		const importantPattern = /!(\s|\/\*.*?\*\/)*important/iu;
+
+		return {
+			Declaration(node) {
+				if (node.important) {
+					const declarationText = context.sourceCode.getText(node);
+					const importantMatch =
+						importantPattern.exec(declarationText);
+
+					const {
+						lineOffset: startLineOffset,
+						columnOffset: startColumnOffset,
+					} = findOffsets(declarationText, importantMatch.index);
+
+					const {
+						lineOffset: endLineOffset,
+						columnOffset: endColumnOffset,
+					} = findOffsets(
+						declarationText,
+						importantMatch.index + importantMatch[0].length,
+					);
+
+					const nodeStartLine = node.loc.start.line;
+					const nodeStartColumn = node.loc.start.column;
+					const startLine = nodeStartLine + startLineOffset;
+					const endLine = nodeStartLine + endLineOffset;
+					const startColumn =
+						(startLine === nodeStartLine ? nodeStartColumn : 1) +
+						startColumnOffset;
+					const endColumn =
+						(endLine === nodeStartLine ? nodeStartColumn : 1) +
+						endColumnOffset;
+
+					context.report({
+						loc: {
+							start: {
+								line: startLine,
+								column: startColumn,
+							},
+							end: {
+								line: endLine,
+								column: endColumn,
+							},
+						},
+						messageId: "unexpectedImportant",
+					});
+				}
+			},
+		};
+	},
+};
diff --git a/src/util.js b/src/util.js
index 7e97e61..d8f0129 100644
--- a/src/util.js
+++ b/src/util.js
@@ -23,3 +23,28 @@
 export function isSyntaxMatchError(error) {
 	return typeof error.syntax === "string";
 }
+
+/**
+ * Finds the line and column offsets for a given offset in a string.
+ * @param {string} text The text to search.
+ * @param {number} offset The offset to find.
+ * @returns {{lineOffset:number,columnOffset:number}} The location of the offset.
+ */
+export function findOffsets(text, offset) {
+	let lineOffset = 0;
+	let columnOffset = 0;
+
+	for (let i = 0; i < offset; i++) {
+		if (text[i] === "\n") {
+			lineOffset++;
+			columnOffset = 0;
+		} else {
+			columnOffset++;
+		}
+	}
+
+	return {
+		lineOffset,
+		columnOffset,
+	};
+}


Test patch:
diff --git a/tests/rules/no-important.test.js b/tests/rules/no-important.test.js
new file mode 100644
index 0000000..813341c
--- /dev/null
+++ b/tests/rules/no-important.test.js
@@ -0,0 +1,264 @@
+/**
+ * @fileoverview Tests for no-important rule.
+ */
+
+//------------------------------------------------------------------------------
+// Imports
+//------------------------------------------------------------------------------
+
+import rule from "../../src/rules/no-important.js";
+import css from "../../src/index.js";
+import { RuleTester } from "eslint";
+import dedent from "dedent";
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+	plugins: {
+		css,
+	},
+	language: "css/css",
+});
+
+ruleTester.run("no-important", rule, {
+	valid: [
+		"a { color: red; }",
+		"a { color: red; background-color: blue; }",
+		"a { color: red; transition: none; }",
+		"body { --custom-property: red; }",
+		"body { --custom-property: important; }",
+		"body { padding: 0; }",
+		"a { color: red; -moz-transition: bar }",
+		"@font-face { font-weight: 100 400 }",
+		'@property --foo { syntax: "*"; inherits: false; }',
+		"a { --my-color: red; color: var(--my-color) }",
+		{
+			code: "a { my-custom-color: red; }",
+			languageOptions: {
+				customSyntax: {
+					properties: {
+						"my-custom-color": "<color>",
+					},
+				},
+			},
+		},
+	],
+	invalid: [
+		{
+			code: "a { color: red !important; }",
+			errors: [
+				{
+					messageId: "unexpectedImportant",
+					line: 1,
+					column: 16,
+					endLine: 1,
+					endColumn: 26,
+				},
+			],
+		},
+		{
+			code: "a { color:red!important; }",
+			errors: [
+				{
+					messageId: "unexpectedImportant",
+					line: 1,
+					column: 14,
+					endLine: 1,
+					endColumn: 24,
+				},
+			],
+		},
+		{
+			code: "a { padding: 10px 20px 30px 40px ! important; }",
+			errors: [
+				{
+					messageId: "unexpectedImportant",
+					line: 1,
+					column: 34,
+					endLine: 1,
+					endColumn: 45,
+				},
+			],
+		},
+		{
+			code: "a { border: 0 !IMPORTANT; }",
+			errors: [
+				{
+					messageId: "unexpectedImportant",
+					line: 1,
+					column: 15,
+					endLine: 1,
+					endColumn: 25,
+				},
+			],
+		},
+		{
+			code: "a { color: red !important; margin: 0 ! important; }",
+			errors: [
+				{
+					messageId: "unexpectedImportant",
+					line: 1,
+					column: 16,
+					endLine: 1,
+					endColumn: 26,
+				},
+				{
+					messageId: "unexpectedImportant",
+					line: 1,
+					column: 38,
+					endLine: 1,
+					endColumn: 49,
+				},
+			],
+		},
+		{
+			code: dedent`
+				a {
+					color: red
+						!important;
+				}
+			`,
+			errors: [
+				{
+					messageId: "unexpectedImportant",
+					line: 3,
+					column: 3,
+					endLine: 3,
+					endColumn: 13,
+				},
+			],
+		},
+		{
+			code: dedent`
+				a {
+					color: red
+						! important;
+				}
+			`,
+			errors: [
+				{
+					messageId: "unexpectedImportant",
+					line: 3,
+					column: 3,
+					endLine: 3,
+					endColumn: 14,
+				},
+			],
+		},
+		{
+			code: "a { color: red /* comment */ !important; }",
+			errors: [
+				{
+					messageId: "unexpectedImportant",
+					line: 1,
+					column: 30,
+					endLine: 1,
+					endColumn: 40,
+				},
+			],
+		},
+		{
+			code: dedent`
+				a {
+					color: red /* comment */
+						!important;
+				}
+			`,
+			errors: [
+				{
+					messageId: "unexpectedImportant",
+					line: 3,
+					column: 3,
+					endLine: 3,
+					endColumn: 13,
+				},
+			],
+		},
+		{
+			code: "a { color: red !/* comment */important; }",
+			errors: [
+				{
+					messageId: "unexpectedImportant",
+					line: 1,
+					column: 16,
+					endLine: 1,
+					endColumn: 39,
+				},
+			],
+		},
+		{
+			code: "a { color: red ! /* comment */ important; }",
+			errors: [
+				{
+					messageId: "unexpectedImportant",
+					line: 1,
+					column: 16,
+					endLine: 1,
+					endColumn: 41,
+				},
+			],
+		},
+		{
+			code: dedent`
+				a {
+					color: red
+						!/* comment */important;
+				}
+			`,
+			errors: [
+				{
+					messageId: "unexpectedImportant",
+					line: 3,
+					column: 3,
+					endLine: 3,
+					endColumn: 26,
+				},
+			],
+		},
+		{
+			code: dedent`
+				a {
+					color: red
+						! /* comment */ important;
+				}
+			`,
+			errors: [
+				{
+					messageId: "unexpectedImportant",
+					line: 3,
+					column: 3,
+					endLine: 3,
+					endColumn: 28,
+				},
+			],
+		},
+		{
+			code: dedent`
+				a {
+					color: red
+						!important;
+					margin: 0
+						! important;
+				}
+			`,
+			errors: [
+				{
+					messageId: "unexpectedImportant",
+					line: 3,
+					column: 3,
+					endLine: 3,
+					endColumn: 13,
+				},
+				{
+					messageId: "unexpectedImportant",
+					line: 5,
+					column: 3,
+					endLine: 5,
+					endColumn: 14,
+				},
+			],
+		},
+	],
+});
diff --git a/tests/types/types.test.ts b/tests/types/types.test.ts
index e3de63e..55777ce 100644
--- a/tests/types/types.test.ts
+++ b/tests/types/types.test.ts
@@ -1,5 +1,58 @@
-import css from "@eslint/css";
+import css, { CSSSourceCode } from "@eslint/css";
 import { ESLint } from "eslint";
+import type { SourceLocation, SourceRange } from "@eslint/core";
+import type {
+	AnPlusB,
+	AtrulePlain,
+	AtrulePreludePlain,
+	AttributeSelector,
+	BlockPlain,
+	BracketsPlain,
+	CDC,
+	CDO,
+	ClassSelector,
+	Combinator,
+	Comment,
+	ConditionPlain,
+	DeclarationListPlain,
+	DeclarationPlain,
+	Dimension,
+	Feature,
+	FeatureFunctionPlain,
+	FeatureRange,
+	FunctionNodePlain,
+	Hash,
+	IdSelector,
+	Identifier,
+	Layer,
+	LayerListPlain,
+	MediaFeature,
+	MediaQueryListPlain,
+	MediaQueryPlain,
+	NestingSelector,
+	NthPlain,
+	NumberNode,
+	Operator,
+	ParenthesesPlain,
+	Percentage,
+	PseudoClassSelectorPlain,
+	PseudoElementSelectorPlain,
+	Ratio,
+	Raw,
+	RulePlain,
+	SelectorListPlain,
+	SelectorPlain,
+	StringNode,
+	StyleSheetPlain,
+	SupportsDeclaration,
+	TypeSelector,
+	UnicodeRange,
+	Url,
+	ValuePlain,
+	WhiteSpace,
+	CssNodePlain,
+} from "@eslint/css-tree";
+import type { CSSRuleDefinition, CSSRuleVisitor } from "@eslint/css/types";
 
 css satisfies ESLint.Plugin;
 css.meta.name satisfies string;
@@ -19,3 +72,204 @@ css.configs.recommended.plugins satisfies object;
 	// Check that all recommended rule names match the names of existing rules in this plugin.
 	null as AssertAllNamesIn<RecommendedRuleName, RuleName>;
 }
+
+(): CSSRuleDefinition => ({
+	create({ sourceCode }): CSSRuleVisitor {
+		sourceCode satisfies CSSSourceCode;
+		sourceCode.ast satisfies StyleSheetPlain;
+		sourceCode.lines satisfies string[];
+		sourceCode.text satisfies string;
+
+		function testVisitor<NodeType extends CssNodePlain>(node: NodeType) {
+			sourceCode.getLoc(node) satisfies SourceLocation;
+			sourceCode.getRange(node) satisfies SourceRange;
+			sourceCode.getParent(node) satisfies CssNodePlain | undefined;
+			sourceCode.getAncestors(node) satisfies CssNodePlain[];
+			sourceCode.getText(node) satisfies string;
+		}
+
+		return {
+			AnPlusB: node => testVisitor<AnPlusB>(node),
+			"AnPlusB:exit": node => testVisitor<AnPlusB>(node),
+			Atrule: node => testVisitor<AtrulePlain>(node),
+			"Atrule:exit": node => testVisitor<AtrulePlain>(node),
+			AtrulePrelude: node => testVisitor<AtrulePreludePlain>(node),
+			"AtrulePrelude:exit": node => testVisitor<AtrulePreludePlain>(node),
+			AttributeSelector: node => testVisitor<AttributeSelector>(node),
+			"AttributeSelector:exit": node =>
+				testVisitor<AttributeSelector>(node),
+			Block: node => testVisitor<BlockPlain>(node),
+			"Block:exit": node => testVisitor<BlockPlain>(node),
+			Brackets: node => testVisitor<BracketsPlain>(node),
+			"Brackets:exit": node => testVisitor<BracketsPlain>(node),
+			CDC: node => testVisitor<CDC>(node),
+			"CDC:exit": node => testVisitor<CDC>(node),
+			CDO: node => testVisitor<CDO>(node),
+			"CDO:exit": node => testVisitor<CDO>(node),
+			ClassSelector: node => testVisitor<ClassSelector>(node),
+			"ClassSelector:exit": node => testVisitor<ClassSelector>(node),
+			Combinator: node => testVisitor<Combinator>(node),
+			"Combinator:exit": node => testVisitor<Combinator>(node),
+			Comment: node => testVisitor<Comment>(node),
+			"Comment:exit": node => testVisitor<Comment>(node),
+			Condition: node => testVisitor<ConditionPlain>(node),
+			"Condition:exit": node => testVisitor<ConditionPlain>(node),
+			Declaration: node => testVisitor<DeclarationPlain>(node),
+			"Declaration:exit": node => testVisitor<DeclarationPlain>(node),
+			DeclarationList: node => testVisitor<DeclarationListPlain>(node),
+			"DeclarationList:exit": node =>
+				testVisitor<DeclarationListPlain>(node),
+			Dimension: node => testVisitor<Dimension>(node),
+			"Dimension:exit": node => testVisitor<Dimension>(node),
+			Feature: node => testVisitor<Feature>(node),
+			"Feature:exit": node => testVisitor<Feature>(node),
+			FeatureFunction: node => testVisitor<FeatureFunctionPlain>(node),
+			"FeatureFunction:exit": node =>
+				testVisitor<FeatureFunctionPlain>(node),
+			FeatureRange: node => testVisitor<FeatureRange>(node),
+			"FeatureRange:exit": node => testVisitor<FeatureRange>(node),
+			Function: node => testVisitor<FunctionNodePlain>(node),
+			"Function:exit": node => testVisitor<FunctionNodePlain>(node),
+			GeneralEnclosed: node => testVisitor<any>(node),
+			"GeneralEnclosed:exit": node => testVisitor<any>(node),
+			Hash: node => testVisitor<Hash>(node),
+			"Hash:exit": node => testVisitor<Hash>(node),
+			IdSelector: node => testVisitor<IdSelector>(node),
+			"IdSelector:exit": node => testVisitor<IdSelector>(node),
+			Identifier: node => testVisitor<Identifier>(node),
+			"Identifier:exit": node => testVisitor<Identifier>(node),
+			Layer: node => testVisitor<Layer>(node),
+			"Layer:exit": node => testVisitor<Layer>(node),
+			LayerList: node => testVisitor<LayerListPlain>(node),
+			"LayerList:exit": node => testVisitor<LayerListPlain>(node),
+			MediaFeature: node => testVisitor<MediaFeature>(node),
+			"MediaFeature:exit": node => testVisitor<MediaFeature>(node),
+			MediaQuery: node => testVisitor<MediaQueryPlain>(node),
+			"MediaQuery:exit": node => testVisitor<MediaQueryPlain>(node),
+			MediaQueryList: node => testVisitor<MediaQueryListPlain>(node),
+			"MediaQueryList:exit": node =>
+				testVisitor<MediaQueryListPlain>(node),
+			NestingSelector: node => testVisitor<NestingSelector>(node),
+			"NestingSelector:exit": node => testVisitor<NestingSelector>(node),
+			Nth: node => testVisitor<NthPlain>(node),
+			"Nth:exit": node => testVisitor<NthPlain>(node),
+			Number: node => testVisitor<NumberNode>(node),
+			"Number:exit": node => testVisitor<NumberNode>(node),
+			Operator: node => testVisitor<Operator>(node),
+			"Operator:exit": node => testVisitor<Operator>(node),
+			Parentheses: node => testVisitor<ParenthesesPlain>(node),
+			"Parentheses:exit": node => testVisitor<ParenthesesPlain>(node),
+			Percentage: node => testVisitor<Percentage>(node),
+			"Percentage:exit": node => testVisitor<Percentage>(node),
+			PseudoClassSelector: node =>
+				testVisitor<PseudoClassSelectorPlain>(node),
+			"PseudoClassSelector:exit": node =>
+				testVisitor<PseudoClassSelectorPlain>(node),
+			PseudoElementSelector: node =>
+				testVisitor<PseudoElementSelectorPlain>(node),
+			"PseudoElementSelector:exit": node =>
+				testVisitor<PseudoElementSelectorPlain>(node),
+			Ratio: node => testVisitor<Ratio>(node),
+			"Ratio:exit": node => testVisitor<Ratio>(node),
+			Raw: node => testVisitor<Raw>(node),
+			"Raw:exit": node => testVisitor<Raw>(node),
+			Rule: node => testVisitor<RulePlain>(node),
+			"Rule:exit": node => testVisitor<RulePlain>(node),
+			Scope: node => testVisitor<any>(node),
+			"Scope:exit": node => testVisitor<any>(node),
+			Selector: node => testVisitor<SelectorPlain>(node),
+			"Selector:exit": node => testVisitor<SelectorPlain>(node),
+			SelectorList: node => testVisitor<SelectorListPlain>(node),
+			"SelectorList:exit": node => testVisitor<SelectorListPlain>(node),
+			String: node => testVisitor<StringNode>(node),
+			"String:exit": node => testVisitor<StringNode>(node),
+			StyleSheet: node => testVisitor<StyleSheetPlain>(node),
+			"StyleSheet:exit": node => testVisitor<StyleSheetPlain>(node),
+			SupportsDeclaration: node => testVisitor<SupportsDeclaration>(node),
+			"SupportsDeclaration:exit": node =>
+				testVisitor<SupportsDeclaration>(node),
+			TypeSelector: node => testVisitor<TypeSelector>(node),
+			"TypeSelector:exit": node => testVisitor<TypeSelector>(node),
+			UnicodeRange: node => testVisitor<UnicodeRange>(node),
+			"UnicodeRange:exit": node => testVisitor<UnicodeRange>(node),
+			Url: node => testVisitor<Url>(node),
+			"Url:exit": node => testVisitor<Url>(node),
+			Value: node => testVisitor<ValuePlain>(node),
+			"Value:exit": node => testVisitor<ValuePlain>(node),
+			WhiteSpace: node => testVisitor<WhiteSpace>(node),
+			"WhiteSpace:exit": node => testVisitor<WhiteSpace>(node),
+		};
+	},
+});
+
+// All options optional - CSSRuleDefinition and CSSRuleDefinition<{}>
+// should be the same type.
+(rule1: CSSRuleDefinition, rule2: CSSRuleDefinition<{}>) => {
+	rule1 satisfies typeof rule2;
+	rule2 satisfies typeof rule1;
+};
+
+// Type restrictions should be enforced
+(): CSSRuleDefinition<{
+	RuleOptions: [string, number];
+	MessageIds: "foo" | "bar";
+	ExtRuleDocs: { foo: string; bar: number };
+}> => ({
+	meta: {
+		messages: {
+			foo: "FOO",
+
+			// @ts-expect-error Wrong type for message ID
+			bar: 42,
+		},
+		docs: {
+			foo: "FOO",
+
+			// @ts-expect-error Wrong type for declared property
+			bar: "BAR",
+
+			// @ts-expect-error Wrong type for predefined property
+			description: 42,
+		},
+	},
+	create({ options }) {
+		// Types for rule options
+		options[0] satisfies string;
+		options[1] satisfies number;
+
+		return {};
+	},
+});
+
+// Undeclared properties should produce an error
+(): CSSRuleDefinition<{
+	MessageIds: "foo" | "bar";
+	ExtRuleDocs: { foo: number; bar: string };
+}> => ({
+	meta: {
+		messages: {
+			foo: "FOO",
+
+			// Declared message ID is not required
+			// bar: "BAR",
+
+			// @ts-expect-error Undeclared message ID is not allowed
+			baz: "BAZ",
+		},
+		docs: {
+			foo: 42,
+
+			// Declared property is not required
+			// bar: "BAR",
+
+			// @ts-expect-error Undeclared property key is not allowed
+			baz: "BAZ",
+
+			// Predefined property is allowed
+			description: "Lorem ipsum",
+		},
+	},
+	create() {
+		return {};
+	},
+});