swc_css_lints/rules/
font_family_no_duplicate_names.rs1use 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}