Skip to content

Commit 7089a80

Browse files
authored
Improve circular dependency detection when using @apply (#6588)
* improve circular dependency detection when using `@apply` I also changed the message to the same message we used in V2. * update changelog
1 parent 6cf3e3e commit 7089a80

File tree

3 files changed

+97
-23
lines changed

3 files changed

+97
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

1212
- Don't mutate custom color palette when overriding per-plugin colors ([#6546](https://github.com/tailwindlabs/tailwindcss/pull/6546))
13+
- Improve circular dependency detection when using `@apply` ([#6588](https://github.com/tailwindlabs/tailwindcss/pull/6588))
1314

1415
## [3.0.6] - 2021-12-16
1516

src/lib/expandApplyAtRules.js

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,33 @@
11
import postcss from 'postcss'
22
import parser from 'postcss-selector-parser'
3+
34
import { resolveMatches } from './generateRules'
45
import bigSign from '../util/bigSign'
56
import escapeClassName from '../util/escapeClassName'
67

7-
function containsBase(selector, classCandidateBase, separator) {
8-
return parser((selectors) => {
9-
let contains = false
8+
function extractClasses(node) {
9+
let classes = new Set()
10+
let container = postcss.root({ nodes: [node.clone()] })
1011

11-
selectors.walkClasses((classSelector) => {
12-
if (classSelector.value.split(separator).pop() === classCandidateBase) {
13-
contains = true
14-
return false
15-
}
16-
})
12+
container.walkRules((rule) => {
13+
parser((selectors) => {
14+
selectors.walkClasses((classSelector) => {
15+
classes.add(classSelector.value)
16+
})
17+
}).processSync(rule.selector)
18+
})
1719

18-
return contains
19-
}).transformSync(selector)
20+
return Array.from(classes)
21+
}
22+
23+
function extractBaseCandidates(candidates, separator) {
24+
let baseClasses = new Set()
25+
26+
for (let candidate of candidates) {
27+
baseClasses.add(candidate.split(separator).pop())
28+
}
29+
30+
return Array.from(baseClasses)
2031
}
2132

2233
function prefix(context, selector) {
@@ -212,15 +223,40 @@ function processApply(root, context) {
212223
let siblings = []
213224

214225
for (let [applyCandidate, important, rules] of candidates) {
215-
let base = applyCandidate.split(context.tailwindConfig.separator).pop()
216-
217226
for (let [meta, node] of rules) {
218-
if (
219-
containsBase(parent.selector, base, context.tailwindConfig.separator) &&
220-
containsBase(node.selector, base, context.tailwindConfig.separator)
221-
) {
227+
let parentClasses = extractClasses(parent)
228+
let nodeClasses = extractClasses(node)
229+
230+
// Add base utility classes from the @apply node to the list of
231+
// classes to check whether it intersects and therefore results in a
232+
// circular dependency or not.
233+
//
234+
// E.g.:
235+
// .foo {
236+
// @apply hover:a; // This applies "a" but with a modifier
237+
// }
238+
//
239+
// We only have to do that with base classes of the `node`, not of the `parent`
240+
// E.g.:
241+
// .hover\:foo {
242+
// @apply bar;
243+
// }
244+
// .bar {
245+
// @apply foo;
246+
// }
247+
//
248+
// This should not result in a circular dependency because we are
249+
// just applying `.foo` and the rule above is `.hover\:foo` which is
250+
// unrelated. However, if we were to apply `hover:foo` then we _did_
251+
// have to include this one.
252+
nodeClasses = nodeClasses.concat(
253+
extractBaseCandidates(nodeClasses, context.tailwindConfig.separator)
254+
)
255+
256+
let intersects = parentClasses.some((selector) => nodeClasses.includes(selector))
257+
if (intersects) {
222258
throw node.error(
223-
`Circular dependency detected when using: \`@apply ${applyCandidate}\``
259+
`You cannot \`@apply\` the \`${applyCandidate}\` utility here because it creates a circular dependency.`
224260
)
225261
}
226262

@@ -250,7 +286,6 @@ function processApply(root, context) {
250286
// Inject the rules, sorted, correctly
251287
let nodes = siblings.sort(([a], [z]) => bigSign(a.sort - z.sort)).map((s) => s[1])
252288

253-
// console.log(parent)
254289
// `parent` refers to the node at `.abc` in: .abc { @apply mt-2 }
255290
parent.after(nodes)
256291
}

tests/apply.test.js

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,9 @@ it('should throw when trying to apply a direct circular dependency', () => {
484484
`
485485

486486
return run(input, config).catch((err) => {
487-
expect(err.reason).toBe('Circular dependency detected when using: `@apply text-red-500`')
487+
expect(err.reason).toBe(
488+
'You cannot `@apply` the `text-red-500` utility here because it creates a circular dependency.'
489+
)
488490
})
489491
})
490492

@@ -514,7 +516,39 @@ it('should throw when trying to apply an indirect circular dependency', () => {
514516
`
515517

516518
return run(input, config).catch((err) => {
517-
expect(err.reason).toBe('Circular dependency detected when using: `@apply a`')
519+
expect(err.reason).toBe(
520+
'You cannot `@apply` the `a` utility here because it creates a circular dependency.'
521+
)
522+
})
523+
})
524+
525+
it('should not throw when the selector is different (but contains the base partially)', () => {
526+
let config = {
527+
content: [{ raw: html`<div class="bg-gray-500"></div>` }],
528+
plugins: [],
529+
}
530+
531+
let input = css`
532+
@tailwind components;
533+
@tailwind utilities;
534+
535+
.focus\:bg-gray-500 {
536+
@apply bg-gray-500;
537+
}
538+
`
539+
540+
return run(input, config).then((result) => {
541+
expect(result.css).toMatchFormattedCss(css`
542+
.bg-gray-500 {
543+
--tw-bg-opacity: 1;
544+
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
545+
}
546+
547+
.focus\:bg-gray-500 {
548+
--tw-bg-opacity: 1;
549+
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
550+
}
551+
`)
518552
})
519553
})
520554

@@ -544,7 +578,9 @@ it('should throw when trying to apply an indirect circular dependency with a mod
544578
`
545579

546580
return run(input, config).catch((err) => {
547-
expect(err.reason).toBe('Circular dependency detected when using: `@apply hover:a`')
581+
expect(err.reason).toBe(
582+
'You cannot `@apply` the `hover:a` utility here because it creates a circular dependency.'
583+
)
548584
})
549585
})
550586

@@ -574,7 +610,9 @@ it('should throw when trying to apply an indirect circular dependency with a mod
574610
`
575611

576612
return run(input, config).catch((err) => {
577-
expect(err.reason).toBe('Circular dependency detected when using: `@apply a`')
613+
expect(err.reason).toBe(
614+
'You cannot `@apply` the `a` utility here because it creates a circular dependency.'
615+
)
578616
})
579617
})
580618

0 commit comments

Comments
 (0)