swc_css_lints/rules/
no_invalid_position_at_import_rule.rs

1use serde::{Deserialize, Serialize};
2use swc_css_ast::*;
3use swc_css_visit::{Visit, VisitWith};
4
5use crate::{
6    pattern::NamePattern,
7    rule::{visitor_rule, LintRule, LintRuleContext},
8    ConfigError,
9};
10
11const MESSAGE: &str = "Unexpected invalid position '@import' rule.";
12
13#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct NoInvalidPositionAtImportRuleConfig {
16    ignore_at_rules: Option<Vec<String>>,
17}
18
19pub fn no_invalid_position_at_import_rule(
20    ctx: LintRuleContext<NoInvalidPositionAtImportRuleConfig>,
21) -> Result<Box<dyn LintRule>, ConfigError> {
22    let ignored = ctx
23        .config()
24        .ignore_at_rules
25        .clone()
26        .unwrap_or_default()
27        .into_iter()
28        .map(NamePattern::try_from)
29        .collect::<Result<_, _>>()?;
30    Ok(visitor_rule(
31        ctx.reaction(),
32        NoInvalidPositionAtImportRule { ctx, ignored },
33    ))
34}
35
36#[derive(Debug, Default)]
37struct NoInvalidPositionAtImportRule {
38    ctx: LintRuleContext<NoInvalidPositionAtImportRuleConfig>,
39    ignored: Vec<NamePattern>,
40}
41
42impl Visit for NoInvalidPositionAtImportRule {
43    fn visit_stylesheet(&mut self, stylesheet: &Stylesheet) {
44        stylesheet.rules.iter().fold(false, |seen, rule| {
45            if seen
46                && rule
47                    .as_at_rule()
48                    .and_then(|at_rule| at_rule.prelude.as_ref())
49                    .map(|prelude| prelude.is_import_prelude())
50                    .unwrap_or_default()
51            {
52                self.ctx.report(rule, MESSAGE);
53            }
54
55            // TODO improve me https://www.w3.org/TR/css-cascade-5/#layer-empty - @import and @namespace rules must be consecutive
56            let Rule::AtRule(at_rule) = rule else {
57                return true;
58            };
59
60            let AtRule {
61                name,
62                prelude,
63                block,
64                ..
65            } = &**at_rule;
66
67            if let Some(prelude) = prelude {
68                match &**prelude {
69                    AtRulePrelude::CharsetPrelude(_) | AtRulePrelude::ImportPrelude(_) => {
70                        return seen
71                    }
72                    AtRulePrelude::LayerPrelude(_) => match block {
73                        Some(block) if block.value.is_empty() => return seen,
74                        None => return seen,
75                        _ => return true,
76                    },
77                    _ => {}
78                }
79            };
80
81            let name = match name {
82                AtRuleName::DashedIdent(dashed_ident) => &dashed_ident.value,
83                AtRuleName::Ident(ident) => &ident.value,
84            };
85
86            if self.ignored.iter().any(|item| item.is_match(name)) {
87                seen
88            } else {
89                true
90            }
91        });
92
93        stylesheet.visit_children_with(self);
94    }
95}