Skip to content

Commit 5b7d158

Browse files
authored
feat(linter): implement noDuplicateProperties (#4029)
1 parent 6e1170e commit 5b7d158

File tree

12 files changed

+723
-81
lines changed

12 files changed

+723
-81
lines changed

crates/biome_configuration/src/analyzer/linter/rules.rs

Lines changed: 101 additions & 79 deletions
Large diffs are not rendered by default.

crates/biome_css_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use biome_analyze::declare_lint_group;
44

55
pub mod no_descending_specificity;
66
pub mod no_duplicate_custom_properties;
7+
pub mod no_duplicate_properties;
78
pub mod no_irregular_whitespace;
89
pub mod no_missing_var_function;
910
pub mod no_unknown_pseudo_class;
@@ -17,6 +18,7 @@ declare_lint_group! {
1718
rules : [
1819
self :: no_descending_specificity :: NoDescendingSpecificity ,
1920
self :: no_duplicate_custom_properties :: NoDuplicateCustomProperties ,
21+
self :: no_duplicate_properties :: NoDuplicateProperties ,
2022
self :: no_irregular_whitespace :: NoIrregularWhitespace ,
2123
self :: no_missing_var_function :: NoMissingVarFunction ,
2224
self :: no_unknown_pseudo_class :: NoUnknownPseudoClass ,
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use std::{borrow::Cow, collections::hash_map::Entry};
2+
3+
use biome_analyze::{context::RuleContext, declare_lint_rule, Rule, RuleDiagnostic, RuleSource};
4+
use biome_console::markup;
5+
use biome_css_syntax::CssDeclarationOrRuleList;
6+
use biome_rowan::{AstNode, TextRange};
7+
use biome_string_case::StrOnlyExtension;
8+
use rustc_hash::FxHashMap;
9+
10+
use crate::services::semantic::Semantic;
11+
12+
declare_lint_rule! {
13+
/// Disallow duplicate properties within declaration blocks.
14+
///
15+
/// This rule checks the declaration blocks for duplicate properties. It ignores custom properties.
16+
///
17+
/// ## Examples
18+
///
19+
/// ### Invalid
20+
///
21+
/// ```css,expect_diagnostic
22+
/// a {
23+
/// color: pink;
24+
/// color: orange;
25+
/// }
26+
/// ```
27+
///
28+
/// ### Valid
29+
///
30+
/// ```css
31+
/// a {
32+
/// color: pink;
33+
/// background: orange;
34+
/// }
35+
/// ```
36+
///
37+
pub NoDuplicateProperties {
38+
version: "next",
39+
name: "noDuplicateProperties",
40+
language: "css",
41+
recommended: true,
42+
sources: &[RuleSource::Stylelint("declaration-block-no-duplicate-properties")],
43+
}
44+
}
45+
46+
impl Rule for NoDuplicateProperties {
47+
type Query = Semantic<CssDeclarationOrRuleList>;
48+
type State = (TextRange, (TextRange, String));
49+
type Signals = Option<Self::State>;
50+
type Options = ();
51+
52+
fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
53+
let node = ctx.query();
54+
let model = ctx.model();
55+
56+
let rule = model.get_rule_by_range(node.range())?;
57+
58+
let mut seen: FxHashMap<Cow<'_, str>, TextRange> = FxHashMap::default();
59+
60+
for declaration in rule.declarations.iter() {
61+
let prop = &declaration.property;
62+
let prop_name = prop.name.to_lowercase_cow();
63+
let prop_range = prop.range;
64+
65+
let is_custom_property = prop_name.starts_with("--");
66+
67+
if is_custom_property {
68+
continue;
69+
}
70+
71+
match seen.entry(prop_name.clone()) {
72+
Entry::Occupied(entry) => {
73+
return Some((*entry.get(), (prop_range, prop_name.to_string())));
74+
}
75+
Entry::Vacant(_) => {
76+
seen.insert(prop_name, prop_range);
77+
}
78+
}
79+
}
80+
81+
None
82+
}
83+
84+
fn diagnostic(_: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
85+
let (first_occurrence_range, (duplicate_range, duplicate_property_name)) = state;
86+
Some(
87+
RuleDiagnostic::new(
88+
rule_category!(),
89+
duplicate_range,
90+
markup! {
91+
"Duplicate properties can lead to unexpected behavior and may override previous declarations unintentionally."
92+
},
93+
)
94+
.detail(first_occurrence_range, markup! {
95+
<Emphasis>{duplicate_property_name}</Emphasis> " is already defined here."
96+
})
97+
.note(markup! {
98+
"Remove or rename the duplicate property to ensure consistent styling."
99+
}),
100+
)
101+
}
102+
}

crates/biome_css_analyze/src/options.rs

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
a {
2+
color: pink;
3+
color: orange;
4+
}
5+
6+
a {
7+
color: pink;
8+
color: pink;
9+
color: pink;
10+
}
11+
12+
a {
13+
color: pink;
14+
color: pink;
15+
color: orange;
16+
}
17+
18+
a {
19+
color: pink;
20+
background: orange;
21+
color: orange;
22+
}
23+
24+
a {
25+
color: pink;
26+
background: orange;
27+
background: pink;
28+
}
29+
30+
a { color: pink; { &:hover { color: orange; color: black; } } }
31+
32+
a { color: pink; @media { color: orange; color: black; } }
33+
34+
@media { color: orange; .foo { color: black; color: white; } }
35+
36+
a { color: pink; @media { color: orange; &::before { color: black; color: white; } } }
37+
38+
a { color: pink; @media { color: orange; .foo { color: black; color: white; } } }
39+
40+
a { -webkit-border-radius: 12px; -webkit-border-radius: 10px; }
41+
42+
a { color: red !important; color: blue; }
43+
44+
a { color: red !important; color: blue !important; }

0 commit comments

Comments
 (0)