Repository: tailwindlabs/tailwindcss (TypeScript)

Issue:
Sibling selectors of components don't work on classes applying them
**What version of Tailwind CSS are you using?**

For example: v3.1.8

**What build tool (or framework if it abstracts the build tool) are you using?**

Tailwind Play

**What browser are you using?**

Chrome

**What operating system are you using?**

macOS

**Reproduction URL**

https://play.tailwindcss.com/Gd9jsdpkCi?file=css

**Describe your issue**

When applying custom components to selectors with a sibling operator, the output CSS does not apply to them properly. For example, with the following code

```css
@layer components {
    .row.row-odd {
        @apply bg-green-400;
    }
    .row.row-even {
        @apply bg-red-400;
    }
    .row-odd + .row-even {
        @apply bg-blue-400;
    }
    .row-even + .row-odd {
        @apply bg-yellow-400;
    }
}

section:nth-of-type(odd) { @apply row row-odd; }
section:nth-of-type(even) { @apply row row-even; }
```

...should output something similar to:

```css
section:nth-of-type(odd) + section:nth-of-type(even) { background: blue; }
section:nth-of-type(even) + section:nth-of-type(odd) { background: yellow; }
```

...and yet it only outputs:

```css
section:nth-of-type(odd) + .row-even { background: blue; }
.row-odd + .section:nth-of-type(even) { background: blue; }
section:nth-of-type(even) + .row-odd { background: yellow; }
.row-even + section:nth-of-type(odd) { background: yellow; }
```

It also seems that when declaring composite rules (multiple classes, such as `.row.row-odd`) this breaks during compilation when applying it to a tag instead of a class, resulting in rules such as `.rowsection` (with no separator, which makes no sense) as a result of the following code:

```css
@layer components {
    .row.row-odd {
        @apply bg-green-400;
    }
}

section {
    @apply row-odd;
}
```

Both behaviors are observable in the output of the Tailwind Play document I linked


Hints:

Patch:
diff --git a/src/lib/expandApplyAtRules.js b/src/lib/expandApplyAtRules.js
--- a/src/lib/expandApplyAtRules.js
+++ b/src/lib/expandApplyAtRules.js
@@ -34,13 +34,13 @@ function extractClasses(node) {
   return Object.assign(classes, { groups: normalizedGroups })
 }
 
-let selectorExtractor = parser((root) => root.nodes.map((node) => node.toString()))
+let selectorExtractor = parser()
 
 /**
  * @param {string} ruleSelectors
  */
 function extractSelectors(ruleSelectors) {
-  return selectorExtractor.transformSync(ruleSelectors)
+  return selectorExtractor.astSync(ruleSelectors)
 }
 
 function extractBaseCandidates(candidates, separator) {
@@ -299,30 +299,61 @@ function processApply(root, context, localCache) {
    * What happens in this function is that we prepend a `.` and escape the candidate.
    * This will result in `.hover\:font-bold`
    * Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover`
+   *
+   * @param {string} selector
+   * @param {string} utilitySelectors
+   * @param {string} candidate
    */
-  // TODO: Should we use postcss-selector-parser for this instead?
   function replaceSelector(selector, utilitySelectors, candidate) {
-    let needle = `.${escapeClassName(candidate)}`
-    let needles = [...new Set([needle, needle.replace(/\\2c /g, '\\,')])]
+    let selectorList = extractSelectors(selector)
     let utilitySelectorsList = extractSelectors(utilitySelectors)
+    let candidateList = extractSelectors(`.${escapeClassName(candidate)}`)
+    let candidateClass = candidateList.nodes[0].nodes[0]
 
-    return extractSelectors(selector)
-      .map((s) => {
-        let replaced = []
+    selectorList.each((sel) => {
+      /** @type {Set<import('postcss-selector-parser').Selector>} */
+      let replaced = new Set()
 
-        for (let utilitySelector of utilitySelectorsList) {
-          let replacedSelector = utilitySelector
-          for (const needle of needles) {
-            replacedSelector = replacedSelector.replace(needle, s)
-          }
-          if (replacedSelector === utilitySelector) {
-            continue
+      utilitySelectorsList.each((utilitySelector) => {
+        utilitySelector = utilitySelector.clone()
+
+        utilitySelector.walkClasses((node) => {
+          if (node.value !== candidateClass.value) {
+            return
           }
-          replaced.push(replacedSelector)
-        }
-        return replaced.join(', ')
+
+          // Since you can only `@apply` class names this is sufficient
+          // We want to replace the matched class name with the selector the user is using
+          // Ex: Replace `.text-blue-500` with `.foo.bar:is(.something-cool)`
+          node.replaceWith(...sel.nodes.map((node) => node.clone()))
+
+          // Record that we did something and we want to use this new selector
+          replaced.add(utilitySelector)
+        })
       })
-      .join(', ')
+
+      // Sort tag names before class names
+      // This happens when replacing `.bar` in `.foo.bar` with a tag like `section`
+      for (const sel of replaced) {
+        sel.sort((a, b) => {
+          if (a.type === 'tag' && b.type === 'class') {
+            return -1
+          } else if (a.type === 'class' && b.type === 'tag') {
+            return 1
+          } else if (a.type === 'class' && b.type === 'pseudo') {
+            return -1
+          } else if (a.type === 'pseudo' && b.type === 'class') {
+            return 1
+          }
+
+          return sel.index(a) - sel.index(b)
+        })
+      }
+
+      sel.replaceWith(...replaced)
+    })
+
+    return selectorList.toString()
   }
 
   let perParentApplies = new Map()


Test patch:
diff --git a/tests/apply.test.js b/tests/apply.test.js
--- a/tests/apply.test.js
+++ b/tests/apply.test.js
@@ -1584,3 +1584,105 @@ it('can apply user utilities that start with a dash', async () => {
     }
   `)
 })
+
+it('can apply joined classes when using elements', async () => {
+  let config = {
+    content: [{ raw: html`<div class="foo-1 -foo-1 new-class"></div>` }],
+    plugins: [],
+  }
+
+  let input = css`
+    .foo.bar {
+      color: red;
+    }
+    .bar.foo {
+      color: green;
+    }
+    header:nth-of-type(odd) {
+      @apply foo;
+    }
+    main {
+      @apply foo bar;
+    }
+    footer {
+      @apply bar;
+    }
+  `
+
+  let result = await run(input, config)
+
+  expect(result.css).toMatchFormattedCss(css`
+    .foo.bar {
+      color: red;
+    }
+    .bar.foo {
+      color: green;
+    }
+    header.bar:nth-of-type(odd) {
+      color: red;
+      color: green;
+    }
+    main.bar {
+      color: red;
+    }
+    main.foo {
+      color: red;
+    }
+    main.bar {
+      color: green;
+    }
+    main.foo {
+      color: green;
+    }
+    footer.foo {
+      color: red;
+      color: green;
+    }
+  `)
+})
+
+it('can produce selectors that replace multiple instances of the same class', async () => {
+  let config = {
+    content: [{ raw: html`<div class="foo-1 -foo-1 new-class"></div>` }],
+    plugins: [],
+  }
+
+  let input = css`
+    .foo + .foo {
+      color: blue;
+    }
+    .bar + .bar {
+      color: fuchsia;
+    }
+    header {
+      @apply foo;
+    }
+    main {
+      @apply foo bar;
+    }
+    footer {
+      @apply bar;
+    }
+  `
+
+  let result = await run(input, config)
+
+  expect(result.css).toMatchFormattedCss(css`
+    .foo + .foo {
+      color: blue;
+    }
+    .bar + .bar {
+      color: fuchsia;
+    }
+    header + header {
+      color: blue;
+    }
+    main + main {
+      color: blue;
+      color: fuchsia;
+    }
+    footer + footer {
+      color: fuchsia;
+    }
+  `)
+})