diff --git a/crates/biome_configuration/src/linter/rules.rs b/crates/biome_configuration/src/linter/rules.rs index 3500a53ca853..ca7c3ac167d4 100644 --- a/crates/biome_configuration/src/linter/rules.rs +++ b/crates/biome_configuration/src/linter/rules.rs @@ -2709,6 +2709,9 @@ pub struct Nursery { #[doc = "Disallow unknown CSS value functions."] #[serde(skip_serializing_if = "Option::is_none")] pub no_unknown_function: Option>, + #[doc = "Disallow unknown media feature names."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_unknown_media_feature_name: Option>, #[doc = "Disallow unknown properties."] #[serde(skip_serializing_if = "Option::is_none")] pub no_unknown_property: Option>, @@ -2792,6 +2795,7 @@ impl Nursery { "noRestrictedImports", "noUndeclaredDependencies", "noUnknownFunction", + "noUnknownMediaFeatureName", "noUnknownProperty", "noUnknownSelectorPseudoElement", "noUnknownUnit", @@ -2839,11 +2843,11 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -2880,6 +2884,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -2996,76 +3001,81 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.no_unknown_property.as_ref() { + if let Some(rule) = self.no_unknown_media_feature_name.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.no_unknown_selector_pseudo_element.as_ref() { + if let Some(rule) = self.no_unknown_property.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.no_unknown_unit.as_ref() { + if let Some(rule) = self.no_unknown_selector_pseudo_element.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.no_unmatchable_anb_selector.as_ref() { + if let Some(rule) = self.no_unknown_unit.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - if let Some(rule) = self.no_useless_string_concat.as_ref() { + if let Some(rule) = self.no_unmatchable_anb_selector.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } - if let Some(rule) = self.no_useless_undefined_initialization.as_ref() { + if let Some(rule) = self.no_useless_string_concat.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); } } - if let Some(rule) = self.use_array_literals.as_ref() { + if let Some(rule) = self.no_useless_undefined_initialization.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } } - if let Some(rule) = self.use_consistent_builtin_instantiation.as_ref() { + if let Some(rule) = self.use_array_literals.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.use_default_switch_clause.as_ref() { + if let Some(rule) = self.use_consistent_builtin_instantiation.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.use_explicit_length_check.as_ref() { + if let Some(rule) = self.use_default_switch_clause.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_focusable_interactive.as_ref() { + if let Some(rule) = self.use_explicit_length_check.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_generic_font_names.as_ref() { + if let Some(rule) = self.use_focusable_interactive.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_generic_font_names.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.use_sorted_classes.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[33])); } } + 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[34])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -3170,76 +3180,81 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.no_unknown_property.as_ref() { + if let Some(rule) = self.no_unknown_media_feature_name.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.no_unknown_selector_pseudo_element.as_ref() { + if let Some(rule) = self.no_unknown_property.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.no_unknown_unit.as_ref() { + if let Some(rule) = self.no_unknown_selector_pseudo_element.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.no_unmatchable_anb_selector.as_ref() { + if let Some(rule) = self.no_unknown_unit.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - if let Some(rule) = self.no_useless_string_concat.as_ref() { + if let Some(rule) = self.no_unmatchable_anb_selector.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } - if let Some(rule) = self.no_useless_undefined_initialization.as_ref() { + if let Some(rule) = self.no_useless_string_concat.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); } } - if let Some(rule) = self.use_array_literals.as_ref() { + if let Some(rule) = self.no_useless_undefined_initialization.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } } - if let Some(rule) = self.use_consistent_builtin_instantiation.as_ref() { + if let Some(rule) = self.use_array_literals.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.use_default_switch_clause.as_ref() { + if let Some(rule) = self.use_consistent_builtin_instantiation.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.use_explicit_length_check.as_ref() { + if let Some(rule) = self.use_default_switch_clause.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_focusable_interactive.as_ref() { + if let Some(rule) = self.use_explicit_length_check.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_generic_font_names.as_ref() { + if let Some(rule) = self.use_focusable_interactive.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_generic_font_names.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.use_sorted_classes.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[33])); } } + 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[34])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3356,6 +3371,10 @@ impl Nursery { .no_unknown_function .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noUnknownMediaFeatureName" => self + .no_unknown_media_feature_name + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noUnknownProperty" => self .no_unknown_property .as_ref() diff --git a/crates/biome_css_analyze/src/keywords.rs b/crates/biome_css_analyze/src/keywords.rs index 4989e8a60aca..bccaa3dde10b 100644 --- a/crates/biome_css_analyze/src/keywords.rs +++ b/crates/biome_css_analyze/src/keywords.rs @@ -855,7 +855,7 @@ pub const OTHER_PSEUDO_ELEMENTS: [&str; 18] = [ "view-transition-old", ]; -pub const VENDER_PREFIXES: [&str; 4] = ["-webkit-", "-moz-", "-ms-", "-o-"]; +pub const VENDOR_PREFIXES: [&str; 4] = ["-webkit-", "-moz-", "-ms-", "-o-"]; // https://github.com/known-css/known-css-properties/blob/master/source/w3c.json pub const KNOWN_PROPERTIES: [&str; 588] = [ @@ -3826,7 +3826,7 @@ pub const KNOWN_SAFARI_PROPERTIES: [&str; 644] = [ ]; // https://github.com/known-css/known-css-properties/blob/master/source/browsers/samsung_internet-23.0.json -pub const KNOWN_SUMSUNG_INTERNET_PROPERTIES: [&str; 608] = [ +pub const KNOWN_SAMSUNG_INTERNET_PROPERTIES: [&str; 608] = [ "-webkit-align-content", "-webkit-align-items", "-webkit-align-self", @@ -4958,6 +4958,69 @@ pub const KNOWN_US_BROWSER_PROPERTIES: [&str; 517] = [ "zoom", ]; +pub const MEDIA_FEATURE_NAMES: [&str; 60] = [ + "any-hover", + "any-pointer", + "aspect-ratio", + "color", + "color-gamut", + "color-index", + "device-aspect-ratio", + "device-height", + "device-posture", + "device-width", + "display-mode", + "dynamic-range", + "environment-blending", + "forced-colors", + "grid", + "height", + "horizontal-viewport-segments", + "hover", + "inverted-colors", + "light-level", + "max-aspect-ratio", + "max-color", + "max-color-index", + "max-device-aspect-ratio", + "max-device-height", + "max-device-width", + "max-height", + "max-monochrome", + "max-resolution", + "max-width", + "min-aspect-ratio", + "min-color", + "min-color-index", + "min-device-aspect-ratio", + "min-device-height", + "min-device-width", + "min-height", + "min-monochrome", + "min-resolution", + "min-width", + "monochrome", + "nav-controls", + "orientation", + "overflow-block", + "overflow-inline", + "pointer", + "prefers-color-scheme", + "prefers-contrast", + "prefers-reduced-data", + "prefers-reduced-motion", + "prefers-reduded-transparency", + "resolution", + "scan", + "screen-spanning", + "scripting", + "update", + "vertical-viewport-segments", + "video-color-gamut", + "video-dynamic-range", + "width", +]; + #[cfg(test)] mod tests { use std::collections::HashSet; @@ -4965,7 +5028,7 @@ mod tests { use super::{ FUNCTION_KEYWORDS, KNOWN_EDGE_PROPERTIES, KNOWN_EXPLORER_PROPERTIES, KNOWN_FIREFOX_PROPERTIES, KNOWN_PROPERTIES, KNOWN_SAFARI_PROPERTIES, - KNOWN_SUMSUNG_INTERNET_PROPERTIES, KNOWN_US_BROWSER_PROPERTIES, + KNOWN_SAMSUNG_INTERNET_PROPERTIES, KNOWN_US_BROWSER_PROPERTIES, MEDIA_FEATURE_NAMES, }; #[test] @@ -5019,7 +5082,7 @@ mod tests { #[test] fn test_kown_sumsung_internet_properties_order() { - for items in KNOWN_SUMSUNG_INTERNET_PROPERTIES.windows(2) { + for items in KNOWN_SAMSUNG_INTERNET_PROPERTIES.windows(2) { assert!(items[0] < items[1], "{} < {}", items[0], items[1]); } } @@ -5030,4 +5093,11 @@ mod tests { assert!(items[0] < items[1], "{} < {}", items[0], items[1]); } } + + #[test] + fn test_media_feature_names_order() { + for items in MEDIA_FEATURE_NAMES.windows(2) { + assert!(items[0] < items[1], "{} < {}", items[0], items[1]); + } + } } diff --git a/crates/biome_css_analyze/src/lint/nursery.rs b/crates/biome_css_analyze/src/lint/nursery.rs index 09eab002a5a8..a5f140e88f3d 100644 --- a/crates/biome_css_analyze/src/lint/nursery.rs +++ b/crates/biome_css_analyze/src/lint/nursery.rs @@ -10,6 +10,7 @@ pub mod no_duplicate_selectors_keyframe_block; pub mod no_important_in_keyframe; pub mod no_invalid_position_at_import_rule; pub mod no_unknown_function; +pub mod no_unknown_media_feature_name; pub mod no_unknown_property; pub mod no_unknown_selector_pseudo_element; pub mod no_unknown_unit; @@ -28,6 +29,7 @@ declare_group! { self :: no_important_in_keyframe :: NoImportantInKeyframe , self :: no_invalid_position_at_import_rule :: NoInvalidPositionAtImportRule , self :: no_unknown_function :: NoUnknownFunction , + self :: no_unknown_media_feature_name :: NoUnknownMediaFeatureName , self :: no_unknown_property :: NoUnknownProperty , self :: no_unknown_selector_pseudo_element :: NoUnknownSelectorPseudoElement , self :: no_unknown_unit :: NoUnknownUnit , diff --git a/crates/biome_css_analyze/src/lint/nursery/no_unknown_media_feature_name.rs b/crates/biome_css_analyze/src/lint/nursery/no_unknown_media_feature_name.rs new file mode 100644 index 000000000000..f9719ac5a55e --- /dev/null +++ b/crates/biome_css_analyze/src/lint/nursery/no_unknown_media_feature_name.rs @@ -0,0 +1,307 @@ +use biome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic, RuleSource}; +use biome_console::markup; +use biome_css_syntax::{ + AnyCssMediaAndCombinableCondition, AnyCssMediaCondition, AnyCssMediaInParens, + AnyCssMediaOrCombinableCondition, AnyCssMediaQuery, AnyCssMediaTypeCondition, + AnyCssMediaTypeQuery, AnyCssQueryFeature, CssMediaAndCondition, CssMediaConditionQuery, + CssMediaOrCondition, CssMediaQueryList, +}; +use biome_rowan::AstNode; + +use crate::utils::is_media_feature_name; + +declare_rule! { + /// Disallow unknown media feature names. + /// + /// This rule considers media feature names defined in the CSS Specifications, up to and including Editor's Drafts, to be known. + /// This rule also checks vendor-prefixed media feature names. + /// + /// Data sources of known CSS media feature are: + /// - MDN reference on [CSS media feature](https://developer.mozilla.org/en-US/docs/Web/CSS/@media) + /// - W3C reference on [Media Queries Level 3](https://www.w3.org/TR/mediaqueries-3/) + /// - W3C reference on [Media Queries Level 4](https://www.w3.org/TR/mediaqueries-4/) + /// - W3C reference on [Media Queries Level 5](https://www.w3.org/TR/mediaqueries-5/) + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```css,expect_diagnostic + /// @media screen and (unknown > 320px) {} + /// ``` + /// + /// ```css,expect_diagnostic + /// @media only screen and (min-width: 320px) and (max-width: 480px) and (unknown: 150dpi) {} + /// ``` + /// + /// ```css,expect_diagnostic + /// @media (not(unknown < 320px)) and (max-width > 640px) {} + /// + /// ```css,expect_diagnostic + /// @media (400px <= unknown <= 700px) {} + /// ``` + /// + /// ### Valid + /// + /// ```css + /// @media screen and (width > 320px) {} + /// ``` + /// + /// ```css + /// @media only screen and (min-width: 320px) and (max-width: 480px) and (resolution: 150dpi) {} + /// ``` + /// + /// ```css + /// @media (not(min-width < 320px)) and (max-width > 640px) {} + /// ``` + /// + /// ```css + /// @media (400px <= width <= 700px) {} + /// ``` + /// + /// ```css + /// @media screen and (-webkit-width > 320px) {} + /// ``` + /// + pub NoUnknownMediaFeatureName { + version: "next", + name: "noUnknownMediaFeatureName", + recommended: false, + sources: &[RuleSource::Stylelint("media-feature-name-no-unknown")], + } +} + +impl Rule for NoUnknownMediaFeatureName { + type Query = Ast; + type State = CssMediaQueryList; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Option { + let media_query_list = ctx.query(); + for any_css_media_query in media_query_list { + match any_css_media_query.ok()? { + AnyCssMediaQuery::CssMediaConditionQuery(css_media_condition_query) => { + if is_invalid_feature_name_included_in_css_media_condition_query( + css_media_condition_query, + )? { + return Some(media_query_list.clone()); + } + } + AnyCssMediaQuery::AnyCssMediaTypeQuery(any_css_media_type_query) => { + if is_invalid_feature_name_included_in_css_media_type_query( + any_css_media_type_query, + )? { + return Some(media_query_list.clone()); + } + } + _ => {} + } + } + None + } + + fn diagnostic(_: &RuleContext, node: &Self::State) -> Option { + let span = node.range(); + Some( + RuleDiagnostic::new( + rule_category!(), + span, + markup! { + "Don't use unknown media feature names." + }, + ) + .note(markup! { + "Unexpected unknown media feature name." + }) + .note(markup! { + "You should use media feature names defined in the CSS Specifications." + }), + ) + } +} + +fn is_invalid_feature_name_included_in_css_media_condition_query( + css_media_condition_query: CssMediaConditionQuery, +) -> Option { + match css_media_condition_query.condition().ok()? { + AnyCssMediaCondition::AnyCssMediaInParens(any_css_media_in_parens) => { + has_invalid_media_feature_name(any_css_media_in_parens) + } + AnyCssMediaCondition::CssMediaAndCondition(css_media_and_condition) => { + is_css_media_and_condition_invalid(css_media_and_condition) + } + AnyCssMediaCondition::CssMediaOrCondition(css_media_or_condition) => { + is_css_media_or_condition_invalid(css_media_or_condition) + } + AnyCssMediaCondition::CssMediaNotCondition(css_media_not_condition) => { + has_invalid_media_feature_name(css_media_not_condition.condition().ok()?) + } + } +} + +fn is_invalid_feature_name_included_in_css_media_type_query( + any_css_media_type_query: AnyCssMediaTypeQuery, +) -> Option { + match any_css_media_type_query { + AnyCssMediaTypeQuery::CssMediaTypeQuery(_) => Some(false), + AnyCssMediaTypeQuery::CssMediaAndTypeQuery(css_media_and_type_query) => { + match css_media_and_type_query.right().ok()? { + AnyCssMediaTypeCondition::AnyCssMediaInParens(any_css_media_in_parens) => { + has_invalid_media_feature_name(any_css_media_in_parens) + } + AnyCssMediaTypeCondition::CssMediaAndCondition(css_media_and_condition) => { + is_css_media_and_condition_invalid(css_media_and_condition) + } + AnyCssMediaTypeCondition::CssMediaNotCondition(css_media_not_condition) => { + has_invalid_media_feature_name(css_media_not_condition.condition().ok()?) + } + } + } + } +} + +fn is_css_media_and_condition_invalid( + css_media_and_condition: CssMediaAndCondition, +) -> Option { + if has_invalid_media_feature_name(css_media_and_condition.left().ok()?)? { + return Some(true); + } + let mut stack = vec![css_media_and_condition.right().ok()?]; + while !stack.is_empty() { + let element = stack.pop()?; + match element { + AnyCssMediaAndCombinableCondition::AnyCssMediaInParens(any_css_media_in_parens) => { + if has_invalid_media_feature_name(any_css_media_in_parens)? { + return Some(true); + } + } + AnyCssMediaAndCombinableCondition::CssMediaAndCondition(css_media_and_condition) => { + if has_invalid_media_feature_name(css_media_and_condition.left().ok()?)? { + return Some(true); + } + stack.push(css_media_and_condition.right().ok()?); + } + } + } + Some(false) +} + +fn is_css_media_or_condition_invalid(css_media_or_condition: CssMediaOrCondition) -> Option { + if has_invalid_media_feature_name(css_media_or_condition.left().ok()?)? { + return Some(true); + } + let mut stack = vec![css_media_or_condition.right().ok()?]; + while !stack.is_empty() { + let element = stack.pop()?; + match element { + AnyCssMediaOrCombinableCondition::AnyCssMediaInParens(any_css_media_in_parens) => { + if has_invalid_media_feature_name(any_css_media_in_parens)? { + return Some(true); + } + } + AnyCssMediaOrCombinableCondition::CssMediaOrCondition(css_media_or_condition) => { + if has_invalid_media_feature_name(css_media_or_condition.left().ok()?)? { + return Some(true); + } + stack.push(css_media_or_condition.right().ok()?); + } + } + } + Some(false) +} + +fn has_invalid_media_feature_name(any_css_media_in_parens: AnyCssMediaInParens) -> Option { + let mut any_css_media_in_parens_stack = vec![any_css_media_in_parens]; + while !any_css_media_in_parens_stack.is_empty() { + let any_css_media_in_parens = any_css_media_in_parens_stack.pop()?; + match any_css_media_in_parens { + AnyCssMediaInParens::CssMediaFeatureInParens(css_media_feature_in_parens) => { + let feature_name = get_feature_name(css_media_feature_in_parens.feature().ok()?)?; + if is_media_feature_name(&feature_name) { + continue; + } + return Some(true); + } + AnyCssMediaInParens::CssMediaConditionInParens(css_media_condition_in_parens) => { + match css_media_condition_in_parens.condition().ok()? { + AnyCssMediaCondition::AnyCssMediaInParens(any_css_media_in_parens) => { + any_css_media_in_parens_stack.push(any_css_media_in_parens); + } + AnyCssMediaCondition::CssMediaAndCondition(css_media_and_condition) => { + any_css_media_in_parens_stack.push(css_media_and_condition.left().ok()?); + let mut css_media_and_condition_stack = + vec![css_media_and_condition.right().ok()?]; + while !css_media_and_condition_stack.is_empty() { + let element = css_media_and_condition_stack.pop()?; + match element { + AnyCssMediaAndCombinableCondition::AnyCssMediaInParens( + any_css_media_in_parens, + ) => { + any_css_media_in_parens_stack.push(any_css_media_in_parens); + } + AnyCssMediaAndCombinableCondition::CssMediaAndCondition( + css_media_and_condition, + ) => { + any_css_media_in_parens_stack + .push(css_media_and_condition.left().ok()?); + css_media_and_condition_stack + .push(css_media_and_condition.right().ok()?); + } + } + } + } + AnyCssMediaCondition::CssMediaOrCondition(css_media_or_condition) => { + any_css_media_in_parens_stack.push(css_media_or_condition.left().ok()?); + let mut css_media_or_condition_stack = + vec![css_media_or_condition.right().ok()?]; + while !css_media_or_condition_stack.is_empty() { + let element = css_media_or_condition_stack.pop()?; + match element { + AnyCssMediaOrCombinableCondition::AnyCssMediaInParens( + any_css_media_in_parens, + ) => { + any_css_media_in_parens_stack.push(any_css_media_in_parens); + } + AnyCssMediaOrCombinableCondition::CssMediaOrCondition( + css_media_or_condition, + ) => { + any_css_media_in_parens_stack + .push(css_media_or_condition.left().ok()?); + css_media_or_condition_stack + .push(css_media_or_condition.right().ok()?); + } + } + } + } + AnyCssMediaCondition::CssMediaNotCondition(css_media_not_condition) => { + any_css_media_in_parens_stack + .push(css_media_not_condition.condition().ok()?); + } + } + } + } + } + Some(false) +} + +fn get_feature_name(any_css_query_feature: AnyCssQueryFeature) -> Option { + let value_token = match any_css_query_feature { + AnyCssQueryFeature::CssQueryFeaturePlain(css_query_feature_plain) => { + css_query_feature_plain.name().ok()?.value_token() + } + AnyCssQueryFeature::CssQueryFeatureRange(css_query_feature_range) => { + css_query_feature_range.left().ok()?.value_token() + } + AnyCssQueryFeature::CssQueryFeatureReverseRange(css_query_feature_reversed_range) => { + css_query_feature_reversed_range.right().ok()?.value_token() + } + AnyCssQueryFeature::CssQueryFeatureRangeInterval(css_query_feature_range_interval) => { + css_query_feature_range_interval.name().ok()?.value_token() + } + AnyCssQueryFeature::CssQueryFeatureBoolean(css_query_feature_boolean) => { + css_query_feature_boolean.name().ok()?.value_token() + } + }; + Some(value_token.ok()?.text().to_string().trim().to_string()) +} diff --git a/crates/biome_css_analyze/src/options.rs b/crates/biome_css_analyze/src/options.rs index 738bdb11c0d0..5f1ef8f119c2 100644 --- a/crates/biome_css_analyze/src/options.rs +++ b/crates/biome_css_analyze/src/options.rs @@ -14,6 +14,7 @@ pub type NoImportantInKeyframe = < lint :: nursery :: no_important_in_keyframe : pub type NoInvalidPositionAtImportRule = < lint :: nursery :: no_invalid_position_at_import_rule :: NoInvalidPositionAtImportRule as biome_analyze :: Rule > :: Options ; pub type NoUnknownFunction = ::Options; +pub type NoUnknownMediaFeatureName = < lint :: nursery :: no_unknown_media_feature_name :: NoUnknownMediaFeatureName as biome_analyze :: Rule > :: Options ; pub type NoUnknownProperty = ::Options; pub type NoUnknownSelectorPseudoElement = < lint :: nursery :: no_unknown_selector_pseudo_element :: NoUnknownSelectorPseudoElement as biome_analyze :: Rule > :: Options ; diff --git a/crates/biome_css_analyze/src/utils.rs b/crates/biome_css_analyze/src/utils.rs index 9725176f44a2..77c26358b956 100644 --- a/crates/biome_css_analyze/src/utils.rs +++ b/crates/biome_css_analyze/src/utils.rs @@ -3,10 +3,10 @@ use crate::keywords::{ FONT_STYLE_KEYWORDS, FONT_VARIANTS_KEYWORDS, FONT_WEIGHT_ABSOLUTE_KEYWORDS, FONT_WEIGHT_NUMERIC_KEYWORDS, FUNCTION_KEYWORDS, KNOWN_CHROME_PROPERTIES, KNOWN_EDGE_PROPERTIES, KNOWN_EXPLORER_PROPERTIES, KNOWN_FIREFOX_PROPERTIES, KNOWN_PROPERTIES, - KNOWN_SAFARI_PROPERTIES, KNOWN_SUMSUNG_INTERNET_PROPERTIES, KNOWN_US_BROWSER_PROPERTIES, - LEVEL_ONE_AND_TWO_PSEUDO_ELEMENTS, LINE_HEIGHT_KEYWORDS, OTHER_PSEUDO_ELEMENTS, - SHADOW_TREE_PSEUDO_ELEMENTS, SYSTEM_FAMILY_NAME_KEYWORDS, VENDER_PREFIXES, - VENDOR_SPECIFIC_PSEUDO_ELEMENTS, + KNOWN_SAFARI_PROPERTIES, KNOWN_SAMSUNG_INTERNET_PROPERTIES, KNOWN_US_BROWSER_PROPERTIES, + LEVEL_ONE_AND_TWO_PSEUDO_ELEMENTS, LINE_HEIGHT_KEYWORDS, MEDIA_FEATURE_NAMES, + OTHER_PSEUDO_ELEMENTS, SHADOW_TREE_PSEUDO_ELEMENTS, SYSTEM_FAMILY_NAME_KEYWORDS, + VENDOR_PREFIXES, VENDOR_SPECIFIC_PSEUDO_ELEMENTS, }; use biome_css_syntax::{AnyCssGenericComponentValue, AnyCssValue, CssGenericComponentValueList}; use biome_rowan::{AstNode, SyntaxNodeCast}; @@ -116,7 +116,7 @@ pub fn is_custom_function(value: &str) -> bool { // Returns the vendor prefix extracted from an input string. pub fn vender_prefix(prop: &str) -> String { - for prefix in VENDER_PREFIXES.iter() { + for prefix in VENDOR_PREFIXES.iter() { if prop.starts_with(prefix) { return (*prefix).to_string(); } @@ -138,7 +138,7 @@ pub fn is_known_properties(prop: &str) -> bool { || KNOWN_EXPLORER_PROPERTIES.binary_search(&prop).is_ok() || KNOWN_FIREFOX_PROPERTIES.binary_search(&prop).is_ok() || KNOWN_SAFARI_PROPERTIES.binary_search(&prop).is_ok() - || KNOWN_SUMSUNG_INTERNET_PROPERTIES + || KNOWN_SAMSUNG_INTERNET_PROPERTIES .binary_search(&prop) .is_ok() || KNOWN_US_BROWSER_PROPERTIES.binary_search(&prop).is_ok() @@ -150,3 +150,27 @@ pub fn vendor_prefixed(props: &str) -> bool { || props.starts_with("-ms-") || props.starts_with("-o-") } + +/// Check if the input string is a media feature name. +pub fn is_media_feature_name(prop: &str) -> bool { + let input = prop.to_lowercase(); + let count = MEDIA_FEATURE_NAMES.binary_search(&input.as_str()); + if count.is_ok() { + return true; + } + let mut has_vendor_prefix = false; + for prefix in VENDOR_PREFIXES.iter() { + if input.starts_with(prefix) { + has_vendor_prefix = true; + break; + } + } + if has_vendor_prefix { + for feature_name in MEDIA_FEATURE_NAMES.iter() { + if input.ends_with(feature_name) { + return true; + } + } + } + false +} diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnknownMediaFeatureName/invalid.css b/crates/biome_css_analyze/tests/specs/nursery/noUnknownMediaFeatureName/invalid.css new file mode 100644 index 000000000000..48f6b6ec9d98 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noUnknownMediaFeatureName/invalid.css @@ -0,0 +1,17 @@ +@media screen and (unknown) { +} + +@media screen, (unknown: 10px) { +} + +@media screen and (unknown > 10px) { +} + +@media only screen and (min-width: 320px) and (max-width: 480px) and (unknown: 150dpi) { +} + +@media (not(unknown < 320px)) and (max-width > 640px) { +} + +@media (400px <= unknown <= 700px) { +} diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnknownMediaFeatureName/invalid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noUnknownMediaFeatureName/invalid.css.snap new file mode 100644 index 000000000000..0fd5c20ef3f6 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noUnknownMediaFeatureName/invalid.css.snap @@ -0,0 +1,138 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: invalid.css +--- +# Input +```css +@media screen and (unknown) { +} + +@media screen, (unknown: 10px) { +} + +@media screen and (unknown > 10px) { +} + +@media only screen and (min-width: 320px) and (max-width: 480px) and (unknown: 150dpi) { +} + +@media (not(unknown < 320px)) and (max-width > 640px) { +} + +@media (400px <= unknown <= 700px) { +} + +``` + +# Diagnostics +``` +invalid.css:1:8 lint/nursery/noUnknownMediaFeatureName ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't use unknown media feature names. + + > 1 │ @media screen and (unknown) { + │ ^^^^^^^^^^^^^^^^^^^^ + 2 │ } + 3 │ + + i Unexpected unknown media feature name. + + i You should use media feature names defined in the CSS Specifications. + + +``` + +``` +invalid.css:4:8 lint/nursery/noUnknownMediaFeatureName ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't use unknown media feature names. + + 2 │ } + 3 │ + > 4 │ @media screen, (unknown: 10px) { + │ ^^^^^^^^^^^^^^^^^^^^^^^ + 5 │ } + 6 │ + + i Unexpected unknown media feature name. + + i You should use media feature names defined in the CSS Specifications. + + +``` + +``` +invalid.css:7:8 lint/nursery/noUnknownMediaFeatureName ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't use unknown media feature names. + + 5 │ } + 6 │ + > 7 │ @media screen and (unknown > 10px) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 8 │ } + 9 │ + + i Unexpected unknown media feature name. + + i You should use media feature names defined in the CSS Specifications. + + +``` + +``` +invalid.css:10:8 lint/nursery/noUnknownMediaFeatureName ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't use unknown media feature names. + + 8 │ } + 9 │ + > 10 │ @media only screen and (min-width: 320px) and (max-width: 480px) and (unknown: 150dpi) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 11 │ } + 12 │ + + i Unexpected unknown media feature name. + + i You should use media feature names defined in the CSS Specifications. + + +``` + +``` +invalid.css:13:8 lint/nursery/noUnknownMediaFeatureName ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't use unknown media feature names. + + 11 │ } + 12 │ + > 13 │ @media (not(unknown < 320px)) and (max-width > 640px) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 14 │ } + 15 │ + + i Unexpected unknown media feature name. + + i You should use media feature names defined in the CSS Specifications. + + +``` + +``` +invalid.css:16:8 lint/nursery/noUnknownMediaFeatureName ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Don't use unknown media feature names. + + 14 │ } + 15 │ + > 16 │ @media (400px <= unknown <= 700px) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 17 │ } + 18 │ + + i Unexpected unknown media feature name. + + i You should use media feature names defined in the CSS Specifications. + + +``` diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnknownMediaFeatureName/valid.css b/crates/biome_css_analyze/tests/specs/nursery/noUnknownMediaFeatureName/valid.css new file mode 100644 index 000000000000..a565d4bf3da3 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noUnknownMediaFeatureName/valid.css @@ -0,0 +1,20 @@ +@media print { +} + +@media screen, print { +} + +@media screen and (min-width: 320px) { +} + +@media screen and (-webkit-min-width: 320px) { +} + +@media only screen and (min-width: 320px) and (max-width: 480px) and (resolution: 150dpi) { +} + +@media (not(min-width < 320px)) and (max-width > 640px) { +} + +@media (400px <= width <= 700px) { +} diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnknownMediaFeatureName/valid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noUnknownMediaFeatureName/valid.css.snap new file mode 100644 index 000000000000..af7d5f158916 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noUnknownMediaFeatureName/valid.css.snap @@ -0,0 +1,28 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: valid.css +--- +# Input +```css +@media print { +} + +@media screen, print { +} + +@media screen and (min-width: 320px) { +} + +@media screen and (-webkit-min-width: 320px) { +} + +@media only screen and (min-width: 320px) and (max-width: 480px) and (resolution: 150dpi) { +} + +@media (not(min-width < 320px)) and (max-width > 640px) { +} + +@media (400px <= width <= 700px) { +} + +``` diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 26ea3da79523..6139445f60ef 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -132,6 +132,7 @@ define_categories! { "lint/nursery/noTypeOnlyImportAttributes": "https://biomejs.dev/linter/rules/no-type-only-import-attributes", "lint/nursery/noUndeclaredDependencies": "https://biomejs.dev/linter/rules/no-undeclared-dependencies", "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", "lint/nursery/noUnknownSelectorPseudoElement": "https://biomejs.dev/linter/rules/no-unknown-selector-pseudo-element", "lint/nursery/noUnknownUnit": "https://biomejs.dev/linter/rules/no-unknown-unit", diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index e8bfe7f5d2b5..e901995e152e 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -988,6 +988,10 @@ export interface Nursery { * Disallow unknown CSS value functions. */ noUnknownFunction?: RuleConfiguration_for_Null; + /** + * Disallow unknown media feature names. + */ + noUnknownMediaFeatureName?: RuleConfiguration_for_Null; /** * Disallow unknown properties. */ @@ -2035,6 +2039,7 @@ export type Category = | "lint/nursery/noTypeOnlyImportAttributes" | "lint/nursery/noUndeclaredDependencies" | "lint/nursery/noUnknownFunction" + | "lint/nursery/noUnknownMediaFeatureName" | "lint/nursery/noUnknownProperty" | "lint/nursery/noUnknownSelectorPseudoElement" | "lint/nursery/noUnknownUnit" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 29f1592f58f1..5c7ddb32e9b2 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1573,6 +1573,13 @@ { "type": "null" } ] }, + "noUnknownMediaFeatureName": { + "description": "Disallow unknown media feature names.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noUnknownProperty": { "description": "Disallow unknown properties.", "anyOf": [