Skip to main content

turbopack_css/
process.rs

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