Skip to content

Commit 08241c3

Browse files
authored
Detect circular dependencies when using @apply (#6365)
* detect circular dependencies when using `@apply` * update changelog * ensure we split by the separator
1 parent 838185b commit 08241c3

File tree

3 files changed

+140
-0
lines changed

3 files changed

+140
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Ensure complex variants with multiple classes work ([#6311](https://github.com/tailwindlabs/tailwindcss/pull/6311))
1313
- Re-add `default` interop to public available functions ([#6348](https://github.com/tailwindlabs/tailwindcss/pull/6348))
14+
- Detect circular dependencies when using `@apply` ([#6365](https://github.com/tailwindlabs/tailwindcss/pull/6365))
1415

1516
## [3.0.0] - 2021-12-09
1617

src/lib/expandApplyAtRules.js

+27
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
11
import postcss from 'postcss'
2+
import parser from 'postcss-selector-parser'
23
import { resolveMatches } from './generateRules'
34
import bigSign from '../util/bigSign'
45
import escapeClassName from '../util/escapeClassName'
56

7+
function containsBase(selector, classCandidateBase, separator) {
8+
return parser((selectors) => {
9+
let contains = false
10+
11+
selectors.walkClasses((classSelector) => {
12+
if (classSelector.value.split(separator).pop() === classCandidateBase) {
13+
contains = true
14+
return false
15+
}
16+
})
17+
18+
return contains
19+
}).transformSync(selector)
20+
}
21+
622
function prefix(context, selector) {
723
let prefix = context.tailwindConfig.prefix
824
return typeof prefix === 'function' ? prefix(selector) : prefix + selector
@@ -196,7 +212,18 @@ function processApply(root, context) {
196212
let siblings = []
197213

198214
for (let [applyCandidate, important, rules] of candidates) {
215+
let base = applyCandidate.split(context.tailwindConfig.separator).pop()
216+
199217
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+
) {
222+
throw node.error(
223+
`Circular dependency detected when using: \`@apply ${applyCandidate}\``
224+
)
225+
}
226+
200227
let root = postcss.root({ nodes: [node.clone()] })
201228
let canRewriteSelector =
202229
node.type !== 'atrule' || (node.type === 'atrule' && node.name !== 'keyframes')

tests/apply.test.js

+112
Original file line numberDiff line numberDiff line change
@@ -465,3 +465,115 @@ it('should apply all the definitions of a class', () => {
465465
`)
466466
})
467467
})
468+
469+
it('should throw when trying to apply a direct circular dependency', () => {
470+
let config = {
471+
content: [{ raw: html`<div class="foo"></div>` }],
472+
plugins: [],
473+
}
474+
475+
let input = css`
476+
@tailwind components;
477+
@tailwind utilities;
478+
479+
@layer components {
480+
.foo:not(.text-red-500) {
481+
@apply text-red-500;
482+
}
483+
}
484+
`
485+
486+
return run(input, config).catch((err) => {
487+
expect(err.reason).toBe('Circular dependency detected when using: `@apply text-red-500`')
488+
})
489+
})
490+
491+
it('should throw when trying to apply an indirect circular dependency', () => {
492+
let config = {
493+
content: [{ raw: html`<div class="a"></div>` }],
494+
plugins: [],
495+
}
496+
497+
let input = css`
498+
@tailwind components;
499+
@tailwind utilities;
500+
501+
@layer components {
502+
.a {
503+
@apply b;
504+
}
505+
506+
.b {
507+
@apply c;
508+
}
509+
510+
.c {
511+
@apply a;
512+
}
513+
}
514+
`
515+
516+
return run(input, config).catch((err) => {
517+
expect(err.reason).toBe('Circular dependency detected when using: `@apply a`')
518+
})
519+
})
520+
521+
it('should throw when trying to apply an indirect circular dependency with a modifier (1)', () => {
522+
let config = {
523+
content: [{ raw: html`<div class="a"></div>` }],
524+
plugins: [],
525+
}
526+
527+
let input = css`
528+
@tailwind components;
529+
@tailwind utilities;
530+
531+
@layer components {
532+
.a {
533+
@apply b;
534+
}
535+
536+
.b {
537+
@apply c;
538+
}
539+
540+
.c {
541+
@apply hover:a;
542+
}
543+
}
544+
`
545+
546+
return run(input, config).catch((err) => {
547+
expect(err.reason).toBe('Circular dependency detected when using: `@apply hover:a`')
548+
})
549+
})
550+
551+
it('should throw when trying to apply an indirect circular dependency with a modifier (2)', () => {
552+
let config = {
553+
content: [{ raw: html`<div class="a"></div>` }],
554+
plugins: [],
555+
}
556+
557+
let input = css`
558+
@tailwind components;
559+
@tailwind utilities;
560+
561+
@layer components {
562+
.a {
563+
@apply b;
564+
}
565+
566+
.b {
567+
@apply hover:c;
568+
}
569+
570+
.c {
571+
@apply a;
572+
}
573+
}
574+
`
575+
576+
return run(input, config).catch((err) => {
577+
expect(err.reason).toBe('Circular dependency detected when using: `@apply a`')
578+
})
579+
})

0 commit comments

Comments
 (0)