Repository: biomejs/biome (language: rust)

Issue:
📎 Implement stylelint/at-rule-no-unknown



Discussion / hints:

Fix patch:
diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs
index 08f5da401271..364831501b2e 100644
--- a/crates/biome_configuration/src/analyzer/linter/rules.rs
+++ b/crates/biome_configuration/src/analyzer/linter/rules.rs
@@ -3365,6 +3365,9 @@ pub struct Nursery {
     #[serde(skip_serializing_if = "Option::is_none")]
     pub no_template_curly_in_string:
         Option<RuleConfiguration<biome_js_analyze::options::NoTemplateCurlyInString>>,
+    #[doc = "Disallow unknown at-rules."]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub no_unknown_at_rule: Option<RuleConfiguration<biome_css_analyze::options::NoUnknownAtRule>>,
     #[doc = "Disallow unknown pseudo-class selectors."]
     #[serde(skip_serializing_if = "Option::is_none")]
     pub no_unknown_pseudo_class:
@@ -3510,6 +3513,7 @@ impl Nursery {
         "noStaticElementInteractions",
         "noSubstr",
         "noTemplateCurlyInString",
+        "noUnknownAtRule",
         "noUnknownPseudoClass",
         "noUnknownPseudoElement",
         "noUnknownTypeSelector",
@@ -3545,6 +3549,7 @@ impl Nursery {
         "noDuplicateProperties",
         "noDuplicatedFields",
         "noMissingVarFunction",
+        "noUnknownAtRule",
         "noUnknownPseudoClass",
         "noUnknownPseudoElement",
         "noUnknownTypeSelector",
@@ -3566,11 +3571,12 @@ impl Nursery {
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]),
-        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]),
-        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]),
-        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47]),
-        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51]),
     ];
     const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]),
@@ -3626,6 +3632,7 @@ impl Nursery {
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53]),
     ];
     #[doc = r" Retrieves the recommended rules"]
     pub(crate) fn is_recommended_true(&self) -> bool {
@@ -3772,141 +3779,146 @@ impl Nursery {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]));
             }
         }
-        if let Some(rule) = self.no_unknown_pseudo_class.as_ref() {
+        if let Some(rule) = self.no_unknown_at_rule.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26]));
             }
         }
-        if let Some(rule) = self.no_unknown_pseudo_element.as_ref() {
+        if let Some(rule) = self.no_unknown_pseudo_class.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27]));
             }
         }
-        if let Some(rule) = self.no_unknown_type_selector.as_ref() {
+        if let Some(rule) = self.no_unknown_pseudo_element.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]));
             }
         }
-        if let Some(rule) = self.no_useless_escape_in_regex.as_ref() {
+        if let Some(rule) = self.no_unknown_type_selector.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]));
             }
         }
-        if let Some(rule) = self.no_useless_string_raw.as_ref() {
+        if let Some(rule) = self.no_useless_escape_in_regex.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]));
             }
         }
-        if let Some(rule) = self.no_useless_undefined.as_ref() {
+        if let Some(rule) = self.no_useless_string_raw.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]));
             }
         }
-        if let Some(rule) = self.no_value_at_rule.as_ref() {
+        if let Some(rule) = self.no_useless_undefined.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]));
             }
         }
-        if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() {
+        if let Some(rule) = self.no_value_at_rule.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]));
             }
         }
-        if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() {
+        if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]));
             }
         }
-        if let Some(rule) = self.use_at_index.as_ref() {
+        if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35]));
             }
         }
-        if let Some(rule) = self.use_collapsed_if.as_ref() {
+        if let Some(rule) = self.use_at_index.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36]));
             }
         }
-        if let Some(rule) = self.use_component_export_only_modules.as_ref() {
+        if let Some(rule) = self.use_collapsed_if.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]));
             }
         }
-        if let Some(rule) = self.use_consistent_curly_braces.as_ref() {
+        if let Some(rule) = self.use_component_export_only_modules.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]));
             }
         }
-        if let Some(rule) = self.use_consistent_member_accessibility.as_ref() {
+        if let Some(rule) = self.use_consistent_curly_braces.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]));
             }
         }
-        if let Some(rule) = self.use_deprecated_reason.as_ref() {
+        if let Some(rule) = self.use_consistent_member_accessibility.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]));
             }
         }
-        if let Some(rule) = self.use_explicit_type.as_ref() {
+        if let Some(rule) = self.use_deprecated_reason.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]));
             }
         }
-        if let Some(rule) = self.use_exports_last.as_ref() {
+        if let Some(rule) = self.use_explicit_type.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]));
             }
         }
-        if let Some(rule) = self.use_google_font_display.as_ref() {
+        if let Some(rule) = self.use_exports_last.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]));
             }
         }
-        if let Some(rule) = self.use_google_font_preconnect.as_ref() {
+        if let Some(rule) = self.use_google_font_display.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44]));
             }
         }
-        if let Some(rule) = self.use_guard_for_in.as_ref() {
+        if let Some(rule) = self.use_google_font_preconnect.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45]));
             }
         }
-        if let Some(rule) = self.use_import_restrictions.as_ref() {
+        if let Some(rule) = self.use_guard_for_in.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46]));
             }
         }
-        if let Some(rule) = self.use_named_operation.as_ref() {
+        if let Some(rule) = self.use_import_restrictions.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47]));
             }
         }
-        if let Some(rule) = self.use_naming_convention.as_ref() {
+        if let Some(rule) = self.use_named_operation.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48]));
             }
         }
-        if let Some(rule) = self.use_sorted_classes.as_ref() {
+        if let Some(rule) = self.use_naming_convention.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49]));
             }
         }
-        if let Some(rule) = self.use_strict_mode.as_ref() {
+        if let Some(rule) = self.use_sorted_classes.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50]));
             }
         }
-        if let Some(rule) = self.use_trim_start_end.as_ref() {
+        if let Some(rule) = self.use_strict_mode.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51]));
             }
         }
-        if let Some(rule) = self.use_valid_autocomplete.as_ref() {
+        if let Some(rule) = self.use_trim_start_end.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52]));
             }
         }
+        if let Some(rule) = self.use_valid_autocomplete.as_ref() {
+            if rule.is_enabled() {
+                index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53]));
+            }
+        }
         index_set
     }
     pub(crate) fn get_disabled_rules(&self) -> FxHashSet<RuleFilter<'static>> {
@@ -4041,141 +4053,146 @@ impl Nursery {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]));
             }
         }
-        if let Some(rule) = self.no_unknown_pseudo_class.as_ref() {
+        if let Some(rule) = self.no_unknown_at_rule.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26]));
             }
         }
-        if let Some(rule) = self.no_unknown_pseudo_element.as_ref() {
+        if let Some(rule) = self.no_unknown_pseudo_class.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27]));
             }
         }
-        if let Some(rule) = self.no_unknown_type_selector.as_ref() {
+        if let Some(rule) = self.no_unknown_pseudo_element.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]));
             }
         }
-        if let Some(rule) = self.no_useless_escape_in_regex.as_ref() {
+        if let Some(rule) = self.no_unknown_type_selector.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]));
             }
         }
-        if let Some(rule) = self.no_useless_string_raw.as_ref() {
+        if let Some(rule) = self.no_useless_escape_in_regex.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]));
             }
         }
-        if let Some(rule) = self.no_useless_undefined.as_ref() {
+        if let Some(rule) = self.no_useless_string_raw.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]));
             }
         }
-        if let Some(rule) = self.no_value_at_rule.as_ref() {
+        if let Some(rule) = self.no_useless_undefined.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]));
             }
         }
-        if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() {
+        if let Some(rule) = self.no_value_at_rule.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]));
             }
         }
-        if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() {
+        if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]));
             }
         }
-        if let Some(rule) = self.use_at_index.as_ref() {
+        if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35]));
             }
         }
-        if let Some(rule) = self.use_collapsed_if.as_ref() {
+        if let Some(rule) = self.use_at_index.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36]));
             }
         }
-        if let Some(rule) = self.use_component_export_only_modules.as_ref() {
+        if let Some(rule) = self.use_collapsed_if.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]));
             }
         }
-        if let Some(rule) = self.use_consistent_curly_braces.as_ref() {
+        if let Some(rule) = self.use_component_export_only_modules.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]));
             }
         }
-        if let Some(rule) = self.use_consistent_member_accessibility.as_ref() {
+        if let Some(rule) = self.use_consistent_curly_braces.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]));
             }
         }
-        if let Some(rule) = self.use_deprecated_reason.as_ref() {
+        if let Some(rule) = self.use_consistent_member_accessibility.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]));
             }
         }
-        if let Some(rule) = self.use_explicit_type.as_ref() {
+        if let Some(rule) = self.use_deprecated_reason.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]));
             }
         }
-        if let Some(rule) = self.use_exports_last.as_ref() {
+        if let Some(rule) = self.use_explicit_type.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]));
             }
         }
-        if let Some(rule) = self.use_google_font_display.as_ref() {
+        if let Some(rule) = self.use_exports_last.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]));
             }
         }
-        if let Some(rule) = self.use_google_font_preconnect.as_ref() {
+        if let Some(rule) = self.use_google_font_display.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44]));
             }
         }
-        if let Some(rule) = self.use_guard_for_in.as_ref() {
+        if let Some(rule) = self.use_google_font_preconnect.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45]));
             }
         }
-        if let Some(rule) = self.use_import_restrictions.as_ref() {
+        if let Some(rule) = self.use_guard_for_in.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46]));
             }
         }
-        if let Some(rule) = self.use_named_operation.as_ref() {
+        if let Some(rule) = self.use_import_restrictions.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47]));
             }
         }
-        if let Some(rule) = self.use_naming_convention.as_ref() {
+        if let Some(rule) = self.use_named_operation.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48]));
             }
         }
-        if let Some(rule) = self.use_sorted_classes.as_ref() {
+        if let Some(rule) = self.use_naming_convention.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49]));
             }
         }
-        if let Some(rule) = self.use_strict_mode.as_ref() {
+        if let Some(rule) = self.use_sorted_classes.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50]));
             }
         }
-        if let Some(rule) = self.use_trim_start_end.as_ref() {
+        if let Some(rule) = self.use_strict_mode.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51]));
             }
         }
-        if let Some(rule) = self.use_valid_autocomplete.as_ref() {
+        if let Some(rule) = self.use_trim_start_end.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52]));
             }
         }
+        if let Some(rule) = self.use_valid_autocomplete.as_ref() {
+            if rule.is_disabled() {
+                index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53]));
+            }
+        }
         index_set
     }
     #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"]
@@ -4316,6 +4333,10 @@ impl Nursery {
                 .no_template_curly_in_string
                 .as_ref()
                 .map(|conf| (conf.level(), conf.get_options())),
+            "noUnknownAtRule" => self
+                .no_unknown_at_rule
+                .as_ref()
+                .map(|conf| (conf.level(), conf.get_options())),
             "noUnknownPseudoClass" => self
                 .no_unknown_pseudo_class
                 .as_ref()
diff --git a/crates/biome_css_analyze/src/lint/nursery.rs b/crates/biome_css_analyze/src/lint/nursery.rs
index 6d16b3056165..b816124eb3c4 100644
--- a/crates/biome_css_analyze/src/lint/nursery.rs
+++ b/crates/biome_css_analyze/src/lint/nursery.rs
@@ -7,6 +7,7 @@ pub mod no_duplicate_custom_properties;
 pub mod no_duplicate_properties;
 pub mod no_irregular_whitespace;
 pub mod no_missing_var_function;
+pub mod no_unknown_at_rule;
 pub mod no_unknown_pseudo_class;
 pub mod no_unknown_pseudo_element;
 pub mod no_unknown_type_selector;
@@ -21,6 +22,7 @@ declare_lint_group! {
             self :: no_duplicate_properties :: NoDuplicateProperties ,
             self :: no_irregular_whitespace :: NoIrregularWhitespace ,
             self :: no_missing_var_function :: NoMissingVarFunction ,
+            self :: no_unknown_at_rule :: NoUnknownAtRule ,
             self :: no_unknown_pseudo_class :: NoUnknownPseudoClass ,
             self :: no_unknown_pseudo_element :: NoUnknownPseudoElement ,
             self :: no_unknown_type_selector :: NoUnknownTypeSelector ,
diff --git a/crates/biome_css_analyze/src/lint/nursery/no_unknown_at_rule.rs b/crates/biome_css_analyze/src/lint/nursery/no_unknown_at_rule.rs
new file mode 100644
index 000000000000..3cfd382362e5
--- /dev/null
+++ b/crates/biome_css_analyze/src/lint/nursery/no_unknown_at_rule.rs
@@ -0,0 +1,97 @@
+use biome_analyze::{
+    context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic, RuleSource,
+};
+use biome_console::markup;
+use biome_css_syntax::{CssUnknownBlockAtRule, CssUnknownValueAtRule};
+use biome_rowan::{declare_node_union, AstNode, TextRange};
+
+declare_lint_rule! {
+    /// Disallow unknown at-rules.
+    ///
+    /// For details on known at-rules, see the [MDN web docs](https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule).
+    ///
+    /// ## Examples
+    ///
+    /// ### Invalid
+    ///
+    /// ```css,expect_diagnostic
+    /// @uNkNoWn {}
+    /// ```
+    ///
+    /// ```css,expect_diagnostic
+    /// @unknown-at-rule {
+    ///   font-size: 14px;
+    /// }
+    /// ```
+    ///
+    /// ### Valid
+    ///
+    /// ```css
+    /// @charset 'UTF-8';
+    /// ```
+    ///
+    /// ```css
+    /// @media (max-width: 960px) {
+    ///   body {
+    ///     font-size: 13px;
+    ///   }
+    /// }
+    /// ```
+    pub NoUnknownAtRule {
+        version: "next",
+        name: "noUnknownAtRule",
+        language: "css",
+        recommended: true,
+        sources: &[RuleSource::Stylelint("at-rule-no-unknown")],
+    }
+}
+
+declare_node_union! {
+  pub AnyUnknownAtRule = CssUnknownBlockAtRule | CssUnknownValueAtRule
+}
+
+pub struct NoUnknownAtRuleState {
+    range: TextRange,
+    name: String,
+}
+
+impl Rule for NoUnknownAtRule {
+    type Query = Ast<AnyUnknownAtRule>;
+    type State = NoUnknownAtRuleState;
+    type Signals = Option<Self::State>;
+    type Options = ();
+
+    fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
+        let node = ctx.query();
+        let rule = match node {
+            AnyUnknownAtRule::CssUnknownBlockAtRule(rule) => rule.name().ok()?,
+            AnyUnknownAtRule::CssUnknownValueAtRule(rule) => rule.name().ok()?,
+        };
+        Some(NoUnknownAtRuleState {
+            range: rule.range(),
+            name: rule.text().to_string(),
+        })
+    }
+
+    fn diagnostic(_: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
+        let span = state.range;
+        let name = &state.name;
+        Some(
+            RuleDiagnostic::new(
+                rule_category!(),
+                span,
+                markup! {
+                    "Unexpected unknown at-rule: "<Emphasis>{ name }</Emphasis>" "
+                },
+            )
+            .note(markup! {
+                    ""<Emphasis>{ name }</Emphasis>" is not a standard CSS at-rule, which may lead to unexpected styling results or failure to interpret the styles as intended."
+            })
+            .note(markup! {
+                "See "<Hyperlink href="https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule">"MDN web docs"</Hyperlink>" for a known list of at-rules."
+            }).note(markup! {
+            "To fix this issue, consider removing the unknown at-rule."
+        })
+        )
+    }
+}
diff --git a/crates/biome_css_analyze/src/options.rs b/crates/biome_css_analyze/src/options.rs
index 2edbb3a490ae..069d6c5acdf5 100644
--- a/crates/biome_css_analyze/src/options.rs
+++ b/crates/biome_css_analyze/src/options.rs
@@ -21,6 +21,8 @@ pub type NoIrregularWhitespace =
 pub type NoMissingVarFunction =
     <lint::nursery::no_missing_var_function::NoMissingVarFunction as biome_analyze::Rule>::Options;
 pub type NoShorthandPropertyOverrides = < lint :: suspicious :: no_shorthand_property_overrides :: NoShorthandPropertyOverrides as biome_analyze :: Rule > :: Options ;
+pub type NoUnknownAtRule =
+    <lint::nursery::no_unknown_at_rule::NoUnknownAtRule as biome_analyze::Rule>::Options;
 pub type NoUnknownFunction =
     <lint::correctness::no_unknown_function::NoUnknownFunction as biome_analyze::Rule>::Options;
 pub type NoUnknownMediaFeatureName = < lint :: correctness :: no_unknown_media_feature_name :: NoUnknownMediaFeatureName as biome_analyze :: Rule > :: Options ;
diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs
index 85a71c6d1024..943b39285873 100644
--- a/crates/biome_diagnostics_categories/src/categories.rs
+++ b/crates/biome_diagnostics_categories/src/categories.rs
@@ -157,7 +157,6 @@ define_categories! {
     "lint/nursery/noInvalidGridAreas": "https://biomejs.dev/linter/rules/use-consistent-grid-areas",
     "lint/nursery/noInvalidPositionAtImportRule": "https://biomejs.dev/linter/rules/no-invalid-position-at-import-rule",
     "lint/nursery/noIrregularWhitespace": "https://biomejs.dev/linter/rules/no-irregular-whitespace",
-    "lint/nursery/useNamingConvention": "https://biomejs.dev/linter/rules/use-naming-convention",
     "lint/nursery/noMissingGenericFamilyKeyword": "https://biomejs.dev/linter/rules/no-missing-generic-family-keyword",
     "lint/nursery/noMissingVarFunction": "https://biomejs.dev/linter/rules/no-missing-var-function",
     "lint/nursery/noNestedTernary": "https://biomejs.dev/linter/rules/no-nested-ternary",
@@ -172,6 +171,7 @@ define_categories! {
     "lint/nursery/noSubstr": "https://biomejs.dev/linter/rules/no-substr",
     "lint/nursery/noTemplateCurlyInString": "https://biomejs.dev/linter/rules/no-template-curly-in-string",
     "lint/nursery/noUndeclaredDependencies": "https://biomejs.dev/linter/rules/no-undeclared-dependencies",
+    "lint/nursery/noUnknownAtRule": "https://biomejs.dev/linter/rules/no-unknown-at-rule",
     "lint/nursery/noUnknownFunction": "https://biomejs.dev/linter/rules/no-unknown-function",
     "lint/nursery/noUnknownMediaFeatureName": "https://biomejs.dev/linter/rules/no-unknown-media-feature-name",
     "lint/nursery/noUnknownProperty": "https://biomejs.dev/linter/rules/no-unknown-property",
@@ -198,13 +198,14 @@ define_categories! {
     "lint/nursery/useDeprecatedReason": "https://biomejs.dev/linter/rules/use-deprecated-reason",
     "lint/nursery/useExplicitFunctionReturnType": "https://biomejs.dev/linter/rules/use-explicit-function-return-type",
     "lint/nursery/useExplicitType": "https://biomejs.dev/linter/rules/use-explicit-function-return-type",
+    "lint/nursery/useExportsLast": "https://biomejs.dev/linter/rules/use-exports-last",
     "lint/nursery/useGoogleFontDisplay": "https://biomejs.dev/linter/rules/use-google-font-display",
     "lint/nursery/useGoogleFontPreconnect": "https://biomejs.dev/linter/rules/use-google-font-preconnect",
-    "lint/nursery/useExportsLast": "https://biomejs.dev/linter/rules/use-exports-last",
     "lint/nursery/useGuardForIn": "https://biomejs.dev/linter/rules/use-guard-for-in",
     "lint/nursery/useImportRestrictions": "https://biomejs.dev/linter/rules/use-import-restrictions",
     "lint/nursery/useJsxCurlyBraceConvention": "https://biomejs.dev/linter/rules/use-jsx-curly-brace-convention",
     "lint/nursery/useNamedOperation": "https://biomejs.dev/linter/rules/use-named-operation",
+    "lint/nursery/useNamingConvention": "https://biomejs.dev/linter/rules/use-naming-convention",
     "lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes",
     "lint/nursery/useStrictMode": "https://biomejs.dev/linter/rules/use-strict-mode",
     "lint/nursery/useTrimStartEnd": "https://biomejs.dev/linter/rules/use-trim-start-end",
diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts
index 77c49ed39316..168017bc4a96 100644
--- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts
+++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts
@@ -1330,6 +1330,10 @@ export interface Nursery {
 	 * Disallow template literal placeholder syntax in regular strings.
 	 */
 	noTemplateCurlyInString?: RuleConfiguration_for_Null;
+	/**
+	 * Disallow unknown at-rules.
+	 */
+	noUnknownAtRule?: RuleConfiguration_for_Null;
 	/**
 	 * Disallow unknown pseudo-class selectors.
 	 */
@@ -3097,7 +3101,6 @@ export type Category =
 	| "lint/nursery/noInvalidGridAreas"
 	| "lint/nursery/noInvalidPositionAtImportRule"
 	| "lint/nursery/noIrregularWhitespace"
-	| "lint/nursery/useNamingConvention"
 	| "lint/nursery/noMissingGenericFamilyKeyword"
 	| "lint/nursery/noMissingVarFunction"
 	| "lint/nursery/noNestedTernary"
@@ -3112,6 +3115,7 @@ export type Category =
 	| "lint/nursery/noSubstr"
 	| "lint/nursery/noTemplateCurlyInString"
 	| "lint/nursery/noUndeclaredDependencies"
+	| "lint/nursery/noUnknownAtRule"
 	| "lint/nursery/noUnknownFunction"
 	| "lint/nursery/noUnknownMediaFeatureName"
 	| "lint/nursery/noUnknownProperty"
@@ -3138,13 +3142,14 @@ export type Category =
 	| "lint/nursery/useDeprecatedReason"
 	| "lint/nursery/useExplicitFunctionReturnType"
 	| "lint/nursery/useExplicitType"
+	| "lint/nursery/useExportsLast"
 	| "lint/nursery/useGoogleFontDisplay"
 	| "lint/nursery/useGoogleFontPreconnect"
-	| "lint/nursery/useExportsLast"
 	| "lint/nursery/useGuardForIn"
 	| "lint/nursery/useImportRestrictions"
 	| "lint/nursery/useJsxCurlyBraceConvention"
 	| "lint/nursery/useNamedOperation"
+	| "lint/nursery/useNamingConvention"
 	| "lint/nursery/useSortedClasses"
 	| "lint/nursery/useStrictMode"
 	| "lint/nursery/useTrimStartEnd"
diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json
index 1a24ccabcbec..ccbbc8c6be8a 100644
--- a/packages/@biomejs/biome/configuration_schema.json
+++ b/packages/@biomejs/biome/configuration_schema.json
@@ -2372,6 +2372,13 @@
 						{ "type": "null" }
 					]
 				},
+				"noUnknownAtRule": {
+					"description": "Disallow unknown at-rules.",
+					"anyOf": [
+						{ "$ref": "#/definitions/RuleConfiguration" },
+						{ "type": "null" }
+					]
+				},
 				"noUnknownPseudoClass": {
 					"description": "Disallow unknown pseudo-class selectors.",
 					"anyOf": [


Test patch:
diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnknownAtRule/invalid.css b/crates/biome_css_analyze/tests/specs/nursery/noUnknownAtRule/invalid.css
new file mode 100644
index 000000000000..43d139851b86
--- /dev/null
+++ b/crates/biome_css_analyze/tests/specs/nursery/noUnknownAtRule/invalid.css
@@ -0,0 +1,7 @@
+@unknown-rule 'UTF-8';
+@uNkNoWn {}
+@UNKNOWN {}
+@unknown-at-rule {}
+@unknown { @unknown-at-rule { font-size: 14px; } }
+@MY-other-at-rule {}
+@not-my-at-rule {}
\ No newline at end of file
diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnknownAtRule/invalid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noUnknownAtRule/invalid.css.snap
new file mode 100644
index 000000000000..fd79f5aad0be
--- /dev/null
+++ b/crates/biome_css_analyze/tests/specs/nursery/noUnknownAtRule/invalid.css.snap
@@ -0,0 +1,177 @@
+---
+source: crates/biome_css_analyze/tests/spec_tests.rs
+expression: invalid.css
+---
+# Input
+```css
+@unknown-rule 'UTF-8';
+@uNkNoWn {}
+@UNKNOWN {}
+@unknown-at-rule {}
+@unknown { @unknown-at-rule { font-size: 14px; } }
+@MY-other-at-rule {}
+@not-my-at-rule {}
+```
+
+# Diagnostics
+```
+invalid.css:1:2 lint/nursery/noUnknownAtRule ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! Unexpected unknown at-rule: unknown-rule 
+  
+  > 1 │ @unknown-rule 'UTF-8';
+      │  ^^^^^^^^^^^^
+    2 │ @uNkNoWn {}
+    3 │ @UNKNOWN {}
+  
+  i unknown-rule is not a standard CSS at-rule, which may lead to unexpected styling results or failure to interpret the styles as intended.
+  
+  i See MDN web docs for a known list of at-rules.
+  
+  i To fix this issue, consider removing the unknown at-rule.
+  
+
+```
+
+```
+invalid.css:2:2 lint/nursery/noUnknownAtRule ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! Unexpected unknown at-rule: uNkNoWn 
+  
+    1 │ @unknown-rule 'UTF-8';
+  > 2 │ @uNkNoWn {}
+      │  ^^^^^^^
+    3 │ @UNKNOWN {}
+    4 │ @unknown-at-rule {}
+  
+  i uNkNoWn is not a standard CSS at-rule, which may lead to unexpected styling results or failure to interpret the styles as intended.
+  
+  i See MDN web docs for a known list of at-rules.
+  
+  i To fix this issue, consider removing the unknown at-rule.
+  
+
+```
+
+```
+invalid.css:3:2 lint/nursery/noUnknownAtRule ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! Unexpected unknown at-rule: UNKNOWN 
+  
+    1 │ @unknown-rule 'UTF-8';
+    2 │ @uNkNoWn {}
+  > 3 │ @UNKNOWN {}
+      │  ^^^^^^^
+    4 │ @unknown-at-rule {}
+    5 │ @unknown { @unknown-at-rule { font-size: 14px; } }
+  
+  i UNKNOWN is not a standard CSS at-rule, which may lead to unexpected styling results or failure to interpret the styles as intended.
+  
+  i See MDN web docs for a known list of at-rules.
+  
+  i To fix this issue, consider removing the unknown at-rule.
+  
+
+```
+
+```
+invalid.css:4:2 lint/nursery/noUnknownAtRule ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! Unexpected unknown at-rule: unknown-at-rule 
+  
+    2 │ @uNkNoWn {}
+    3 │ @UNKNOWN {}
+  > 4 │ @unknown-at-rule {}
+      │  ^^^^^^^^^^^^^^^
+    5 │ @unknown { @unknown-at-rule { font-size: 14px; } }
+    6 │ @MY-other-at-rule {}
+  
+  i unknown-at-rule is not a standard CSS at-rule, which may lead to unexpected styling results or failure to interpret the styles as intended.
+  
+  i See MDN web docs for a known list of at-rules.
+  
+  i To fix this issue, consider removing the unknown at-rule.
+  
+
+```
+
+```
+invalid.css:5:2 lint/nursery/noUnknownAtRule ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! Unexpected unknown at-rule: unknown 
+  
+    3 │ @UNKNOWN {}
+    4 │ @unknown-at-rule {}
+  > 5 │ @unknown { @unknown-at-rule { font-size: 14px; } }
+      │  ^^^^^^^
+    6 │ @MY-other-at-rule {}
+    7 │ @not-my-at-rule {}
+  
+  i unknown is not a standard CSS at-rule, which may lead to unexpected styling results or failure to interpret the styles as intended.
+  
+  i See MDN web docs for a known list of at-rules.
+  
+  i To fix this issue, consider removing the unknown at-rule.
+  
+
+```
+
+```
+invalid.css:5:13 lint/nursery/noUnknownAtRule ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! Unexpected unknown at-rule: unknown-at-rule 
+  
+    3 │ @UNKNOWN {}
+    4 │ @unknown-at-rule {}
+  > 5 │ @unknown { @unknown-at-rule { font-size: 14px; } }
+      │             ^^^^^^^^^^^^^^^
+    6 │ @MY-other-at-rule {}
+    7 │ @not-my-at-rule {}
+  
+  i unknown-at-rule is not a standard CSS at-rule, which may lead to unexpected styling results or failure to interpret the styles as intended.
+  
+  i See MDN web docs for a known list of at-rules.
+  
+  i To fix this issue, consider removing the unknown at-rule.
+  
+
+```
+
+```
+invalid.css:6:2 lint/nursery/noUnknownAtRule ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! Unexpected unknown at-rule: MY-other-at-rule 
+  
+    4 │ @unknown-at-rule {}
+    5 │ @unknown { @unknown-at-rule { font-size: 14px; } }
+  > 6 │ @MY-other-at-rule {}
+      │  ^^^^^^^^^^^^^^^^
+    7 │ @not-my-at-rule {}
+  
+  i MY-other-at-rule is not a standard CSS at-rule, which may lead to unexpected styling results or failure to interpret the styles as intended.
+  
+  i See MDN web docs for a known list of at-rules.
+  
+  i To fix this issue, consider removing the unknown at-rule.
+  
+
+```
+
+```
+invalid.css:7:2 lint/nursery/noUnknownAtRule ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! Unexpected unknown at-rule: not-my-at-rule 
+  
+    5 │ @unknown { @unknown-at-rule { font-size: 14px; } }
+    6 │ @MY-other-at-rule {}
+  > 7 │ @not-my-at-rule {}
+      │  ^^^^^^^^^^^^^^
+  
+  i not-my-at-rule is not a standard CSS at-rule, which may lead to unexpected styling results or failure to interpret the styles as intended.
+  
+  i See MDN web docs for a known list of at-rules.
+  
+  i To fix this issue, consider removing the unknown at-rule.
+  
+
+```
diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnknownAtRule/valid.css b/crates/biome_css_analyze/tests/specs/nursery/noUnknownAtRule/valid.css
new file mode 100644
index 000000000000..a702e5ea94f6
--- /dev/null
+++ b/crates/biome_css_analyze/tests/specs/nursery/noUnknownAtRule/valid.css
@@ -0,0 +1,123 @@
+@starting-style {
+    opacity: 0;
+}
+@charset 'UTF-8';
+@container (min-width: 700px)
+@CONTAINER (min-width: 500px)
+@CHARSET 'UTF-8';
+@charset 'iso-8859-15';
+@import url("fineprint.css") print;
+@import 'custom.css'
+@import url('landscape.css') screen and (orientation:landscape);
+@namespace url(http://www.w3.org/1999/xhtml);
+@namespace prefix url(XML-namespace-URL);
+@media print {
+    body {
+        font-size: 10pt
+    }
+}
+@media (max-width: 960px) {
+    body {
+        font-size: 13px
+    }
+}
+@media screen,
+print {
+    body {
+        line-height: 1.2
+    }
+}
+@supports (--foo: green) {
+    body {
+        color: green;
+    }
+}
+@supports ((perspective: 10px) or (-webkit-perspective: 10px)) {
+    font-size: 10pt
+}
+@counter-style win-list {
+    system: fixed;
+    symbols: url(gold-medal.svg);
+    suffix: ' ';
+}
+@document url(http://www.w3.org/),
+url-prefix(http: //www.w3.org/Style/), domain(mozilla.org), regexp('https:.*')
+@page :left {
+    margin-left: 4cm;
+}
+@page {
+    @top-center {
+        content: none
+    }
+}
+@font-face {
+    font-family: MyHelvetica;
+    src: local("Helvetica"), url(MgOpenModern.ttf);
+}
+@keyframes identifier {
+    0% {
+        top: 0;
+        left: 0;
+    }
+
+    30% {
+        top: 50px;
+    }
+
+    68%,
+    100% {
+        top: 100px;
+        left: 100%;
+    }
+}
+@-webkit-keyframes identifier {
+    0% {
+        top: 0;
+        left: 0;
+    }
+
+    30% {
+        top: 50px;
+    }
+
+    68%,
+    100% {
+        top: 100px;
+        left: 100%;
+    }
+}
+
+@counter-style winners-list {
+    system: fixed;
+    symbols: url(gold-medal.svg);
+    suffix: " ";
+}
+
+@font-feature-values Font One {
+    @styleset {
+        nice-style: 12;
+    }
+}
+
+@layer framework {
+    h1 {
+        background: white;
+    }
+}
+/* TODO: the parser not supported yet */
+/* @position-try --foo {}
+@view-transition {
+    navigation: auto;
+} */
+
+/* These at-rules have already been removed from the draft. Support will be provided if there's strong demand*/
+/* @scroll-timeline foo {}
+@viewport {
+     min-width: 640px;
+     max-width: 800px;
+ }
+
+ @viewport {
+     orientation: landscape;
+ } */
+
diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnknownAtRule/valid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noUnknownAtRule/valid.css.snap
new file mode 100644
index 000000000000..a19019e117e7
--- /dev/null
+++ b/crates/biome_css_analyze/tests/specs/nursery/noUnknownAtRule/valid.css.snap
@@ -0,0 +1,131 @@
+---
+source: crates/biome_css_analyze/tests/spec_tests.rs
+expression: valid.css
+---
+# Input
+```css
+@starting-style {
+    opacity: 0;
+}
+@charset 'UTF-8';
+@container (min-width: 700px)
+@CONTAINER (min-width: 500px)
+@CHARSET 'UTF-8';
+@charset 'iso-8859-15';
+@import url("fineprint.css") print;
+@import 'custom.css'
+@import url('landscape.css') screen and (orientation:landscape);
+@namespace url(http://www.w3.org/1999/xhtml);
+@namespace prefix url(XML-namespace-URL);
+@media print {
+    body {
+        font-size: 10pt
+    }
+}
+@media (max-width: 960px) {
+    body {
+        font-size: 13px
+    }
+}
+@media screen,
+print {
+    body {
+        line-height: 1.2
+    }
+}
+@supports (--foo: green) {
+    body {
+        color: green;
+    }
+}
+@supports ((perspective: 10px) or (-webkit-perspective: 10px)) {
+    font-size: 10pt
+}
+@counter-style win-list {
+    system: fixed;
+    symbols: url(gold-medal.svg);
+    suffix: ' ';
+}
+@document url(http://www.w3.org/),
+url-prefix(http: //www.w3.org/Style/), domain(mozilla.org), regexp('https:.*')
+@page :left {
+    margin-left: 4cm;
+}
+@page {
+    @top-center {
+        content: none
+    }
+}
+@font-face {
+    font-family: MyHelvetica;
+    src: local("Helvetica"), url(MgOpenModern.ttf);
+}
+@keyframes identifier {
+    0% {
+        top: 0;
+        left: 0;
+    }
+
+    30% {
+        top: 50px;
+    }
+
+    68%,
+    100% {
+        top: 100px;
+        left: 100%;
+    }
+}
+@-webkit-keyframes identifier {
+    0% {
+        top: 0;
+        left: 0;
+    }
+
+    30% {
+        top: 50px;
+    }
+
+    68%,
+    100% {
+        top: 100px;
+        left: 100%;
+    }
+}
+
+@counter-style winners-list {
+    system: fixed;
+    symbols: url(gold-medal.svg);
+    suffix: " ";
+}
+
+@font-feature-values Font One {
+    @styleset {
+        nice-style: 12;
+    }
+}
+
+@layer framework {
+    h1 {
+        background: white;
+    }
+}
+/* TODO: the parser not supported yet */
+/* @position-try --foo {}
+@view-transition {
+    navigation: auto;
+} */
+
+/* These at-rules have already been removed from the draft. Support will be provided if there's strong demand*/
+/* @scroll-timeline foo {}
+@viewport {
+     min-width: 640px;
+     max-width: 800px;
+ }
+
+ @viewport {
+     orientation: landscape;
+ } */
+
+
+```