diff --git a/crates/biome_configuration/src/linter/rules.rs b/crates/biome_configuration/src/linter/rules.rs index b7d0a0076ae0..bf9a47a74a80 100644 --- a/crates/biome_configuration/src/linter/rules.rs +++ b/crates/biome_configuration/src/linter/rules.rs @@ -2662,6 +2662,9 @@ pub struct Nursery { #[doc = "Disallow using a callback in asynchronous tests and hooks."] #[serde(skip_serializing_if = "Option::is_none")] pub no_done_callback: Option>, + #[doc = "Disallow duplicate @import rules."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_duplicate_at_import_rules: Option>, #[doc = "Disallow duplicate conditions in if-else-if chains"] #[serde(skip_serializing_if = "Option::is_none")] pub no_duplicate_else_if: Option>, @@ -2751,6 +2754,7 @@ impl Nursery { "noConstantMathMinMaxClamp", "noCssEmptyBlock", "noDoneCallback", + "noDuplicateAtImportRules", "noDuplicateElseIf", "noDuplicateFontNames", "noDuplicateJsonKeys", @@ -2776,6 +2780,7 @@ impl Nursery { const RECOMMENDED_RULES: &'static [&'static str] = &[ "noCssEmptyBlock", "noDoneCallback", + "noDuplicateAtImportRules", "noDuplicateElseIf", "noDuplicateFontNames", "noDuplicateJsonKeys", @@ -2797,9 +2802,10 @@ impl Nursery { 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]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -2828,6 +2834,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -2869,111 +2876,116 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4])); } } - if let Some(rule) = self.no_duplicate_else_if.as_ref() { + if let Some(rule) = self.no_duplicate_at_import_rules.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5])); } } - if let Some(rule) = self.no_duplicate_font_names.as_ref() { + if let Some(rule) = self.no_duplicate_else_if.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6])); } } - if let Some(rule) = self.no_duplicate_json_keys.as_ref() { + if let Some(rule) = self.no_duplicate_font_names.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); } } - if let Some(rule) = self.no_duplicate_selectors_keyframe_block.as_ref() { + if let Some(rule) = self.no_duplicate_json_keys.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } } - if let Some(rule) = self.no_evolving_any.as_ref() { + if let Some(rule) = self.no_duplicate_selectors_keyframe_block.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } } - if let Some(rule) = self.no_flat_map_identity.as_ref() { + if let Some(rule) = self.no_evolving_any.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } } - if let Some(rule) = self.no_important_in_keyframe.as_ref() { + if let Some(rule) = self.no_flat_map_identity.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - 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[12])); } } - 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[13])); } } - 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[14])); } } - 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[15])); } } - 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[16])); } } - if let Some(rule) = self.no_unknown_function.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[17])); } } - if let Some(rule) = self.no_unknown_unit.as_ref() { + if let Some(rule) = self.no_unknown_function.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.no_useless_undefined_initialization.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[19])); } } - 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[20])); } } - 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[21])); } } - 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[22])); } } - if let Some(rule) = self.use_generic_font_names.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[23])); } } - 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[24])); } } - 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[25])); } } + 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[26])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -3003,111 +3015,116 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4])); } } - if let Some(rule) = self.no_duplicate_else_if.as_ref() { + if let Some(rule) = self.no_duplicate_at_import_rules.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5])); } } - if let Some(rule) = self.no_duplicate_font_names.as_ref() { + if let Some(rule) = self.no_duplicate_else_if.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6])); } } - if let Some(rule) = self.no_duplicate_json_keys.as_ref() { + if let Some(rule) = self.no_duplicate_font_names.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); } } - if let Some(rule) = self.no_duplicate_selectors_keyframe_block.as_ref() { + if let Some(rule) = self.no_duplicate_json_keys.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } } - if let Some(rule) = self.no_evolving_any.as_ref() { + if let Some(rule) = self.no_duplicate_selectors_keyframe_block.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } } - if let Some(rule) = self.no_flat_map_identity.as_ref() { + if let Some(rule) = self.no_evolving_any.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } } - if let Some(rule) = self.no_important_in_keyframe.as_ref() { + if let Some(rule) = self.no_flat_map_identity.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - 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[12])); } } - 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[13])); } } - 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[14])); } } - 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[15])); } } - 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[16])); } } - if let Some(rule) = self.no_unknown_function.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[17])); } } - if let Some(rule) = self.no_unknown_unit.as_ref() { + if let Some(rule) = self.no_unknown_function.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.no_useless_undefined_initialization.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[19])); } } - 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[20])); } } - 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[21])); } } - 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[22])); } } - if let Some(rule) = self.use_generic_font_names.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[23])); } } - 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[24])); } } - 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[25])); } } + 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[26])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3164,6 +3181,10 @@ impl Nursery { .no_done_callback .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noDuplicateAtImportRules" => self + .no_duplicate_at_import_rules + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noDuplicateElseIf" => self .no_duplicate_else_if .as_ref() diff --git a/crates/biome_css_analyze/src/lint/nursery.rs b/crates/biome_css_analyze/src/lint/nursery.rs index 8e1e51340d73..5455fea8eba7 100644 --- a/crates/biome_css_analyze/src/lint/nursery.rs +++ b/crates/biome_css_analyze/src/lint/nursery.rs @@ -4,6 +4,7 @@ use biome_analyze::declare_group; pub mod no_color_invalid_hex; pub mod no_css_empty_block; +pub mod no_duplicate_at_import_rules; pub mod no_duplicate_font_names; pub mod no_duplicate_selectors_keyframe_block; pub mod no_important_in_keyframe; @@ -17,6 +18,7 @@ declare_group! { rules : [ self :: no_color_invalid_hex :: NoColorInvalidHex , self :: no_css_empty_block :: NoCssEmptyBlock , + self :: no_duplicate_at_import_rules :: NoDuplicateAtImportRules , 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_duplicate_at_import_rules.rs b/crates/biome_css_analyze/src/lint/nursery/no_duplicate_at_import_rules.rs new file mode 100644 index 000000000000..6440e77351b7 --- /dev/null +++ b/crates/biome_css_analyze/src/lint/nursery/no_duplicate_at_import_rules.rs @@ -0,0 +1,127 @@ +use std::collections::{HashMap, HashSet}; + +use biome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic, RuleSource}; +use biome_console::markup; +use biome_css_syntax::{AnyCssAtRule, AnyCssRule, CssImportAtRule, CssRuleList}; +use biome_rowan::AstNode; + +declare_rule! { + /// Disallow duplicate `@import` rules. + /// + /// This rule checks if the file urls of the @import rules are duplicates. + /// + /// This rule also checks the imported media queries and alerts of duplicates. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```css,expect_diagnostic + /// @import 'a.css'; + /// @import 'a.css'; + /// ``` + /// + /// ```css,expect_diagnostic + /// @import "a.css"; + /// @import 'a.css'; + /// ``` + /// + /// ```css,expect_diagnostic + /// @import url('a.css'); + /// @import url('a.css'); + /// ``` + /// + /// ### Valid + /// + /// ```css + /// @import 'a.css'; + /// @import 'b.css'; + /// ``` + /// + /// ```css + /// @import url('a.css') tv; + /// @import url('a.css') projection; + /// ``` + /// + pub NoDuplicateAtImportRules { + version: "next", + name: "noDuplicateAtImportRules", + recommended: true, + sources: &[RuleSource::Stylelint("no-duplicate-at-import-rules")], + } +} + +impl Rule for NoDuplicateAtImportRules { + type Query = Ast; + type State = CssImportAtRule; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Option { + let node = ctx.query(); + let mut import_url_map: HashMap> = HashMap::new(); + for rule in node { + match rule { + AnyCssRule::CssAtRule(item) => match item.rule().ok()? { + AnyCssAtRule::CssImportAtRule(import_rule) => { + let import_url = import_rule + .url() + .ok()? + .text() + .to_lowercase() + .replace("url(", "") + .replace(')', ""); + if let Some(media_query_set) = import_url_map.get_mut(&import_url) { + // if the current import_rule has no media queries or there are no queries saved in the + // media_query_set, this is always a duplicate + if import_rule.media().text().is_empty() || media_query_set.is_empty() { + return Some(import_rule); + } + + for media in import_rule.media() { + match media { + Ok(media) => { + if !media_query_set.insert(media.text().to_lowercase()) { + return Some(import_rule); + } + } + _ => return None, + } + } + } else { + let mut media_set: HashSet = HashSet::new(); + for media in import_rule.media() { + match media { + Ok(media) => { + media_set.insert(media.text().to_lowercase()); + } + _ => return None, + } + } + import_url_map.insert(import_url, media_set); + } + } + _ => return None, + }, + _ => return None, + } + } + None + } + + fn diagnostic(_: &RuleContext, node: &Self::State) -> Option { + let span = node.range(); + Some( + RuleDiagnostic::new( + rule_category!(), + span, + markup! { + "Each ""@import"" should be unique unless differing by media queries." + }, + ) + .note(markup! { + "Consider removing one of the duplicated imports." + }), + ) + } +} diff --git a/crates/biome_css_analyze/src/options.rs b/crates/biome_css_analyze/src/options.rs index a56a05c517f2..cacfd2fec994 100644 --- a/crates/biome_css_analyze/src/options.rs +++ b/crates/biome_css_analyze/src/options.rs @@ -6,6 +6,7 @@ pub type NoColorInvalidHex = ::Options; pub type NoCssEmptyBlock = ::Options; +pub type NoDuplicateAtImportRules = < lint :: nursery :: no_duplicate_at_import_rules :: NoDuplicateAtImportRules as biome_analyze :: Rule > :: Options ; pub type NoDuplicateFontNames = ::Options; pub type NoDuplicateSelectorsKeyframeBlock = < lint :: nursery :: no_duplicate_selectors_keyframe_block :: NoDuplicateSelectorsKeyframeBlock as biome_analyze :: Rule > :: Options ; diff --git a/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalid.css b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalid.css new file mode 100644 index 000000000000..73943ae070b0 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalid.css @@ -0,0 +1,3 @@ +@import "a.css"; +@import "b.css"; +@import "a.css"; diff --git a/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalid.css.snap new file mode 100644 index 000000000000..dd5d39c3353e --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalid.css.snap @@ -0,0 +1,28 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: invalid.css +--- +# Input +```css +@import "a.css"; +@import "b.css"; +@import "a.css"; + +``` + +# Diagnostics +``` +invalid.css:3:2 lint/nursery/noDuplicateAtImportRules ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Each @import should be unique unless differing by media queries. + + 1 │ @import "a.css"; + 2 │ @import "b.css"; + > 3 │ @import "a.css"; + │ ^^^^^^^^^^^^^^^ + 4 │ + + i Consider removing one of the duplicated imports. + + +``` diff --git a/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalidMedia.css b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalidMedia.css new file mode 100644 index 000000000000..b5df7f354a2a --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalidMedia.css @@ -0,0 +1,3 @@ +@import url("a.css") tv; +@import url("a.css") projection; +@import "a.css"; diff --git a/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalidMedia.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalidMedia.css.snap new file mode 100644 index 000000000000..cbb4170fd51d --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalidMedia.css.snap @@ -0,0 +1,28 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: invalidMedia.css +--- +# Input +```css +@import url("a.css") tv; +@import url("a.css") projection; +@import "a.css"; + +``` + +# Diagnostics +``` +invalidMedia.css:3:2 lint/nursery/noDuplicateAtImportRules ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Each @import should be unique unless differing by media queries. + + 1 │ @import url("a.css") tv; + 2 │ @import url("a.css") projection; + > 3 │ @import "a.css"; + │ ^^^^^^^^^^^^^^^ + 4 │ + + i Consider removing one of the duplicated imports. + + +``` diff --git a/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalidMultipleMedia.css b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalidMultipleMedia.css new file mode 100644 index 000000000000..83c5d2d721ce --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalidMultipleMedia.css @@ -0,0 +1,3 @@ +@import url("a.css") tv, projection; +@import url("a.css") mobile; +@import url("a.css") tv; diff --git a/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalidMultipleMedia.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalidMultipleMedia.css.snap new file mode 100644 index 000000000000..5a95a2b76d96 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalidMultipleMedia.css.snap @@ -0,0 +1,28 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: invalidMultipleMedia.css +--- +# Input +```css +@import url("a.css") tv, projection; +@import url("a.css") mobile; +@import url("a.css") tv; + +``` + +# Diagnostics +``` +invalidMultipleMedia.css:3:2 lint/nursery/noDuplicateAtImportRules ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Each @import should be unique unless differing by media queries. + + 1 │ @import url("a.css") tv, projection; + 2 │ @import url("a.css") mobile; + > 3 │ @import url("a.css") tv; + │ ^^^^^^^^^^^^^^^^^^^^^^^ + 4 │ + + i Consider removing one of the duplicated imports. + + +``` diff --git a/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalidUrls.css b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalidUrls.css new file mode 100644 index 000000000000..0b00168d4cdf --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalidUrls.css @@ -0,0 +1,2 @@ +@import url("c.css"); +@import url("c.css"); diff --git a/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalidUrls.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalidUrls.css.snap new file mode 100644 index 000000000000..b74f5e25a41b --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/invalidUrls.css.snap @@ -0,0 +1,26 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: invalidUrls.css +--- +# Input +```css +@import url("c.css"); +@import url("c.css"); + +``` + +# Diagnostics +``` +invalidUrls.css:2:2 lint/nursery/noDuplicateAtImportRules ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Each @import should be unique unless differing by media queries. + + 1 │ @import url("c.css"); + > 2 │ @import url("c.css"); + │ ^^^^^^^^^^^^^^^^^^^^ + 3 │ + + i Consider removing one of the duplicated imports. + + +``` diff --git a/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/valid.css b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/valid.css new file mode 100644 index 000000000000..c1b89db6e4c9 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/valid.css @@ -0,0 +1,9 @@ +/* should not generate diagnostics */ +@import "a.css"; +@import "b.css"; + +@import url("c.css"); +@import url("d.css"); + +@import url("e.css") tv; +@import url("e.css") projection; diff --git a/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/valid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/valid.css.snap new file mode 100644 index 000000000000..46ad9f4898c5 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noDuplicateAtImportRules/valid.css.snap @@ -0,0 +1,17 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: valid.css +--- +# Input +```css +/* should not generate diagnostics */ +@import "a.css"; +@import "b.css"; + +@import url("c.css"); +@import url("d.css"); + +@import url("e.css") tv; +@import url("e.css") projection; + +``` diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 4328b399bff4..00036e8365e4 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -110,12 +110,12 @@ define_categories! { "lint/correctness/useValidForDirection": "https://biomejs.dev/linter/rules/use-valid-for-direction", "lint/correctness/useYield": "https://biomejs.dev/linter/rules/use-yield", "lint/nursery/colorNoInvalidHex": "https://biomejs.dev/linter/rules/color-no-invalid-hex", - "lint/nursery/useArrayLiterals": "https://biomejs.dev/linter/rules/use-array-literals", "lint/nursery/noColorInvalidHex": "https://biomejs.dev/linter/rules/no-color-invalid-hex", "lint/nursery/noConsole": "https://biomejs.dev/linter/rules/no-console", "lint/nursery/noConstantMathMinMaxClamp": "https://biomejs.dev/linter/rules/no-constant-math-min-max-clamp", "lint/nursery/noCssEmptyBlock": "https://biomejs.dev/linter/rules/no-css-empty-block", "lint/nursery/noDoneCallback": "https://biomejs.dev/linter/rules/no-done-callback", + "lint/nursery/noDuplicateAtImportRules": "https://biomejs.dev/linter/rules/no-duplicate-at-import-rules", "lint/nursery/noDuplicateElseIf": "https://biomejs.dev/linter/rules/no-duplicate-else-if", "lint/nursery/noDuplicateFontNames": "https://biomejs.dev/linter/rules/no-font-family-duplicate-names", "lint/nursery/noDuplicateJsonKeys": "https://biomejs.dev/linter/rules/no-duplicate-json-keys", @@ -131,12 +131,13 @@ 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/noUselessUndefinedInitialization": "https://biomejs.dev/linter/rules/no-useless-undefined-initialization", "lint/nursery/noUnknownUnit": "https://biomejs.dev/linter/rules/no-unknown-unit", + "lint/nursery/noUselessUndefinedInitialization": "https://biomejs.dev/linter/rules/no-useless-undefined-initialization", + "lint/nursery/useArrayLiterals": "https://biomejs.dev/linter/rules/use-array-literals", "lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment", "lint/nursery/useConsistentBuiltinInstantiation": "https://biomejs.dev/linter/rules/use-consistent-new-builtin", - "lint/nursery/useGenericFontNames": "https://biomejs.dev/linter/rules/use-generic-font-names", "lint/nursery/useDefaultSwitchClause": "https://biomejs.dev/linter/rules/use-default-switch-clause", + "lint/nursery/useGenericFontNames": "https://biomejs.dev/linter/rules/use-generic-font-names", "lint/nursery/useImportRestrictions": "https://biomejs.dev/linter/rules/use-import-restrictions", "lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes", "lint/performance/noAccumulatingSpread": "https://biomejs.dev/linter/rules/no-accumulating-spread", diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 67bfc2b0def4..567161017c31 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -928,6 +928,10 @@ export interface Nursery { * Disallow using a callback in asynchronous tests and hooks. */ noDoneCallback?: RuleConfiguration_for_Null; + /** + * Disallow duplicate @import rules. + */ + noDuplicateAtImportRules?: RuleConfiguration_for_Null; /** * Disallow duplicate conditions in if-else-if chains */ @@ -1981,12 +1985,12 @@ export type Category = | "lint/correctness/useValidForDirection" | "lint/correctness/useYield" | "lint/nursery/colorNoInvalidHex" - | "lint/nursery/useArrayLiterals" | "lint/nursery/noColorInvalidHex" | "lint/nursery/noConsole" | "lint/nursery/noConstantMathMinMaxClamp" | "lint/nursery/noCssEmptyBlock" | "lint/nursery/noDoneCallback" + | "lint/nursery/noDuplicateAtImportRules" | "lint/nursery/noDuplicateElseIf" | "lint/nursery/noDuplicateFontNames" | "lint/nursery/noDuplicateJsonKeys" @@ -2002,12 +2006,13 @@ export type Category = | "lint/nursery/noTypeOnlyImportAttributes" | "lint/nursery/noUndeclaredDependencies" | "lint/nursery/noUnknownFunction" - | "lint/nursery/noUselessUndefinedInitialization" | "lint/nursery/noUnknownUnit" + | "lint/nursery/noUselessUndefinedInitialization" + | "lint/nursery/useArrayLiterals" | "lint/nursery/useBiomeSuppressionComment" | "lint/nursery/useConsistentBuiltinInstantiation" - | "lint/nursery/useGenericFontNames" | "lint/nursery/useDefaultSwitchClause" + | "lint/nursery/useGenericFontNames" | "lint/nursery/useImportRestrictions" | "lint/nursery/useSortedClasses" | "lint/performance/noAccumulatingSpread" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index e9c85634ec87..0ce6cebde88f 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1468,6 +1468,13 @@ { "type": "null" } ] }, + "noDuplicateAtImportRules": { + "description": "Disallow duplicate @import rules.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noDuplicateElseIf": { "description": "Disallow duplicate conditions in if-else-if chains", "anyOf": [