-
-
Notifications
You must be signed in to change notification settings - Fork 482
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(biome_css_analyze): implement
noDescendingSpecificity
(#4097)
- Loading branch information
1 parent
b2d46a5
commit 295efb9
Showing
22 changed files
with
939 additions
and
85 deletions.
There are no files selected for viewing
164 changes: 93 additions & 71 deletions
164
crates/biome_configuration/src/analyzer/linter/rules.rs
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
203 changes: 203 additions & 0 deletions
203
crates/biome_css_analyze/src/lint/nursery/no_descending_specificity.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
use rustc_hash::{FxHashMap, FxHashSet}; | ||
|
||
use biome_analyze::{context::RuleContext, declare_lint_rule, Rule, RuleDiagnostic, RuleSource}; | ||
use biome_console::markup; | ||
use biome_css_semantic::model::{Rule as CssSemanticRule, RuleId, SemanticModel, Specificity}; | ||
use biome_css_syntax::{AnyCssSelector, CssRoot}; | ||
use biome_rowan::TextRange; | ||
|
||
use biome_rowan::AstNode; | ||
|
||
use crate::services::semantic::Semantic; | ||
|
||
declare_lint_rule! { | ||
/// Disallow a lower specificity selector from coming after a higher specificity selector. | ||
/// | ||
/// This rule prohibits placing selectors with lower specificity after selectors with higher specificity. | ||
/// By maintaining the order of the source and specificity as consistently as possible, it enhances readability. | ||
/// | ||
/// ## Examples | ||
/// | ||
/// ### Invalid | ||
/// | ||
/// ```css,expect_diagnostic | ||
/// b a { color: red; } | ||
/// a { color: red; } | ||
/// ``` | ||
/// | ||
/// ```css,expect_diagnostic | ||
/// a { | ||
/// & > b { color: red; } | ||
/// } | ||
/// b { color: red; } | ||
/// ``` | ||
/// | ||
/// ```css,expect_diagnostic | ||
/// :root input { | ||
/// color: red; | ||
/// } | ||
/// html input { | ||
/// color: red; | ||
/// } | ||
/// ``` | ||
/// | ||
/// | ||
/// ### Valid | ||
/// | ||
/// ```css | ||
/// a { color: red; } | ||
/// b a { color: red; } | ||
/// ``` | ||
/// | ||
/// ```css | ||
/// b { color: red; } | ||
/// a { | ||
/// & > b { color: red; } | ||
/// } | ||
/// ``` | ||
/// | ||
/// ```css | ||
/// a:hover { color: red; } | ||
/// a { color: red; } | ||
/// ``` | ||
/// | ||
/// ```css | ||
/// a b { | ||
/// color: red; | ||
/// } | ||
/// /* This selector is overwritten by the one above it, but this is not an error because the rule only evaluates it as a compound selector */ | ||
/// :where(a) :is(b) { | ||
/// color: blue; | ||
/// } | ||
/// ``` | ||
/// | ||
pub NoDescendingSpecificity { | ||
version: "next", | ||
name: "noDescendingSpecificity", | ||
language: "css", | ||
recommended: true, | ||
sources: &[RuleSource::Stylelint("no-descending-specificity")], | ||
} | ||
} | ||
|
||
#[derive(Debug)] | ||
pub struct DescendingSelector { | ||
high: (TextRange, Specificity), | ||
low: (TextRange, Specificity), | ||
} | ||
/// find tail selector | ||
/// ```css | ||
/// a b:hover { | ||
/// ^^^^^^^ | ||
/// } | ||
/// ``` | ||
fn find_tail_selector(selector: &AnyCssSelector) -> Option<String> { | ||
match selector { | ||
AnyCssSelector::CssCompoundSelector(s) => { | ||
let simple = s | ||
.simple_selector() | ||
.map_or(String::new(), |s| s.syntax().text_trimmed().to_string()); | ||
let sub = s.sub_selectors().syntax().text_trimmed().to_string(); | ||
|
||
let last_selector = [simple, sub].join(""); | ||
Some(last_selector) | ||
} | ||
AnyCssSelector::CssComplexSelector(s) => { | ||
s.right().as_ref().ok().and_then(find_tail_selector) | ||
} | ||
_ => None, | ||
} | ||
} | ||
|
||
/// This function traverses the CSS rules starting from the given rule and checks for selectors that have the same tail selector. | ||
/// For each selector, it compares its specificity with the previously encountered specificity of the same tail selector. | ||
/// If a lower specificity selector is found after a higher specificity selector with the same tail selector, it records this as a descending selector. | ||
fn find_descending_selector( | ||
rule: &CssSemanticRule, | ||
model: &SemanticModel, | ||
visited_rules: &mut FxHashSet<RuleId>, | ||
visited_selectors: &mut FxHashMap<String, (TextRange, Specificity)>, | ||
descending_selectors: &mut Vec<DescendingSelector>, | ||
) { | ||
if visited_rules.contains(&rule.id) { | ||
return; | ||
} else { | ||
visited_rules.insert(rule.id); | ||
}; | ||
|
||
for selector in &rule.selectors { | ||
let tail_selector = if let Some(s) = find_tail_selector(&selector.original) { | ||
s | ||
} else { | ||
continue; | ||
}; | ||
|
||
if let Some((last_textrange, last_specificity)) = visited_selectors.get(&tail_selector) { | ||
if last_specificity > &selector.specificity { | ||
descending_selectors.push(DescendingSelector { | ||
high: (*last_textrange, last_specificity.clone()), | ||
low: (selector.range, selector.specificity.clone()), | ||
}); | ||
} | ||
} else { | ||
visited_selectors.insert( | ||
tail_selector, | ||
(selector.range, selector.specificity.clone()), | ||
); | ||
} | ||
} | ||
|
||
for child_id in &rule.child_ids { | ||
if let Some(child_rule) = model.get_rule_by_id(*child_id) { | ||
find_descending_selector( | ||
child_rule, | ||
model, | ||
visited_rules, | ||
visited_selectors, | ||
descending_selectors, | ||
); | ||
} | ||
} | ||
} | ||
|
||
impl Rule for NoDescendingSpecificity { | ||
type Query = Semantic<CssRoot>; | ||
type State = DescendingSelector; | ||
type Signals = Vec<Self::State>; | ||
type Options = (); | ||
|
||
fn run(ctx: &RuleContext<Self>) -> Self::Signals { | ||
let model = ctx.model(); | ||
let mut visited_rules = FxHashSet::default(); | ||
let mut visited_selectors = FxHashMap::default(); | ||
let mut descending_selectors = Vec::new(); | ||
for rule in model.rules() { | ||
find_descending_selector( | ||
rule, | ||
model, | ||
&mut visited_rules, | ||
&mut visited_selectors, | ||
&mut descending_selectors, | ||
); | ||
} | ||
|
||
descending_selectors | ||
} | ||
|
||
fn diagnostic(_: &RuleContext<Self>, node: &Self::State) -> Option<RuleDiagnostic> { | ||
Some( | ||
RuleDiagnostic::new( | ||
rule_category!(), | ||
node.low.0, | ||
markup! { | ||
"Descending specificity selector found. This selector specificity is "{node.low.1.to_string()} | ||
}, | ||
).detail(node.high.0, markup!( | ||
"This selector specificity is "{node.high.1.to_string()} | ||
)) | ||
.note(markup! { | ||
"Descending specificity selector may not applied. Consider rearranging the order of the selectors. See "<Hyperlink href="https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity">"MDN web docs"</Hyperlink>" for more details." | ||
}), | ||
) | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
7 changes: 7 additions & 0 deletions
7
...iome_css_analyze/tests/specs/nursery/noDescendingSpecificity/complex_selector.invalid.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
b a { | ||
color: red; | ||
} | ||
|
||
a { | ||
color: red; | ||
} |
40 changes: 40 additions & 0 deletions
40
...css_analyze/tests/specs/nursery/noDescendingSpecificity/complex_selector.invalid.css.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
--- | ||
source: crates/biome_css_analyze/tests/spec_tests.rs | ||
expression: complex_selector.invalid.css | ||
--- | ||
# Input | ||
```css | ||
b a { | ||
color: red; | ||
} | ||
a { | ||
color: red; | ||
} | ||
``` | ||
|
||
# Diagnostics | ||
``` | ||
complex_selector.invalid.css:5:1 lint/nursery/noDescendingSpecificity ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
! Descending specificity selector found. This selector specificity is (0, 0, 1) | ||
3 │ } | ||
4 │ | ||
> 5 │ a { | ||
│ ^ | ||
6 │ color: red; | ||
7 │ } | ||
i This selector specificity is (0, 0, 2) | ||
> 1 │ b a { | ||
│ ^^^ | ||
2 │ color: red; | ||
3 │ } | ||
i Descending specificity selector may not applied. Consider rearranging the order of the selectors. See MDN web docs for more details. | ||
``` |
15 changes: 15 additions & 0 deletions
15
..._analyze/tests/specs/nursery/noDescendingSpecificity/function_pseudo_selector.invalid.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
:is(#a, a) f { | ||
color: red; | ||
} | ||
|
||
:is(a, b, c, d) f { | ||
color: red; | ||
} | ||
|
||
:is(#fake#fake#fake#fake#fake#fake, *) g { | ||
color: red; | ||
} | ||
|
||
:where(*) g { | ||
color: red; | ||
} |
73 changes: 73 additions & 0 deletions
73
...yze/tests/specs/nursery/noDescendingSpecificity/function_pseudo_selector.invalid.css.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
--- | ||
source: crates/biome_css_analyze/tests/spec_tests.rs | ||
expression: function_pseudo_selector.invalid.css | ||
--- | ||
# Input | ||
```css | ||
:is(#a, a) f { | ||
color: red; | ||
} | ||
:is(a, b, c, d) f { | ||
color: red; | ||
} | ||
:is(#fake#fake#fake#fake#fake#fake, *) g { | ||
color: red; | ||
} | ||
:where(*) g { | ||
color: red; | ||
} | ||
``` | ||
|
||
# Diagnostics | ||
``` | ||
function_pseudo_selector.invalid.css:5:1 lint/nursery/noDescendingSpecificity ━━━━━━━━━━━━━━━━━━━━━━ | ||
! Descending specificity selector found. This selector specificity is (0, 0, 2) | ||
3 │ } | ||
4 │ | ||
> 5 │ :is(a, b, c, d) f { | ||
│ ^^^^^^^^^^^^^^^^^ | ||
6 │ color: red; | ||
7 │ } | ||
i This selector specificity is (1, 0, 1) | ||
> 1 │ :is(#a, a) f { | ||
│ ^^^^^^^^^^^^ | ||
2 │ color: red; | ||
3 │ } | ||
i Descending specificity selector may not applied. Consider rearranging the order of the selectors. See MDN web docs for more details. | ||
``` | ||
|
||
``` | ||
function_pseudo_selector.invalid.css:13:1 lint/nursery/noDescendingSpecificity ━━━━━━━━━━━━━━━━━━━━━ | ||
! Descending specificity selector found. This selector specificity is (0, 0, 1) | ||
11 │ } | ||
12 │ | ||
> 13 │ :where(*) g { | ||
│ ^^^^^^^^^^^ | ||
14 │ color: red; | ||
15 │ } | ||
i This selector specificity is (6, 0, 1) | ||
7 │ } | ||
8 │ | ||
> 9 │ :is(#fake#fake#fake#fake#fake#fake, *) g { | ||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
10 │ color: red; | ||
11 │ } | ||
i Descending specificity selector may not applied. Consider rearranging the order of the selectors. See MDN web docs for more details. | ||
``` |
9 changes: 9 additions & 0 deletions
9
crates/biome_css_analyze/tests/specs/nursery/noDescendingSpecificity/nested.invalid.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
a { | ||
& > b { | ||
color: red; | ||
} | ||
} | ||
|
||
b { | ||
color: red; | ||
} |
Oops, something went wrong.