Skip to content

Commit 3f2afaf

Browse files
Upgrade: Improve heuristics around important codemod (#14774)
This PR improves the heuristics around the important codemod (e.g. `!border` => `border!`) as we noticed a few more cases where we the current heuristics was not enough. Specifically, we made it not migrate the candidate in the following conditions: - When there's an immediate property access: `{ "foo": !border.something + ""}` - When it's used as condition in the template language: `<div v-if="something && !border"></div>` or `<div x-if="!border"></div>` ## Test plan I added test cases to the unit tests and updated the integration test to contain a more sophisticated example. --------- Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
1 parent 8605426 commit 3f2afaf

File tree

4 files changed

+75
-34
lines changed

4 files changed

+75
-34
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

1212
- Ensure individual logical property utilities are sorted later than left/right pair utilities ([#14777](https://github.com/tailwindlabs/tailwindcss/pull/14777))
13+
- Don't migrate important modifiers inside conditional statements in Vue and Alpine (e.g. `<div v-if="!border" />`) ([#14774](https://github.com/tailwindlabs/tailwindcss/pull/14774))
1314
- _Upgrade (experimental)_: Ensure `@import` statements for relative CSS files are actually migrated to use relative path syntax ([#14769](https://github.com/tailwindlabs/tailwindcss/pull/14769))
1415

1516
## [4.0.0-alpha.29] - 2024-10-23

integrations/upgrade/js-config.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,10 @@ test(
123123
@tailwind components;
124124
@tailwind utilities;
125125
`,
126+
// prettier-ignore
126127
'src/test.js': ts`
127128
export default {
128-
shouldNotUse: !border.shouldUse,
129+
'shouldNotMigrate': !border.test + '',
129130
}
130131
`,
131132
'node_modules/my-external-lib/src/template.html': html`
@@ -275,7 +276,7 @@ test(
275276
276277
--- src/test.js ---
277278
export default {
278-
shouldNotUse: !border.shouldUse,
279+
'shouldNotMigrate': !border.test + '',
279280
}
280281
"
281282
`)

packages/@tailwindcss-upgrade/src/template/codemods/important.test.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,31 @@ test('does not match false positives', async () => {
3838
).toEqual('!border')
3939
})
4040

41-
test('does not match false positives with spaces at the end of the line', async () => {
41+
test('does not match false positives', async () => {
4242
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
4343
base: __dirname,
4444
})
4545

46-
expect(
47-
important(designSystem, {}, '!border', {
48-
contents: `let notBorder = !border \n`,
49-
start: 16,
50-
end: 16 + '!border'.length,
51-
}),
52-
).toEqual('!border')
46+
function shouldNotDetect(example: string, candidate = '!border') {
47+
expect(
48+
important(designSystem, {}, candidate, {
49+
contents: example,
50+
start: example.indexOf(candidate),
51+
end: example.indexOf(candidate) + candidate.length,
52+
}),
53+
).toEqual('!border')
54+
}
55+
56+
shouldNotDetect(`let notBorder = !border \n`)
57+
shouldNotDetect(`{ "foo": !border.something + ""}\n`)
58+
shouldNotDetect(`<div v-if="something && !border"></div>\n`)
59+
shouldNotDetect(`<div v-else-if="something && !border"></div>\n`)
60+
shouldNotDetect(`<div v-show="something && !border"></div>\n`)
61+
shouldNotDetect(`<div v-if="!border || !border"></div>\n`)
62+
shouldNotDetect(`<div v-else-if="!border || !border"></div>\n`)
63+
shouldNotDetect(`<div v-show="!border || !border"></div>\n`)
64+
shouldNotDetect(`<div v-if="!border"></div>\n`)
65+
shouldNotDetect(`<div v-else-if="!border"></div>\n`)
66+
shouldNotDetect(`<div v-show="!border"></div>\n`)
67+
shouldNotDetect(`<div x-if="!border"></div>\n`)
5368
})

packages/@tailwindcss-upgrade/src/template/codemods/important.ts

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@ import { parseCandidate } from '../../../../tailwindcss/src/candidate'
33
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
44
import { printCandidate } from '../candidates'
55

6+
const QUOTES = ['"', "'", '`']
7+
const LOGICAL_OPERATORS = ['&&', '||', '===', '==', '!=', '!==', '>', '>=', '<', '<=']
8+
const CONDITIONAL_TEMPLATE_SYNTAX = [
9+
// Vue
10+
/v-else-if=['"]$/,
11+
/v-if=['"]$/,
12+
/v-show=['"]$/,
13+
14+
// Alpine
15+
/x-if=['"]$/,
16+
/x-show=['"]$/,
17+
]
18+
619
// In v3 the important modifier `!` sits in front of the utility itself, not
720
// before any of the variants. In v4, we want it to be at the end of the utility
821
// so that it's always in the same location regardless of whether you used
@@ -25,7 +38,7 @@ export function important(
2538
end: number
2639
},
2740
): string {
28-
for (let candidate of parseCandidate(rawCandidate, designSystem)) {
41+
nextCandidate: for (let candidate of parseCandidate(rawCandidate, designSystem)) {
2942
if (candidate.important && candidate.raw[candidate.raw.length - 1] !== '!') {
3043
// The important migration is one of the most broad migrations with a high
3144
// potential of matching false positives since `!` is a valid character in
@@ -34,32 +47,54 @@ export function important(
3447
// on the side of caution and only migrate candidates that we are certain
3548
// are inside of a string.
3649
if (location) {
37-
let isQuoteBeforeCandidate = false
50+
let currentLineBeforeCandidate = ''
3851
for (let i = location.start - 1; i >= 0; i--) {
3952
let char = location.contents.at(i)!
4053
if (char === '\n') {
4154
break
4255
}
43-
if (isQuote(char)) {
44-
isQuoteBeforeCandidate = true
45-
break
46-
}
56+
currentLineBeforeCandidate = char + currentLineBeforeCandidate
4757
}
48-
49-
let isQuoteAfterCandidate = false
58+
let currentLineAfterCandidate = ''
5059
for (let i = location.end; i < location.contents.length; i++) {
5160
let char = location.contents.at(i)!
5261
if (char === '\n') {
5362
break
5463
}
55-
if (isQuote(char)) {
56-
isQuoteAfterCandidate = true
57-
break
58-
}
64+
currentLineAfterCandidate += char
5965
}
6066

67+
// Heuristic 1: Require the candidate to be inside quotes
68+
let isQuoteBeforeCandidate = QUOTES.some((quote) =>
69+
currentLineBeforeCandidate.includes(quote),
70+
)
71+
let isQuoteAfterCandidate = QUOTES.some((quote) =>
72+
currentLineAfterCandidate.includes(quote),
73+
)
6174
if (!isQuoteBeforeCandidate || !isQuoteAfterCandidate) {
62-
continue
75+
continue nextCandidate
76+
}
77+
78+
// Heuristic 2: Disallow object access immediately following the candidate
79+
if (currentLineAfterCandidate[0] === '.') {
80+
continue nextCandidate
81+
}
82+
83+
// Heuristic 3: Disallow logical operators preceding or following the candidate
84+
for (let operator of LOGICAL_OPERATORS) {
85+
if (
86+
currentLineAfterCandidate.trim().startsWith(operator) ||
87+
currentLineBeforeCandidate.trim().endsWith(operator)
88+
) {
89+
continue nextCandidate
90+
}
91+
}
92+
93+
// Heuristic 4: Disallow conditional template syntax
94+
for (let rule of CONDITIONAL_TEMPLATE_SYNTAX) {
95+
if (rule.test(currentLineBeforeCandidate)) {
96+
continue nextCandidate
97+
}
6398
}
6499
}
65100

@@ -72,14 +107,3 @@ export function important(
72107

73108
return rawCandidate
74109
}
75-
76-
function isQuote(char: string) {
77-
switch (char) {
78-
case '"':
79-
case "'":
80-
case '`':
81-
return true
82-
default:
83-
return false
84-
}
85-
}

0 commit comments

Comments
 (0)