turbopack_css/
process.rs

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