Upgrade: Do not extract class names from functions (#15566)

This PR prevents the migration of utilities detected in function names,
e.g.: the use of `shadow` inside `filter: 'drop-shadow(…)'`.

## Test plan

Have content like this in a project you're migrating using the upgrade
tool:

```js
{
  filter: 'drop-shadow(0 0 0.5rem #000)'
}
```

This was verified by adding unit tests to the specific codemods and
adding an integration test.
This commit is contained in:
Philipp Spiess 2025-01-07 15:31:48 +01:00 committed by GitHub
parent c84acf8e82
commit d4f693f7f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 57 additions and 20 deletions

View File

@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix `inset-shadow-*` suggestions in IntelliSense ([#15471](https://github.com/tailwindlabs/tailwindcss/pull/15471))
- Only compile arbitrary values ending in `]` ([#15503](https://github.com/tailwindlabs/tailwindcss/pull/15503))
- Improve performance and memory usage ([#15529](https://github.com/tailwindlabs/tailwindcss/pull/15529))
- _Upgrade (experimental)_: Do not extract class names from functions (e.g. `shadow` in `filter: 'drop-shadow(…)'`) ([#15566](https://github.com/tailwindlabs/tailwindcss/pull/15566))
### Changed
@ -782,4 +783,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [4.0.0-alpha.1] - 2024-03-06
- First 4.0.0-alpha.1 release

View File

@ -137,6 +137,7 @@ test(
'src/test.js': ts`
export default {
'shouldNotMigrate': !border.test + '',
'filter': 'drop-shadow(0 0 0.5rem #000)',
}
`,
'src/index.html': html`
@ -288,6 +289,7 @@ test(
--- src/test.js ---
export default {
'shouldNotMigrate': !border.test + '',
'filter': 'drop-shadow(0 0 0.5rem #000)',
}
"
`)

View File

@ -38,31 +38,31 @@ test('does not match false positives', async () => {
).toEqual('!border')
})
test('does not match false positives', async () => {
test('does not replace classes in invalid positions', async () => {
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
base: __dirname,
})
function shouldNotDetect(example: string, candidate = '!border') {
function shouldNotReplace(example: string, candidate = '!border') {
expect(
important(designSystem, {}, candidate, {
contents: example,
start: example.indexOf(candidate),
end: example.indexOf(candidate) + candidate.length,
}),
).toEqual('!border')
).toEqual(candidate)
}
shouldNotDetect(`let notBorder = !border \n`)
shouldNotDetect(`{ "foo": !border.something + ""}\n`)
shouldNotDetect(`<div v-if="something && !border"></div>\n`)
shouldNotDetect(`<div v-else-if="something && !border"></div>\n`)
shouldNotDetect(`<div v-show="something && !border"></div>\n`)
shouldNotDetect(`<div v-if="!border || !border"></div>\n`)
shouldNotDetect(`<div v-else-if="!border || !border"></div>\n`)
shouldNotDetect(`<div v-show="!border || !border"></div>\n`)
shouldNotDetect(`<div v-if="!border"></div>\n`)
shouldNotDetect(`<div v-else-if="!border"></div>\n`)
shouldNotDetect(`<div v-show="!border"></div>\n`)
shouldNotDetect(`<div x-if="!border"></div>\n`)
shouldNotReplace(`let notBorder = !border \n`)
shouldNotReplace(`{ "foo": !border.something + ""}\n`)
shouldNotReplace(`<div v-if="something && !border"></div>\n`)
shouldNotReplace(`<div v-else-if="something && !border"></div>\n`)
shouldNotReplace(`<div v-show="something && !border"></div>\n`)
shouldNotReplace(`<div v-if="!border || !border"></div>\n`)
shouldNotReplace(`<div v-else-if="!border || !border"></div>\n`)
shouldNotReplace(`<div v-show="!border || !border"></div>\n`)
shouldNotReplace(`<div v-if="!border"></div>\n`)
shouldNotReplace(`<div v-else-if="!border"></div>\n`)
shouldNotReplace(`<div v-show="!border"></div>\n`)
shouldNotReplace(`<div x-if="!border"></div>\n`)
})

View File

@ -39,3 +39,33 @@ test.each([
expect(await legacyClasses(designSystem, {}, candidate)).toEqual(result)
})
test('does not replace classes in invalid positions', async () => {
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
base: __dirname,
})
async function shouldNotReplace(example: string, candidate = 'shadow') {
expect(
await legacyClasses(designSystem, {}, candidate, {
contents: example,
start: example.indexOf(candidate),
end: example.indexOf(candidate) + candidate.length,
}),
).toEqual(candidate)
}
await shouldNotReplace(`let notShadow = shadow \n`)
await shouldNotReplace(`{ "foo": shadow.something + ""}\n`)
await shouldNotReplace(`<div v-if="something && shadow"></div>\n`)
await shouldNotReplace(`<div v-else-if="something && shadow"></div>\n`)
await shouldNotReplace(`<div v-show="something && shadow"></div>\n`)
await shouldNotReplace(`<div v-if="shadow || shadow"></div>\n`)
await shouldNotReplace(`<div v-else-if="shadow || shadow"></div>\n`)
await shouldNotReplace(`<div v-show="shadow || shadow"></div>\n`)
await shouldNotReplace(`<div v-if="shadow"></div>\n`)
await shouldNotReplace(`<div v-else-if="shadow"></div>\n`)
await shouldNotReplace(`<div v-show="shadow"></div>\n`)
await shouldNotReplace(`<div x-if="shadow"></div>\n`)
await shouldNotReplace(`<div style={{filter: 'drop-shadow(30px 10px 4px #4444dd)'}}/>\n`)
})

View File

@ -29,19 +29,24 @@ export function isSafeMigration(location: { contents: string; start: number; end
currentLineAfterCandidate += char
}
// Heuristic 1: Require the candidate to be inside quotes
// Heuristic: Require the candidate to be inside quotes
let isQuoteBeforeCandidate = QUOTES.some((quote) => currentLineBeforeCandidate.includes(quote))
let isQuoteAfterCandidate = QUOTES.some((quote) => currentLineAfterCandidate.includes(quote))
if (!isQuoteBeforeCandidate || !isQuoteAfterCandidate) {
return false
}
// Heuristic 2: Disallow object access immediately following the candidate
// Heuristic: Disallow object access immediately following the candidate
if (currentLineAfterCandidate[0] === '.') {
return false
}
// Heuristic 3: Disallow logical operators preceding or following the candidate
// Heuristic: Disallow function call expressions immediately following the candidate
if (currentLineAfterCandidate.trim().startsWith('(')) {
return false
}
// Heuristic: Disallow logical operators preceding or following the candidate
for (let operator of LOGICAL_OPERATORS) {
if (
currentLineAfterCandidate.trim().startsWith(operator) ||
@ -51,7 +56,7 @@ export function isSafeMigration(location: { contents: string; start: number; end
}
}
// Heuristic 4: Disallow conditional template syntax
// Heuristic: Disallow conditional template syntax
for (let rule of CONDITIONAL_TEMPLATE_SYNTAX) {
if (rule.test(currentLineBeforeCandidate)) {
return false