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) => IssueSource::from_single_line_col(
464 source,
465 SourcePos {
466 line: loc.line,
468 column: loc.column - 1,
469 },
470 ),
471 None => IssueSource::from_source_only(source),
472 };
473
474 ParsingIssue {
475 msg: err.kind.to_string().into(),
476 stage: IssueStage::Parse,
477 source,
478 }
479 .resolved_cell()
480 .emit();
481 return Ok(ParseCssResult::Unparsable.cell());
482 }
483
484 _ => {
485 }
487 }
488 }
489
490 let targets =
491 *get_lightningcss_browser_targets(environment.as_deref().copied(), true)
492 .await?;
493
494 if let Err(e) = ss.minify(MinifyOptions {
499 targets,
500 ..Default::default()
501 }) {
502 let source = match &e.loc {
503 Some(loc) => IssueSource::from_single_line_col(
504 source,
505 SourcePos {
506 line: loc.line,
508 column: loc.column - 1,
509 },
510 ),
511 None => IssueSource::from_source_only(source),
512 };
513 ParsingIssue {
514 msg: e.kind.to_string().into(),
515 stage: IssueStage::Transform,
516 source,
517 }
518 .resolved_cell()
519 .emit();
520 return Ok(ParseCssResult::Unparsable.cell());
521 }
522
523 stylesheet_into_static(&ss, without_warnings(config.clone()))
524 }
525 Err(e) => {
526 let source = match &e.loc {
527 Some(loc) => IssueSource::from_single_line_col(
528 source,
529 SourcePos {
530 line: loc.line,
532 column: loc.column - 1,
533 },
534 ),
535 None => IssueSource::from_source_only(source),
536 };
537 ParsingIssue {
538 msg: e.kind.to_string().into(),
539 stage: IssueStage::Parse,
540 source,
541 }
542 .resolved_cell()
543 .emit();
544 return Ok(ParseCssResult::Unparsable.cell());
545 }
546 }
547 };
548
549 let config = without_warnings(config);
550 let mut stylesheet = stylesheet_into_static(&stylesheet, config.clone());
551
552 let (references, url_references) =
553 analyze_references(&mut stylesheet, source, origin, import_context).await?;
554
555 Ok(ParseCssResult::Ok {
556 code: content_vc,
557 stylesheet,
558 references: ResolvedVc::cell(references),
559 url_references: ResolvedVc::cell(url_references),
560 options: config,
561 }
562 .cell())
563}
564
565struct CssValidator {
574 errors: Vec<CssError>,
575}
576
577#[derive(Debug, PartialEq, Eq)]
578enum CssError {
579 CssSelectorInModuleNotPure { selector: String },
580}
581
582impl CssError {
583 fn report(self, source: ResolvedVc<Box<dyn Source>>) {
584 match self {
585 CssError::CssSelectorInModuleNotPure { selector } => {
586 ParsingIssue {
587 msg: format!(
588 "Selector \"{selector}\" is not pure. Pure selectors must contain at \
589 least one local class or id."
590 )
591 .into(),
592 stage: IssueStage::Transform,
593 source: IssueSource::from_source_only(source),
595 }
596 .resolved_cell()
597 .emit();
598 }
599 }
600 }
601}
602
603impl lightningcss::visitor::Visitor<'_> for CssValidator {
605 type Error = ();
606
607 fn visit_types(&self) -> lightningcss::visitor::VisitTypes {
608 visit_types!(SELECTORS)
609 }
610
611 fn visit_selector(
612 &mut self,
613 selector: &mut lightningcss::selector::Selector<'_>,
614 ) -> Result<(), Self::Error> {
615 fn is_selector_problematic(sel: &lightningcss::selector::Selector) -> bool {
616 sel.iter_raw_parse_order_from(0).all(is_problematic)
617 }
618
619 fn is_problematic(c: &lightningcss::selector::Component) -> bool {
620 match c {
621 parcel_selectors::parser::Component::ID(..)
622 | parcel_selectors::parser::Component::Class(..) => false,
623
624 parcel_selectors::parser::Component::Combinator(..)
625 | parcel_selectors::parser::Component::AttributeOther(..)
626 | parcel_selectors::parser::Component::AttributeInNoNamespaceExists { .. }
627 | parcel_selectors::parser::Component::AttributeInNoNamespace { .. }
628 | parcel_selectors::parser::Component::ExplicitUniversalType
629 | parcel_selectors::parser::Component::Negation(..) => true,
630
631 parcel_selectors::parser::Component::Where(sel) => {
632 sel.iter().all(is_selector_problematic)
633 }
634
635 parcel_selectors::parser::Component::LocalName(local) => {
636 !matches!(&*local.name.0, "html" | "body")
638 }
639 _ => false,
640 }
641 }
642
643 if is_selector_problematic(selector) {
644 let selector_string = selector
645 .to_css_string(PrinterOptions {
646 minify: false,
647 ..Default::default()
648 })
649 .expect("selector.to_css_string should not fail");
650 self.errors.push(CssError::CssSelectorInModuleNotPure {
651 selector: selector_string,
652 });
653 }
654
655 Ok(())
656 }
657}
658
659fn generate_css_source_map(source_map: &parcel_sourcemap::SourceMap) -> Result<Rope> {
660 let mut builder = SourceMapBuilder::new(None);
661
662 for src in source_map.get_sources() {
663 builder.add_source(format!("{SOURCE_URL_PROTOCOL}///{src}").into());
664 }
665
666 for (idx, content) in source_map.get_sources_content().iter().enumerate() {
667 builder.set_source_contents(idx as _, Some(content.clone().into()));
668 }
669
670 for m in source_map.get_mappings() {
671 builder.add_raw(
672 m.generated_line,
673 m.generated_column,
674 m.original.map(|v| v.original_line).unwrap_or_default(),
675 m.original.map(|v| v.original_column).unwrap_or_default(),
676 Some(0),
677 None,
678 false,
679 );
680 }
681
682 let mut map = builder.into_sourcemap();
683 add_default_ignore_list(&mut map);
684 let mut result = vec![];
685 map.to_writer(&mut result)?;
686 Ok(Rope::from(result))
687}
688
689#[turbo_tasks::value]
690struct ParsingIssue {
691 msg: RcStr,
692 stage: IssueStage,
693 source: IssueSource,
694}
695
696#[turbo_tasks::value_impl]
697impl Issue for ParsingIssue {
698 #[turbo_tasks::function]
699 fn file_path(&self) -> Vc<FileSystemPath> {
700 self.source.file_path()
701 }
702
703 #[turbo_tasks::function]
704 fn stage(&self) -> Vc<IssueStage> {
705 self.stage.clone().cell()
706 }
707
708 #[turbo_tasks::function]
709 fn title(&self) -> Vc<StyledString> {
710 StyledString::Text(match self.stage {
711 IssueStage::Parse => rcstr!("Parsing CSS source code failed"),
712 IssueStage::Transform => rcstr!("Transforming CSS failed"),
713 _ => rcstr!("CSS processing failed"),
714 })
715 .cell()
716 }
717
718 #[turbo_tasks::function]
719 fn source(&self) -> Vc<OptionIssueSource> {
720 Vc::cell(Some(self.source))
721 }
722
723 #[turbo_tasks::function]
724 fn description(&self) -> Result<Vc<OptionStyledString>> {
725 Ok(Vc::cell(Some(
726 StyledString::Text(self.msg.clone()).resolved_cell(),
727 )))
728 }
729}
730
731#[cfg(test)]
732mod tests {
733 use lightningcss::{
734 css_modules::Pattern,
735 stylesheet::{ParserOptions, StyleSheet},
736 visitor::Visit,
737 };
738
739 use super::{CssError, CssValidator};
740
741 fn lint_lightningcss(code: &str) -> Vec<CssError> {
742 let mut ss = StyleSheet::parse(
743 code,
744 ParserOptions {
745 css_modules: Some(lightningcss::css_modules::Config {
746 pattern: Pattern::default(),
747 dashed_idents: false,
748 grid: false,
749 container: false,
750 ..Default::default()
751 }),
752 ..Default::default()
753 },
754 )
755 .unwrap();
756
757 let mut validator = CssValidator { errors: Vec::new() };
758 ss.visit(&mut validator).unwrap();
759
760 validator.errors
761 }
762
763 #[track_caller]
764 fn assert_lint_success(code: &str) {
765 assert_eq!(lint_lightningcss(code), vec![], "lightningcss: {code}");
766 }
767
768 #[track_caller]
769 fn assert_lint_failure(code: &str) {
770 assert_ne!(lint_lightningcss(code), vec![], "lightningcss: {code}");
771 }
772
773 #[test]
774 fn css_module_pure_lint() {
775 assert_lint_success(
776 "html {
777 --foo: 1;
778 }",
779 );
780
781 assert_lint_success(
782 "#id {
783 color: red;
784 }",
785 );
786
787 assert_lint_success(
788 ".class {
789 color: red;
790 }",
791 );
792
793 assert_lint_success(
794 "html.class {
795 color: red;
796 }",
797 );
798
799 assert_lint_success(
800 ".class > * {
801 color: red;
802 }",
803 );
804
805 assert_lint_success(
806 ".class * {
807 color: red;
808 }",
809 );
810
811 assert_lint_success(
812 ":where(.main > *) {
813 color: red;
814 }",
815 );
816
817 assert_lint_success(
818 ":where(.main > *, .root > *) {
819 color: red;
820 }",
821 );
822 assert_lint_success(
823 ".style {
824 background-image: var(--foo);
825 }",
826 );
827
828 assert_lint_failure(
829 "div {
830 color: red;
831 }",
832 );
833
834 assert_lint_failure(
835 "div > span {
836 color: red;
837 }",
838 );
839
840 assert_lint_failure(
841 "div span {
842 color: red;
843 }",
844 );
845
846 assert_lint_failure(
847 "div[data-foo] {
848 color: red;
849 }",
850 );
851
852 assert_lint_failure(
853 "div[data-foo=\"bar\"] {
854 color: red;
855 }",
856 );
857
858 assert_lint_failure(
859 "div[data-foo=\"bar\"] span {
860 color: red;
861 }",
862 );
863
864 assert_lint_failure(
865 "* {
866 --foo: 1;
867 }",
868 );
869
870 assert_lint_failure(
871 "[data-foo] {
872 --foo: 1;
873 }",
874 );
875
876 assert_lint_failure(
877 ":not(.class) {
878 --foo: 1;
879 }",
880 );
881
882 assert_lint_failure(
883 ":not(div) {
884 --foo: 1;
885 }",
886 );
887
888 assert_lint_failure(
889 ":where(div > *) {
890 color: red;
891 }",
892 );
893
894 assert_lint_failure(
895 ":where(div) {
896 color: red;
897 }",
898 );
899 }
900}