Skip to content

Commit c1207c2

Browse files
Upgrade: Redce number of false-positive migrations of the important modifier
1 parent 557ed8c commit c1207c2

File tree

5 files changed

+117
-5
lines changed

5 files changed

+117
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
- _Upgrade (experimental)_: Ensure legacy theme values ending in `1` (like `theme(spacing.1)`) are correctly migrated to custom properties ([#14724](https://github.com/tailwindlabs/tailwindcss/pull/14724))
2424
- _Upgrade (experimental)_: Migrate arbitrary values to bare values for the `from-*`, `via-*`, and `to-*` utilities ([#14725](https://github.com/tailwindlabs/tailwindcss/pull/14725))
2525
- _Upgrade (experimental)_: Ensure `layer(utilities)` is removed from `@import` to keep `@utility` top-level ([#14738](https://github.com/tailwindlabs/tailwindcss/pull/14738))
26+
- _Upgrade (experimental)_: Don't migrate important modifiers that are actually logical negations (e.g. `let foo = !border` to `let foo = border!`) ([#14737](https://github.com/tailwindlabs/tailwindcss/pull/14737))
2627

2728
### Changed
2829

integrations/upgrade/js-config.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ test(
7272
@tailwind components;
7373
@tailwind utilities;
7474
`,
75+
'src/test.js': ts`
76+
export default {
77+
shouldNotUse: !border.shouldUse,
78+
}
79+
`,
7580
'node_modules/my-external-lib/src/template.html': html`
7681
<div class="text-red-500">
7782
Hello world!
@@ -82,7 +87,7 @@ test(
8287
async ({ exec, fs }) => {
8388
await exec('npx @tailwindcss/upgrade')
8489

85-
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
90+
expect(await fs.dumpFiles('src/**/*.{css,js}')).toMatchInlineSnapshot(`
8691
"
8792
--- src/input.css ---
8893
@import 'tailwindcss';
@@ -134,6 +139,11 @@ test(
134139
}
135140
}
136141
}
142+
143+
--- src/test.js ---
144+
export default {
145+
shouldNotUse: !border.shouldUse,
146+
}
137147
"
138148
`)
139149

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,39 @@ test.each([
1515
base: __dirname,
1616
})
1717

18-
expect(important(designSystem, {}, candidate)).toEqual(result)
18+
expect(
19+
important(designSystem, {}, candidate, {
20+
contents: `"${candidate}"`,
21+
start: 1,
22+
end: candidate.length + 1,
23+
}),
24+
).toEqual(result)
25+
})
26+
27+
test('does not match false positives', async () => {
28+
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
29+
base: __dirname,
30+
})
31+
32+
expect(
33+
important(designSystem, {}, '!border', {
34+
contents: `let notBorder = !border\n`,
35+
start: 16,
36+
end: 16 + '!border'.length,
37+
}),
38+
).toEqual('!border')
39+
})
40+
41+
test('does not match false positives with spaces at the end of the line', async () => {
42+
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
43+
base: __dirname,
44+
})
45+
46+
expect(
47+
important(designSystem, {}, '!border', {
48+
contents: `let notBorder = !border \n`,
49+
start: 16,
50+
end: 16 + '!border'.length,
51+
}),
52+
).toEqual('!border')
1953
})

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

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,50 @@ export function important(
1919
designSystem: DesignSystem,
2020
_userConfig: Config,
2121
rawCandidate: string,
22+
location?: {
23+
contents: string
24+
start: number
25+
end: number
26+
},
2227
): string {
23-
for (let candidate of parseCandidate(rawCandidate, designSystem)) {
28+
nextCandidate: for (let candidate of parseCandidate(rawCandidate, designSystem)) {
2429
if (candidate.important && candidate.raw[candidate.raw.length - 1] !== '!') {
30+
// The important migration is one of the most broad migrations with a high
31+
// potential of matching false positives since `!` is a valid character in
32+
// most programming languages. Since v4 is technically backward compatible
33+
// with v3 in that it can read `!` in the front of the utility too, we err
34+
// on the side of caution and only migrate candidates that we are certain
35+
// are inside of a string.
36+
if (location) {
37+
let isQuoteBeforeCandidate = false
38+
for (let i = location.start - 1; i >= 0; i--) {
39+
let char = location.contents.at(i)!
40+
if (char === '\n') {
41+
break
42+
}
43+
if (isQuote(char)) {
44+
isQuoteBeforeCandidate = true
45+
break
46+
}
47+
}
48+
49+
let isQuoteAfterCandidate = false
50+
for (let i = location.end; i < location.contents.length; i++) {
51+
let char = location.contents.at(i)!
52+
if (char === '\n') {
53+
break
54+
}
55+
if (isQuote(char)) {
56+
isQuoteAfterCandidate = true
57+
break
58+
}
59+
}
60+
61+
if (!isQuoteBeforeCandidate || !isQuoteAfterCandidate) {
62+
continue nextCandidate
63+
}
64+
}
65+
2566
// The printCandidate function will already put the exclamation mark in
2667
// the right place, so we just need to mark this candidate as requiring a
2768
// migration.
@@ -31,3 +72,14 @@ export function important(
3172

3273
return rawCandidate
3374
}
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+
}

packages/@tailwindcss-upgrade/src/template/migrate.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ export type Migration = (
1717
designSystem: DesignSystem,
1818
userConfig: Config,
1919
rawCandidate: string,
20+
location?: {
21+
contents: string
22+
start: number
23+
end: number
24+
},
2025
) => string
2126

2227
export const DEFAULT_MIGRATIONS: Migration[] = [
@@ -34,9 +39,15 @@ export function migrateCandidate(
3439
designSystem: DesignSystem,
3540
userConfig: Config,
3641
rawCandidate: string,
42+
// Location is only set when migrating a candidate from a source file
43+
location?: {
44+
contents: string
45+
start: number
46+
end: number
47+
},
3748
): string {
3849
for (let migration of DEFAULT_MIGRATIONS) {
39-
rawCandidate = migration(designSystem, userConfig, rawCandidate)
50+
rawCandidate = migration(designSystem, userConfig, rawCandidate, location)
4051
}
4152
return rawCandidate
4253
}
@@ -52,7 +63,11 @@ export default async function migrateContents(
5263
let changes: StringChange[] = []
5364

5465
for (let { rawCandidate, start, end } of candidates) {
55-
let migratedCandidate = migrateCandidate(designSystem, userConfig, rawCandidate)
66+
let migratedCandidate = migrateCandidate(designSystem, userConfig, rawCandidate, {
67+
contents,
68+
start,
69+
end,
70+
})
5671

5772
if (migratedCandidate === rawCandidate) {
5873
continue

0 commit comments

Comments
 (0)