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#[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 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#[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)] pub 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 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
406fn 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 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 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 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 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 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 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
627struct 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 source: IssueSource::from_source_only(source),
658 }
659 .resolved_cell()
660 .emit();
661 }
662 }
663 }
664}
665
666impl 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 !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}