diff --git a/CHANGELOG.md b/CHANGELOG.md index fa555db9f265..52d6d51f66a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure complex variants with multiple classes work ([#6311](https://github.com/tailwindlabs/tailwindcss/pull/6311)) - Re-add `default` interop to public available functions ([#6348](https://github.com/tailwindlabs/tailwindcss/pull/6348)) +- Detect circular dependencies when using `@apply` ([#6365](https://github.com/tailwindlabs/tailwindcss/pull/6365)) ## [3.0.0] - 2021-12-09 diff --git a/src/lib/expandApplyAtRules.js b/src/lib/expandApplyAtRules.js index 592acb163a0b..561343266945 100644 --- a/src/lib/expandApplyAtRules.js +++ b/src/lib/expandApplyAtRules.js @@ -1,8 +1,24 @@ import postcss from 'postcss' +import parser from 'postcss-selector-parser' import { resolveMatches } from './generateRules' import bigSign from '../util/bigSign' import escapeClassName from '../util/escapeClassName' +function containsBase(selector, classCandidateBase, separator) { + return parser((selectors) => { + let contains = false + + selectors.walkClasses((classSelector) => { + if (classSelector.value.split(separator).pop() === classCandidateBase) { + contains = true + return false + } + }) + + return contains + }).transformSync(selector) +} + function prefix(context, selector) { let prefix = context.tailwindConfig.prefix return typeof prefix === 'function' ? prefix(selector) : prefix + selector @@ -196,7 +212,18 @@ function processApply(root, context) { let siblings = [] for (let [applyCandidate, important, rules] of candidates) { + let base = applyCandidate.split(context.tailwindConfig.separator).pop() + for (let [meta, node] of rules) { + if ( + containsBase(parent.selector, base, context.tailwindConfig.separator) && + containsBase(node.selector, base, context.tailwindConfig.separator) + ) { + throw node.error( + `Circular dependency detected when using: \`@apply ${applyCandidate}\`` + ) + } + let root = postcss.root({ nodes: [node.clone()] }) let canRewriteSelector = node.type !== 'atrule' || (node.type === 'atrule' && node.name !== 'keyframes') diff --git a/tests/apply.test.js b/tests/apply.test.js index dd899febc4c8..d8e75138f9ba 100644 --- a/tests/apply.test.js +++ b/tests/apply.test.js @@ -465,3 +465,115 @@ it('should apply all the definitions of a class', () => { `) }) }) + +it('should throw when trying to apply a direct circular dependency', () => { + let config = { + content: [{ raw: html`
` }], + plugins: [], + } + + let input = css` + @tailwind components; + @tailwind utilities; + + @layer components { + .foo:not(.text-red-500) { + @apply text-red-500; + } + } + ` + + return run(input, config).catch((err) => { + expect(err.reason).toBe('Circular dependency detected when using: `@apply text-red-500`') + }) +}) + +it('should throw when trying to apply an indirect circular dependency', () => { + let config = { + content: [{ raw: html`
` }], + plugins: [], + } + + let input = css` + @tailwind components; + @tailwind utilities; + + @layer components { + .a { + @apply b; + } + + .b { + @apply c; + } + + .c { + @apply a; + } + } + ` + + return run(input, config).catch((err) => { + expect(err.reason).toBe('Circular dependency detected when using: `@apply a`') + }) +}) + +it('should throw when trying to apply an indirect circular dependency with a modifier (1)', () => { + let config = { + content: [{ raw: html`
` }], + plugins: [], + } + + let input = css` + @tailwind components; + @tailwind utilities; + + @layer components { + .a { + @apply b; + } + + .b { + @apply c; + } + + .c { + @apply hover:a; + } + } + ` + + return run(input, config).catch((err) => { + expect(err.reason).toBe('Circular dependency detected when using: `@apply hover:a`') + }) +}) + +it('should throw when trying to apply an indirect circular dependency with a modifier (2)', () => { + let config = { + content: [{ raw: html`
` }], + plugins: [], + } + + let input = css` + @tailwind components; + @tailwind utilities; + + @layer components { + .a { + @apply b; + } + + .b { + @apply hover:c; + } + + .c { + @apply a; + } + } + ` + + return run(input, config).catch((err) => { + expect(err.reason).toBe('Circular dependency detected when using: `@apply a`') + }) +})