turbopack_css/
process.rs

1use std::sync::{Arc, RwLock};
2
3use anyhow::{Result, bail};
4use lightningcss::{
5    css_modules::{CssModuleExport, Pattern, Segment},
6    stylesheet::{MinifyOptions, ParserOptions, PrinterOptions, StyleSheet, ToCssResult},
7    targets::{BrowserslistConfig, Features, Targets},
8    traits::ToCss,
9    values::url::Url,
10    visit_types,
11    visitor::Visit,
12};
13use rustc_hash::FxHashMap;
14use smallvec::smallvec;
15use swc_core::base::sourcemap::SourceMapBuilder;
16use tracing::Instrument;
17use turbo_rcstr::{RcStr, rcstr};
18use turbo_tasks::{FxIndexMap, ResolvedVc, ValueToString, Vc};
19use turbo_tasks_fs::{FileContent, FileSystemPath, rope::Rope};
20use turbopack_core::{
21    SOURCE_URL_PROTOCOL,
22    asset::{Asset, AssetContent},
23    chunk::{ChunkingContext, MinifyType},
24    environment::Environment,
25    issue::{
26        Issue, IssueExt, IssueSource, IssueStage, OptionIssueSource, OptionStyledString,
27        StyledString,
28    },
29    reference::ModuleReferences,
30    reference_type::ImportContext,
31    resolve::origin::ResolveOrigin,
32    source::Source,
33    source_map::{OptionStringifiedSourceMap, utils::add_default_ignore_list},
34    source_pos::SourcePos,
35};
36
37use crate::{
38    CssModuleAssetType,
39    lifetime_util::stylesheet_into_static,
40    references::{
41        analyze_references,
42        url::{UrlAssetReference, replace_url_references, resolve_url_reference},
43    },
44};
45
46#[derive(Debug)]
47pub struct StyleSheetLike<'i, 'o>(pub(crate) StyleSheet<'i, 'o>);
48
49impl PartialEq for StyleSheetLike<'_, '_> {
50    fn eq(&self, _: &Self) -> bool {
51        false
52    }
53}
54
55pub type CssOutput = (ToCssResult, Option<Rope>);
56
57#[turbo_tasks::value(transparent)]
58struct LightningCssTargets(#[turbo_tasks(trace_ignore)] pub Targets);
59
60/// Returns the LightningCSS targets for the given browserslist query.
61#[turbo_tasks::function]
62async fn get_lightningcss_browser_targets(
63    environment: Option<ResolvedVc<Environment>>,
64    handle_nesting: bool,
65) -> Result<Vc<LightningCssTargets>> {
66    match environment {
67        Some(environment) => {
68            let browserslist_query = environment.browserslist_query().owned().await?;
69            let browserslist_browsers =
70                lightningcss::targets::Browsers::from_browserslist_with_config(
71                    browserslist_query.split(','),
72                    BrowserslistConfig {
73                        ignore_unknown_versions: true,
74                        ..Default::default()
75                    },
76                )?;
77
78            Ok(if handle_nesting {
79                Vc::cell(Targets {
80                    browsers: browserslist_browsers,
81                    include: Features::Nesting | Features::MediaRangeSyntax,
82                    ..Default::default()
83                })
84            } else {
85                Vc::cell(Targets {
86                    browsers: browserslist_browsers,
87                    include: Features::MediaRangeSyntax,
88                    ..Default::default()
89                })
90            })
91        }
92        // Default when empty environment is passed.
93        None => Ok(Vc::cell(Default::default())),
94    }
95}
96
97impl StyleSheetLike<'_, '_> {
98    pub fn to_static(
99        &self,
100        options: ParserOptions<'static, 'static>,
101    ) -> StyleSheetLike<'static, 'static> {
102        StyleSheetLike(stylesheet_into_static(&self.0, options))
103    }
104
105    pub async fn to_css(
106        &self,
107        code: &str,
108        minify_type: MinifyType,
109        enable_srcmap: bool,
110        handle_nesting: bool,
111        mut origin_source_map: Option<parcel_sourcemap::SourceMap>,
112        environment: Option<ResolvedVc<Environment>>,
113    ) -> Result<CssOutput> {
114        let ss = &self.0;
115        let mut srcmap = if enable_srcmap {
116            Some(parcel_sourcemap::SourceMap::new(""))
117        } else {
118            None
119        };
120
121        let targets =
122            *get_lightningcss_browser_targets(environment.as_deref().copied(), handle_nesting)
123                .await?;
124
125        let result = ss.to_css(PrinterOptions {
126            minify: matches!(minify_type, MinifyType::Minify { .. }),
127            source_map: srcmap.as_mut(),
128            targets,
129            analyze_dependencies: None,
130            ..Default::default()
131        })?;
132
133        if let Some(srcmap) = &mut srcmap {
134            debug_assert_eq!(ss.sources.len(), 1);
135
136            if let Some(origin_source_map) = origin_source_map.as_mut() {
137                let _ = srcmap.extends(origin_source_map);
138            } else {
139                srcmap.add_sources(ss.sources.clone());
140                srcmap.set_source_content(0, code)?;
141            }
142        }
143
144        let srcmap = match srcmap {
145            Some(srcmap) => Some(generate_css_source_map(&srcmap)?),
146            None => None,
147        };
148
149        Ok((result, srcmap))
150    }
151}
152
153/// Multiple [ModuleReference]s
154#[turbo_tasks::value(transparent)]
155pub struct UnresolvedUrlReferences(pub Vec<(String, ResolvedVc<UrlAssetReference>)>);
156
157#[turbo_tasks::value(shared, serialization = "none", eq = "manual", cell = "new")]
158#[allow(clippy::large_enum_variant)] // This is a turbo-tasks value
159pub enum ParseCssResult {
160    Ok {
161        code: ResolvedVc<FileContent>,
162
163        #[turbo_tasks(trace_ignore)]
164        stylesheet: StyleSheetLike<'static, 'static>,
165
166        references: ResolvedVc<ModuleReferences>,
167
168        url_references: ResolvedVc<UnresolvedUrlReferences>,
169
170        #[turbo_tasks(trace_ignore)]
171        options: ParserOptions<'static, 'static>,
172    },
173    Unparsable,
174    NotFound,
175}
176
177#[turbo_tasks::value(shared, serialization = "none", eq = "manual", cell = "new")]
178pub enum CssWithPlaceholderResult {
179    Ok {
180        parse_result: ResolvedVc<ParseCssResult>,
181
182        references: ResolvedVc<ModuleReferences>,
183
184        url_references: ResolvedVc<UnresolvedUrlReferences>,
185
186        #[turbo_tasks(trace_ignore)]
187        exports: Option<FxIndexMap<String, CssModuleExport>>,
188
189        #[turbo_tasks(trace_ignore)]
190        placeholders: FxHashMap<String, Url<'static>>,
191    },
192    Unparsable,
193    NotFound,
194}
195
196#[turbo_tasks::value(shared, serialization = "none")]
197pub enum FinalCssResult {
198    Ok {
199        #[turbo_tasks(trace_ignore)]
200        output_code: String,
201
202        source_map: ResolvedVc<OptionStringifiedSourceMap>,
203    },
204    Unparsable,
205    NotFound,
206}
207
208#[turbo_tasks::function]
209pub async fn process_css_with_placeholder(
210    parse_result: ResolvedVc<ParseCssResult>,
211    environment: Option<ResolvedVc<Environment>>,
212) -> Result<Vc<CssWithPlaceholderResult>> {
213    let result = parse_result.await?;
214
215    match &*result {
216        ParseCssResult::Ok {
217            stylesheet,
218            references,
219            url_references,
220            code,
221            ..
222        } => {
223            let code = code.await?;
224            let code = match &*code {
225                FileContent::Content(v) => v.content().to_str()?,
226                _ => bail!("this case should be filtered out while parsing"),
227            };
228
229            // We use NoMinify because this is not a final css. We need to replace url references,
230            // and we do final codegen with proper minification.
231            let (result, _) = stylesheet
232                .to_css(&code, MinifyType::NoMinify, false, false, None, environment)
233                .await?;
234
235            let exports = result.exports.map(|exports| {
236                let mut exports = exports.into_iter().collect::<FxIndexMap<_, _>>();
237
238                exports.sort_keys();
239
240                exports
241            });
242
243            Ok(CssWithPlaceholderResult::Ok {
244                parse_result,
245                exports,
246                references: *references,
247                url_references: *url_references,
248                placeholders: FxHashMap::default(),
249            }
250            .cell())
251        }
252        ParseCssResult::Unparsable => Ok(CssWithPlaceholderResult::Unparsable.cell()),
253        ParseCssResult::NotFound => Ok(CssWithPlaceholderResult::NotFound.cell()),
254    }
255}
256
257#[turbo_tasks::function]
258pub async fn finalize_css(
259    result: Vc<CssWithPlaceholderResult>,
260    chunking_context: Vc<Box<dyn ChunkingContext>>,
261    minify_type: MinifyType,
262    origin_source_map: Vc<OptionStringifiedSourceMap>,
263    environment: Option<ResolvedVc<Environment>>,
264) -> Result<Vc<FinalCssResult>> {
265    let result = result.await?;
266    match &*result {
267        CssWithPlaceholderResult::Ok {
268            parse_result,
269            url_references,
270            ..
271        } => {
272            let (mut stylesheet, code) = match &*parse_result.await? {
273                ParseCssResult::Ok {
274                    stylesheet,
275                    options,
276                    code,
277                    ..
278                } => (stylesheet.to_static(options.clone()), *code),
279                ParseCssResult::Unparsable => return Ok(FinalCssResult::Unparsable.cell()),
280                ParseCssResult::NotFound => return Ok(FinalCssResult::NotFound.cell()),
281            };
282
283            let url_references = *url_references;
284
285            let mut url_map = FxHashMap::default();
286
287            for (src, reference) in (*url_references.await?).iter() {
288                let resolved = resolve_url_reference(**reference, chunking_context).await?;
289                if let Some(v) = resolved.as_ref().cloned() {
290                    url_map.insert(RcStr::from(src.as_str()), v);
291                }
292            }
293
294            replace_url_references(&mut stylesheet, &url_map);
295
296            let code = code.await?;
297            let code = match &*code {
298                FileContent::Content(v) => v.content().to_str()?,
299                _ => bail!("this case should be filtered out while parsing"),
300            };
301
302            let origin_source_map = if let Some(rope) = &*origin_source_map.await? {
303                Some(parcel_sourcemap::SourceMap::from_json("", &rope.to_str()?)?)
304            } else {
305                None
306            };
307
308            let (result, srcmap) = stylesheet
309                .to_css(
310                    &code,
311                    minify_type,
312                    true,
313                    true,
314                    origin_source_map,
315                    environment,
316                )
317                .await?;
318
319            Ok(FinalCssResult::Ok {
320                output_code: result.code,
321                source_map: ResolvedVc::cell(srcmap),
322            }
323            .cell())
324        }
325        CssWithPlaceholderResult::Unparsable => Ok(FinalCssResult::Unparsable.cell()),
326        CssWithPlaceholderResult::NotFound => Ok(FinalCssResult::NotFound.cell()),
327    }
328}
329
330#[turbo_tasks::value_trait]
331pub trait ParseCss {
332    #[turbo_tasks::function]
333    async fn parse_css(self: Vc<Self>) -> Result<Vc<ParseCssResult>>;
334}
335
336#[turbo_tasks::value_trait]
337pub trait ProcessCss: ParseCss {
338    #[turbo_tasks::function]
339    async fn get_css_with_placeholder(self: Vc<Self>) -> Result<Vc<CssWithPlaceholderResult>>;
340
341    #[turbo_tasks::function]
342    async fn finalize_css(
343        self: Vc<Self>,
344        chunking_context: Vc<Box<dyn ChunkingContext>>,
345        minify_type: MinifyType,
346    ) -> Result<Vc<FinalCssResult>>;
347}
348
349#[turbo_tasks::function]
350pub async fn parse_css(
351    source: ResolvedVc<Box<dyn Source>>,
352    origin: ResolvedVc<Box<dyn ResolveOrigin>>,
353    import_context: Option<ResolvedVc<ImportContext>>,
354    ty: CssModuleAssetType,
355    environment: Option<ResolvedVc<Environment>>,
356) -> Result<Vc<ParseCssResult>> {
357    let span = tracing::info_span!(
358        "parse css",
359        name = display(source.ident().to_string().await?)
360    );
361    async move {
362        let content = source.content();
363        let ident_str = &*source.ident().to_string().await?;
364        Ok(match &*content.await? {
365            AssetContent::Redirect { .. } => ParseCssResult::Unparsable.cell(),
366            AssetContent::File(file_content) => match &*file_content.await? {
367                FileContent::NotFound => ParseCssResult::NotFound.cell(),
368                FileContent::Content(file) => match file.content().to_str() {
369                    Err(_err) => ParseCssResult::Unparsable.cell(),
370                    Ok(string) => {
371                        process_content(
372                            *file_content,
373                            string.into_owned(),
374                            ident_str,
375                            source,
376                            origin,
377                            import_context,
378                            ty,
379                            environment,
380                        )
381                        .await?
382                    }
383                },
384            },
385        })
386    }
387    .instrument(span)
388    .await
389}
390
391async fn process_content(
392    content_vc: ResolvedVc<FileContent>,
393    code: String,
394    filename: &str,
395    source: ResolvedVc<Box<dyn Source>>,
396    origin: ResolvedVc<Box<dyn ResolveOrigin>>,
397    import_context: Option<ResolvedVc<ImportContext>>,
398    ty: CssModuleAssetType,
399    environment: Option<ResolvedVc<Environment>>,
400) -> Result<Vc<ParseCssResult>> {
401    #[allow(clippy::needless_lifetimes)]
402    fn without_warnings<'o, 'i>(config: ParserOptions<'o, 'i>) -> ParserOptions<'o, 'static> {
403        ParserOptions {
404            filename: config.filename,
405            css_modules: config.css_modules,
406            source_index: config.source_index,
407            error_recovery: config.error_recovery,
408            warnings: None,
409            flags: config.flags,
410        }
411    }
412
413    let config = ParserOptions {
414        css_modules: match ty {
415            CssModuleAssetType::Module => Some(lightningcss::css_modules::Config {
416                pattern: Pattern {
417                    segments: smallvec![
418                        Segment::Name,
419                        Segment::Literal("__"),
420                        Segment::Hash,
421                        Segment::Literal("__"),
422                        Segment::Local,
423                    ],
424                },
425                dashed_idents: false,
426                grid: false,
427                container: false,
428                ..Default::default()
429            }),
430
431            _ => None,
432        },
433        filename: filename.to_string(),
434        error_recovery: true,
435        ..Default::default()
436    };
437
438    let stylesheet = StyleSheetLike({
439        let warnings: Arc<RwLock<_>> = Default::default();
440
441        match StyleSheet::parse(
442            &code,
443            ParserOptions {
444                warnings: Some(warnings.clone()),
445                ..config.clone()
446            },
447        ) {
448            Ok(mut ss) => {
449                if matches!(ty, CssModuleAssetType::Module) {
450                    let mut validator = CssValidator { errors: Vec::new() };
451                    ss.visit(&mut validator).unwrap();
452
453                    for err in validator.errors {
454                        err.report(source);
455                    }
456                }
457
458                // We need to collect here because we need to avoid holding the lock while calling
459                // `.await` in the loop.
460                let warnings = warnings.read().unwrap().iter().cloned().collect::<Vec<_>>();
461                for err in warnings.iter() {
462                    match err.kind {
463                        lightningcss::error::ParserError::UnexpectedToken(_)
464                        | lightningcss::error::ParserError::UnexpectedImportRule
465                        | lightningcss::error::ParserError::SelectorError(..)
466                        | lightningcss::error::ParserError::EndOfInput => {
467                            let source = match &err.loc {
468                                Some(loc) => {
469                                    let pos = SourcePos {
470                                        line: loc.line as _,
471                                        column: (loc.column - 1) as _,
472                                    };
473                                    IssueSource::from_line_col(source, pos, pos)
474                                }
475                                None => IssueSource::from_source_only(source),
476                            };
477
478                            ParsingIssue {
479                                msg: err.kind.to_string().into(),
480                                stage: IssueStage::Parse,
481                                source,
482                            }
483                            .resolved_cell()
484                            .emit();
485                            return Ok(ParseCssResult::Unparsable.cell());
486                        }
487
488                        _ => {
489                            // Ignore
490                        }
491                    }
492                }
493
494                let targets =
495                    *get_lightningcss_browser_targets(environment.as_deref().copied(), true)
496                        .await?;
497
498                // minify() is actually transform, and it performs operations like CSS modules
499                // handling.
500                //
501                // See: https://github.com/parcel-bundler/lightningcss/issues/935#issuecomment-2739325537
502                if let Err(e) = ss.minify(MinifyOptions {
503                    targets,
504                    ..Default::default()
505                }) {
506                    let source = match &e.loc {
507                        Some(loc) => {
508                            let pos = SourcePos {
509                                line: loc.line as _,
510                                column: (loc.column - 1) as _,
511                            };
512                            IssueSource::from_line_col(source, pos, pos)
513                        }
514                        None => IssueSource::from_source_only(source),
515                    };
516                    ParsingIssue {
517                        msg: e.kind.to_string().into(),
518                        stage: IssueStage::Transform,
519                        source,
520                    }
521                    .resolved_cell()
522                    .emit();
523                    return Ok(ParseCssResult::Unparsable.cell());
524                }
525
526                stylesheet_into_static(&ss, without_warnings(config.clone()))
527            }
528            Err(e) => {
529                let source = match &e.loc {
530                    Some(loc) => {
531                        let pos = SourcePos {
532                            line: loc.line as _,
533                            column: (loc.column - 1) as _,
534                        };
535                        IssueSource::from_line_col(source, pos, pos)
536                    }
537                    None => IssueSource::from_source_only(source),
538                };
539                ParsingIssue {
540                    msg: e.kind.to_string().into(),
541                    stage: IssueStage::Parse,
542                    source,
543                }
544                .resolved_cell()
545                .emit();
546                return Ok(ParseCssResult::Unparsable.cell());
547            }
548        }
549    });
550
551    let config = without_warnings(config);
552    let mut stylesheet = stylesheet.to_static(config.clone());
553
554    let (references, url_references) =
555        analyze_references(&mut stylesheet, source, origin, import_context).await?;
556
557    Ok(ParseCssResult::Ok {
558        code: content_vc,
559        stylesheet,
560        references: ResolvedVc::cell(references),
561        url_references: ResolvedVc::cell(url_references),
562        options: config,
563    }
564    .cell())
565}
566
567/// Visitor that lints wrong css module usage.
568///
569/// ```css
570/// button {
571/// }
572/// ```
573///
574/// is wrong for a css module because it doesn't have a class name.
575struct CssValidator {
576    errors: Vec<CssError>,
577}
578
579#[derive(Debug, PartialEq, Eq)]
580enum CssError {
581    CssSelectorInModuleNotPure { selector: String },
582}
583
584impl CssError {
585    fn report(self, source: ResolvedVc<Box<dyn Source>>) {
586        match self {
587            CssError::CssSelectorInModuleNotPure { selector } => {
588                ParsingIssue {
589                    msg: format!(
590                        "Selector \"{selector}\" is not pure. Pure selectors must contain at \
591                         least one local class or id."
592                    )
593                    .into(),
594                    stage: IssueStage::Transform,
595                    // TODO: This should include the location of the selector in the file.
596                    source: IssueSource::from_source_only(source),
597                }
598                .resolved_cell()
599                .emit();
600            }
601        }
602    }
603}
604
605/// We only visit top-level selectors.
606impl lightningcss::visitor::Visitor<'_> for CssValidator {
607    type Error = ();
608
609    fn visit_types(&self) -> lightningcss::visitor::VisitTypes {
610        visit_types!(SELECTORS)
611    }
612
613    fn visit_selector(
614        &mut self,
615        selector: &mut lightningcss::selector::Selector<'_>,
616    ) -> Result<(), Self::Error> {
617        fn is_selector_problematic(sel: &lightningcss::selector::Selector) -> bool {
618            sel.iter_raw_parse_order_from(0).all(is_problematic)
619        }
620
621        fn is_problematic(c: &lightningcss::selector::Component) -> bool {
622            match c {
623                parcel_selectors::parser::Component::ID(..)
624                | parcel_selectors::parser::Component::Class(..) => false,
625
626                parcel_selectors::parser::Component::Combinator(..)
627                | parcel_selectors::parser::Component::AttributeOther(..)
628                | parcel_selectors::parser::Component::AttributeInNoNamespaceExists { .. }
629                | parcel_selectors::parser::Component::AttributeInNoNamespace { .. }
630                | parcel_selectors::parser::Component::ExplicitUniversalType
631                | parcel_selectors::parser::Component::Negation(..) => true,
632
633                parcel_selectors::parser::Component::Where(sel) => {
634                    sel.iter().all(is_selector_problematic)
635                }
636
637                parcel_selectors::parser::Component::LocalName(local) => {
638                    // Allow html and body. They are not pure selectors but are allowed.
639                    !matches!(&*local.name.0, "html" | "body")
640                }
641                _ => false,
642            }
643        }
644
645        if is_selector_problematic(selector) {
646            let selector_string = selector
647                .to_css_string(PrinterOptions {
648                    minify: false,
649                    ..Default::default()
650                })
651                .expect("selector.to_css_string should not fail");
652            self.errors.push(CssError::CssSelectorInModuleNotPure {
653                selector: selector_string,
654            });
655        }
656
657        Ok(())
658    }
659}
660
661fn generate_css_source_map(source_map: &parcel_sourcemap::SourceMap) -> Result<Rope> {
662    let mut builder = SourceMapBuilder::new(None);
663
664    for src in source_map.get_sources() {
665        builder.add_source(format!("{SOURCE_URL_PROTOCOL}///{src}").into());
666    }
667
668    for (idx, content) in source_map.get_sources_content().iter().enumerate() {
669        builder.set_source_contents(idx as _, Some(content.clone().into()));
670    }
671
672    for m in source_map.get_mappings() {
673        builder.add_raw(
674            m.generated_line,
675            m.generated_column,
676            m.original.map(|v| v.original_line).unwrap_or_default(),
677            m.original.map(|v| v.original_column).unwrap_or_default(),
678            Some(0),
679            None,
680            false,
681        );
682    }
683
684    let mut map = builder.into_sourcemap();
685    add_default_ignore_list(&mut map);
686    let mut result = vec![];
687    map.to_writer(&mut result)?;
688    Ok(Rope::from(result))
689}
690
691#[turbo_tasks::value]
692struct ParsingIssue {
693    msg: RcStr,
694    stage: IssueStage,
695    source: IssueSource,
696}
697
698#[turbo_tasks::value_impl]
699impl Issue for ParsingIssue {
700    #[turbo_tasks::function]
701    fn file_path(&self) -> Vc<FileSystemPath> {
702        self.source.file_path()
703    }
704
705    #[turbo_tasks::function]
706    fn stage(&self) -> Vc<IssueStage> {
707        self.stage.clone().cell()
708    }
709
710    #[turbo_tasks::function]
711    fn title(&self) -> Vc<StyledString> {
712        StyledString::Text(match self.stage {
713            IssueStage::Parse => rcstr!("Parsing CSS source code failed"),
714            IssueStage::Transform => rcstr!("Transforming CSS failed"),
715            _ => rcstr!("CSS processing failed"),
716        })
717        .cell()
718    }
719
720    #[turbo_tasks::function]
721    fn source(&self) -> Vc<OptionIssueSource> {
722        Vc::cell(Some(self.source))
723    }
724
725    #[turbo_tasks::function]
726    fn description(&self) -> Result<Vc<OptionStyledString>> {
727        Ok(Vc::cell(Some(
728            StyledString::Text(self.msg.clone()).resolved_cell(),
729        )))
730    }
731}
732
733#[cfg(test)]
734mod tests {
735    use lightningcss::{
736        css_modules::Pattern,
737        stylesheet::{ParserOptions, StyleSheet},
738        visitor::Visit,
739    };
740
741    use super::{CssError, CssValidator};
742
743    fn lint_lightningcss(code: &str) -> Vec<CssError> {
744        let mut ss = StyleSheet::parse(
745            code,
746            ParserOptions {
747                css_modules: Some(lightningcss::css_modules::Config {
748                    pattern: Pattern::default(),
749                    dashed_idents: false,
750                    grid: false,
751                    container: false,
752                    ..Default::default()
753                }),
754                ..Default::default()
755            },
756        )
757        .unwrap();
758
759        let mut validator = CssValidator { errors: Vec::new() };
760        ss.visit(&mut validator).unwrap();
761
762        validator.errors
763    }
764
765    #[track_caller]
766    fn assert_lint_success(code: &str) {
767        assert_eq!(lint_lightningcss(code), vec![], "lightningcss: {code}");
768    }
769
770    #[track_caller]
771    fn assert_lint_failure(code: &str) {
772        assert_ne!(lint_lightningcss(code), vec![], "lightningcss: {code}");
773    }
774
775    #[test]
776    fn css_module_pure_lint() {
777        assert_lint_success(
778            "html {
779                --foo: 1;
780            }",
781        );
782
783        assert_lint_success(
784            "#id {
785                color: red;
786            }",
787        );
788
789        assert_lint_success(
790            ".class {
791                color: red;
792            }",
793        );
794
795        assert_lint_success(
796            "html.class {
797                color: red;
798            }",
799        );
800
801        assert_lint_success(
802            ".class > * {
803                color: red;
804            }",
805        );
806
807        assert_lint_success(
808            ".class * {
809                color: red;
810            }",
811        );
812
813        assert_lint_success(
814            ":where(.main > *) {
815                color: red;
816            }",
817        );
818
819        assert_lint_success(
820            ":where(.main > *, .root > *) {
821                color: red;
822            }",
823        );
824        assert_lint_success(
825            ".style {
826                background-image: var(--foo);
827            }",
828        );
829
830        assert_lint_failure(
831            "div {
832                color: red;
833            }",
834        );
835
836        assert_lint_failure(
837            "div > span {
838                color: red;
839            }",
840        );
841
842        assert_lint_failure(
843            "div span {
844                color: red;
845            }",
846        );
847
848        assert_lint_failure(
849            "div[data-foo] {
850                color: red;
851            }",
852        );
853
854        assert_lint_failure(
855            "div[data-foo=\"bar\"] {
856                color: red;
857            }",
858        );
859
860        assert_lint_failure(
861            "div[data-foo=\"bar\"] span {
862                color: red;
863            }",
864        );
865
866        assert_lint_failure(
867            "* {
868                --foo: 1;
869            }",
870        );
871
872        assert_lint_failure(
873            "[data-foo] {
874                --foo: 1;
875            }",
876        );
877
878        assert_lint_failure(
879            ":not(.class) {
880                --foo: 1;
881            }",
882        );
883
884        assert_lint_failure(
885            ":not(div) {
886                --foo: 1;
887            }",
888        );
889
890        assert_lint_failure(
891            ":where(div > *) {
892                color: red;
893            }",
894        );
895
896        assert_lint_failure(
897            ":where(div) {
898                color: red;
899            }",
900        );
901    }
902}