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