Skip to content

Commit

Permalink
feat(biome_css_analyzer): implement noDuplicateAtImportRules (#2658)
Browse files Browse the repository at this point in the history
  • Loading branch information
DerTimonius committed May 2, 2024
1 parent 7aed8d9 commit 150dd0e
Show file tree
Hide file tree
Showing 17 changed files with 361 additions and 50 deletions.
109 changes: 65 additions & 44 deletions crates/biome_configuration/src/linter/rules.rs

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions crates/biome_css_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 ,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CssRuleList>;
type State = CssImportAtRule;
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
let node = ctx.query();
let mut import_url_map: HashMap<String, HashSet<String>> = 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<String> = 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<Self>, node: &Self::State) -> Option<RuleDiagnostic> {
let span = node.range();
Some(
RuleDiagnostic::new(
rule_category!(),
span,
markup! {
"Each "<Emphasis>"@import"</Emphasis>" should be unique unless differing by media queries."
},
)
.note(markup! {
"Consider removing one of the duplicated imports."
}),
)
}
}
1 change: 1 addition & 0 deletions crates/biome_css_analyze/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub type NoColorInvalidHex =
<lint::nursery::no_color_invalid_hex::NoColorInvalidHex as biome_analyze::Rule>::Options;
pub type NoCssEmptyBlock =
<lint::nursery::no_css_empty_block::NoCssEmptyBlock as biome_analyze::Rule>::Options;
pub type NoDuplicateAtImportRules = < lint :: nursery :: no_duplicate_at_import_rules :: NoDuplicateAtImportRules as biome_analyze :: Rule > :: Options ;
pub type NoDuplicateFontNames =
<lint::nursery::no_duplicate_font_names::NoDuplicateFontNames as biome_analyze::Rule>::Options;
pub type NoDuplicateSelectorsKeyframeBlock = < lint :: nursery :: no_duplicate_selectors_keyframe_block :: NoDuplicateSelectorsKeyframeBlock as biome_analyze :: Rule > :: Options ;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@import "a.css";
@import "b.css";
@import "a.css";
Original file line number Diff line number Diff line change
@@ -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.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@import url("a.css") tv;
@import url("a.css") projection;
@import "a.css";
Original file line number Diff line number Diff line change
@@ -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.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@import url("a.css") tv, projection;
@import url("a.css") mobile;
@import url("a.css") tv;
Original file line number Diff line number Diff line change
@@ -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.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@import url("c.css");
@import url("c.css");
Original file line number Diff line number Diff line change
@@ -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.
```
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
```
7 changes: 4 additions & 3 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
11 changes: 8 additions & 3 deletions packages/@biomejs/backend-jsonrpc/src/workspace.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions packages/@biomejs/biome/configuration_schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 150dd0e

Please sign in to comment.