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