From ed86da6283040ad0d427a6a3cb8da0472ef816b3 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 10 Dec 2021 15:43:20 +0100 Subject: [PATCH 1/3] detect circular dependencies when using `@apply` --- src/lib/expandApplyAtRules.js | 24 ++++++++ tests/apply.test.js | 112 ++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/src/lib/expandApplyAtRules.js b/src/lib/expandApplyAtRules.js index 592acb163a0b..e72e8814c3cb 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) { + return parser((selectors) => { + let contains = false + + selectors.walkClasses((classSelector) => { + if (classSelector.value.split(':').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,15 @@ 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) && containsBase(node.selector, base)) { + 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`') + }) +}) From 25ebd14d1e1cc167c98e66b8a0e988b568ab8b74 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 10 Dec 2021 15:46:19 +0100 Subject: [PATCH 2/3] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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 From 996130a14f5bab0fb43fd14190e42875635e7576 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 10 Dec 2021 15:47:29 +0100 Subject: [PATCH 3/3] ensure we split by the separator --- src/lib/expandApplyAtRules.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/lib/expandApplyAtRules.js b/src/lib/expandApplyAtRules.js index e72e8814c3cb..561343266945 100644 --- a/src/lib/expandApplyAtRules.js +++ b/src/lib/expandApplyAtRules.js @@ -4,12 +4,12 @@ import { resolveMatches } from './generateRules' import bigSign from '../util/bigSign' import escapeClassName from '../util/escapeClassName' -function containsBase(selector, classCandidateBase) { +function containsBase(selector, classCandidateBase, separator) { return parser((selectors) => { let contains = false selectors.walkClasses((classSelector) => { - if (classSelector.value.split(':').pop() === classCandidateBase) { + if (classSelector.value.split(separator).pop() === classCandidateBase) { contains = true return false } @@ -215,7 +215,10 @@ function processApply(root, context) { let base = applyCandidate.split(context.tailwindConfig.separator).pop() for (let [meta, node] of rules) { - if (containsBase(parent.selector, base) && containsBase(node.selector, base)) { + 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}\`` )