diff --git a/CHANGELOG.md b/CHANGELOG.md index 68a1143d06df..e772f3eba5f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Nothing yet! +### Fixed + +- Fix infinite loop when `theme()` function is called recursively inside `@theme` blocks ## [4.1.6] - 2025-05-09 diff --git a/packages/tailwindcss/src/css-functions.test.ts b/packages/tailwindcss/src/css-functions.test.ts index 9c7502b8d4e5..5004b2bcb6ba 100644 --- a/packages/tailwindcss/src/css-functions.test.ts +++ b/packages/tailwindcss/src/css-functions.test.ts @@ -433,6 +433,27 @@ describe('--theme(…)', () => { }" `) }) + + test('--theme(…) prevents infinite loops with circular references', async () => { + expect( + await compileCss(css` + @theme { + --font-sans: 'Inter', --theme(--font-sans); + } + .font { + font-family: var(--font-sans); + } + `), + ).toMatchInlineSnapshot(` + ":root, :host { + --font-sans: "Inter", var(--font-sans); + } + + .font { + font-family: var(--font-sans); + }" + `) + }) }) describe('theme(…)', () => { @@ -903,6 +924,19 @@ describe('theme(…)', () => { }" `) }) + + test('theme(…) prevents infinite loops with circular references', async () => { + await expect( + compileCss(css` + @theme { + --font-sans: 'Inter', theme(fontFamily.sans); + } + .font { + font-family: var(--font-sans); + } + `), + ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Could not resolve value for theme function: \`theme(fontFamily.sans)\`. The resolved value \`'Inter', theme(fontFamily.sans)\` contains a recursive reference to itself.]`) + }) }) describe('with CSS variable syntax', () => { diff --git a/packages/tailwindcss/src/css-functions.ts b/packages/tailwindcss/src/css-functions.ts index 618835fec40a..3ba220242023 100644 --- a/packages/tailwindcss/src/css-functions.ts +++ b/packages/tailwindcss/src/css-functions.ts @@ -145,6 +145,14 @@ function legacyTheme( ) } + // Detect eventual recursive theme function calls. + let regex = new RegExp('theme\\(\\s*[\'\"]?' + path) + if (typeof resolvedValue === 'string' && resolvedValue.match(regex)) { + throw new Error( + `Could not resolve value for theme function: \`theme(${path})\`. The resolved value \`${resolvedValue}\` contains a recursive reference to itself.`, + ) + } + return resolvedValue }