use lightningcss::stylesheet::StyleSheet;
use crate::diagnostic::{LintDiagnostic, Severity};
use super::{CssLintResult, CssRule, CssRuleMeta};
static META: CssRuleMeta = CssRuleMeta {
name: "css/prefer-nested-selectors",
description: "Recommend using CSS nesting for descendant selectors",
default_severity: Severity::Warning,
};
pub struct PreferNestedSelectors;
impl CssRule for PreferNestedSelectors {
fn meta(&self) -> &'static CssRuleMeta {
&META
}
fn check<'i>(
&self,
source: &'i str,
_stylesheet: &StyleSheet<'i, 'i>,
offset: usize,
result: &mut CssLintResult,
) {
let bytes = source.as_bytes();
let mut i = 0;
while i < bytes.len() {
if let Some(selector_start) = find_selector_start(bytes, i) {
if let Some(brace_pos) = find_next_brace(bytes, selector_start) {
let selector = &source[selector_start..brace_pos];
let trimmed = selector.trim();
if is_descendant_selector(trimmed) {
if let Some((_parent, _child)) = split_descendant_selector(trimmed) {
let start = (offset + selector_start) as u32;
let end = (offset + brace_pos) as u32;
result.add_diagnostic(
LintDiagnostic::warn(
META.name,
"Consider using CSS nesting for descendant selectors",
start,
end,
)
.with_help(
"Use CSS nesting syntax to nest child selectors inside parent selectors",
),
);
}
}
i = brace_pos + 1;
} else {
i += 1;
}
} else {
break;
}
}
}
}
#[inline]
fn find_selector_start(bytes: &[u8], start: usize) -> Option<usize> {
for (offset, &byte) in bytes[start..].iter().enumerate() {
match byte {
b'.' | b'#' => return Some(start + offset),
b'a'..=b'z' | b'A'..=b'Z' => {
return Some(start + offset);
}
b' ' | b'\n' | b'\r' | b'\t' | b'}' => continue,
_ => continue,
}
}
None
}
#[inline]
fn find_next_brace(bytes: &[u8], start: usize) -> Option<usize> {
for (offset, &byte) in bytes[start..].iter().enumerate() {
if byte == b'{' {
return Some(start + offset);
}
if byte == b'@' || byte == b'}' {
return None;
}
}
None
}
#[inline]
#[allow(dead_code)]
fn find_closing_brace(bytes: &[u8], open_pos: usize) -> usize {
let mut depth = 1;
for (offset, &byte) in bytes[open_pos + 1..].iter().enumerate() {
match byte {
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
return open_pos + 1 + offset;
}
}
_ => {}
}
}
bytes.len()
}
#[inline]
fn is_descendant_selector(selector: &str) -> bool {
let bytes = selector.as_bytes();
let mut bracket_depth: usize = 0;
let mut paren_depth: usize = 0;
let mut in_quote = false;
let mut quote_char: u8 = 0;
for &b in bytes {
if !in_quote && (b == b'"' || b == b'\'') {
in_quote = true;
quote_char = b;
continue;
}
if in_quote && b == quote_char {
in_quote = false;
continue;
}
if in_quote {
continue;
}
match b {
b'[' => bracket_depth += 1,
b']' => bracket_depth = bracket_depth.saturating_sub(1),
b'(' => paren_depth += 1,
b')' => paren_depth = paren_depth.saturating_sub(1),
b' ' if bracket_depth == 0 && paren_depth == 0 => {
return true;
}
b'>' | b'+' | b'~' if bracket_depth == 0 && paren_depth == 0 => {
return true;
}
_ => {}
}
}
false
}
#[inline]
fn split_descendant_selector(selector: &str) -> Option<(&str, &str)> {
let bytes = selector.as_bytes();
let mut bracket_depth: usize = 0;
let mut paren_depth: usize = 0;
for (i, &b) in bytes.iter().enumerate() {
match b {
b'[' => bracket_depth += 1,
b']' => bracket_depth = bracket_depth.saturating_sub(1),
b'(' => paren_depth += 1,
b')' => paren_depth = paren_depth.saturating_sub(1),
b' ' | b'>' | b'+' | b'~' if bracket_depth == 0 && paren_depth == 0 => {
let parent = selector[..i].trim();
let child = selector[i..]
.trim()
.trim_start_matches([' ', '>', '+', '~'])
.trim();
if !parent.is_empty() && !child.is_empty() {
return Some((parent, child));
}
}
_ => {}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rules::css::CssLinter;
fn create_linter() -> CssLinter {
let mut linter = CssLinter::new();
linter.add_rule(Box::new(PreferNestedSelectors));
linter
}
#[test]
fn test_simple_selector() {
let linter = create_linter();
let result = linter.lint(".button { color: red; }", 0);
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_descendant_selector() {
let linter = create_linter();
let result = linter.lint(".parent .child { color: red; }", 0);
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_child_selector() {
let linter = create_linter();
let result = linter.lint(".parent > .child { color: red; }", 0);
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_element_descendant() {
let linter = create_linter();
let result = linter.lint("div span { color: red; }", 0);
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_complex_selector() {
let linter = create_linter();
let result = linter.lint(".parent .child { color: red; }", 0);
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_attribute_selector() {
let linter = create_linter();
let result = linter.lint("[data-foo=\"bar baz\"] { color: red; }", 0);
assert_eq!(result.warning_count, 0);
}
}