Skip to content

Commit 772275d

Browse files
Fix infinite recursion in theme variable lookups (#1473)
Fixes #1463 We'll have to bail in some cases that we didn't before to fix this. This seems fine though. The core problem was that given this theme: ```css @theme { --radius: calc(var(--radius)); } ``` We'd see this CSS: ```css .rounded { border-radius: calc(var(--radius)); } ``` And we'd try to replace variables in `calc(var(--radius))`. This doesn't _start_ with a var(…) so our recursion guards didn't catch this. This entire system is a bit fragile and I'll be working to clean this up along with how we handle pixel equivalents, light-dark replacements, color-mix replacements, etc… in some future PRs (#1330 is one of those but I want to reorganize + tweak some code first in an earlier PR which will make that one "nicer")
1 parent 59b34d2 commit 772275d

File tree

3 files changed

+43
-4
lines changed

3 files changed

+43
-4
lines changed

packages/tailwindcss-language-service/src/util/rewriting/index.test.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ test('recursive theme replacements', () => {
111111
}
112112

113113
expect(replaceCssVarsWithFallbacks(state, 'var(--color-a)')).toBe('var(--color-a)')
114-
expect(replaceCssVarsWithFallbacks(state, 'var(--color-b)')).toBe('rgb(var(--color-b))')
114+
// TODO: -> rgb(var(--color-b))
115+
expect(replaceCssVarsWithFallbacks(state, 'var(--color-b)')).toBe('var(--color-b)')
115116
expect(replaceCssVarsWithFallbacks(state, 'var(--color-c)')).toBe('rgb(255 255 255)')
116117

117118
// This one is wrong but fixing it without breaking the infinite recursion guard is complex
@@ -152,7 +153,8 @@ test('recursive theme replacements (inlined)', () => {
152153
}
153154

154155
expect(inlineThemeValues('var(--color-a)', state)).toBe('var(--color-a)')
155-
expect(inlineThemeValues('var(--color-b)', state)).toBe('rgb(var(--color-b))')
156+
// TODO: -> rgb(var(--color-b))
157+
expect(inlineThemeValues('var(--color-b)', state)).toBe('var(--color-b)')
156158
expect(inlineThemeValues('var(--color-c)', state)).toBe('rgb(255 255 255)')
157159

158160
// This one is wrong but fixing it without breaking the infinite recursion guard is complex
@@ -192,6 +194,9 @@ test('Inlining calc expressions using the design system', () => {
192194
['--spacing', '0.25rem'],
193195
['--color-red-500', 'oklch(0.637 0.237 25.331)'],
194196
['--font-size.md', '1rem'],
197+
['--wrapped', 'calc(var(--wrapped))'],
198+
['--unbalanced-1', 'calc(var(--unbalanced-1)'],
199+
['--unbalanced-2', 'calc(1px + 2px'],
195200
])
196201

197202
let state: State = {
@@ -246,4 +251,31 @@ test('Inlining calc expressions using the design system', () => {
246251
expect(addThemeValues('var(--font-size\\.md)', state, settings)).toBe(
247252
'var(--font-size\\.md) /* 1rem = 10px */',
248253
)
254+
255+
// Variables that replace with themselves. This could be handled better
256+
expect(addThemeValues('calc(var(--wrapped))', state, settings)).toBe(
257+
'calc(var(--wrapped) /* calc(var(--wrapped)) */)',
258+
)
259+
expect(addThemeValues('var(--wrapped)', state, settings)).toBe(
260+
'var(--wrapped) /* calc(var(--wrapped)) */',
261+
)
262+
263+
// Unbalanced calc expressions. Whether or not these "work" is undefined. If
264+
// these change results that's okay.
265+
expect(addThemeValues('calc(var(--unbalanced-1))', state, settings)).toBe(
266+
'calc(var(--unbalanced-1) /* calc(var(--unbalanced-1) */)',
267+
)
268+
expect(addThemeValues('var(--unbalanced-1)', state, settings)).toBe(
269+
'var(--unbalanced-1) /* calc(var(--unbalanced-1) */',
270+
)
271+
272+
// It would be fine for either of these to not replace at all
273+
expect(addThemeValues('calc(var(--unbalanced-2))', state, settings)).toBe(
274+
'calc(var(--unbalanced-2)) /* 3px */',
275+
)
276+
expect(addThemeValues('var(--unbalanced-2)', state, settings)).toBe(
277+
'var(--unbalanced-2) /* calc(1px + 2px */',
278+
)
279+
280+
expect(addThemeValues('calc(1 + 2', state, settings)).toBe('calc(1 + 2')
249281
})

packages/tailwindcss-language-service/src/util/rewriting/replacements.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ export function replaceCssVars(
7676
})
7777

7878
if (replacement !== null) {
79+
// If we're replacing this variable with a reference back it *itself*
80+
// we should skip over it
81+
if (replacement.includes(`var(${varName})`) || replacement.includes(`var(${varName},`)) {
82+
break
83+
}
84+
7985
str = str.slice(0, i) + replacement + str.slice(j + 1)
8086
}
8187

@@ -121,7 +127,7 @@ export function replaceCssCalc(str: string, replace: CssCalcReplacer): string {
121127

122128
let depth = 0
123129

124-
for (let j = i + 5; i < str.length; ++j) {
130+
for (let j = i + 5; j < str.length; ++j) {
125131
if (str[j] === '(') {
126132
depth++
127133
} else if (str[j] === ')' && depth > 0) {

packages/vscode-tailwindcss/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
## Prerelease
44

5-
- Nothing yet!
5+
- Fix infinite recursion in theme variable lookups ([#1473](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1473))
6+
- Fix infinite recursion when replacing unbalanced calc expressions ([#1473](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1473))
67

78
## 0.14.27
89

0 commit comments

Comments
 (0)