Skip to content
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

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
564b995
Bootstrapped and added basic logic and failing tests.
abidjappie Apr 25, 2024
08c949e
Added some helpful comments and save progress.
abidjappie Apr 25, 2024
c4674af
WIP: it currently returns everything in a single run, we need to spli…
abidjappie Apr 30, 2024
99af5a2
Working solution excluding some edge cases, still using a visitor nee…
abidjappie Apr 30, 2024
b6b3d1e
WIP: removed the visitor, simplified the logic, added stylelint test …
abidjappie May 2, 2024
87a6f5e
WIP: Added option cases, many more test cases are passing. Next: pass…
abidjappie May 3, 2024
3965886
WIP: Fixed test case. some cleaning.
abidjappie May 3, 2024
3d58e8e
All the current valid and invalid cases are now working as expected. …
abidjappie May 5, 2024
3ed2b90
Added option test cases, updated the diagnostics. WIP: cleanup.
abidjappie May 5, 2024
65e6ba9
Ran gen-lint.
abidjappie May 6, 2024
0b51c52
Added more test cases, adjusted the at-rule logic.
abidjappie May 6, 2024
40de23d
Code structure improvements and simplifications.
abidjappie May 7, 2024
2ed1405
Linting.
abidjappie May 7, 2024
8feeeda
Added additional test cases, updated the diagnostic messages.
abidjappie May 7, 2024
648ffdd
Fixed typo.
abidjappie May 7, 2024
6ed13f2
Fixed merge conflicts, gen.
abidjappie May 7, 2024
e667f3c
Better diagnostic reporting.
abidjappie May 7, 2024
7e2b209
Addressed linter feedback. WIP: addressing review feedback.
abidjappie May 7, 2024
4471c4f
WIP: added comments.
abidjappie May 8, 2024
f453e8c
WIP: added comments.
abidjappie May 8, 2024
911eacf
Handle the text in the diagnostics.
abidjappie May 8, 2024
04556f6
Fixed merge conflicts, gen.
abidjappie May 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
273 changes: 72 additions & 201 deletions crates/biome_css_analyze/src/lint/nursery/no_duplicate_selectors.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use std::collections::HashSet;
use std::vec;

use biome_analyze::{AddVisitor, Phases, QueryMatch, Queryable, ServiceBag, Visitor, VisitorContext};
use biome_analyze::{AddVisitor, Ast, Phases, QueryMatch, Queryable, ServiceBag, Visitor, VisitorContext};
use biome_analyze::{context::RuleContext, declare_rule, Rule, RuleDiagnostic, RuleSource};
use biome_console::markup;
use biome_css_syntax::{AnyCssRule, AnyCssSelector, CssComplexSelector, CssCompoundSelector, CssDeclarationOrRuleBlock, CssLanguage, CssNestedQualifiedRule, CssQualifiedRule, CssRelativeSelector, CssRuleList};
use biome_rowan::{AstNode, Language, SyntaxNode, TextRange, WalkEvent};
use biome_css_syntax::{AnyCssRule, AnyCssSelector, CssAtRule, CssComplexSelector, CssCompoundSelector, CssDeclarationBlock, CssDeclarationOrAtRuleBlock, CssDeclarationOrRuleBlock, CssLanguage, CssNestedQualifiedRule, CssQualifiedRule, CssRelativeSelector, CssRuleBlock, CssRuleList, CssSelectorList, CssSyntaxList, CssSyntaxNode};
use biome_rowan::{AstNode, AstNodeList, Language, SyntaxList, SyntaxNode, SyntaxNodeCast, TextRange, WalkEvent};

declare_rule! {
///
Expand All @@ -18,87 +18,44 @@ declare_rule! {
}
}

#[derive(Default)]
struct DeclarationOrRuleBlockListVisitor {
stack: Vec<CssDeclarationOrRuleBlock>
}

impl Visitor for DeclarationOrRuleBlockListVisitor {
type Language = CssLanguage;

fn visit(
&mut self,
event: &WalkEvent<SyntaxNode<Self::Language>>,
mut ctx: VisitorContext<Self::Language>
) {
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();
}
}
}
}
}

pub struct DeclarationOrRuleBlockList(Vec<CssDeclarationOrRuleBlock>);

impl QueryMatch for DeclarationOrRuleBlockList {
fn text_range(&self) -> TextRange {
todo!()
}
}

impl Queryable for DeclarationOrRuleBlockList {
type Input = Self;
type Output = Vec<CssDeclarationOrRuleBlock>;
type Language = CssLanguage;
type Services = ();

fn build_visitor(
analyzer: &mut impl AddVisitor<Self::Language>,
_root: &<Self::Language as Language>::Root,
) {
analyzer.add_visitor(Phases::Syntax, DeclarationOrRuleBlockListVisitor::default);
}

fn unwrap_match(_services: &ServiceBag, query: &Self::Input) -> Self::Output {
query.0.clone()
}
}

impl Rule for NoDuplicateSelectors {
type Query = DeclarationOrRuleBlockList;
type State = SyntaxNode<CssLanguage>;
type Query = Ast<CssRuleList>;
type State = CssSyntaxNode;
type Signals = Vec<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Vec<Self::State> {
let node_list = ctx.query();
let node = ctx.query();
let mut resolved_list = HashSet::new();
let mut output: Vec<CssSyntaxNode> = vec!();

let selectors = node
.syntax()
.descendants()
.filter_map(|x|x.cast::<AnyCssSelector>());

for selector in selectors {
let mut this_selctor_list = selector.clone().into_syntax().parent().unwrap();

if let Some(parent_selector) = CssComplexSelector::cast_ref(&this_selctor_list) {
// Don't handle the children of complex selectors
this_selctor_list = parent_selector.into_syntax().parent().unwrap()
} else if let Some(parent_selector) = CssRelativeSelector::cast_ref(&this_selctor_list) {
// Don't handle the children of complex relative
this_selctor_list = parent_selector.into_syntax().parent().unwrap();
}

let mut select_list = HashSet::new();
let mut output: Vec<SyntaxNode<CssLanguage>> = vec!();
let this_rule = this_selctor_list.parent().unwrap();
let this_block = this_rule.grand_parent().unwrap();

for node in node_list {
if let Some(this_rule) = node.syntax().parent() {
let handled_rule = handle_css_rule(this_rule);
for (selector_node, selector) in handled_rule {
let resolved_selectors = resolve_nested_selectors(selector, node.clone());
for resolved in resolved_selectors {
if !select_list.insert(resolved) {
output.push(selector_node.clone());
}
}
let resolved = resolve_nested_selectors(selector.clone().text(), this_block);
for r in resolved {
println!("resolved: {:?}", r);
if !resolved_list.insert(r) {
output.push(selector.clone().into_syntax());
}
}
}

output
}

Expand All @@ -122,32 +79,41 @@ impl Rule for NoDuplicateSelectors {
}

// TODO: need to handle AtRules etc.
fn resolve_nested_selectors(selector: String, node: CssDeclarationOrRuleBlock) -> Vec<String> {
fn resolve_nested_selectors(selector: String, block: CssSyntaxNode) -> Vec<String> {
let mut parent_selectors: Vec<String> = vec!();

if let Some(this_rule) = node.syntax().parent() {
if let Some(_qualified_rule) = CssQualifiedRule::cast_ref(&this_rule) {
// Highest Level
return vec!(selector);
}

let parent_node = get_parent_block(this_rule);

if let Some(parent_block) = parent_node.clone() {
if let Some(parent_node_parent) = parent_block.into_syntax().parent() {
if let Some(parent_rule) = CssQualifiedRule::cast_ref(&parent_node_parent) {
for selector in parent_rule.prelude() {
if let Ok(selector) = selector {
parent_selectors.push(handle_css_selector(selector.into()));
}
}
}
if let Some(parent_rule) = CssNestedQualifiedRule::cast_ref(&parent_node_parent) {
for selector in parent_rule.prelude() {
if let Ok(selector) = selector {
parent_selectors.push(handle_css_selector(selector.into()));
}
let parent_node = get_parent_block(block);

match &parent_node {
None => {
return vec!(selector)
},
Some(parent_block) => {
if let Some(parent_node_parent) = parent_block.parent() {
if let Some(parent_rule) = AnyCssRule::cast_ref(&parent_node_parent){
match parent_rule {
AnyCssRule::CssBogusRule(_) => todo!(),
AnyCssRule::CssAtRule(parent_rule) => {
// Treat the AtRule as a selector
let rule = parent_rule.rule().unwrap();
parent_selectors.push(rule.text());
},
AnyCssRule::CssNestedQualifiedRule(parent_rule) => {
for selector in parent_rule.prelude() {
if let Ok(selector) = selector {
parent_selectors.push(selector.text());
}
}
},
AnyCssRule::CssQualifiedRule(parent_rule) => {
for selector in parent_rule.prelude() {
if let Ok(selector) = selector {
parent_selectors.push(selector.text());
}
}
},
}

}
}

Expand All @@ -171,114 +137,19 @@ fn resolve_nested_selectors(selector: String, node: CssDeclarationOrRuleBlock) -
None => result,
}
});
return resolved_selectors
}
}
vec!(selector)
}

// This does not handle the highest level rules
fn get_parent_block(this_rule: SyntaxNode<CssLanguage>) -> Option<CssDeclarationOrRuleBlock> {
if let Some(nested_qualified_rule) = CssNestedQualifiedRule::cast_ref(&this_rule) {
if let Some(rule_grand_parent) = nested_qualified_rule.syntax().grand_parent(){
if let Some(css_declaration_block) = CssDeclarationOrRuleBlock::cast_ref(&rule_grand_parent) {
return Some(css_declaration_block)
if resolved_selectors.len() > 0 {
return resolved_selectors
}
}
}
None
}

fn handle_css_rule(rule: SyntaxNode<CssLanguage>) -> Vec<(SyntaxNode<CssLanguage>, String)> {
let mut selector_list = vec!();
if let Some(any_css_rule) = AnyCssRule::cast_ref(&rule) {
match any_css_rule {
AnyCssRule::CssAtRule(_) => todo!(),
AnyCssRule::CssBogusRule(_) => todo!(),
AnyCssRule::CssNestedQualifiedRule(nested_qualified_rule) => {
for selector in nested_qualified_rule.prelude() {
if let Ok(valid_selector) = selector {
let selector_syntax = valid_selector.into_syntax();
selector_list.push(
(selector_syntax.clone(), handle_css_selector(selector_syntax))
);
}
}
},
AnyCssRule::CssQualifiedRule(qualified_rule) => {
for selector in qualified_rule.prelude() {
if let Ok(valid_selector) = selector {
let selector_syntax = valid_selector.into_syntax();
selector_list.push(
(selector_syntax.clone(), handle_css_selector(selector_syntax))
);
}
}
},
}
return vec!(selector)
},
}
selector_list
}

fn handle_css_selector(selector: SyntaxNode<CssLanguage>) -> String {
if let Some(complex_selector) = CssComplexSelector::cast_ref(&selector) {
let mut resolved = complex_selector.text();
// This is to handle the special case of an empty combinator
// i.e. .foo .bar == .bar .foo
let mut left_right :Vec<String> = vec!();

if let Ok(left) = complex_selector.left() {
left_right.push(handle_css_selector(left.into_syntax()));
}
if let Ok(right) = complex_selector.right() {
left_right.push(handle_css_selector(right.into_syntax()));
}
if let Ok(combinator) = complex_selector.combinator() {
if combinator.text() == " " {
left_right.sort()
}
resolved = left_right.join(combinator.text());
}

return resolved
}
if let Some(relative_selector) = CssRelativeSelector::cast_ref(&selector) {
let mut resolved = String::new();
if let Some(combinator) = relative_selector.combinator() {
resolved.push_str(combinator.text());
}
if let Ok(selector) = relative_selector.selector(){
resolved.push_str(&handle_css_selector(selector.into()));
}
return resolved
}
if let Some(compound_selector) = CssCompoundSelector::cast_ref(&selector) {
return format_compound_selector(compound_selector)
}
if let Some(any) = AnyCssSelector::cast_ref(&selector) {
return any.text();
// This does not handle the highest level rules
fn get_parent_block(this_block: CssSyntaxNode) -> Option<CssSyntaxNode> {
if let Some(parent_rule) = this_block.parent() {
return parent_rule.grand_parent();
}
selector.to_string()
return None
}

fn format_compound_selector (selector: CssCompoundSelector) -> String {
let nesting_selector_token = selector.nesting_selector_token();
let sub_selectors = selector.sub_selectors();
let simple_selector = selector.simple_selector();

let mut resolved = String::new();
if let Some(token) = nesting_selector_token {
resolved.push_str(&token.text().trim());
}
if let Some(selector) = simple_selector {
resolved.push_str(&selector.text());
}
let mut sub_selector_string: Vec<String> = sub_selectors.into_iter().map(|s|{
return s.text()
}).collect();
sub_selector_string.sort();
if sub_selector_string.len() > 0 {
resolved.push_str(&sub_selector_string.join(""));
}
return resolved
}