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