Skip to content

Commit 6b28e58

Browse files
Upgrade: Improve heuristics around important codemod
1 parent 2e0446c commit 6b28e58

File tree

4 files changed

+73
-35
lines changed

4 files changed

+73
-35
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- Nothing yet!
10+
### Fixed
11+
12+
- Don't migrate important modifiers when these are used inside conditionals of certain template languages (e.g. `<div v-if="!border"/>`) ([#14774](https://github.com/tailwindlabs/tailwindcss/pull/14774))
1113

1214
## [4.0.0-alpha.29] - 2024-10-23
1315

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: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,28 @@ 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-show="something && !border"></div>\n`)
60+
shouldNotDetect(`<div v-if="!border || !border"></div>\n`)
61+
shouldNotDetect(`<div v-show="!border || !border"></div>\n`)
62+
shouldNotDetect(`<div v-if="!border"></div>\n`)
63+
shouldNotDetect(`<div v-show="!border"></div>\n`)
64+
shouldNotDetect(`<div x-if="!border"></div>\n`)
5365
})

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

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ 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-if=['"]$/,
11+
/v-show=['"]$/,
12+
13+
// Alpine
14+
/x-show=['"]$/,
15+
/x-if=['"]$/,
16+
]
17+
618
// In v3 the important modifier `!` sits in front of the utility itself, not
719
// before any of the variants. In v4, we want it to be at the end of the utility
820
// so that it's always in the same location regardless of whether you used
@@ -25,7 +37,7 @@ export function important(
2537
end: number
2638
},
2739
): string {
28-
for (let candidate of parseCandidate(rawCandidate, designSystem)) {
40+
nextCandidate: for (let candidate of parseCandidate(rawCandidate, designSystem)) {
2941
if (candidate.important && candidate.raw[candidate.raw.length - 1] !== '!') {
3042
// The important migration is one of the most broad migrations with a high
3143
// potential of matching false positives since `!` is a valid character in
@@ -34,32 +46,54 @@ export function important(
3446
// on the side of caution and only migrate candidates that we are certain
3547
// are inside of a string.
3648
if (location) {
37-
let isQuoteBeforeCandidate = false
49+
let currentLineBeforeCandidate = ''
3850
for (let i = location.start - 1; i >= 0; i--) {
3951
let char = location.contents.at(i)!
4052
if (char === '\n') {
4153
break
4254
}
43-
if (isQuote(char)) {
44-
isQuoteBeforeCandidate = true
45-
break
46-
}
55+
currentLineBeforeCandidate = char + currentLineBeforeCandidate
4756
}
48-
49-
let isQuoteAfterCandidate = false
57+
let currentLineAfterCandidate = ''
5058
for (let i = location.end; i < location.contents.length; i++) {
5159
let char = location.contents.at(i)!
5260
if (char === '\n') {
5361
break
5462
}
55-
if (isQuote(char)) {
56-
isQuoteAfterCandidate = true
57-
break
58-
}
63+
currentLineAfterCandidate += char
5964
}
6065

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

@@ -72,14 +106,3 @@ export function important(
72106

73107
return rawCandidate
74108
}
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)