Skip to content

Commit

Permalink
Detect circular dependencies when using @apply (#6365)
Browse files Browse the repository at this point in the history
* detect circular dependencies when using `@apply`

* update changelog

* ensure we split by the separator
  • Loading branch information
RobinMalfait authored Dec 10, 2021
1 parent 838185b commit 08241c3
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 27 additions & 0 deletions src/lib/expandApplyAtRules.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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')
Expand Down
112 changes: 112 additions & 0 deletions tests/apply.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`<div class="foo"></div>` }],
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`<div class="a"></div>` }],
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`<div class="a"></div>` }],
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`<div class="a"></div>` }],
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`')
})
})

0 comments on commit 08241c3

Please sign in to comment.