Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
add more safety checks
Before even gathering context of the candidate's surroundings, we can
already look at the candidate itself to know whether a migration will be
safe.

If a candidate doesn't even parse at all, we can stop early.
If a candidate uses arbitrary property syntax such as `[color:red]`,
there is like a 0% chance that this will cause an actual issue..

and so on.
  • Loading branch information
RobinMalfait committed May 14, 2025
commit 5884994f2f230ec5793b9a5821541395207138c0
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { parseCandidate } from '../../../../tailwindcss/src/candidate'
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'

const QUOTES = ['"', "'", '`']
const LOGICAL_OPERATORS = ['&&', '||', '?', '===', '==', '!=', '!==', '>', '>=', '<', '<=']
const CONDITIONAL_TEMPLATE_SYNTAX = [
Expand All @@ -12,7 +15,60 @@ const CONDITIONAL_TEMPLATE_SYNTAX = [
]
const NEXT_PLACEHOLDER_PROP = /placeholder=\{?['"]$/

export function isSafeMigration(location: { contents: string; start: number; end: number }) {
export function isSafeMigration(
rawCandidate: string,
location: { contents: string; start: number; end: number },
designSystem: DesignSystem,
): boolean {
let [candidate] = Array.from(parseCandidate(rawCandidate, designSystem))

// If we can't parse the candidate, then it's not a candidate at all.
if (!candidate) {
return false
}

// When we have variants, we can assume that the candidate is safe to migrate
// because that requires colons.
//
// E.g.: `hover:focus:flex`
if (candidate.variants.length > 0) {
return true
}

// When we have an arbitrary property, the candidate has such a particular
// structure it's very likely to be safe.
//
// E.g.: `[color:red]`
if (candidate.kind === 'arbitrary') {
return true
}

// A static candidate is very likely safe if it contains a dash.
//
// E.g.: `items-center`
if (candidate.kind === 'static' && candidate.root.includes('-')) {
return true
}

// A functional candidate is very likely safe if it contains a value (which
// implies a `-`). Or if the root contains a dash.
//
// E.g.: `bg-red-500`, `bg-position-20`
if (
(candidate.kind === 'functional' && candidate.value !== null) ||
(candidate.kind === 'functional' && candidate.root.includes('-'))
) {
return true
}

// If the candidate contains a modifier, it's very likely to be safe because
// it implies that it contains a `/`.
//
// E.g.: `text-sm/7`
if (candidate.kind === 'functional' && candidate.modifier) {
return true
}

let currentLineBeforeCandidate = ''
for (let i = location.start - 1; i >= 0; i--) {
let char = location.contents.at(i)!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function migrateImportant(
// with v3 in that it can read `!` in the front of the utility too, we err
// on the side of caution and only migrate candidates that we are certain
// are inside of a string.
if (location && !isSafeMigration(location)) {
if (location && !isSafeMigration(rawCandidate, location, designSystem)) {
continue nextCandidate
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ test('does not replace classes in invalid positions', async () => {
}

// Skip this migration if we think that the migration is unsafe
if (location && !isSafeMigration(location)) {
if (location && !isSafeMigration(candidate, location, designSystem)) {
return candidate
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export async function migrateCandidate(
},
): Promise<string> {
// Skip this migration if we think that the migration is unsafe
if (location && !isSafeMigration(location)) {
if (location && !isSafeMigration(rawCandidate, location, designSystem)) {
return rawCandidate
}

Expand Down