Skip to content

Commit 4d2e4e8

Browse files
WIP
1 parent e8474de commit 4d2e4e8

6 files changed

Lines changed: 477 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
| Name                       | Description | 💼 | ⚠️ | 🔧 | 💡 |
1616
| :--------------------------------------------------------------------- | :---------------------------------------------------------------------------------- | :-- | :-- | :-- | :-- |
1717
| [classnames-order](docs/rules/classnames-order.md) | Enforces a consistent order for the Tailwind CSS classnames, based on the compiler. | || 🔧 | |
18+
| [enforces-shorthand](docs/rules/enforces-shorthand.md) | Avoid using multiple Tailwind CSS classnames when not required. | || 🔧 | |
1819
| [no-contradicting-classname](docs/rules/no-contradicting-classname.md) | Avoid contradicting Tailwind CSS classnames. || | | 💡 |
1920
| [no-custom-classname](docs/rules/no-custom-classname.md) | Detects classnames which do not belong to Tailwind CSS. | || | 💡 |
2021

ROADMAP.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# eslint-plugin-tailwindcss roadmap
22

3+
// Infos:
4+
//
5+
https://github.com/francoismassart/eslint-plugin-tailwindcss/blob/HEAD/docs/rules/enforces-shorthand.md
6+
7+
https://github.com/francoismassart/eslint-plugin-tailwindcss/blob/HEAD/docs/rules/enforces-negative-arbitrary-values.md
8+
9+
https://github.com/francoismassart/eslint-plugin-tailwindcss/blob/HEAD/docs/rules/no-arbitrary-value.md
10+
11+
À confirmer si c'est possible:
12+
13+
https://github.com/francoismassart/eslint-plugin-tailwindcss/blob/HEAD/docs/rules/no-unnecessary-arbitrary-value.md
14+
315
## April 2026
416

517
- `no-contradicting-classname`

docs/rules/enforces-shorthand.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Avoid using multiple Tailwind CSS classnames when not required
2+
3+
⚠️ This rule _warns_ in the ✅ `recommended` config.
4+
5+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6+
7+
<!-- end auto-generated rule header -->
8+
9+
## Options
10+
11+
<!-- begin auto-generated rule options list -->
12+
13+
<!-- end auto-generated rule options list -->

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import {
55
classnamesOrder,
66
RULE_NAME as CLASSNAMES_ORDER,
77
} from "./rules/classnames-order";
8+
import {
9+
enforcesShorthand,
10+
RULE_NAME as ENFORCES_SHORTHAND,
11+
} from "./rules/enforces-shorthand";
812
import {
913
noContradictingClassname,
1014
RULE_NAME as NO_CONTRADICTING_CLASSNAME,
@@ -37,13 +41,15 @@ const plugin = {
3741
},
3842
rules: {
3943
[CLASSNAMES_ORDER]: classnamesOrder,
44+
[ENFORCES_SHORTHAND]: enforcesShorthand,
4045
[NO_CUSTOM_CLASSNAME]: noCustomClassname,
4146
[NO_CONTRADICTING_CLASSNAME]: noContradictingClassname,
4247
},
4348
} satisfies FlatConfig.Plugin;
4449

4550
const recommended = {
4651
[CLASSNAMES_ORDER]: "warn",
52+
[ENFORCES_SHORTHAND]: "warn",
4753
[NO_CUSTOM_CLASSNAME]: "warn",
4854
[NO_CONTRADICTING_CLASSNAME]: "error",
4955
} as const;
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import * as Parser from "@typescript-eslint/parser";
2+
import { RuleTester, TestCaseError } from "@typescript-eslint/rule-tester";
3+
4+
import {
5+
generalSettings,
6+
withAngularParser,
7+
} from "../utils/parser/test-helpers";
8+
import { enforcesShorthand, MessageIds, RULE_NAME } from "./enforces-shorthand";
9+
10+
const generateError = (
11+
obsoleteClassnames: Array<string>,
12+
shorthand: string,
13+
): TestCaseError<MessageIds> => {
14+
return {
15+
messageId: "fix:use-shorthand",
16+
data: {
17+
classnames: obsoleteClassnames.map((cls) => `'${cls}'`).join(", "),
18+
shorthand: shorthand,
19+
},
20+
};
21+
};
22+
23+
const ruleTester = new RuleTester({
24+
languageOptions: {
25+
parser: Parser,
26+
parserOptions: {
27+
ecmaFeatures: {
28+
jsx: true,
29+
},
30+
},
31+
},
32+
settings: {
33+
tailwindcss: {
34+
...generalSettings,
35+
},
36+
},
37+
});
38+
39+
ruleTester.run(RULE_NAME, enforcesShorthand, {
40+
valid: [
41+
// Angular / Native HTML + static text
42+
`<h1 class="-mt- h-100 md:h-full">valid</h1>`,
43+
`<h1 class="w-full md:h-full">modifiers</h1>`,
44+
].map((testedNgCode) => ({
45+
code: testedNgCode,
46+
languageOptions: withAngularParser,
47+
})),
48+
invalid: [
49+
{
50+
code: `ctl("overflow-x-hidden overflow-y-hidden")`,
51+
errors: [
52+
generateError(
53+
["overflow-x-hidden", "overflow-y-hidden"],
54+
"overflow-hidden",
55+
),
56+
],
57+
output: `ctl("overflow-hidden")`,
58+
},
59+
{
60+
code: `ctl("overscroll-x-none overscroll-y-none")`,
61+
errors: [
62+
generateError(
63+
["overscroll-x-none", "overscroll-y-none"],
64+
"overscroll-none",
65+
),
66+
],
67+
output: `ctl("overscroll-none")`,
68+
},
69+
{
70+
code: `ctl("top-0 right-0 bottom-0")`,
71+
errors: [generateError(["top-0", "bottom-0"], "inset-y-0")],
72+
output: `ctl("right-0 inset-y-0")`,
73+
},
74+
{
75+
code: `ctl("top-0 right-0 bottom-0 left-0")`,
76+
errors: [
77+
generateError(["right-0", "left-0"], "inset-x-0"),
78+
generateError(["top-0", "bottom-0"], "inset-y-0"),
79+
],
80+
output: [
81+
`ctl("top-0 bottom-0 inset-x-0")`,
82+
`ctl("inset-x-0 inset-y-0")`,
83+
`ctl("inset-0")`,
84+
],
85+
},
86+
{
87+
code: `ctl("inset-y-0 right-0 left-0")`,
88+
errors: [generateError(["right-0", "left-0"], "inset-x-0")],
89+
output: [`ctl("inset-y-0 inset-x-0")`, `ctl("inset-0")`],
90+
},
91+
{
92+
code: `ctl("-inset-y-10 -inset-x-10")`,
93+
errors: [generateError(["-inset-x-10", "-inset-y-10"], "-inset-10")],
94+
output: [`ctl("-inset-10")`],
95+
},
96+
{
97+
code: `ctl("gap-x-10 gap-y-10")`,
98+
errors: [generateError(["gap-x-10", "gap-y-10"], "gap-10")],
99+
output: [`ctl("gap-10")`],
100+
},
101+
{
102+
code: `<h1 class="block md:-mx-10 md:-my-10">margin</h1>`,
103+
errors: [generateError(["md:-mx-10", "md:-my-10"], "md:-m-10")],
104+
output: `<h1 class="block md:-m-10">margin</h1>`,
105+
languageOptions: withAngularParser,
106+
},
107+
{
108+
code: `<h1 class="md:pl-10 md:pr-10">padding</h1>`,
109+
errors: [generateError(["md:pl-10", "md:pr-10"], "md:px-10")],
110+
output: `<h1 class="md:px-10">padding</h1>`,
111+
languageOptions: withAngularParser,
112+
},
113+
{
114+
code: `ctl("w-1/2 h-1/2")`,
115+
errors: [generateError(["w-1/2", "h-1/2"], "size-1/2")],
116+
output: [`ctl("size-1/2")`],
117+
},
118+
{
119+
code: `ctl("debug overflow-hidden text-ellipsis whitespace-nowrap")`,
120+
errors: [
121+
generateError(
122+
["overflow-hidden", "text-ellipsis", "whitespace-nowrap"],
123+
"truncate",
124+
),
125+
],
126+
output: [`ctl("debug truncate")`],
127+
},
128+
],
129+
});

0 commit comments

Comments
 (0)