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>, + #[doc = "Disallow unknown at-rules."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_unknown_at_rule: Option>, #[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> { @@ -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; + type State = NoUnknownAtRuleState; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Option { + 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, state: &Self::State) -> Option { + let span = state.range; + let name = &state.name; + Some( + RuleDiagnostic::new( + rule_category!(), + span, + markup! { + "Unexpected unknown at-rule: "{ name }" " + }, + ) + .note(markup! { + ""{ name }" 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 ""MDN web docs"" 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 = ::Options; pub type NoShorthandPropertyOverrides = < lint :: suspicious :: no_shorthand_property_overrides :: NoShorthandPropertyOverrides as biome_analyze :: Rule > :: Options ; +pub type NoUnknownAtRule = + ::Options; pub type NoUnknownFunction = ::Options; pub type NoUnknownMediaFeatureName = < lint :: correctness :: no_unknown_media_feature_name :: NoUnknownMediaFeatureName as biome_analyze :: Rule > :: Options ; 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; + } */ + + +``` 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": [