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(trace_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
163 #[turbo_tasks(trace_ignore)]
164 stylesheet: StyleSheet<'static>,
165
166 references: ResolvedVc<ModuleReferences>,
167
168 url_references: ResolvedVc<UnresolvedUrlReferences>,
169
170 #[turbo_tasks(trace_ignore)]
171 options: ParserOptions<'static>,
172 },
173 Unparsable,
174 NotFound,
175}
176
177#[turbo_tasks::value(shared, serialization = "skip", eq = "manual", cell = "new")]
178pub enum CssWithPlaceholderResult {
179 Ok {
180 parse_result: ResolvedVc<ParseCssResult>,
181
182 references: ResolvedVc<ModuleReferences>,
183
184 url_references: ResolvedVc<UnresolvedUrlReferences>,
185
186 #[turbo_tasks(trace_ignore)]
187 exports: Option<FxIndexMap<String, CssModuleExport>>,
188
189 #[turbo_tasks(trace_ignore)]
190 placeholders: FxHashMap<String, Url<'static>>,
191 },
192 Unparsable,
193 NotFound,
194}
195
196#[turbo_tasks::value(shared, serialization = "skip")]
197#[allow(clippy::large_enum_variant)] pub enum FinalCssResult {
199 Ok {
200 #[turbo_tasks(trace_ignore)]
201 output_code: String,
202
203 source_map: Option<StructuredSourceMap>,
204 },
205 Unparsable,
206 NotFound,
207}
208
209#[turbo_tasks::function]
210pub async fn process_css_with_placeholder(
211 parse_result: ResolvedVc<ParseCssResult>,
212 environment: Option<ResolvedVc<Environment>>,
213 feature_flags: LightningCssFeatureFlags,
214) -> Result<Vc<CssWithPlaceholderResult>> {
215 let result = parse_result.await?;
216
217 match &*result {
218 ParseCssResult::Ok {
219 stylesheet,
220 references,
221 url_references,
222 code,
223 ..
224 } => {
225 let code = code.await?;
226 let code = match &*code {
227 FileContent::Content(v) => v.content().to_str()?,
228 _ => bail!("this case should be filtered out while parsing"),
229 };
230
231 let (result, _) = stylesheet_to_css(
234 stylesheet,
235 &code,
236 MinifyType::NoMinify,
237 false,
238 false,
239 None,
240 environment,
241 feature_flags,
242 )
243 .await?;
244
245 let exports = result.exports.map(|exports| {
246 let mut exports = exports.into_iter().collect::<FxIndexMap<_, _>>();
247
248 exports.sort_keys();
249
250 exports
251 });
252
253 Ok(CssWithPlaceholderResult::Ok {
254 parse_result,
255 exports,
256 references: *references,
257 url_references: *url_references,
258 placeholders: FxHashMap::default(),
259 }
260 .cell())
261 }
262 ParseCssResult::Unparsable => Ok(CssWithPlaceholderResult::Unparsable.cell()),
263 ParseCssResult::NotFound => Ok(CssWithPlaceholderResult::NotFound.cell()),
264 }
265}
266
267#[turbo_tasks::function]
268pub async fn finalize_css(
269 result: Vc<CssWithPlaceholderResult>,
270 chunking_context: Vc<Box<dyn ChunkingContext>>,
271 minify_type: MinifyType,
272 origin_source_map: Vc<FileContent>,
273 environment: Option<ResolvedVc<Environment>>,
274 feature_flags: LightningCssFeatureFlags,
275) -> Result<Vc<FinalCssResult>> {
276 let result = result.await?;
277 match &*result {
278 CssWithPlaceholderResult::Ok {
279 parse_result,
280 url_references,
281 ..
282 } => {
283 let (mut stylesheet, code) = match &*parse_result.await? {
284 ParseCssResult::Ok {
285 stylesheet,
286 options,
287 code,
288 ..
289 } => (stylesheet_into_static(stylesheet, options.clone()), *code),
290 ParseCssResult::Unparsable => return Ok(FinalCssResult::Unparsable.cell()),
291 ParseCssResult::NotFound => return Ok(FinalCssResult::NotFound.cell()),
292 };
293
294 let url_references = *url_references;
295
296 let mut url_map = FxHashMap::default();
297
298 for (src, reference) in (*url_references.await?).iter() {
299 let resolved = resolve_url_reference(**reference, chunking_context).await?;
300 if let Some(v) = resolved.as_ref().cloned() {
301 url_map.insert(RcStr::from(src.as_str()), v);
302 }
303 }
304
305 replace_url_references(&mut stylesheet, &url_map);
306
307 let code = code.await?;
308 let code = match &*code {
309 FileContent::Content(v) => v.content().to_str()?,
310 _ => bail!("this case should be filtered out while parsing"),
311 };
312
313 let origin_source_map = if let Some(rope) = origin_source_map.await?.as_content() {
314 Some(parcel_sourcemap::SourceMap::from_json(
315 "",
316 &rope.content().to_str()?,
317 )?)
318 } else {
319 None
320 };
321
322 let (result, srcmap) = stylesheet_to_css(
323 &stylesheet,
324 &code,
325 minify_type,
326 true,
327 true,
328 origin_source_map,
329 environment,
330 feature_flags,
331 )
332 .await?;
333
334 Ok(FinalCssResult::Ok {
335 output_code: result.code,
336 source_map: srcmap,
337 }
338 .cell())
339 }
340 CssWithPlaceholderResult::Unparsable => Ok(FinalCssResult::Unparsable.cell()),
341 CssWithPlaceholderResult::NotFound => Ok(FinalCssResult::NotFound.cell()),
342 }
343}
344
345#[turbo_tasks::value_trait]
346pub trait ParseCss {
347 #[turbo_tasks::function]
348 async fn parse_css(self: Vc<Self>) -> Result<Vc<ParseCssResult>>;
349}
350
351#[turbo_tasks::value_trait]
352pub trait ProcessCss: ParseCss {
353 #[turbo_tasks::function]
354 async fn get_css_with_placeholder(self: Vc<Self>) -> Result<Vc<CssWithPlaceholderResult>>;
355
356 #[turbo_tasks::function]
357 async fn finalize_css(
358 self: Vc<Self>,
359 chunking_context: Vc<Box<dyn ChunkingContext>>,
360 minify_type: MinifyType,
361 ) -> Result<Vc<FinalCssResult>>;
362}
363
364#[turbo_tasks::function]
365pub async fn parse_css(
366 source: ResolvedVc<Box<dyn Source>>,
367 origin: ResolvedVc<Box<dyn ResolveOrigin>>,
368 import_context: Option<ResolvedVc<ImportContext>>,
369 ty: CssModuleType,
370 environment: Option<ResolvedVc<Environment>>,
371 feature_flags: LightningCssFeatureFlags,
372) -> Result<Vc<ParseCssResult>> {
373 let span = tracing::info_span!(
374 "parse css",
375 name = display(source.ident().to_string().await?)
376 );
377 async move {
378 let content = source.content();
379 let ident_str = &*source.ident().to_string().await?;
380 Ok(match &*content.await? {
381 AssetContent::Redirect { .. } => ParseCssResult::Unparsable.cell(),
382 AssetContent::File(file_content) => match &*file_content.await? {
383 FileContent::NotFound => ParseCssResult::NotFound.cell(),
384 FileContent::Content(file) => match file.content().to_str() {
385 Err(_err) => ParseCssResult::Unparsable.cell(),
386 Ok(string) => {
387 process_content(
388 *file_content,
389 string.into_owned(),
390 ident_str,
391 source,
392 origin,
393 import_context,
394 ty,
395 environment,
396 feature_flags,
397 )
398 .await?
399 }
400 },
401 },
402 })
403 }
404 .instrument(span)
405 .await
406}
407
408fn strip_bom(code: &str) -> &str {
410 code.strip_prefix('\u{feff}').unwrap_or(code)
411}
412
413fn source_pos_for_loc(loc: &lightningcss::error::ErrorLocation, code_had_bom: bool) -> SourcePos {
421 SourcePos {
422 line: loc.line,
423 column: if code_had_bom && loc.line == 0 {
425 loc.column
426 } else {
427 loc.column - 1
428 },
429 }
430}
431
432fn parse_css_stylesheet<'a>(
437 code: &'a str,
438 config: ParserOptions<'a>,
439 ty: CssModuleType,
440 source: ResolvedVc<Box<dyn Source>>,
441) -> Result<StyleSheet<'a>, lightningcss::error::Error<lightningcss::error::ParserError<'a>>> {
442 let code = strip_bom(code);
445 let mut ss = StyleSheet::parse(code, config)?;
446
447 if matches!(ty, CssModuleType::Module) {
448 let mut validator = CssValidator { errors: Vec::new() };
449 ss.visit(&mut validator).unwrap();
450
451 for err in validator.errors {
452 err.report(source);
453 }
454 }
455
456 Ok(ss)
457}
458
459async fn process_content(
460 content_vc: ResolvedVc<FileContent>,
461 code: String,
462 filename: &str,
463 source: ResolvedVc<Box<dyn Source>>,
464 origin: ResolvedVc<Box<dyn ResolveOrigin>>,
465 import_context: Option<ResolvedVc<ImportContext>>,
466 ty: CssModuleType,
467 environment: Option<ResolvedVc<Environment>>,
468 feature_flags: LightningCssFeatureFlags,
469) -> Result<Vc<ParseCssResult>> {
470 #[allow(clippy::needless_lifetimes)]
471 fn without_warnings<'i>(config: ParserOptions<'i>) -> ParserOptions<'static> {
472 ParserOptions {
473 filename: config.filename,
474 css_modules: config.css_modules,
475 source_index: config.source_index,
476 error_recovery: config.error_recovery,
477 warnings: None,
478 flags: config.flags,
479 }
480 }
481
482 let code_had_bom = code.starts_with('\u{feff}');
483
484 let mut flags = ParserFlags::empty();
488 let include_features = Features::from_bits_truncate(feature_flags.include)
489 & !Features::from_bits_truncate(feature_flags.exclude);
490 flags.set(
491 ParserFlags::CUSTOM_MEDIA,
492 include_features.contains(Features::CustomMediaQueries),
493 );
494
495 let config = ParserOptions {
496 flags,
497 css_modules: match ty {
498 CssModuleType::Module => Some(lightningcss::css_modules::Config {
499 pattern: Pattern {
500 segments: smallvec![
501 Segment::Name,
502 Segment::Literal(Cow::Borrowed("__")),
503 Segment::Hash,
504 Segment::Literal(Cow::Borrowed("__")),
505 Segment::Local,
506 ],
507 },
508 dashed_idents: false,
509 grid: false,
510 container: false,
511 ..Default::default()
512 }),
513
514 _ => None,
515 },
516 filename: filename.to_string(),
517 error_recovery: true,
518 ..Default::default()
519 };
520
521 let stylesheet = {
522 let warnings: Arc<RwLock<_>> = Default::default();
523
524 match parse_css_stylesheet(
525 &code,
526 ParserOptions {
527 warnings: Some(warnings.clone()),
528 ..config.clone()
529 },
530 ty,
531 source,
532 ) {
533 Ok(mut ss) => {
534 for err in warnings.read().unwrap().iter() {
535 let severity = match err.kind {
540 lightningcss::error::ParserError::SelectorError(
541 lightningcss::error::SelectorError::UnsupportedPseudoClass(_)
542 | lightningcss::error::SelectorError::UnsupportedPseudoElement(_),
543 ) => IssueSeverity::Warning,
544
545 lightningcss::error::ParserError::UnexpectedToken(_)
546 | lightningcss::error::ParserError::UnexpectedImportRule
547 | lightningcss::error::ParserError::SelectorError(..)
548 | lightningcss::error::ParserError::EndOfInput => IssueSeverity::Error,
549
550 _ => IssueSeverity::Warning,
551 };
552
553 let issue_source = match &err.loc {
554 Some(loc) => IssueSource::from_single_line_col(
555 source,
556 source_pos_for_loc(loc, code_had_bom),
557 ),
558 None => IssueSource::from_source_only(source),
559 };
560
561 ParsingIssue {
562 severity,
563 msg: err.kind.to_string().into(),
564 stage: IssueStage::Parse,
565 source: issue_source,
566 }
567 .resolved_cell()
568 .emit();
569 }
570
571 let targets = *get_lightningcss_browser_targets(
572 environment.as_deref().copied(),
573 true,
574 feature_flags,
575 )
576 .await?;
577
578 if let Err(e) = ss.minify(MinifyOptions {
583 targets,
584 ..Default::default()
585 }) {
586 let issue_source = match &e.loc {
587 Some(loc) => IssueSource::from_single_line_col(
588 source,
589 source_pos_for_loc(loc, code_had_bom),
590 ),
591 None => IssueSource::from_source_only(source),
592 };
593 ParsingIssue {
594 severity: IssueSeverity::Error,
595 msg: e.kind.to_string().into(),
596 stage: IssueStage::Transform,
597 source: issue_source,
598 }
599 .resolved_cell()
600 .emit();
601 match parse_css_stylesheet(
604 &code,
605 ParserOptions {
606 warnings: None,
607 ..config.clone()
608 },
609 ty,
610 source,
611 ) {
612 Ok(fresh) => {
613 stylesheet_into_static(&fresh, without_warnings(config.clone()))
614 }
615 Err(_) => return Ok(ParseCssResult::Unparsable.cell()),
616 }
617 } else {
618 stylesheet_into_static(&ss, without_warnings(config.clone()))
619 }
620 }
621 Err(e) => {
622 let issue_source = match &e.loc {
623 Some(loc) => IssueSource::from_single_line_col(
624 source,
625 source_pos_for_loc(loc, code_had_bom),
626 ),
627 None => IssueSource::from_source_only(source),
628 };
629 ParsingIssue {
630 severity: IssueSeverity::Error,
631 msg: e.kind.to_string().into(),
632 stage: IssueStage::Parse,
633 source: issue_source,
634 }
635 .resolved_cell()
636 .emit();
637 return Ok(ParseCssResult::Unparsable.cell());
638 }
639 }
640 };
641
642 let config = without_warnings(config);
643 let mut stylesheet = stylesheet_into_static(&stylesheet, config.clone());
644
645 let (references, url_references) =
646 analyze_references(&mut stylesheet, source, origin, import_context).await?;
647
648 Ok(ParseCssResult::Ok {
649 code: content_vc,
650 stylesheet,
651 references: ResolvedVc::cell(references),
652 url_references: ResolvedVc::cell(url_references),
653 options: config,
654 }
655 .cell())
656}
657
658struct CssValidator {
667 errors: Vec<CssError>,
668}
669
670#[derive(Debug, PartialEq, Eq)]
671enum CssError {
672 CssSelectorInModuleNotPure { selector: String },
673}
674
675impl CssError {
676 fn report(self, source: ResolvedVc<Box<dyn Source>>) {
677 match self {
678 CssError::CssSelectorInModuleNotPure { selector } => {
679 ParsingIssue {
680 severity: IssueSeverity::Error,
681 msg: format!(
682 "Selector \"{selector}\" is not pure. Pure selectors must contain at \
683 least one local class or id."
684 )
685 .into(),
686 stage: IssueStage::Transform,
687 source: IssueSource::from_source_only(source),
689 }
690 .resolved_cell()
691 .emit();
692 }
693 }
694 }
695}
696
697impl lightningcss::visitor::Visitor<'_> for CssValidator {
699 type Error = ();
700
701 fn visit_types(&self) -> lightningcss::visitor::VisitTypes {
702 visit_types!(SELECTORS)
703 }
704
705 fn visit_selector(
706 &mut self,
707 selector: &mut lightningcss::selector::Selector<'_>,
708 ) -> Result<(), Self::Error> {
709 fn is_selector_problematic(sel: &lightningcss::selector::Selector) -> bool {
710 sel.iter_raw_parse_order_from(0).all(is_problematic)
711 }
712
713 fn is_problematic(c: &lightningcss::selector::Component) -> bool {
714 match c {
715 parcel_selectors::parser::Component::ID(..)
716 | parcel_selectors::parser::Component::Class(..) => false,
717
718 parcel_selectors::parser::Component::Combinator(..)
719 | parcel_selectors::parser::Component::AttributeOther(..)
720 | parcel_selectors::parser::Component::AttributeInNoNamespaceExists { .. }
721 | parcel_selectors::parser::Component::AttributeInNoNamespace { .. }
722 | parcel_selectors::parser::Component::ExplicitUniversalType
723 | parcel_selectors::parser::Component::Negation(..) => true,
724
725 parcel_selectors::parser::Component::Where(sel) => {
726 sel.iter().all(is_selector_problematic)
727 }
728
729 parcel_selectors::parser::Component::LocalName(local) => {
730 !matches!(&*local.name.0, "html" | "body")
732 }
733 _ => false,
734 }
735 }
736
737 if is_selector_problematic(selector) {
738 let selector_string = selector
739 .to_css_string(PrinterOptions {
740 minify: false,
741 ..Default::default()
742 })
743 .expect("selector.to_css_string should not fail");
744 self.errors.push(CssError::CssSelectorInModuleNotPure {
745 selector: selector_string,
746 });
747 }
748
749 Ok(())
750 }
751}
752
753fn generate_css_source_map(
754 source_map: &parcel_sourcemap::SourceMap,
755) -> Result<StructuredSourceMap> {
756 let mut builder = SourceMapBuilder::new(None);
757
758 for src in source_map.get_sources() {
759 builder.add_source(format!("{SOURCE_URL_PROTOCOL}///{src}").into());
760 }
761
762 for (idx, content) in source_map.get_sources_content().iter().enumerate() {
763 builder.set_source_contents(idx as _, Some(content.clone().into()));
764 }
765
766 for m in source_map.get_mappings() {
767 builder.add_raw(
768 m.generated_line,
769 m.generated_column,
770 m.original.map(|v| v.original_line).unwrap_or_default(),
771 m.original.map(|v| v.original_column).unwrap_or_default(),
772 Some(0),
773 None,
774 false,
775 );
776 }
777
778 let mut map = builder.into_sourcemap();
779 add_default_ignore_list(&mut map);
780 StructuredSourceMap::from_swc_map(map)
781}
782
783#[turbo_tasks::value]
784struct ParsingIssue {
785 severity: IssueSeverity,
786 msg: RcStr,
787 stage: IssueStage,
788 source: IssueSource,
789}
790
791#[async_trait]
792#[turbo_tasks::value_impl]
793impl Issue for ParsingIssue {
794 fn severity(&self) -> IssueSeverity {
795 self.severity
796 }
797
798 async fn file_path(&self) -> Result<FileSystemPath> {
799 self.source.file_path().await
800 }
801
802 fn stage(&self) -> IssueStage {
803 self.stage.clone()
804 }
805
806 async fn title(&self) -> Result<StyledString> {
807 Ok(StyledString::Text(match self.stage {
808 IssueStage::Parse => rcstr!("Parsing CSS source code failed"),
809 IssueStage::Transform => rcstr!("Transforming CSS failed"),
810 _ => rcstr!("CSS processing failed"),
811 }))
812 }
813
814 fn source(&self) -> Option<IssueSource> {
815 Some(self.source)
816 }
817
818 async fn description(&self) -> Result<Option<StyledString>> {
819 Ok(Some(StyledString::Text(self.msg.clone())))
820 }
821
822 async fn additional_sources(&self) -> Result<Vec<AdditionalIssueSource>> {
823 if let Some(additional) = self.source.to_generated_code_source().await? {
824 return Ok(vec![additional]);
825 }
826 Ok(vec![])
827 }
828}
829
830#[cfg(test)]
831mod tests {
832 use lightningcss::{
833 css_modules::Pattern,
834 stylesheet::{ParserOptions, StyleSheet},
835 visitor::Visit,
836 };
837
838 use super::{CssError, CssValidator, source_pos_for_loc, strip_bom};
839
840 fn lint_lightningcss(code: &str) -> Vec<CssError> {
841 let mut ss = StyleSheet::parse(
842 code,
843 ParserOptions {
844 css_modules: Some(lightningcss::css_modules::Config {
845 pattern: Pattern::default(),
846 dashed_idents: false,
847 grid: false,
848 container: false,
849 ..Default::default()
850 }),
851 ..Default::default()
852 },
853 )
854 .unwrap();
855
856 let mut validator = CssValidator { errors: Vec::new() };
857 ss.visit(&mut validator).unwrap();
858
859 validator.errors
860 }
861
862 #[track_caller]
863 fn assert_lint_success(code: &str) {
864 assert_eq!(lint_lightningcss(code), vec![], "lightningcss: {code}");
865 }
866
867 #[track_caller]
868 fn assert_lint_failure(code: &str) {
869 assert_ne!(lint_lightningcss(code), vec![], "lightningcss: {code}");
870 }
871
872 #[test]
873 fn css_module_pure_lint() {
874 assert_lint_success(
875 "html {
876 --foo: 1;
877 }",
878 );
879
880 assert_lint_success(
881 "#id {
882 color: red;
883 }",
884 );
885
886 assert_lint_success(
887 ".class {
888 color: red;
889 }",
890 );
891
892 assert_lint_success(
893 "html.class {
894 color: red;
895 }",
896 );
897
898 assert_lint_success(
899 ".class > * {
900 color: red;
901 }",
902 );
903
904 assert_lint_success(
905 ".class * {
906 color: red;
907 }",
908 );
909
910 assert_lint_success(
911 ":where(.main > *) {
912 color: red;
913 }",
914 );
915
916 assert_lint_success(
917 ":where(.main > *, .root > *) {
918 color: red;
919 }",
920 );
921 assert_lint_success(
922 ".style {
923 background-image: var(--foo);
924 }",
925 );
926
927 assert_lint_failure(
928 "div {
929 color: red;
930 }",
931 );
932
933 assert_lint_failure(
934 "div > span {
935 color: red;
936 }",
937 );
938
939 assert_lint_failure(
940 "div span {
941 color: red;
942 }",
943 );
944
945 assert_lint_failure(
946 "div[data-foo] {
947 color: red;
948 }",
949 );
950
951 assert_lint_failure(
952 "div[data-foo=\"bar\"] {
953 color: red;
954 }",
955 );
956
957 assert_lint_failure(
958 "div[data-foo=\"bar\"] span {
959 color: red;
960 }",
961 );
962
963 assert_lint_failure(
964 "* {
965 --foo: 1;
966 }",
967 );
968
969 assert_lint_failure(
970 "[data-foo] {
971 --foo: 1;
972 }",
973 );
974
975 assert_lint_failure(
976 ":not(.class) {
977 --foo: 1;
978 }",
979 );
980
981 assert_lint_failure(
982 ":not(div) {
983 --foo: 1;
984 }",
985 );
986
987 assert_lint_failure(
988 ":where(div > *) {
989 color: red;
990 }",
991 );
992
993 assert_lint_failure(
994 ":where(div) {
995 color: red;
996 }",
997 );
998 }
999
1000 #[test]
1001 fn strip_bom_lets_lightningcss_parse() {
1002 let with_bom = "\u{feff}@layer a {}";
1003
1004 assert!(StyleSheet::parse(with_bom, ParserOptions::default()).is_err());
1005 assert!(StyleSheet::parse(strip_bom(with_bom), ParserOptions::default()).is_ok());
1006 assert_eq!(strip_bom("@layer a {}"), "@layer a {}");
1007 }
1008
1009 #[test]
1010 fn source_pos_for_loc_corrects_line_one_column_for_bom_files() {
1011 let loc = lightningcss::error::ErrorLocation {
1013 filename: String::new(),
1014 line: 0,
1015 column: 12,
1016 };
1017
1018 assert_eq!(source_pos_for_loc(&loc, false).column, 11);
1020
1021 assert_eq!(source_pos_for_loc(&loc, true).column, 12);
1025
1026 let later_line = lightningcss::error::ErrorLocation {
1029 filename: String::new(),
1030 line: 1,
1031 column: 12,
1032 };
1033 assert_eq!(
1034 source_pos_for_loc(&later_line, true).column,
1035 source_pos_for_loc(&later_line, false).column,
1036 );
1037 }
1038}