Repository: tailwindlabs/tailwindcss (TypeScript)

Issue:
Tailwind looks for classes inside `:not()` in utilities and components when in at-rules
<!-- 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?**

v3.3.3

**What build tool (or framework if it abstracts the build tool) are you using?**

Next.js 13.4.13 (Tool agnostic bug)

**What version of Node.js are you using?**

For example: v18.16.0

**What browser are you using?**

For example: Chrome

**What operating system are you using?**

For example: Windows

**Reproduction URL**

https://play.tailwindcss.com/Tg2m8Otzun

**Describe your issue**

Tailwind searches for importance for things like `disabled` in places that are not classnames.

As shown in the example above, DaisyUI applies variants upon hover when the component is not disabled.
Because Tailwind sees a string `!disabled`, the styles applied in Daisy's hover variant become important and thus overshadow things like the active style.

If you hold click on the menu options in the playground, nothing happens. However, if you remove the `!disabled` span and hold click, you will see the proper `:active` styles become applied

This also happens in React (checking a variable, for instance) and is documented here: https://github.com/saadeghi/daisyui/issues/2240


Hints:
Hey, thanks for reporting this.

> Tailwind searches for importance for things like disabled in places that are not classnames.

This is by design. Not everyone uses Tailwind in HTML but in JSX, Svelte, Pug, etc… Parsing files to find class names and to do so properly would have significant limitations as we would have to understand the code, how it works at build time, and how it works at runtime. This is not feasible for Tailwind itself. Tailwind CSS scans entire source files for matches generally regardless of where that match occurs. This means you can get false positives but you can typically use the `blocklist` feature to work around this.

> If you hold click on the menu options in the playground, nothing happens. However, if you remove the !disabled span and hold click, you will see the proper :active styles become applied

This is definitely a bug. We do not traverse classes inside `:not()` — but it is still (incorrectly) traversed when inside an at rule.

Minimized repro: https://play.tailwindcss.com/GMBdxJHeL9?file=config

I'll look at this in the am.
@thecrypticace I think the level of how tailwind scans the code is too much
![image](https://github.com/tailwindlabs/tailwindcss/assets/26060677/2c8e4e04-2aeb-4193-832f-b7ffea42fdb8)


Patch:
diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js
--- a/src/lib/setupContextUtils.js
+++ b/src/lib/setupContextUtils.js
@@ -147,43 +147,45 @@ function getClasses(selector, mutate) {
   return parser.transformSync(selector)
 }
 
+/**
+ * Ignore everything inside a :not(...). This allows you to write code like
+ * `div:not(.foo)`. If `.foo` is never found in your code, then we used to
+ * not generated it. But now we will ignore everything inside a `:not`, so
+ * that it still gets generated.
+ *
+ * @param {selectorParser.Root} selectors
+ */
+function ignoreNot(selectors) {
+  selectors.walkPseudos((pseudo) => {
+    if (pseudo.value === ':not') {
+      pseudo.remove()
+    }
+  })
+}
+
 function extractCandidates(node, state = { containsNonOnDemandable: false }, depth = 0) {
   let classes = []
+  let selectors = []
 
-  // Handle normal rules
   if (node.type === 'rule') {
-    // Ignore everything inside a :not(...). This allows you to write code like
-    // `div:not(.foo)`. If `.foo` is never found in your code, then we used to
-    // not generated it. But now we will ignore everything inside a `:not`, so
-    // that it still gets generated.
-    function ignoreNot(selectors) {
-      selectors.walkPseudos((pseudo) => {
-        if (pseudo.value === ':not') {
-          pseudo.remove()
-        }
-      })
-    }
+    // Handle normal rules
+    selectors.push(...node.selectors)
+  } else if (node.type === 'atrule') {
+    // Handle at-rules (which contains nested rules)
+    node.walkRules((rule) => selectors.push(...rule.selectors))
+  }
 
-    for (let selector of node.selectors) {
-      let classCandidates = getClasses(selector, ignoreNot)
-      // At least one of the selectors contains non-"on-demandable" candidates.
-      if (classCandidates.length === 0) {
-        state.containsNonOnDemandable = true
-      }
+  for (let selector of selectors) {
+    let classCandidates = getClasses(selector, ignoreNot)
 
-      for (let classCandidate of classCandidates) {
-        classes.push(classCandidate)
-      }
+    // At least one of the selectors contains non-"on-demandable" candidates.
+    if (classCandidates.length === 0) {
+      state.containsNonOnDemandable = true
     }
-  }
 
-  // Handle at-rules (which contains nested rules)
-  else if (node.type === 'atrule') {
-    node.walkRules((rule) => {
-      for (let classCandidate of rule.selectors.flatMap((selector) => getClasses(selector))) {
-        classes.push(classCandidate)
-      }
-    })
+    for (let classCandidate of classCandidates) {
+      classes.push(classCandidate)
+    }
   }
 
   if (depth === 0) {


Test patch:
diff --git a/tests/basic-usage.test.js b/tests/basic-usage.test.js
--- a/tests/basic-usage.test.js
+++ b/tests/basic-usage.test.js
@@ -760,3 +760,35 @@ test('handled quoted arbitrary values containing escaped spaces', async () => {
         `
   )
 })
+
+test('Skips classes inside :not() when nested inside an at-rule', async () => {
+  let config = {
+    content: [
+      {
+        raw: html` <div class="disabled !disabled"></div> `,
+      },
+    ],
+    corePlugins: { preflight: false },
+    plugins: [
+      function ({ addUtilities }) {
+        addUtilities({
+          '.hand:not(.disabled)': {
+            '@supports (cursor: pointer)': {
+              cursor: 'pointer',
+            },
+          },
+        })
+      },
+    ],
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  // We didn't find the hand class therefore
+  // nothing should be generated
+  let result = await run(input, config)
+
+  expect(result.css).toMatchFormattedCss(css``)
+})