swc_css_lints/rules/
font_family_no_duplicate_names.rs

1use rustc_hash::FxHashSet;
2use serde::{Deserialize, Serialize};
3use swc_common::Span;
4use swc_css_ast::*;
5use swc_css_visit::{Visit, VisitWith};
6
7use crate::{
8    dataset::is_generic_font_keyword,
9    pattern::NamePattern,
10    rule::{visitor_rule, LintRule, LintRuleContext},
11    ConfigError,
12};
13
14#[derive(Debug, Clone, Default, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct FontFamilyNoDuplicateNamesConfig {
17    ignore_font_family_names: Option<Vec<String>>,
18}
19
20pub fn font_family_no_duplicate_names(
21    ctx: LintRuleContext<FontFamilyNoDuplicateNamesConfig>,
22) -> Result<Box<dyn LintRule>, ConfigError> {
23    let ignored = ctx
24        .config()
25        .ignore_font_family_names
26        .clone()
27        .unwrap_or_default()
28        .into_iter()
29        .map(NamePattern::try_from)
30        .collect::<Result<_, _>>()?;
31    Ok(visitor_rule(
32        ctx.reaction(),
33        FontFamilyNoDuplicateNames { ctx, ignored },
34    ))
35}
36
37#[derive(Debug, Default)]
38struct FontFamilyNoDuplicateNames {
39    ctx: LintRuleContext<FontFamilyNoDuplicateNamesConfig>,
40    ignored: Vec<NamePattern>,
41}
42
43impl FontFamilyNoDuplicateNames {
44    fn check_component_values(&self, values: &[ComponentValue]) {
45        let (mut fonts, last) = values.iter().fold(
46            (
47                Vec::with_capacity(values.len()),
48                Option::<(String, Span)>::None,
49            ),
50            |(mut fonts, last_identifier), item| match item {
51                ComponentValue::Ident(ident) => {
52                    let Ident { value, span, .. } = &**ident;
53                    if let Some((mut identifier, last_span)) = last_identifier {
54                        identifier.push(' ');
55                        identifier.push_str(value);
56                        (fonts, Some((identifier, last_span.with_hi(span.hi()))))
57                    } else {
58                        (fonts, Some((value.to_string(), *span)))
59                    }
60                }
61                ComponentValue::Str(s) if s.raw.is_some() => {
62                    let raw = s.raw.as_ref().unwrap();
63                    fonts.push((FontNameKind::from(raw), s.span));
64                    (fonts, None)
65                }
66                ComponentValue::Delimiter(delimiter) if delimiter.value.is_comma() => {
67                    if let Some((identifier, span)) = last_identifier {
68                        fonts.push((FontNameKind::from(identifier), span));
69                    }
70                    (fonts, None)
71                }
72                _ => (fonts, last_identifier),
73            },
74        );
75        if let Some((identifier, span)) = last {
76            fonts.push((FontNameKind::from(identifier), span));
77        }
78
79        fonts
80            .iter()
81            .fold(FxHashSet::default(), |mut seen, (font, span)| {
82                let name = font.name();
83                if seen.contains(&font) && self.ignored.iter().all(|item| !item.is_match(name)) {
84                    self.ctx
85                        .report(span, format!("Unexpected duplicate name '{name}'."));
86                }
87                seen.insert(font);
88                seen
89            });
90    }
91}
92
93impl Visit for FontFamilyNoDuplicateNames {
94    fn visit_declaration(&mut self, declaration: &Declaration) {
95        match &declaration.name {
96            DeclarationName::Ident(Ident { value, .. })
97                if value.eq_ignore_ascii_case("font-family") =>
98            {
99                self.check_component_values(&declaration.value);
100            }
101            DeclarationName::Ident(Ident { value, .. }) if value.eq_ignore_ascii_case("font") => {
102                let index = declaration
103                    .value
104                    .iter()
105                    .enumerate()
106                    .rev()
107                    .find(|(_, item)| {
108                        matches!(
109                            item,
110                            ComponentValue::Integer(..)
111                                | ComponentValue::Number(..)
112                                | ComponentValue::Percentage(..)
113                                | ComponentValue::Dimension(..)
114                                | ComponentValue::Ratio(..)
115                                | ComponentValue::CalcSum(..)
116                        )
117                    })
118                    .map(|(i, _)| i);
119                if let Some(index) = index {
120                    self.check_component_values(&declaration.value[(index + 1)..]);
121                }
122            }
123            _ => {}
124        }
125        declaration.visit_children_with(self);
126    }
127}
128
129#[derive(Hash, PartialEq, Eq)]
130enum FontNameKind {
131    Normal(String),
132    Keyword(String),
133}
134
135impl FontNameKind {
136    #[inline]
137    fn name(&self) -> &str {
138        match self {
139            Self::Normal(name) => name.as_str(),
140            Self::Keyword(name) => name.as_str(),
141        }
142    }
143}
144
145impl<S> From<S> for FontNameKind
146where
147    S: AsRef<str>,
148{
149    fn from(name: S) -> Self {
150        if let Some(name) = name
151            .as_ref()
152            .strip_prefix('\'')
153            .and_then(|name| name.strip_suffix('\''))
154            .map(|name| name.trim())
155        {
156            if is_generic_font_keyword(name) {
157                Self::Keyword(name.to_string())
158            } else {
159                Self::Normal(name.to_string())
160            }
161        } else if let Some(name) = name
162            .as_ref()
163            .strip_prefix('"')
164            .and_then(|name| name.strip_suffix('"'))
165            .map(|name| name.trim())
166        {
167            if is_generic_font_keyword(name) {
168                Self::Keyword(name.to_string())
169            } else {
170                Self::Normal(name.to_string())
171            }
172        } else {
173            Self::Normal(name.as_ref().trim().to_string())
174        }
175    }
176}