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