diff --git a/crates/biome_configuration/src/linter/rules.rs b/crates/biome_configuration/src/linter/rules.rs index bf0ce4e26aea..ba24f89b516b 100644 --- a/crates/biome_configuration/src/linter/rules.rs +++ b/crates/biome_configuration/src/linter/rules.rs @@ -2681,6 +2681,9 @@ pub struct Nursery { #[doc = "Disallow to use unnecessary callback on flatMap."] #[serde(skip_serializing_if = "Option::is_none")] pub no_flat_map_identity: Option>, + #[doc = "Disallow invalid !important within keyframe declarations"] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_important_in_keyframe: Option>, #[doc = "Checks that the assertion function, for example expect, is placed inside an it() function call."] #[serde(skip_serializing_if = "Option::is_none")] pub no_misplaced_assertion: Option>, @@ -2731,6 +2734,7 @@ impl Nursery { "noDuplicateSelectorsKeyframeBlock", "noEvolvingAny", "noFlatMapIdentity", + "noImportantInKeyframe", "noMisplacedAssertion", "noNodejsModules", "noReactSpecificProps", @@ -2748,6 +2752,7 @@ impl Nursery { "noDuplicateSelectorsKeyframeBlock", "noEvolvingAny", "noFlatMapIdentity", + "noImportantInKeyframe", ]; const RECOMMENDED_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3]), @@ -2758,6 +2763,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -2778,6 +2784,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -2849,41 +2856,46 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } } - if let Some(rule) = self.no_misplaced_assertion.as_ref() { + if let Some(rule) = self.no_important_in_keyframe.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.no_nodejs_modules.as_ref() { + if let Some(rule) = self.no_misplaced_assertion.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - if let Some(rule) = self.no_react_specific_props.as_ref() { + if let Some(rule) = self.no_nodejs_modules.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } } - if let Some(rule) = self.no_restricted_imports.as_ref() { + if let Some(rule) = self.no_react_specific_props.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - if let Some(rule) = self.no_undeclared_dependencies.as_ref() { + if let Some(rule) = self.no_restricted_imports.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.no_undeclared_dependencies.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - 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[17])); } } + 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[18])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -2943,41 +2955,46 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } } - if let Some(rule) = self.no_misplaced_assertion.as_ref() { + if let Some(rule) = self.no_important_in_keyframe.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.no_nodejs_modules.as_ref() { + if let Some(rule) = self.no_misplaced_assertion.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - if let Some(rule) = self.no_react_specific_props.as_ref() { + if let Some(rule) = self.no_nodejs_modules.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } } - if let Some(rule) = self.no_restricted_imports.as_ref() { + if let Some(rule) = self.no_react_specific_props.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - if let Some(rule) = self.no_undeclared_dependencies.as_ref() { + if let Some(rule) = self.no_restricted_imports.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.no_undeclared_dependencies.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - 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[17])); } } + 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[18])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3058,6 +3075,10 @@ impl Nursery { .no_flat_map_identity .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noImportantInKeyframe" => self + .no_important_in_keyframe + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noMisplacedAssertion" => self .no_misplaced_assertion .as_ref() diff --git a/crates/biome_css_analyze/src/lint/nursery.rs b/crates/biome_css_analyze/src/lint/nursery.rs index febf115bfbb4..b3bb74236122 100644 --- a/crates/biome_css_analyze/src/lint/nursery.rs +++ b/crates/biome_css_analyze/src/lint/nursery.rs @@ -6,6 +6,7 @@ pub mod no_color_invalid_hex; pub mod no_css_empty_block; pub mod no_duplicate_font_names; pub mod no_duplicate_selectors_keyframe_block; +pub mod no_important_in_keyframe; declare_group! { pub Nursery { @@ -15,6 +16,7 @@ declare_group! { self :: no_css_empty_block :: NoCssEmptyBlock , self :: no_duplicate_font_names :: NoDuplicateFontNames , self :: no_duplicate_selectors_keyframe_block :: NoDuplicateSelectorsKeyframeBlock , + self :: no_important_in_keyframe :: NoImportantInKeyframe , ] } } diff --git a/crates/biome_css_analyze/src/lint/nursery/no_important_in_keyframe.rs b/crates/biome_css_analyze/src/lint/nursery/no_important_in_keyframe.rs new file mode 100644 index 000000000000..c061e146f0c9 --- /dev/null +++ b/crates/biome_css_analyze/src/lint/nursery/no_important_in_keyframe.rs @@ -0,0 +1,90 @@ +use biome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic, RuleSource}; +use biome_console::markup; +use biome_css_syntax::{ + AnyCssDeclarationBlock, AnyCssKeyframesItem, CssDeclarationImportant, CssKeyframesBlock, +}; +use biome_rowan::AstNode; + +declare_rule! { + /// Disallow invalid `!important` within keyframe declarations + /// + /// Using `!important` within keyframes declarations is completely ignored in some browsers. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```css,expect_diagnostic + /// @keyframes foo { + /// from { + /// opacity: 0; + /// } + /// to { + /// opacity: 1 !important; + /// } + /// } + /// ``` + /// + /// ### Valid + /// + /// ```css + /// @keyframes foo { + /// from { + /// opacity: 0; + /// } + /// to { + /// opacity: 1; + /// } + /// } + /// ``` + /// + pub NoImportantInKeyframe { + version: "next", + name: "noImportantInKeyframe", + recommended: true, + sources:&[RuleSource::Stylelint("keyframe-declaration-no-important")], + } +} + +impl Rule for NoImportantInKeyframe { + type Query = Ast; + type State = CssDeclarationImportant; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Option { + let node = ctx.query(); + for item in node.items() { + let AnyCssKeyframesItem::CssKeyframesItem(keyframe_item) = item else { + return None; + }; + let AnyCssDeclarationBlock::CssDeclarationBlock(block_declaration) = + keyframe_item.block().ok()? + else { + return None; + }; + for colon_declaration in block_declaration.declarations() { + if let Some(important) = colon_declaration.declaration().ok()?.important() { + return Some(important); + } + } + } + None + } + + fn diagnostic(_ctx: &RuleContext, node: &Self::State) -> Option { + let span = node.range(); + Some( + RuleDiagnostic::new( + rule_category!(), + span, + markup! { + "Using ""!important"" within keyframes declaration is completely ignored in some browsers." + }, + ) + .note(markup! { + "Consider removing useless ""!important"" declaration." + }), + ) + } +} diff --git a/crates/biome_css_analyze/src/options.rs b/crates/biome_css_analyze/src/options.rs index 8f7bc969c7c1..0b8e9e100af8 100644 --- a/crates/biome_css_analyze/src/options.rs +++ b/crates/biome_css_analyze/src/options.rs @@ -9,3 +9,4 @@ pub type NoCssEmptyBlock = pub type NoDuplicateFontNames = ::Options; pub type NoDuplicateSelectorsKeyframeBlock = < lint :: nursery :: no_duplicate_selectors_keyframe_block :: NoDuplicateSelectorsKeyframeBlock as biome_analyze :: Rule > :: Options ; +pub type NoImportantInKeyframe = < lint :: nursery :: no_important_in_keyframe :: NoImportantInKeyframe as biome_analyze :: Rule > :: Options ; diff --git a/crates/biome_css_analyze/tests/specs/nursery/noImportantInKeyframe/invalid.css b/crates/biome_css_analyze/tests/specs/nursery/noImportantInKeyframe/invalid.css new file mode 100644 index 000000000000..ba880051986b --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noImportantInKeyframe/invalid.css @@ -0,0 +1,8 @@ +@keyframes foo { + from { + opacity: 0; + } + to { + opacity: 1 !important; + } +} \ No newline at end of file diff --git a/crates/biome_css_analyze/tests/specs/nursery/noImportantInKeyframe/invalid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noImportantInKeyframe/invalid.css.snap new file mode 100644 index 000000000000..5bc8fb0a0517 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noImportantInKeyframe/invalid.css.snap @@ -0,0 +1,33 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: invalid.css +--- +# Input +```css +@keyframes foo { + from { + opacity: 0; + } + to { + opacity: 1 !important; + } +} +``` + +# Diagnostics +``` +invalid.css:6:16 lint/nursery/noImportantInKeyframe ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Using !important within keyframes declaration is completely ignored in some browsers. + + 4 │ } + 5 │ to { + > 6 │ opacity: 1 !important; + │ ^^^^^^^^^^ + 7 │ } + 8 │ } + + i Consider removing useless !important declaration. + + +``` diff --git a/crates/biome_css_analyze/tests/specs/nursery/noImportantInKeyframe/valid.css b/crates/biome_css_analyze/tests/specs/nursery/noImportantInKeyframe/valid.css new file mode 100644 index 000000000000..0bc149a0789f --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noImportantInKeyframe/valid.css @@ -0,0 +1,10 @@ +@keyframes foo { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +a { color: pink !important; } \ No newline at end of file diff --git a/crates/biome_css_analyze/tests/specs/nursery/noImportantInKeyframe/valid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noImportantInKeyframe/valid.css.snap new file mode 100644 index 000000000000..e0328ae1aa07 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noImportantInKeyframe/valid.css.snap @@ -0,0 +1,17 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: valid.css +--- +# Input +```css +@keyframes foo { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +a { color: pink !important; } +``` diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index ce22b36987a8..3a19914772fc 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -121,6 +121,7 @@ define_categories! { "lint/nursery/noDuplicateSelectorsKeyframeBlock": "https://biomejs.dev/linter/rules/no-duplicate-selectors-keyframe-block", "lint/nursery/noEvolvingAny": "https://biomejs.dev/linter/rules/no-evolving-any", "lint/nursery/noFlatMapIdentity": "https://biomejs.dev/linter/rules/no-flat-map-identity", + "lint/nursery/noImportantInKeyframe": "https://biomejs.dev/linter/rules/no-important-in-keyframe", "lint/nursery/noMisplacedAssertion": "https://biomejs.dev/linter/rules/no-misplaced-assertion", "lint/nursery/noNodejsModules": "https://biomejs.dev/linter/rules/no-nodejs-modules", "lint/nursery/noReactSpecificProps": "https://biomejs.dev/linter/rules/no-react-specific-props", diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 00e25c38cb25..64e80b479d75 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -952,6 +952,10 @@ export interface Nursery { * Disallow to use unnecessary callback on flatMap. */ noFlatMapIdentity?: RuleConfiguration_for_Null; + /** + * Disallow invalid !important within keyframe declarations + */ + noImportantInKeyframe?: RuleConfiguration_for_Null; /** * Checks that the assertion function, for example expect, is placed inside an it() function call. */ @@ -1955,6 +1959,7 @@ export type Category = | "lint/nursery/noDuplicateSelectorsKeyframeBlock" | "lint/nursery/noEvolvingAny" | "lint/nursery/noFlatMapIdentity" + | "lint/nursery/noImportantInKeyframe" | "lint/nursery/noMisplacedAssertion" | "lint/nursery/noNodejsModules" | "lint/nursery/noReactSpecificProps" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 8179aed4c82a..0de94326fae3 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1510,6 +1510,13 @@ { "type": "null" } ] }, + "noImportantInKeyframe": { + "description": "Disallow invalid !important within keyframe declarations", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noMisplacedAssertion": { "description": "Checks that the assertion function, for example expect, is placed inside an it() function call.", "anyOf": [