use memchr::memmem;
use lightningcss::stylesheet::StyleSheet;
use crate::diagnostic::{LintDiagnostic, Severity};
use super::{CssLintResult, CssRule, CssRuleMeta};
static META: CssRuleMeta = CssRuleMeta {
name: "css/no-utility-classes",
description: "Warn against implementing utility classes in component styles",
default_severity: Severity::Warning,
};
static EXACT_UTILITY_PATTERNS: &[&str] = &[
".flex",
".block",
".inline",
".hidden",
".grid",
".items-center",
".justify-center",
".justify-between",
".flex-col",
".flex-row",
".flex-wrap",
".text-center",
".text-left",
".text-right",
".font-bold",
".font-semibold",
".italic",
".underline",
".w-full",
".h-full",
".w-screen",
".h-screen",
".absolute",
".relative",
".fixed",
".sticky",
];
static PREFIX_UTILITY_PATTERNS: &[&str] = &[
".m-",
".p-",
".mt-",
".mb-",
".ml-",
".mr-",
".mx-",
".my-",
".pt-",
".pb-",
".pl-",
".pr-",
".px-",
".py-",
".bg-",
".text-",
".w-",
".h-",
".gap-",
".rounded-",
".border-",
];
pub struct NoUtilityClasses;
impl CssRule for NoUtilityClasses {
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();
for &pattern in EXACT_UTILITY_PATTERNS {
let finder = memmem::Finder::new(pattern.as_bytes());
let mut search_start = 0;
while let Some(pos) = finder.find(&bytes[search_start..]) {
let absolute_pos = search_start + pos;
let is_selector_start = absolute_pos == 0
|| matches!(
bytes.get(absolute_pos - 1),
Some(b' ' | b'\n' | b'\r' | b'\t' | b'{' | b'}' | b',')
);
let end_pos = absolute_pos + pattern.len();
let is_exact_match = end_pos >= bytes.len()
|| matches!(
bytes.get(end_pos),
Some(b' ' | b'{' | b',' | b'\n' | b'\r' | b'\t')
);
if is_selector_start && is_exact_match {
result.add_diagnostic(
LintDiagnostic::warn(
META.name,
"Utility class should be in global styles, not component styles",
(offset + absolute_pos) as u32,
(offset + end_pos) as u32,
)
.with_help(
"Use semantic class names in components, or import utility classes from a global stylesheet",
),
);
}
search_start = absolute_pos + 1;
}
}
for &pattern in PREFIX_UTILITY_PATTERNS {
let finder = memmem::Finder::new(pattern.as_bytes());
let mut search_start = 0;
while let Some(pos) = finder.find(&bytes[search_start..]) {
let absolute_pos = search_start + pos;
let is_selector_start = absolute_pos == 0
|| matches!(
bytes.get(absolute_pos - 1),
Some(b' ' | b'\n' | b'\r' | b'\t' | b'{' | b'}' | b',')
);
let next_pos = absolute_pos + pattern.len();
let is_followed_by_digit =
next_pos < bytes.len() && bytes[next_pos].is_ascii_digit();
if is_selector_start && is_followed_by_digit {
let mut end = next_pos;
while end < bytes.len()
&& (bytes[end].is_ascii_alphanumeric()
|| bytes[end] == b'-'
|| bytes[end] == b'_')
{
end += 1;
}
result.add_diagnostic(
LintDiagnostic::warn(
META.name,
"Utility class should be in global styles, not component styles",
(offset + absolute_pos) as u32,
(offset + end) as u32,
)
.with_help(
"Use semantic class names in components, or import utility classes from a global stylesheet",
),
);
}
search_start = absolute_pos + 1;
}
}
}
}
#[cfg(test)]
mod tests {
use super::NoUtilityClasses;
use crate::rules::css::CssLinter;
fn create_linter() -> CssLinter {
let mut linter = CssLinter::new();
linter.add_rule(Box::new(NoUtilityClasses));
linter
}
#[test]
fn test_valid_semantic_class() {
let linter = create_linter();
let result = linter.lint(".my-component { display: flex; }", 0);
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_warns_flex_utility() {
let linter = create_linter();
let result = linter.lint(".flex { display: flex; }", 0);
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_warns_margin_utility() {
let linter = create_linter();
let result = linter.lint(".mt-4 { margin-top: 1rem; }", 0);
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_warns_text_center() {
let linter = create_linter();
let result = linter.lint(".text-center { text-align: center; }", 0);
assert_eq!(result.warning_count, 1);
}
}