Skip to content

Commit aa58304

Browse files
authored
fix: add missing logical classes (#368)
* fix: add missing logical classes * chore: improve error messages * test: preserve important modifier when changing to multiple classes
1 parent d7df2ce commit aa58304

3 files changed

Lines changed: 81 additions & 14 deletions

File tree

docs/rules/enforce-logical-properties.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ The rule reports physical classes and auto-fixes them to their logical equivalen
4444
| `float-right` | `float-end` |
4545
| `clear-left` | `clear-start` |
4646
| `clear-right` | `clear-end` |
47+
| `h-*` | `block-*` |
48+
| `w-*` | `inline-*` |
49+
| `min-h-*` | `min-block-*` |
50+
| `min-w-*` | `min-inline-*` |
51+
| `max-h-*` | `max-block-*` |
52+
| `max-w-*` | `max-inline-*` |
53+
| `size-*` | `block-* inline-*` |
4754

4855
<br/>
4956

src/rules/enforce-logical-properties.test.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,14 @@ const testCases = [
5959
["float-left", "float-start"],
6060
["float-right", "float-end"],
6161
["clear-left", "clear-start"],
62-
["clear-right", "clear-end"]
62+
["clear-right", "clear-end"],
63+
64+
["h-4", "block-4"],
65+
["w-4", "inline-4"],
66+
["min-h-4", "min-block-4"],
67+
["min-w-4", "min-inline-4"],
68+
["max-h-4", "max-block-4"],
69+
["max-w-4", "max-inline-4"]
6370
] satisfies [string, string][];
6471

6572
describe.runIf(getTailwindCSSVersion().major >= 4)(enforceLogicalProperties.name, () => {
@@ -136,6 +143,20 @@ describe.runIf(getTailwindCSSVersion().major >= 4)(enforceLogicalProperties.name
136143
vue: `<template><img class="text-right!" /></template>`,
137144
vueOutput: `<template><img class="text-end!" /></template>`,
138145

146+
errors: 1
147+
},
148+
{
149+
angular: `<img class="size-4!" />`,
150+
angularOutput: `<img class="block-4! inline-4!" />`,
151+
html: `<img class="size-4!" />`,
152+
htmlOutput: `<img class="block-4! inline-4!" />`,
153+
jsx: `() => <img class="size-4!" />`,
154+
jsxOutput: `() => <img class="block-4! inline-4!" />`,
155+
svelte: `<img class="size-4!" />`,
156+
svelteOutput: `<img class="block-4! inline-4!" />`,
157+
vue: `<template><img class="size-4!" /></template>`,
158+
vueOutput: `<template><img class="block-4! inline-4!" /></template>`,
159+
139160
errors: 1
140161
}
141162
]
@@ -247,4 +268,28 @@ describe.runIf(getTailwindCSSVersion().major >= 4)(enforceLogicalProperties.name
247268
);
248269
});
249270

271+
it("should split size classes into logical block and inline classes", () => {
272+
lint(
273+
enforceLogicalProperties,
274+
{
275+
invalid: [
276+
{
277+
angular: `<img class="size-4 hover:size-[12px]" />`,
278+
angularOutput: `<img class="block-4 inline-4 hover:block-[12px] hover:inline-[12px]" />`,
279+
html: `<img class="size-4 hover:size-[12px]" />`,
280+
htmlOutput: `<img class="block-4 inline-4 hover:block-[12px] hover:inline-[12px]" />`,
281+
jsx: `() => <img class="size-4 hover:size-[12px]" />`,
282+
jsxOutput: `() => <img class="block-4 inline-4 hover:block-[12px] hover:inline-[12px]" />`,
283+
svelte: `<img class="size-4 hover:size-[12px]" />`,
284+
svelteOutput: `<img class="block-4 inline-4 hover:block-[12px] hover:inline-[12px]" />`,
285+
vue: `<template><img class="size-4 hover:size-[12px]" /></template>`,
286+
vueOutput: `<template><img class="block-4 inline-4 hover:block-[12px] hover:inline-[12px]" /></template>`,
287+
288+
errors: 2
289+
}
290+
]
291+
}
292+
);
293+
});
294+
250295
});

src/rules/enforce-logical-properties.ts

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export const enforceLogicalProperties = createRule({
1919
recommended: false,
2020

2121
messages: {
22-
replaceable: "Physical class detected. Replace \"{{ className }}\" with logical class \"{{fix}}\"."
22+
multiple: "Physical class detected. Replace \"{{ className }}\" with logical classes \"{{fix}}\".",
23+
single: "Physical class detected. Replace \"{{ className }}\" with logical class \"{{fix}}\"."
2324
},
2425

2526
initialize: ctx => {
@@ -82,8 +83,16 @@ const mappings = [
8283
[/^float-left$/, "float-start"],
8384
[/^float-right$/, "float-end"],
8485
[/^clear-left$/, "clear-start"],
85-
[/^clear-right$/, "clear-end"]
86-
] satisfies [before: RegExp, after: string][];
86+
[/^clear-right$/, "clear-end"],
87+
88+
[/^h-(.*)$/, "block-$1"],
89+
[/^w-(.*)$/, "inline-$1"],
90+
[/^min-h-(.*)$/, "min-block-$1"],
91+
[/^min-w-(.*)$/, "min-inline-$1"],
92+
[/^max-h-(.*)$/, "max-block-$1"],
93+
[/^max-w-(.*)$/, "max-inline-$1"],
94+
[/^size-(.*)$/, ["block-$1", "inline-$1"]]
95+
] satisfies [before: RegExp, after: string[] | string][];
8796

8897

8998
function lintLiterals(ctx: Context<typeof enforceLogicalProperties>, literals: Literal[]) {
@@ -93,13 +102,13 @@ function lintLiterals(ctx: Context<typeof enforceLogicalProperties>, literals: L
93102
const { dissectedClasses, warnings } = getDissectedClasses(async(ctx), classes);
94103

95104
const possibleFixes = Object.values(dissectedClasses).flatMap(dissectedClass => {
96-
const replacementBase = getReplacementBase(dissectedClass.base);
105+
const replacementBases = getReplacementBases(dissectedClass.base);
97106

98-
if(!replacementBase){
107+
if(!replacementBases){
99108
return [];
100109
}
101110

102-
return [buildClass(ctx, { ...dissectedClass, base: replacementBase })];
111+
return replacementBases.map(base => buildClass(ctx, { ...dissectedClass, base }));
103112
});
104113

105114
const { unknownClasses } = getUnknownClasses(async(ctx), possibleFixes);
@@ -111,39 +120,45 @@ function lintLiterals(ctx: Context<typeof enforceLogicalProperties>, literals: L
111120
return;
112121
}
113122

114-
const replacementBase = getReplacementBase(dissectedClass.base);
123+
const replacementBases = getReplacementBases(dissectedClass.base);
115124

116-
if(!replacementBase){
125+
if(!replacementBases){
117126
return;
118127
}
119128

120-
const fix = buildClass(ctx, { ...dissectedClass, base: replacementBase });
129+
const fixClasses = replacementBases.map(base => buildClass(ctx, { ...dissectedClass, base }));
130+
const hasUnknownFix = fixClasses.some(fixClass => unknownClasses.includes(fixClass));
121131

122-
if(unknownClasses.includes(fix)){
132+
if(hasUnknownFix){
123133
return;
124134
}
125135

136+
const fix = fixClasses.join(" ");
137+
const id = fixClasses.length > 1 ? "multiple" : "single";
138+
126139
return {
127140
data: {
128141
className,
129142
fix
130143
},
131144
fix,
132-
id: "replaceable",
145+
id,
133146
warnings
134147
} as const;
135148
});
136149
}
137150
}
138151

139-
function getReplacementBase(base: string) {
152+
function getReplacementBases(base: string): string[] | undefined {
140153
for(const [pattern, replacement] of mappings){
141154
const match = base.match(pattern);
142155

143156
if(!match){
144157
continue;
145158
}
146159

147-
return replacePlaceholders(replacement, match);
160+
return Array.isArray(replacement)
161+
? replacement.map(part => replacePlaceholders(part, match))
162+
: [replacePlaceholders(replacement, match)];
148163
}
149164
}

0 commit comments

Comments
 (0)