Skip to content

Commit 5c82545

Browse files
Guard against infinitely recursive theme key lookup (#1332)
When constructing the design system we compute color swatches for all classes. This process involves: - Computing the CSS for a utility - Inlining any values from the theme, recursively - Scanning for CSS color syntax that we can compute a swatch for We guard against directly self-recursive replacements because of how the replacement itself functions. However, if you wrapped the variable in something then we would effectively recurse into the expression and try to replace the variable again. This caused the expansion to then be wrapped and then try again resulting in an infinite loop. This fixes it by keeping track of seen variables during a replacement and if we've seen it _and_ the expansion contains a var(…) anywhere we'll only replace it once. This logic is not perfect as the thing that matters is that the replacement _itself_ results in a recursion but this is good enough for now. Fixes #1329
1 parent 4fb66e1 commit 5c82545

File tree

3 files changed

+56
-4
lines changed

3 files changed

+56
-4
lines changed

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

+43-3
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,49 @@ test('replacing CSS variables with their fallbacks (when they have them)', () =>
7979
expect(replaceCssVarsWithFallbacks(state, 'var(--level-3)')).toBe('blue')
8080

8181
// Circular replacements don't cause infinite loops
82-
expect(replaceCssVarsWithFallbacks(state, 'var(--circular-1)')).toBe('var(--circular-3)')
83-
expect(replaceCssVarsWithFallbacks(state, 'var(--circular-2)')).toBe('var(--circular-1)')
84-
expect(replaceCssVarsWithFallbacks(state, 'var(--circular-3)')).toBe('var(--circular-2)')
82+
expect(replaceCssVarsWithFallbacks(state, 'var(--circular-1)')).toBe('var(--circular-1)')
83+
expect(replaceCssVarsWithFallbacks(state, 'var(--circular-2)')).toBe('var(--circular-2)')
84+
expect(replaceCssVarsWithFallbacks(state, 'var(--circular-3)')).toBe('var(--circular-3)')
85+
})
86+
87+
test('recursive theme replacements', () => {
88+
let map = new Map<string, string>([
89+
['--color-a', 'var(--color-a)'],
90+
['--color-b', 'rgb(var(--color-b))'],
91+
['--color-c', 'rgb(var(--channel) var(--channel) var(--channel))'],
92+
['--channel', '255'],
93+
94+
['--color-d', 'rgb(var(--indirect) var(--indirect) var(--indirect))'],
95+
['--indirect', 'var(--channel)'],
96+
['--channel', '255'],
97+
98+
['--mutual-a', 'calc(var(--mutual-b) * 1)'],
99+
['--mutual-b', 'calc(var(--mutual-a) + 1)'],
100+
])
101+
102+
let state: State = {
103+
enabled: true,
104+
designSystem: {
105+
theme: { prefix: null } as any,
106+
resolveThemeValue: (name) => map.get(name) ?? null,
107+
} as DesignSystem,
108+
}
109+
110+
expect(replaceCssVarsWithFallbacks(state, 'var(--color-a)')).toBe('var(--color-a)')
111+
expect(replaceCssVarsWithFallbacks(state, 'var(--color-b)')).toBe('rgb(var(--color-b))')
112+
expect(replaceCssVarsWithFallbacks(state, 'var(--color-c)')).toBe('rgb(255 255 255)')
113+
114+
// This one is wrong but fixing it without breaking the infinite recursion guard is complex
115+
expect(replaceCssVarsWithFallbacks(state, 'var(--color-d)')).toBe(
116+
'rgb(255 var(--indirect) var(--indirect))',
117+
)
118+
119+
expect(replaceCssVarsWithFallbacks(state, 'var(--mutual-a)')).toBe(
120+
'calc(calc(var(--mutual-a) + 1) * 1)',
121+
)
122+
expect(replaceCssVarsWithFallbacks(state, 'var(--mutual-b)')).toBe(
123+
'calc(calc(var(--mutual-b) * 1) + 1)',
124+
)
85125
})
86126

87127
test('Evaluating CSS calc expressions', () => {

packages/tailwindcss-language-service/src/util/rewriting/var-fallbacks.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,25 @@ import { resolveVariableValue } from './lookup'
33
import { replaceCssVars } from './replacements'
44

55
export function replaceCssVarsWithFallbacks(state: State, str: string): string {
6+
let seen = new Set<string>()
7+
68
return replaceCssVars(str, {
79
replace({ name, fallback }) {
810
// Replace with the value from the design system first. The design system
911
// take precedences over other sources as that emulates the behavior of a
1012
// browser where the fallback is only used if the variable is defined.
1113
if (state.designSystem && name.startsWith('--')) {
14+
// TODO: This isn't quite right as we might skip expanding a variable
15+
// that should be expanded
16+
if (seen.has(name)) return null
1217
let value = resolveVariableValue(state.designSystem, name)
13-
if (value !== null) return value
18+
if (value !== null) {
19+
if (value.includes('var(')) {
20+
seen.add(name)
21+
}
22+
23+
return value
24+
}
1425
}
1526

1627
if (fallback) {

packages/vscode-tailwindcss/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Warn when using a blocklisted class in v4 ([#1310](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1310))
66
- Support class function hovers in Svelte and HTML `<script>` blocks ([#1311](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1311))
7+
- Guard against recursive theme key lookup ([#1332](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1332))
78

89
# 0.14.15
910

0 commit comments

Comments
 (0)