Repository: tailwindlabs/tailwindcss (TypeScript)

Issue:
Adding custom utilities that use `:where()` selector, generates empty CSS selector when used with a responsive prefix
<!-- Please provide all of the information requested below. We're a small team and without all of this information it's not possible for us to help and your bug report will be closed. -->

**What version of Tailwind CSS are you using?**

3.1.8

**Reproduction URL**

https://play.tailwindcss.com/6lUxA2zYzo?file=config

**Describe your issue**

This works:
```html
<div class="sm:make-it-red">
```
```js
addUtilities({
  '.make-it-red': {
    backgroundColor: 'red',
  },
}),{
  variants: ["responsive"],
}
```
But this doesn't work:
```html
<div class="sm:make-it-pink">
```
```js
addUtilities({
  ':where(.make-it-pink)': {
    backgroundColor: 'hotpink',
  },
}),{
  variants: ["responsive"],
}
```
Expected CSS:
```css
@media (min-width: 640px) {
  .sm\:make-it-red {
    background-color: red
  }

   :where(.sm\:make-it-pink){
    background-color: hotpink
  }
}
```
Generated CSS:
```css
@media (min-width: 640px) {
  .sm\:make-it-red {
    background-color: red
  }

   {
    background-color: hotpink
  }
}
```
As you can see [here](https://play.tailwindcss.com/6lUxA2zYzo?file=config) it generates empty selector name.

I wonder if there's a way to make it work to have `:where()` selectors with responsive prefixes.


Hints:

Patch:
diff --git a/src/util/formatVariantSelector.js b/src/util/formatVariantSelector.js
--- a/src/util/formatVariantSelector.js
+++ b/src/util/formatVariantSelector.js
@@ -81,6 +81,29 @@ function resortSelector(sel) {
   return sel
 }
 
+function eliminateIrrelevantSelectors(sel, base) {
+  let hasClassesMatchingCandidate = false
+
+  sel.walk((child) => {
+    if (child.type === 'class' && child.value === base) {
+      hasClassesMatchingCandidate = true
+      return false // Stop walking
+    }
+  })
+
+  if (!hasClassesMatchingCandidate) {
+    sel.remove()
+  }
+
+  // We do NOT recursively eliminate sub selectors that don't have the base class
+  // as this is NOT a safe operation. For example, if we have:
+  // `.space-x-2 > :not([hidden]) ~ :not([hidden])`
+  // We cannot remove the [hidden] from the :not() because it would change the
+  // meaning of the selector.
+
+  // TODO: Can we do this for :matches, :is, and :where?
+}
+
 export function finalizeSelector(
   format,
   {
@@ -115,13 +138,7 @@ export function finalizeSelector(
   // Remove extraneous selectors that do not include the base class/candidate being matched against
   // For example if we have a utility defined `.a, .b { color: red}`
   // And the formatted variant is sm:b then we want the final selector to be `.sm\:b` and not `.a, .sm\:b`
-  ast.each((node) => {
-    let hasClassesMatchingCandidate = node.some((n) => n.type === 'class' && n.value === base)
-
-    if (!hasClassesMatchingCandidate) {
-      node.remove()
-    }
-  })
+  ast.each((sel) => eliminateIrrelevantSelectors(sel, base))
 
   // Normalize escaped classes, e.g.:
   //


Test patch:
diff --git a/tests/variants.test.js b/tests/variants.test.js
--- a/tests/variants.test.js
+++ b/tests/variants.test.js
@@ -944,3 +944,88 @@ test('multi-class utilities handle selector-mutating variants correctly', () =>
     `)
   })
 })
+
+test('class inside pseudo-class function :has', () => {
+  let config = {
+    content: [
+      { raw: html`<div class="foo hover:foo sm:foo"></div>` },
+      { raw: html`<div class="bar hover:bar sm:bar"></div>` },
+      { raw: html`<div class="baz hover:baz sm:baz"></div>` },
+    ],
+    corePlugins: { preflight: false },
+  }
+
+  let input = css`
+    @tailwind utilities;
+    @layer utilities {
+      :where(.foo) {
+        color: red;
+      }
+      :matches(.foo, .bar, .baz) {
+        color: orange;
+      }
+      :is(.foo) {
+        color: yellow;
+      }
+      html:has(.foo) {
+        color: green;
+      }
+    }
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      :where(.foo) {
+        color: red;
+      }
+      :matches(.foo, .bar, .baz) {
+        color: orange;
+      }
+      :is(.foo) {
+        color: yellow;
+      }
+      html:has(.foo) {
+        color: green;
+      }
+
+      :where(.hover\:foo:hover) {
+        color: red;
+      }
+      :matches(.hover\:foo:hover, .bar, .baz) {
+        color: orange;
+      }
+      :matches(.foo, .hover\:bar:hover, .baz) {
+        color: orange;
+      }
+      :matches(.foo, .bar, .hover\:baz:hover) {
+        color: orange;
+      }
+      :is(.hover\:foo:hover) {
+        color: yellow;
+      }
+      html:has(.hover\:foo:hover) {
+        color: green;
+      }
+      @media (min-width: 640px) {
+        :where(.sm\:foo) {
+          color: red;
+        }
+        :matches(.sm\:foo, .bar, .baz) {
+          color: orange;
+        }
+        :matches(.foo, .sm\:bar, .baz) {
+          color: orange;
+        }
+        :matches(.foo, .bar, .sm\:baz) {
+          color: orange;
+        }
+        :is(.sm\:foo) {
+          color: yellow;
+        }
+        html:has(.sm\:foo) {
+          color: green;
+        }
+      }
+    `)
+  })
+})