-
-
Notifications
You must be signed in to change notification settings - Fork 474
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(biome_css_analyze): noDuplicateSelectors #2660
base: main
Are you sure you want to change the base?
feat(biome_css_analyze): noDuplicateSelectors #2660
Conversation
…t it into multiple runs.
…ds to be simplified.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add
.bar,
.bar,
.bar {
width: 12;
}
Ensure it reports either for the first and the second or the second and the third.
Also it either reports per selector list or separately.
i.e. should it report
! Duplicate selectors.
> 1 │ .bar,
^^^^
> 2 │ .bar,
^^^^
> 3 │ .bar, {
4 │ width: 12;
5 │ }
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Mouvedia thank you for the suggestions. I will update the test cases and pay attention to this case.
Probably, to be consistent with stylelint
it should only report on the first selector
in this case.
In the case of two separate rules:
.bar {}
.bar {}
it will report on the second one!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@abidjappie see stylelint/stylelint#7583
i.e. it's planned
…cases. TODO: finish updating the logic - some issues with resolving.
crates/biome_css_analyze/tests/specs/nursery/noDuplicateSelectors/valid.css
Show resolved
Hide resolved
…TODO: add tests for Options.
crates/biome_css_analyze/tests/specs/nursery/noDuplicateSelectors/invalid.css
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know why you requested a review from a rust noob but here you go.
let selectors = node.rules().syntax().descendants().filter(|x| { | ||
x.clone().cast::<AnyCssSelector>().is_some() | ||
|| x.clone().cast::<AnyCssRelativeSelector>().is_some() | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question
refactor
This looks similar to l159-162; could it become a function?
i.e. <T>
disclaimer: rust is not my main language but you asked for a review
output.push(DuplicateSelector { | ||
first: first.selector_node.clone(), | ||
duplicate: selector_list.clone(), | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor
refactor
see l186-189, l146-149
Thank you for the feedback! Since you were providing helpful feedback before, I wanted to do a final check that your feedback was adequately addressed. Anything that can help improve the experience provided by this rule is good I guess 👍 |
CodSpeed Performance ReportMerging #2660 will not alter performanceComparing Summary
|
let this_list = selector.clone().parent().unwrap(); | ||
|
||
// i.e not actually a list | ||
if let Some(_parent_sel) = CssComplexSelector::cast_ref(&this_list) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use ::can_cast
if you need a boolean
continue; | ||
} | ||
|
||
let this_rule = this_list.parent().unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't unwrap()
, it's very unsafe. Inside a rule, you can usually do this: .ok()?
instead. We convert Result
to Option
and then use the ?
operator.
/// Disallow duplicate selectors. | ||
/// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we have more documentation here? Maybe a longer explanation of what it does?
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] | ||
#[serde(rename_all = "camelCase", deny_unknown_fields)] | ||
pub struct NoDuplicateSelectorsOptions { | ||
pub disallow_in_list: bool, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we document the option using doc comments (///
)? They will appear in the configuration schema
/// ```json | ||
/// { | ||
/// "noDuplicateSelectors": { | ||
/// "options": { | ||
/// "disallowInList": true | ||
/// } | ||
/// } | ||
/// } | ||
/// ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// ```json | |
/// { | |
/// "noDuplicateSelectors": { | |
/// "options": { | |
/// "disallowInList": true | |
/// } | |
/// } | |
/// } | |
/// ``` | |
/// ```json5, ignore | |
/// { | |
/// // ... | |
/// "noDuplicateSelectors": { | |
/// "options": { | |
/// "disallowInList": true | |
/// } | |
/// } | |
/// // ... | |
/// } | |
/// ``` |
} | ||
} | ||
|
||
fn normalize_complex_selector(selector: AnyCssSelector) -> String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we document the function? "Normalize" has a very broad meaning and changes based the context.
fn normalize_complex_selector(selector: AnyCssSelector) -> String { | ||
let mut selector_text = String::new(); | ||
|
||
if let Some(complex_selector) = CssComplexSelector::cast_ref(&selector.clone().into_syntax()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if let Some(complex_selector) = CssComplexSelector::cast_ref(&selector.clone().into_syntax()) { | |
if let AnyCssSelector::CssComplexSelector(complex_selector) = selector { |
No need cloning and into_syntax
} | ||
|
||
#[derive(Debug, Eq)] | ||
struct ResolvedSelector { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's document this type and how it will be used. Considering that we use a specific Hash
, it's worth the explanation
// | ||
// Read our guidelines to write great diagnostics: | ||
// https://docs.rs/biome_analyze/latest/biome_analyze/#what-a-rule-should-say-to-the-user | ||
// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// | |
// Read our guidelines to write great diagnostics: | |
// https://docs.rs/biome_analyze/latest/biome_analyze/#what-a-rule-should-say-to-the-user | |
// |
// Read our guidelines to write great diagnostics: | ||
// https://docs.rs/biome_analyze/latest/biome_analyze/#what-a-rule-should-say-to-the-user | ||
// | ||
let duplicate_text = node.duplicate.to_string(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't use the to_string
of a node because it will write trivia too. Instead, use a typed node or a union of nodes, and implement a text()
that returns the exact name you require printing.
@ematipico thank you for the feedback! will address ASAP. |
@abidjappie As for now, I just did a dumb review of your code. However, I noticed that the logic of the code is quite lengthy, so we would appreciate it if you explained how you tackled the problem and the logic of your code in the description of the PR. Without this information, it will be very difficult to get a sensible review, and it will take a long time to merge this. |
@ematipico thank you for the feedback so far. I added some documentation and comments, as well as addressing the feedback from before as best as I could. I would like to make this as easy as possible to review, so if the documentation is lacking or if there is any additional content I should prepare please let me know! 🙇 |
Thank you @abidjappie for the explanation. Here are some thoughts about how to approach the problem Technical
Docs
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sure to cargo insta accept
these snapshots
@ematipico thank you for the explanations! It's super insightful. 🙇 I think my next steps will be to set this PR back to draft or close it (I would like to use it a reference though) and prepare some more manageable smaller PR's. My previous attempt using the visitor was doing something like this: match event {
WalkEvent::Enter(node) => {
if let Some(node) = CssDeclarationOrRuleBlock::cast_ref(node) {
self.stack.push(node);
}
}
WalkEvent::Leave(node) => {
if let Some(_node) = CssRuleList::cast_ref(node) {
ctx.match_query(DeclarationOrRuleBlockList(self.stack.clone()));
self.stack.clear();
}
}
} But still resolving it using recursion, I think I need to wrap my head around solving this from a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should take a look at stylelint/stylelint#7869.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the feedback!
Summary
This PR aims to implement
no-duplicate-selectors
from the recommended Stylelint rules. See here:https://github.com/stylelint/stylelint/tree/main/lib/rules/no-duplicate-selectors
Approach
This rule is run on
CssRoot
and the analysis is performed on either:disallow_in_list = true
:CssComplexSelector
andCssRelativeSelector
disallow_in_list = false
:CssSelectorList
andCssRelativeSelectorList
, i.e it treats the entire list as a selector instead of comparing individual selectors in the list.The motivation behind using
CssRoot
instead of some union of selectors is that we need to compare all the selectors within the root against each other, so having the rule trigger once per root allows us to maintain a single working state.Nested selectors are
resolved
to an equivalent flat selector in text form. The resolved selectors are then checked for duplicates using aHashSet
.Resolution
The resolution method is inspired by postcss-resolve-nested-selector which is used by the original stylelint
no-duplicate-selectors
. To resolve the selectors: recursively find the parent rule and and resolve the selectors in the prelude.There is special handling for
CssAtRule
- since children of the at rule cannot be compared to other at-rules (it has a unique context) we use ahash
to make them unique. i.e. only children within the same at-rule are comparable.Caveats
resolves #2514
Test Plan
Passes tests from
Stylelint
: https://github.com/stylelint/stylelint/blob/main/lib/rules/no-duplicate-selectors/__tests__/index.mjs