Skip to content

Commit

Permalink
feat(biome_css_analyzer): implement selector-pseudo-class-no-unknown (#…
Browse files Browse the repository at this point in the history
…3034)

Co-authored-by: ty <62130798+togami2864@users.noreply.github.com>
  • Loading branch information
tunamaguro and togami2864 authored Jun 9, 2024
1 parent 14f4f56 commit aa2b52f
Show file tree
Hide file tree
Showing 14 changed files with 850 additions and 58 deletions.
117 changes: 69 additions & 48 deletions crates/biome_configuration/src/linter/rules.rs

Large diffs are not rendered by default.

103 changes: 103 additions & 0 deletions crates/biome_css_analyze/src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,109 @@ pub const OTHER_PSEUDO_ELEMENTS: [&str; 18] = [

pub const VENDOR_PREFIXES: [&str; 4] = ["-webkit-", "-moz-", "-ms-", "-o-"];

pub const AT_RULE_PAGE_PSEUDO_CLASSES: [&str; 4] = ["first", "right", "left", "blank"];

pub const WEBKIT_SCROLLBAR_PSEUDO_ELEMENTS: [&str; 7] = [
"-webkit-resizer",
"-webkit-scrollbar",
"-webkit-scrollbar-button",
"-webkit-scrollbar-corner",
"-webkit-scrollbar-thumb",
"-webkit-scrollbar-track",
"-webkit-scrollbar-track-piece",
];

pub const WEBKIT_SCROLLBAR_PSEUDO_CLASSES: [&str; 11] = [
"horizontal",
"vertical",
"decrement",
"increment",
"start",
"end",
"double-button",
"single-button",
"no-button",
"corner-present",
"window-inactive",
];

pub const A_NPLUS_BNOTATION_PSEUDO_CLASSES: [&str; 4] = [
"nth-column",
"nth-last-column",
"nth-last-of-type",
"nth-of-type",
];

pub const A_NPLUS_BOF_SNOTATION_PSEUDO_CLASSES: [&str; 2] = ["nth-child", "nth-last-child"];

pub const LINGUISTIC_PSEUDO_CLASSES: [&str; 2] = ["dir", "lang"];

pub const LOGICAL_COMBINATIONS_PSEUDO_CLASSES: [&str; 5] = ["has", "is", "matches", "not", "where"];

/// See https://drafts.csswg.org/selectors/#resource-pseudos
pub const RESOURCE_STATE_PSEUDO_CLASSES: [&str; 7] = [
"playing",
"paused",
"seeking",
"buffering",
"stalled",
"muted",
"volume-locked",
];

pub const OTHER_PSEUDO_CLASSES: [&str; 50] = [
"active",
"any-link",
"autofill",
"blank",
"checked",
"current",
"default",
"defined",
"disabled",
"empty",
"enabled",
"first-child",
"first-of-type",
"focus",
"focus-visible",
"focus-within",
"fullscreen",
"fullscreen-ancestor",
"future",
"host",
"host-context",
"hover",
"indeterminate",
"in-range",
"invalid",
"last-child",
"last-of-type",
"link",
"modal",
"only-child",
"only-of-type",
"optional",
"out-of-range",
"past",
"placeholder-shown",
"picture-in-picture",
"popover-open",
"read-only",
"read-write",
"required",
"root",
"scope",
"state",
"target",
"unresolved",
"user-invalid",
"user-valid",
"valid",
"visited",
"window-inactive",
];

// https://github.com/known-css/known-css-properties/blob/master/source/w3c.json
pub const KNOWN_PROPERTIES: [&str; 588] = [
"-webkit-line-clamp",
Expand Down
22 changes: 20 additions & 2 deletions crates/biome_css_analyze/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,30 @@ mod tests {
String::from_utf8(buffer).unwrap()
}

const SOURCE: &str = r#"@font-face { font-family: Gentium; }"#;
const SOURCE: &str = r#"
/* valid */
a:hover {}
:not(p) {}
a:before { }
input:not([type='submit'])
:root { }
:--heading { }
:popover-open {}
.test::-webkit-scrollbar-button:horizontal:decrement {}
@page :first { }
/* invalid */
a:unknown { }
a:pseudo-class { }
body:not(div):noot(span) {}
:first { }
@page :blank:unknown { }
"#;

let parsed = parse_css(SOURCE, CssParserOptions::default());

let mut error_ranges: Vec<TextRange> = Vec::new();
let rule_filter = RuleFilter::Rule("nursery", "noMissingGenericFamilyKeyword");
let rule_filter = RuleFilter::Rule("nursery", "noUnknownPseudoClassSelector");
let options = AnalyzerOptions::default();
analyze(
&parsed.tree(),
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_css_analyze/src/lint/nursery.rs

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
use crate::{
keywords::{WEBKIT_SCROLLBAR_PSEUDO_CLASSES, WEBKIT_SCROLLBAR_PSEUDO_ELEMENTS},
utils::{is_custom_selector, is_known_pseudo_class, is_page_pseudo_class, vendor_prefixed},
};
use biome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic, RuleSource};
use biome_console::markup;
use biome_css_syntax::{
CssBogusPseudoClass, CssPageSelectorPseudo, CssPseudoClassFunctionCompoundSelector,
CssPseudoClassFunctionCompoundSelectorList, CssPseudoClassFunctionIdentifier,
CssPseudoClassFunctionNth, CssPseudoClassFunctionRelativeSelectorList,
CssPseudoClassFunctionSelector, CssPseudoClassFunctionSelectorList,
CssPseudoClassFunctionValueList, CssPseudoClassIdentifier, CssPseudoElementSelector,
};
use biome_rowan::{declare_node_union, AstNode, TextRange};

declare_rule! {
/// Disallow unknown pseudo-class selectors.
///
/// For details on known pseudo-class, see the [MDN web docs](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes)
///
/// This rule ignores vendor-prefixed pseudo-class selectors.
///
/// ## Examples
///
/// ### Invalid
///
/// ```css,expect_diagnostic
/// a:unknown {}
/// ```
///
/// ```css,expect_diagnostic
/// a:UNKNOWN {}
/// ```
///
/// ```css,expect_diagnostic
/// a:hoverr {}
/// ```
///
/// ### Valid
///
/// ```css
/// a:hover {}
/// ```
///
/// ```css
/// a:focus {}
/// ```
///
/// ```css
/// :not(p) {}
/// ```
///
/// ```css
/// input:-moz-placeholder {}
/// ```
///
pub NoUnknownPseudoClassSelector {
version: "next",
name: "noUnknownPseudoClassSelector",
language: "css",
recommended: true,
sources: &[RuleSource::Stylelint("selector-pseudo-class-no-unknown")],
}
}
declare_node_union! {
pub AnyPseudoLike =
CssPseudoClassFunctionCompoundSelector
| CssPseudoClassFunctionCompoundSelectorList
| CssPseudoClassFunctionIdentifier
| CssPseudoClassFunctionNth
| CssPseudoClassFunctionRelativeSelectorList
| CssPseudoClassFunctionSelector
| CssPseudoClassFunctionSelectorList
| CssPseudoClassFunctionValueList
| CssPseudoClassIdentifier
| CssBogusPseudoClass
| CssPageSelectorPseudo
}

fn is_webkit_pseudo_class(node: &AnyPseudoLike) -> bool {
let mut prev_element = node.syntax().parent().and_then(|p| p.prev_sibling());
while let Some(prev) = &prev_element {
let maybe_selector = CssPseudoElementSelector::cast_ref(prev);
if let Some(selector) = maybe_selector.as_ref() {
return WEBKIT_SCROLLBAR_PSEUDO_ELEMENTS.contains(&selector.text().trim_matches(':'));
};
prev_element = prev.prev_sibling();
}

false
}

#[derive(Debug, Clone, Copy)]
enum PseudoClassType {
PagePseudoClass,
WebkitScrollbarPseudoClass,
Other,
}

pub struct NoUnknownPseudoClassSelectorState {
class_name: String,
span: TextRange,
class_type: PseudoClassType,
}

impl Rule for NoUnknownPseudoClassSelector {
type Query = Ast<AnyPseudoLike>;
type State = NoUnknownPseudoClassSelectorState;
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
let pseudo_class = ctx.query();
let (name, span) = match pseudo_class {
AnyPseudoLike::CssBogusPseudoClass(class) => Some((class.text(), class.range())),
AnyPseudoLike::CssPseudoClassFunctionCompoundSelector(selector) => {
let name = selector.name().ok()?;
Some((name.text().to_string(), name.text_range()))
}
AnyPseudoLike::CssPseudoClassFunctionCompoundSelectorList(selector_list) => {
let name = selector_list.name().ok()?;
Some((name.text().to_string(), name.text_range()))
}
AnyPseudoLike::CssPseudoClassFunctionIdentifier(ident) => {
let name = ident.name_token().ok()?;
Some((name.text().to_string(), name.text_range()))
}
AnyPseudoLike::CssPseudoClassFunctionNth(func_nth) => {
let name = func_nth.name().ok()?;
Some((name.text().to_string(), name.text_range()))
}
AnyPseudoLike::CssPseudoClassFunctionRelativeSelectorList(selector_list) => {
let name = selector_list.name_token().ok()?;
Some((name.token_text_trimmed().to_string(), name.text_range()))
}
AnyPseudoLike::CssPseudoClassFunctionSelector(selector) => {
let name = selector.name().ok()?;
Some((name.text().to_string(), name.text_range()))
}
AnyPseudoLike::CssPseudoClassFunctionSelectorList(selector_list) => {
let name = selector_list.name().ok()?;
Some((name.text().to_string(), name.text_range()))
}
AnyPseudoLike::CssPseudoClassFunctionValueList(func_value_list) => {
let name = func_value_list.name_token().ok()?;
Some((name.text().to_string(), name.text_range()))
}
AnyPseudoLike::CssPseudoClassIdentifier(ident) => {
let name = ident.name().ok()?;
Some((name.text().to_string(), name.range()))
}
AnyPseudoLike::CssPageSelectorPseudo(page_pseudo) => {
let name = page_pseudo.selector().ok()?;
Some((name.token_text_trimmed().to_string(), name.text_range()))
}
}?;

let pseudo_type = match &pseudo_class {
AnyPseudoLike::CssPageSelectorPseudo(_) => PseudoClassType::PagePseudoClass,
_ => {
if is_webkit_pseudo_class(pseudo_class) {
PseudoClassType::WebkitScrollbarPseudoClass
} else {
PseudoClassType::Other
}
}
};

let lower_name = name.to_lowercase();

let is_valid_class = match pseudo_type {
PseudoClassType::PagePseudoClass => is_page_pseudo_class(&lower_name),
PseudoClassType::WebkitScrollbarPseudoClass => {
WEBKIT_SCROLLBAR_PSEUDO_CLASSES.contains(&lower_name.as_str())
}
PseudoClassType::Other => {
is_custom_selector(&lower_name)
|| vendor_prefixed(&lower_name)
|| is_known_pseudo_class(&lower_name)
}
};

if is_valid_class {
None
} else {
Some(NoUnknownPseudoClassSelectorState {
class_name: name,
span,
class_type: pseudo_type,
})
}
}

fn diagnostic(_: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
let Self::State {
class_name,
span,
class_type,
} = state;
let mut diag = RuleDiagnostic::new(
rule_category!(),
span,
markup! {
"Unexpected unknown pseudo-class "<Emphasis>{ class_name }</Emphasis>" "
},
);
match class_type {
PseudoClassType::PagePseudoClass => {
diag = diag.note(markup! {
"See "<Hyperlink href="https://developer.mozilla.org/en-US/docs/Web/CSS/@page">"MDN web docs"</Hyperlink>" for more details."
});
}
PseudoClassType::WebkitScrollbarPseudoClass => {
diag = diag.note(markup! {
"See "<Hyperlink href="https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-scrollbar">"MDN web docs"</Hyperlink>" for more details."
});
}
PseudoClassType::Other => {
diag = diag.note(markup! {
"See "<Hyperlink href="https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes">"MDN web docs"</Hyperlink>" for more details."
});
}
};
Some(diag)
}
}
1 change: 1 addition & 0 deletions crates/biome_css_analyze/src/options.rs

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

Loading

0 comments on commit aa2b52f

Please sign in to comment.