Repository: tailwindlabs/tailwindcss (TypeScript)

Issue:
Using `:where(.anything)` in a plugin and having `!anything` inside HTML, creates invalid CSS
<!-- 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.2.6

**What build tool (or framework if it abstracts the build tool) are you using?**

Tailwind Play (or any tool)

**Reproduction URL**

https://play.tailwindcss.com/Oy7NHRkftL?file=config

**Describe your issue**

If there is string with `!` prefix with the same class from a plugin that has a `:where()` selector, Tailwind creates invalid CSS

HTML:
```
!btn
```
tailwind.config.js:
```js
const plugin = require('tailwindcss/plugin')
module.exports = {
  plugins: [
    plugin(function({ addComponents }) {
      addComponents({
        '.btn': {
          backgroundColor: 'red',
        },
        ':where(.btn)': {
          backgroundColor: 'blue',
        },
      })
    })
  ]
}
```
Generated CSS:
```css
.\!btn {
  background-color: red !important
}

 {
  background-color: blue !important
}
```
As you can see it can't apply important to the `:where()` selector and it generates an empty selector.


Hints:
Thanks for reporting! Here's a slightly simplified reproduction for our own reference:

https://play.tailwindcss.com/4MqOxcleAv?file=config

Patch:
diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js
--- a/src/lib/generateRules.js
+++ b/src/lib/generateRules.js
@@ -3,10 +3,14 @@ import selectorParser from 'postcss-selector-parser'
 import parseObjectStyles from '../util/parseObjectStyles'
 import isPlainObject from '../util/isPlainObject'
 import prefixSelector from '../util/prefixSelector'
-import { updateAllClasses, filterSelectorsForClass, getMatchingTypes } from '../util/pluginUtils'
+import { updateAllClasses, getMatchingTypes } from '../util/pluginUtils'
 import log from '../util/log'
 import * as sharedState from './sharedState'
-import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector'
+import {
+  formatVariantSelector,
+  finalizeSelector,
+  eliminateIrrelevantSelectors,
+} from '../util/formatVariantSelector'
 import { asClass } from '../util/nameClass'
 import { normalize } from '../util/dataTypes'
 import { isValidVariantFormatString, parseVariant } from './setupContextUtils'
@@ -111,22 +115,28 @@ function applyImportant(matches, classCandidate) {
   if (matches.length === 0) {
     return matches
   }
+
   let result = []
 
   for (let [meta, rule] of matches) {
     let container = postcss.root({ nodes: [rule.clone()] })
+
     container.walkRules((r) => {
-      r.selector = updateAllClasses(
-        filterSelectorsForClass(r.selector, classCandidate),
-        (className) => {
-          if (className === classCandidate) {
-            return `!${className}`
-          }
-          return className
-        }
+      let ast = selectorParser().astSync(r.selector)
+
+      // Remove extraneous selectors that do not include the base candidate
+      ast.each((sel) => eliminateIrrelevantSelectors(sel, classCandidate))
+
+      // Update all instances of the base candidate to include the important marker
+      updateAllClasses(ast, (className) =>
+        className === classCandidate ? `!${className}` : className
       )
+
+      r.selector = ast.toString()
+
       r.walkDecls((d) => (d.important = true))
     })
+
     result.push([{ ...meta, important: true }, container.nodes[0]])
   }
 
diff --git a/src/util/formatVariantSelector.js b/src/util/formatVariantSelector.js
--- a/src/util/formatVariantSelector.js
+++ b/src/util/formatVariantSelector.js
@@ -120,7 +120,7 @@ function resortSelector(sel) {
  * @param {Selector} ast
  * @param {string} base
  */
-function eliminateIrrelevantSelectors(sel, base) {
+export function eliminateIrrelevantSelectors(sel, base) {
   let hasClassesMatchingCandidate = false
 
   sel.walk((child) => {
diff --git a/src/util/pluginUtils.js b/src/util/pluginUtils.js
--- a/src/util/pluginUtils.js
+++ b/src/util/pluginUtils.js
@@ -1,4 +1,3 @@
-import selectorParser from 'postcss-selector-parser'
 import escapeCommas from './escapeCommas'
 import { withAlphaValue } from './withAlphaVariable'
 import {
@@ -21,37 +20,19 @@ import negateValue from './negateValue'
 import { backgroundSize } from './validateFormalSyntax'
 import { flagEnabled } from '../featureFlags.js'
 
+/**
+ * @param {import('postcss-selector-parser').Container} selectors
+ * @param {(className: string) => string} updateClass
+ * @returns {string}
+ */
 export function updateAllClasses(selectors, updateClass) {
-  let parser = selectorParser((selectors) => {
-    selectors.walkClasses((sel) => {
-      let updatedClass = updateClass(sel.value)
-      sel.value = updatedClass
-      if (sel.raws && sel.raws.value) {
-        sel.raws.value = escapeCommas(sel.raws.value)
-      }
-    })
-  })
-
-  let result = parser.processSync(selectors)
+  selectors.walkClasses((sel) => {
+    sel.value = updateClass(sel.value)
 
-  return result
-}
-
-export function filterSelectorsForClass(selectors, classCandidate) {
-  let parser = selectorParser((selectors) => {
-    selectors.each((sel) => {
-      const containsClass = sel.nodes.some(
-        (node) => node.type === 'class' && node.value === classCandidate
-      )
-      if (!containsClass) {
-        sel.remove()
-      }
-    })
+    if (sel.raws && sel.raws.value) {
+      sel.raws.value = escapeCommas(sel.raws.value)
+    }
   })
-
-  let result = parser.processSync(selectors)
-
-  return result
 }
 
 function resolveArbitraryValue(modifier, validate) {


Test patch:
diff --git a/tests/important-modifier.test.js b/tests/important-modifier.test.js
--- a/tests/important-modifier.test.js
+++ b/tests/important-modifier.test.js
@@ -108,4 +108,46 @@ crosscheck(() => {
       `)
     })
   })
+
+  test('the important modifier works on utilities using :where()', () => {
+    let config = {
+      content: [
+        {
+          raw: html` <div class="btn hover:btn !btn hover:focus:disabled:!btn"></div> `,
+        },
+      ],
+      corePlugins: { preflight: false },
+      plugins: [
+        function ({ addComponents }) {
+          addComponents({
+            ':where(.btn)': {
+              backgroundColor: '#00f',
+            },
+          })
+        },
+      ],
+    }
+
+    let input = css`
+      @tailwind components;
+      @tailwind utilities;
+    `
+
+    return run(input, config).then((result) => {
+      expect(result.css).toMatchFormattedCss(css`
+        :where(.\!btn) {
+          background-color: #00f !important;
+        }
+        :where(.btn) {
+          background-color: #00f;
+        }
+        :where(.hover\:btn:hover) {
+          background-color: #00f;
+        }
+        :where(.hover\:focus\:disabled\:\!btn:disabled:focus:hover) {
+          background-color: #00f !important;
+        }
+      `)
+    })
+  })
 })