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