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