Repository: francoismassart/eslint-plugin-tailwindcss
Pull request #306 (2024-01-15 09:05:49)

feat: support tailwindcss@3.4.0
See #297


Patch:
diff --git a/lib/config/groups.js b/lib/config/groups.js
index d01057f..234c89d 100644
--- a/lib/config/groups.js
+++ b/lib/config/groups.js
@@ -507,6 +507,10 @@ module.exports.groups = [
         type: 'Max-Height',
         members: 'max\\-h\\-(?<value>${maxHeight})',
       },
+      {
+        type: 'Size',
+        members: 'size\\-(?<value>${size})',
+      },
     ],
   },
   {
@@ -594,6 +598,10 @@ module.exports.groups = [
         members: 'overflow\\-(ellipsis|clip)',
         deprecated: true,
       },
+      {
+        type: 'Text Wrap',
+        members: 'text\\-(wrap|nowrap|balance|pretty)',
+      },
       {
         type: 'Text Indent',
         members: '(indent\\-(?<value>${textIndent})|\\-indent\\-(?<negativeValue>${-textIndent}))',
@@ -1341,6 +1349,10 @@ module.exports.groups = [
         type: 'Screen Readers',
         members: '(not\\-)?sr\\-only',
       },
+      {
+        type: 'Forced Color Adjust',
+        members: 'forced\\-color\\-adjust\\-(auto|none)',
+      },
     ],
   },
   {
diff --git a/lib/rules/enforces-shorthand.js b/lib/rules/enforces-shorthand.js
index 9e40ba3..1fcc366 100644
--- a/lib/rules/enforces-shorthand.js
+++ b/lib/rules/enforces-shorthand.js
@@ -75,18 +75,28 @@ module.exports = {
 
     // These are shorthand candidates that do not share the same parent type
     const complexEquivalences = [
-      [["overflow-hidden", "text-ellipsis", "whitespace-nowrap"], "truncate"]
-    ]
+      {
+        needles: ['overflow-hidden', 'text-ellipsis', 'whitespace-nowrap'],
+        shorthand: 'truncate',
+        mode: 'exact',
+      },
+      {
+        needles: ['w-', 'h-'],
+        shorthand: 'size-',
+        mode: 'value',
+      },
+    ];
 
     // Init assets
     const targetProperties = {
       Layout: ['Overflow', 'Overscroll Behavior', 'Top / Right / Bottom / Left'],
       'Flexbox & Grid': ['Gap'],
       Spacing: ['Padding', 'Margin'],
+      Sizing: ['Width', 'Height'],
       Borders: ['Border Radius', 'Border Width', 'Border Color'],
       Tables: ['Border Spacing'],
       Transforms: ['Scale'],
-      Typography: ['Text Overflow', 'Whitespace']
+      Typography: ['Text Overflow', 'Whitespace'],
     };
 
     // We don't want to affect other rules by object reference
@@ -219,53 +229,79 @@ module.exports = {
       const validated = [];
 
       // Handle sets of classnames with different parent types
-      let remaining = parsed
-      for (const [inputSet, outputClassname] of complexEquivalences) {
+      let remaining = parsed;
+      for (const { needles: inputSet, shorthand: outputClassname, mode } of complexEquivalences) {
         if (remaining.length < inputSet.length) {
-          continue
-        }
-
-        const parsedElementsInInputSet = remaining.filter(remainingClass => inputSet.some(inputClass => remainingClass.name.includes(inputClass)))
-
-        // Make sure all required classes for the shorthand are present
-        if (parsedElementsInInputSet.length !== inputSet.length) {
-          continue
+          continue;
         }
 
-        // Make sure the classes share all the same variants
-        if (new Set(parsedElementsInInputSet.map(p => p.variants)).size !== 1) {
-          continue
-        }
-
-        // Make sure the classes share all the same importance
-        if (new Set(parsedElementsInInputSet.map(p => p.important)).size !== 1) {
-          continue
-        }
+        // Matching classes
+        const parsedElementsInInputSet = remaining.filter((remainingClass) => {
+          if (mode === 'exact') {
+            // Test if the name contains the target class, eg. 'text-ellipsis' inside 'md:text-ellipsis'...
+            return inputSet.some((inputClass) => remainingClass.name.includes(inputClass));
+          }
+          // Test if the body of the class matches, eg. 'h-' inside 'h-10'
+          if (mode === 'value') {
+            return inputSet.some((inputClassPattern) => inputClassPattern === remainingClass.body);
+          }
+        });
 
-        const index = parsedElementsInInputSet[0].index
-        const variants = parsedElementsInInputSet[0].variants
-        const important = parsedElementsInInputSet[0].important ? "!" : ""
+        const variantGroups = new Map();
+        parsedElementsInInputSet.forEach((o) => {
+          const val = mode === 'value' ? o.value : '';
+          const v = `${o.variants}${o.important ? '!' : ''}${val}`;
+          if (!variantGroups.has(v)) {
+            variantGroups.set(
+              v,
+              parsedElementsInInputSet.filter(
+                (c) => c.variants === o.variants && c.important === o.important && (val === '' || c.value === val)
+              )
+            );
+          }
+        });
+        const validKeys = new Set();
+        variantGroups.forEach((classes, key) => {
+          let skip = false;
+          // Make sure all required classes for the shorthand are present
+          if (classes.length < inputSet.length) {
+            skip = true;
+          }
+          // Make sure the classes share all the single/shared/same value
+          if (mode === 'value' && new Set(classes.map((p) => p.value)).size !== 1) {
+            skip = true;
+          }
+          if (!skip) {
+            validKeys.add(key);
+          }
+        });
+        validKeys.forEach((k) => {
+          const candidates = variantGroups.get(k);
+          const index = candidates[0].index;
+          const variants = candidates[0].variants;
+          const important = candidates[0].important ? '!' : '';
+          const classValue = mode === 'value' ? candidates[0].value : '';
 
-        const patchedClassname = `${variants}${important}${mergedConfig.prefix}${outputClassname}`
-        troubles.push([parsedElementsInInputSet.map((c) => `${c.name}`), patchedClassname]);
+          const patchedClassname = `${variants}${important}${mergedConfig.prefix}${outputClassname}${classValue}`;
+          troubles.push([candidates.map((c) => `${c.name}`), patchedClassname]);
 
-        const validatedClassname = groupUtil.parseClassname(patchedClassname, targetGroups, mergedConfig, index)
-        validated.push(validatedClassname);
+          const validatedClassname = groupUtil.parseClassname(patchedClassname, targetGroups, mergedConfig, index);
+          validated.push(validatedClassname);
 
-        remaining = remaining.filter(p => !parsedElementsInInputSet.includes(p))
+          remaining = remaining.filter((p) => !candidates.includes(p));
+        });
       }
 
       // Handle sets of classnames with the same parent type
-
       // Each group parentType
       const checkedGroups = [];
-      remaining.forEach((classname) => {
+      remaining.forEach((classname, idx, arr) => {
         // Valid candidate
         if (classname.parentType === '') {
           validated.push(classname);
         } else if (!checkedGroups.includes(classname.parentType)) {
           checkedGroups.push(classname.parentType);
-          const sameType = parsed.filter((cls) => cls.parentType === classname.parentType);
+          const sameType = remaining.filter((cls) => cls.parentType === classname.parentType);
           // Comparing same parentType classnames
           const checkedVariantsValue = [];
           sameType.forEach((cls) => {
@@ -404,27 +440,22 @@ module.exports = {
         }
       }
 
-      troubles
-        .filter((trouble) => {
-          // Only valid issue if there are classes to replace
-          return trouble[0].length;
-        })
-        .forEach((issue) => {
-          if (originalClassNamesValue !== validatedClassNamesValue) {
-            validatedClassNamesValue = prefix + validatedClassNamesValue + suffix;
-            context.report({
-              node: node,
-              messageId: 'shorthandCandidateDetected',
-              data: {
-                classnames: issue[0].join(', '),
-                shorthand: issue[1],
-              },
-              fix: function (fixer) {
-                return fixer.replaceTextRange([start, end], validatedClassNamesValue);
-              },
-            });
-          }
-        });
+      troubles.forEach((issue) => {
+        if (originalClassNamesValue !== validatedClassNamesValue) {
+          validatedClassNamesValue = prefix + validatedClassNamesValue + suffix;
+          context.report({
+            node: node,
+            messageId: 'shorthandCandidateDetected',
+            data: {
+              classnames: issue[0].join(', '),
+              shorthand: issue[1],
+            },
+            fix: function (fixer) {
+              return fixer.replaceTextRange([start, end], validatedClassNamesValue);
+            },
+          });
+        }
+      });
     };
 
     //----------------------------------------------------------------------
diff --git a/lib/util/groupMethods.js b/lib/util/groupMethods.js
index 4ee1558..7b59571 100644
--- a/lib/util/groupMethods.js
+++ b/lib/util/groupMethods.js
@@ -160,6 +160,7 @@ function generateOptions(propName, keys, config, isNegative = false) {
     case 'height':
     case 'lineHeight':
     case 'maxHeight':
+    case 'size':
     case 'maxWidth':
     case 'minHeight':
     case 'minWidth':
diff --git a/package-lock.json b/package-lock.json
index 19669da..8e7aa1d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "eslint-plugin-tailwindcss",
-  "version": "3.13.1",
+  "version": "3.14.0-beta.1",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "eslint-plugin-tailwindcss",
-      "version": "3.13.1",
+      "version": "3.14.0-beta.1",
       "license": "MIT",
       "dependencies": {
         "fast-glob": "^3.2.5",
@@ -23,7 +23,7 @@
         "daisyui": "^2.6.4",
         "eslint": "^7.1.0",
         "mocha": "^10.2.0",
-        "tailwindcss": "^3.3.2",
+        "tailwindcss": "^3.4.0",
         "typescript": "4.3.5",
         "vue-eslint-parser": "^9.1.0"
       },
@@ -31,7 +31,7 @@
         "node": ">=12.13.0"
       },
       "peerDependencies": {
-        "tailwindcss": "^3.3.2"
+        "tailwindcss": "^3.4.0"
       }
     },
     "node_modules/@alloc/quick-lru": {
@@ -1211,9 +1211,9 @@
       "dev": true
     },
     "node_modules/fast-glob": {
-      "version": "3.2.12",
-      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
-      "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+      "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
       "dependencies": {
         "@nodelib/fs.stat": "^2.0.2",
         "@nodelib/fs.walk": "^1.2.3",
@@ -1627,9 +1627,9 @@
       "dev": true
     },
     "node_modules/jiti": {
-      "version": "1.18.2",
-      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
-      "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==",
+      "version": "1.21.0",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
+      "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
       "dev": true,
       "bin": {
         "jiti": "bin/jiti.js"
@@ -2777,9 +2777,9 @@
       "dev": true
     },
     "node_modules/tailwindcss": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz",
-      "integrity": "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==",
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz",
+      "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==",
       "dev": true,
       "dependencies": {
         "@alloc/quick-lru": "^5.2.0",
@@ -2787,10 +2787,10 @@
         "chokidar": "^3.5.3",
         "didyoumean": "^1.2.2",
         "dlv": "^1.1.3",
-        "fast-glob": "^3.2.12",
+        "fast-glob": "^3.3.0",
         "glob-parent": "^6.0.2",
         "is-glob": "^4.0.3",
-        "jiti": "^1.18.2",
+        "jiti": "^1.19.1",
         "lilconfig": "^2.1.0",
         "micromatch": "^4.0.5",
         "normalize-path": "^3.0.0",
@@ -2802,7 +2802,6 @@
         "postcss-load-config": "^4.0.1",
         "postcss-nested": "^6.0.1",
         "postcss-selector-parser": "^6.0.11",
-        "postcss-value-parser": "^4.2.0",
         "resolve": "^1.22.2",
         "sucrase": "^3.32.0"
       },
@@ -4085,9 +4084,9 @@
       "dev": true
     },
     "fast-glob": {
-      "version": "3.2.12",
-      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
-      "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+      "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
       "requires": {
         "@nodelib/fs.stat": "^2.0.2",
         "@nodelib/fs.walk": "^1.2.3",
@@ -4389,9 +4388,9 @@
       "dev": true
     },
     "jiti": {
-      "version": "1.18.2",
-      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
-      "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==",
+      "version": "1.21.0",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
+      "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
       "dev": true
     },
     "js-tokens": {
@@ -5181,9 +5180,9 @@
       }
     },
     "tailwindcss": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz",
-      "integrity": "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==",
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz",
+      "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==",
       "dev": true,
       "requires": {
         "@alloc/quick-lru": "^5.2.0",
@@ -5191,10 +5190,10 @@
         "chokidar": "^3.5.3",
         "didyoumean": "^1.2.2",
         "dlv": "^1.1.3",
-        "fast-glob": "^3.2.12",
+        "fast-glob": "^3.3.0",
         "glob-parent": "^6.0.2",
         "is-glob": "^4.0.3",
-        "jiti": "^1.18.2",
+        "jiti": "^1.19.1",
         "lilconfig": "^2.1.0",
         "micromatch": "^4.0.5",
         "normalize-path": "^3.0.0",
@@ -5206,7 +5205,6 @@
         "postcss-load-config": "^4.0.1",
         "postcss-nested": "^6.0.1",
         "postcss-selector-parser": "^6.0.11",
-        "postcss-value-parser": "^4.2.0",
         "resolve": "^1.22.2",
         "sucrase": "^3.32.0"
       },
diff --git a/package.json b/package.json
index 53bb1c6..eb99550 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-tailwindcss",
-  "version": "3.13.1",
+  "version": "3.14.0-beta.1",
   "description": "Rules enforcing best practices while using Tailwind CSS",
   "keywords": [
     "eslint",
@@ -18,13 +18,14 @@
   "bugs": "https://github.com/francoismassart/eslint-plugin-tailwindcss/issues",
   "main": "lib/index.js",
   "scripts": {
+    "wip": "mocha tests/lib/rules/no-arbitrary-value.js",
     "test": "mocha tests --recursive"
   },
   "files": [
     "lib"
   ],
   "peerDependencies": {
-    "tailwindcss": "^3.3.2"
+    "tailwindcss": "^3.4.0"
   },
   "dependencies": {
     "fast-glob": "^3.2.5",
@@ -41,7 +42,7 @@
     "daisyui": "^2.6.4",
     "eslint": "^7.1.0",
     "mocha": "^10.2.0",
-    "tailwindcss": "^3.3.2",
+    "tailwindcss": "^3.4.0",
     "typescript": "4.3.5",
     "vue-eslint-parser": "^9.1.0"
   },


Test patch:
diff --git a/tests/lib/rules/classnames-order.js b/tests/lib/rules/classnames-order.js
index 894367c..b797f01 100644
--- a/tests/lib/rules/classnames-order.js
+++ b/tests/lib/rules/classnames-order.js
@@ -84,7 +84,7 @@ ruleTester.run("classnames-order", rule, {
       ],
     },
     {
-      code: "<div className={ctl(`w-full p-10 ${live && 'bg-blue-100 dark:bg-purple-400 sm:rounded-lg'}`)}>ctl + exp</div>",
+      code: "<div className={ctl(`w-full p-10 ${live && 'bg-blue-100 sm:rounded-lg dark:bg-purple-400'}`)}>ctl + exp</div>",
     },
     {
       code: "<div className={ctl(`h-48 w-48 rounded-full bg-blue-500 ${className}`)}>ctl + var</div>",
@@ -243,7 +243,7 @@ ruleTester.run("classnames-order", rule, {
     {
       // Kitchensink
       code: `
-      <div class="custom custom2 aspect-auto group peer container prose prose-slate aspect-h-9 aspect-w-16 sr-only pointer-events-auto visible sticky inset-0 inset-x-2 inset-y-1 bottom-5 left-6 right-4 top-3 isolate z-auto order-1 col-span-1 col-start-2 col-end-3 row-span-1 row-start-2 row-end-3 float-left clear-both m-0 mx-1.5 my-px mb-3 ml-4 mr-2 mt-1 box-border block h-0 max-h-fit min-h-full w-0 min-w-full max-w-fit flex-none flex-shrink shrink flex-grow grow basis-0 table-fixed border-collapse origin-bottom-right translate-x-px translate-y-2 rotate-90 skew-x-2 skew-y-0 scale-0 scale-x-75 scale-y-50 transform transform-gpu transform-none animate-spin cursor-text touch-auto select-none resize-none snap-x snap-mandatory snap-center snap-always scroll-m-0 scroll-mx-1.5 scroll-my-px scroll-mb-3 scroll-ml-4 scroll-mr-2 scroll-mt-1 scroll-p-0 scroll-px-1.5 scroll-py-px scroll-pb-3 scroll-pl-4 scroll-pr-2 scroll-pt-1 list-outside list-decimal appearance-none columns-1 break-before-avoid-page break-inside-avoid-column break-after-auto auto-cols-min grid-flow-col auto-rows-min grid-cols-1 grid-rows-1 flex-col flex-wrap place-content-between place-items-stretch content-between items-baseline justify-start justify-items-center gap-0 gap-x-1 gap-y-2 space-x-1 space-y-0 space-y-reverse space-x-reverse divide-x-2 divide-y-4 divide-y-reverse divide-x-reverse divide-dashed divide-black place-self-end self-baseline justify-self-stretch overflow-hidden overflow-x-auto overflow-y-scroll overscroll-auto overscroll-y-none overscroll-x-contain scroll-smooth overflow-ellipsis text-ellipsis whitespace-nowrap break-words rounded rounded-b-sm rounded-l-none rounded-r-2xl rounded-t-xl rounded-bl-none rounded-br-sm rounded-tl-xl rounded-tr-2xl border-0 border-x-2 border-y-2 border-b border-l border-r-4 border-t-4 border-solid border-black border-x-slate-50 border-y-transparent border-b-inherit border-l-current border-r-transparent border-t-white bg-current bg-opacity-50 bg-none from-current via-black to-orange-500 decoration-clone box-decoration-clone bg-auto bg-fixed bg-clip-padding bg-bottom bg-no-repeat bg-origin-border fill-white stroke-white stroke-2 object-contain object-bottom p-0 px-1.5 py-px pb-3 pl-4 pr-2 pt-1 text-left indent-px align-top font-sans text-lg font-semibold uppercase italic ordinal leading-4 tracking-normal text-current underline decoration-black decoration-double decoration-from-font underline-offset-auto subpixel-antialiased caret-black accent-black opacity-50 bg-blend-lighten mix-blend-darken shadow-lg shadow-white outline-dotted outline-0 outline-offset-1 outline-current ring-1 ring-inset ring-white ring-opacity-50 ring-offset-2 ring-offset-current blur brightness-50 contrast-100 drop-shadow-xl grayscale hue-rotate-90 invert saturate-100 sepia filter backdrop-blur-lg backdrop-brightness-110 backdrop-contrast-125 backdrop-grayscale backdrop-hue-rotate-90 backdrop-invert backdrop-opacity-30 backdrop-saturate-100 backdrop-sepia transition-all delay-150 duration-1000 ease-linear will-change-scroll content-none line-clamp-2 [--scroll-offset:56px]">Kitchensink</div>
+      <div class="custom custom2 aspect-auto group peer container prose prose-slate aspect-h-9 aspect-w-16 sr-only pointer-events-auto visible sticky inset-0 inset-x-2 inset-y-1 bottom-5 left-6 right-4 top-3 isolate z-auto order-1 col-span-1 col-start-2 col-end-3 row-span-1 row-start-2 row-end-3 float-left clear-both m-0 mx-1.5 my-px mb-3 ml-4 mr-2 mt-1 box-border line-clamp-2 block h-0 max-h-fit min-h-full w-0 min-w-full max-w-fit flex-none flex-shrink shrink flex-grow grow basis-0 table-fixed border-collapse origin-bottom-right translate-x-px translate-y-2 rotate-90 skew-x-2 skew-y-0 scale-0 scale-x-75 scale-y-50 transform transform-gpu transform-none animate-spin cursor-text touch-auto select-none resize-none snap-x snap-mandatory snap-center snap-always scroll-m-0 scroll-mx-1.5 scroll-my-px scroll-mb-3 scroll-ml-4 scroll-mr-2 scroll-mt-1 scroll-p-0 scroll-px-1.5 scroll-py-px scroll-pb-3 scroll-pl-4 scroll-pr-2 scroll-pt-1 list-outside list-decimal appearance-none columns-1 break-before-avoid-page break-inside-avoid-column break-after-auto auto-cols-min grid-flow-col auto-rows-min grid-cols-1 grid-rows-1 flex-col flex-wrap place-content-between place-items-stretch content-between items-baseline justify-start justify-items-center gap-0 gap-x-1 gap-y-2 space-x-1 space-y-0 space-y-reverse space-x-reverse divide-x-2 divide-y-4 divide-y-reverse divide-x-reverse divide-dashed divide-black place-self-end self-baseline justify-self-stretch overflow-hidden overflow-x-auto overflow-y-scroll overscroll-auto overscroll-y-none overscroll-x-contain scroll-smooth overflow-ellipsis text-ellipsis whitespace-nowrap break-words rounded rounded-b-sm rounded-l-none rounded-r-2xl rounded-t-xl rounded-bl-none rounded-br-sm rounded-tl-xl rounded-tr-2xl border-0 border-x-2 border-y-2 border-b border-l border-r-4 border-t-4 border-solid border-black border-x-slate-50 border-y-transparent border-b-inherit border-l-current border-r-transparent border-t-white bg-current bg-opacity-50 bg-none from-current via-black to-orange-500 decoration-clone box-decoration-clone bg-auto bg-fixed bg-clip-padding bg-bottom bg-no-repeat bg-origin-border fill-white stroke-white stroke-2 object-contain object-bottom p-0 px-1.5 py-px pb-3 pl-4 pr-2 pt-1 text-left indent-px align-top font-sans text-lg font-semibold uppercase italic ordinal leading-4 tracking-normal text-current underline decoration-black decoration-double decoration-from-font underline-offset-auto subpixel-antialiased caret-black accent-black opacity-50 bg-blend-lighten mix-blend-darken shadow-lg shadow-white outline-dotted outline-0 outline-offset-1 outline-current ring-1 ring-inset ring-white ring-opacity-50 ring-offset-2 ring-offset-current blur brightness-50 contrast-100 drop-shadow-xl grayscale hue-rotate-90 invert saturate-100 sepia filter backdrop-blur-lg backdrop-brightness-110 backdrop-contrast-125 backdrop-grayscale backdrop-hue-rotate-90 backdrop-invert backdrop-opacity-30 backdrop-saturate-100 backdrop-sepia transition-all delay-150 duration-1000 ease-linear will-change-scroll content-none [--scroll-offset:56px]">Kitchensink</div>
       `,
       options: sharedOptions,
     },
diff --git a/tests/lib/rules/enforces-shorthand.js b/tests/lib/rules/enforces-shorthand.js
index f21411f..d14368b 100644
--- a/tests/lib/rules/enforces-shorthand.js
+++ b/tests/lib/rules/enforces-shorthand.js
@@ -393,8 +393,7 @@ ruleTester.run("shorthands", rule, {
       output: `
       <div className={ctl(\`
         p-8
-        w-48
-        h-48
+        size-48
         text-white
         bg-black/50
         hover:bg-black/70
@@ -406,21 +405,21 @@ ruleTester.run("shorthands", rule, {
       \`)}>
         Multilines
       </div>`,
-      errors: [generateError(["py-8", "px-8"], "p-8")],
+      errors: [generateError(["w-48", "h-48"], "size-48"), generateError(["py-8", "px-8"], "p-8")],
     },
     {
       code: `classnames(['py-8 px-8 w-48 h-48 text-white'])`,
-      output: `classnames(['p-8 w-48 h-48 text-white'])`,
-      errors: [generateError(["py-8", "px-8"], "p-8")],
+      output: `classnames(['p-8 size-48 text-white'])`,
+      errors: [generateError(["w-48", "h-48"], "size-48"), generateError(["py-8", "px-8"], "p-8")],
     },
     {
-      code: `classnames({'py-8 px-8 w-48 h-48 text-white': true})`,
-      output: `classnames({'p-8 w-48 h-48 text-white': true})`,
+      code: `classnames({'py-8 px-8 text-white': true})`,
+      output: `classnames({'p-8 text-white': true})`,
       errors: [generateError(["py-8", "px-8"], "p-8")],
     },
     {
-      code: `classnames({'!py-8 !px-8 w-48 h-48 text-white': true})`,
-      output: `classnames({'!p-8 w-48 h-48 text-white': true})`,
+      code: `classnames({'!py-8 !px-8 text-white': true})`,
+      output: `classnames({'!p-8 text-white': true})`,
       errors: [generateError(["!py-8", "!px-8"], "!p-8")],
     },
     {
@@ -652,7 +651,9 @@ ruleTester.run("shorthands", rule, {
         Possible shorthand when using truncate with hover
       </div>
       `,
-      errors: [generateError(["hover:overflow-hidden", "hover:text-ellipsis", "hover:whitespace-nowrap"], "hover:truncate")],
+      errors: [
+        generateError(["hover:overflow-hidden", "hover:text-ellipsis", "hover:whitespace-nowrap"], "hover:truncate"),
+      ],
     },
     {
       code: `
@@ -665,7 +666,12 @@ ruleTester.run("shorthands", rule, {
         Possible shorthand when using truncate with hover, breakpoint, important and prefix
       </div>
       `,
-      errors: [generateError(["hover:sm:!tw-overflow-hidden", "hover:sm:!tw-text-ellipsis", "hover:sm:!tw-whitespace-nowrap"], "hover:sm:!tw-truncate")],
+      errors: [
+        generateError(
+          ["hover:sm:!tw-overflow-hidden", "hover:sm:!tw-text-ellipsis", "hover:sm:!tw-whitespace-nowrap"],
+          "hover:sm:!tw-truncate"
+        ),
+      ],
       options: [
         {
           config: { prefix: "tw-" },
@@ -685,5 +691,15 @@ ruleTester.run("shorthands", rule, {
       `,
       errors: [generateError(["overflow-hidden", "text-ellipsis", "whitespace-nowrap"], "truncate")],
     },
+    {
+      code: `<div class="h-10 w-10">New size-* utilities</div>`,
+      output: `<div class="size-10">New size-* utilities</div>`,
+      errors: [generateError(["h-10", "w-10"], "size-10")],
+    },
+    {
+      code: `<div class="h-10 md:h-5 md:w-5 lg:w-10">New size-* utilities</div>`,
+      output: `<div class="h-10 md:size-5 lg:w-10">New size-* utilities</div>`,
+      errors: [generateError(["md:h-5", "md:w-5"], "md:size-5")],
+    },
   ],
 });
diff --git a/tests/lib/rules/no-arbitrary-value.js b/tests/lib/rules/no-arbitrary-value.js
index bd7f7ce..f564813 100644
--- a/tests/lib/rules/no-arbitrary-value.js
+++ b/tests/lib/rules/no-arbitrary-value.js
@@ -156,5 +156,9 @@ ruleTester.run("no-arbitrary-value", rule, {
       filename: "test.vue",
       parser: require.resolve("vue-eslint-parser"),
     },
+    {
+      code: `<div className={'min-h-[75dvh]'}>Dynamic viewport units</div>`,
+      errors: generateErrors(["min-h-[75dvh]"]),
+    },
   ],
 });
diff --git a/tests/lib/rules/no-contradicting-classname.js b/tests/lib/rules/no-contradicting-classname.js
index 9787027..89adda1 100644
--- a/tests/lib/rules/no-contradicting-classname.js
+++ b/tests/lib/rules/no-contradicting-classname.js
@@ -295,9 +295,17 @@ ruleTester.run("no-contradicting-classname", rule, {
       code: `
       <div class="bg-[url('/image.jpg')] bg-center">Issue #186</div>`,
     },
+    {
+      code: `<section className="bg-[image:var(--bg-small)] bg-center" />`,
+    },
     {
       code: `
-      <section className="bg-[image:var(--bg-small)] bg-center" />`,
+        <div>
+          <div className={'h-svh min-h-svh max-h-svh'}>Dynamic viewport units</div>
+          <div className={'h-lvh min-h-lvh max-h-lvh'}>Dynamic viewport units</div>
+          <div className={'h-dvh min-h-dvh max-h-dvh'}>Dynamic viewport units</div>
+        </div>
+      `,
     },
   ],
 
@@ -709,6 +717,22 @@ ruleTester.run("no-contradicting-classname", rule, {
       </div>`,
       errors: generateErrors(["sm:bg-[url('foo.jpg')] sm:bg-[url('bar.jpg')]"]),
     },
+    {
+      code: `<div className={'h-svh h-lvh h-dvh min-h-svh min-h-lvh min-h-dvh max-h-svh max-h-lvh max-h-dvh'}>Dynamic viewport units</div>`,
+      errors: generateErrors(["h-svh h-lvh h-dvh", "min-h-svh min-h-lvh min-h-dvh", "max-h-svh max-h-lvh max-h-dvh"]),
+    },
+    {
+      code: `<div className={'size-5 size-10'}>Dynamic viewport units</div>`,
+      errors: generateErrors(["size-5 size-10"]),
+    },
+    {
+      code: `<h1 class="text-wrap text-nowrap">Balanced headlines with text-wrap utilities</h1>`,
+      errors: generateErrors(["text-wrap text-nowrap"]),
+    },
+    {
+      code: `<div class="grid grid-rows-4 grid-flow-col gap-4 grid-rows-subgrid">Subgrid support</div>`,
+      errors: generateErrors(["grid-rows-4 grid-rows-subgrid"]),
+    },
     // {
     //   code: `
     //   <div class="scale-75 transform-none">
diff --git a/tests/lib/rules/no-custom-classname.js b/tests/lib/rules/no-custom-classname.js
index d4bcda5..0840cb0 100644
--- a/tests/lib/rules/no-custom-classname.js
+++ b/tests/lib/rules/no-custom-classname.js
@@ -993,6 +993,76 @@ ruleTester.run("no-custom-classname", rule, {
         }}>Spread inside classname object</div>
       `,
     },
+    {
+      code: `
+        <div>
+          <div className={'h-svh min-h-svh max-h-svh'}>Dynamic viewport units</div>
+          <div className={'h-lvh min-h-lvh max-h-lvh'}>Dynamic viewport units</div>
+          <div className={'h-dvh min-h-dvh max-h-dvh'}>Dynamic viewport units</div>
+        </div>
+      `,
+    },
+    {
+      code: `
+      <label class="has-[:checked]:ring-indigo-500 has-[:checked]:text-indigo-900 has-[:checked]:bg-indigo-50">New :has() variant</label>
+      `,
+    },
+    {
+      code: `
+      <ul class="*:rounded-full *:border *:border-sky-100 *:bg-sky-50 *:px-2 *:py-0.5 dark:text-sky-300 dark:*:border-sky-500/15 dark:*:bg-sky-500/10">
+        <li>Sales</li>
+        <li>Marketing</li>
+        <li>SEO</li>
+      </ul>
+      `,
+    },
+    {
+      code: `<button class="size-10">New size-* utilities</button>`,
+    },
+    {
+      code: `<h1 class="text-wrap sm:text-nowrap md:text-balance lg:text-pretty">Balanced headlines with text-wrap utilities</h1>`,
+    },
+    {
+      code: `
+      <div class="grid grid-cols-4 gap-4">
+        <div>01</div>
+        <div>05</div>
+        <div class="grid grid-cols-subgrid gap-4 col-span-3">
+          <div class="col-start-2">06</div>
+        </div>
+      </div>`,
+    },
+    {
+      code: `
+      <div class="grid grid-rows-4 grid-flow-col gap-4">
+        <div>01</div>
+        <div>05</div>
+        <div class="grid grid-rows-subgrid gap-4 row-span-3">
+            <div class="row-start-2">06</div>
+        </div>
+        <div>07</div>
+        <div>10</div>
+      </div>`,
+    },
+    {
+      code: `<div class="min-w-12">Extended min-width, max-width, and min-height scales</div>`,
+    },
+    {
+      code: `<div class="opacity-35">Extended opacity scale</div>`,
+    },
+    {
+      code: `<div class="grid grid-rows-9">Extended grid-rows-* scale</div>`,
+    },
+    {
+      code: `
+      <form>
+        <input type="checkbox" class="appearance-none forced-colors:appearance-auto" />
+      </form>`,
+    },
+    {
+      code: `
+      <h1 class="forced-color-adjust-none">New forced-color-adjust utilities</h1>`,
+    },
   ],
 
   invalid: [
@@ -1414,5 +1484,9 @@ ruleTester.run("no-custom-classname", rule, {
       code: `<div className="\u3000px-1 py-2\u3000">Full-width space before and after classes</div>`,
       errors: generateErrors("\u3000px-1 py-2\u3000"),
     },
+    {
+      code: `<div class="grid grid-flow-col gap-4 grid-rows-supagrid">Subgrid support</div>`,
+      errors: generateErrors("grid-rows-supagrid"),
+    },
   ],
 });