Repository: francoismassart/eslint-plugin-tailwindcss (js)

Issue:
[BUG] The `tailwindcss/enforces-shorthand` rule loses the importance flag
**Describe the bug**
The automatic fix converts `!pl-0 !pr-0` to `px-0` (losing `!`).

**To Reproduce**
Steps to reproduce the behavior:
1. Write `!pl-0 !pr-0`
2. Fix the problem with autofix.
3. See error

**Expected behavior**
The result should be `!px-0`.

**Screenshots**
![image](https://user-images.githubusercontent.com/625005/151829542-04151895-382b-43f2-b588-d006a3277f97.png)
![image](https://user-images.githubusercontent.com/625005/151829588-54da5119-29e9-4d6f-ab22-b860cb59a05d.png)


**Environment (please complete the following information):**
 - OS: Windows 10
 - Softwares + version used:
     -  VSCode: 1.63.2
     - eslint-plugin-tailwindcss: 3.3.6
     - tailwindcss: 3.0.17

**eslint config file or live demo**
[By providing a link to a live demo, a demo video or a github repo fixing the issue will be easier.
](https://play.tailwindcss.com/2CUyJCnfN3)

Pull request:
fix(enforces-shorthand): looses important flag
# `enforces-shorthand` rule loses the importance flag

## Description

See issue #114
Fixes #114 

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

- Tested via unit test
- Beta version released for reviews

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes


Patch:
diff --git a/lib/rules/enforces-shorthand.js b/lib/rules/enforces-shorthand.js
index 7cd8053..db55d2b 100644
--- a/lib/rules/enforces-shorthand.js
+++ b/lib/rules/enforces-shorthand.js
@@ -216,11 +216,11 @@ module.exports = {
           // Comparing same parentType classnames
           const checkedVariantsValue = [];
           sameType.forEach((cls) => {
-            const key = cls.variants + cls.value;
+            const key = cls.variants + (cls.important ? '!' : '') + cls.value;
             if (!checkedVariantsValue.includes(key)) {
               checkedVariantsValue.push(key);
               const sameVariantAndValue = sameType.filter((v) => {
-                return v.variants === cls.variants && v.value === cls.value;
+                return !(v.variants !== cls.variants || v.value !== cls.value || v.important !== cls.important);
               });
               if (sameVariantAndValue.length === 1) {
                 validated.push(cls);
@@ -241,13 +241,15 @@ module.exports = {
                 const hasX = sameVariantAndValue.some((c) => c.shorthand === 'x') || (hasL && hasR);
                 const hasY = sameVariantAndValue.some((c) => c.shorthand === 'y') || (hasT && hasB);
                 const hasAll = sameVariantAndValue.some((c) => c.shorthand === 'all') || (hasY && hasX);
+                const important = cls.important ? '!' : '';
                 const isNegative = ('' + cls.value).substring(0, 1) === '-';
                 const minus = isNegative ? '-' : '';
                 const absoluteVal = isNegative ? ('' + cls.value).substring(1) : cls.value;
 
                 if (hasAll) {
                   const all = getBodyByShorthand(targetGroups, classname.parentType, 'all');
-                  const patchedName = `${cls.variants}${minus}${all}${absoluteVal.length ? '-' + absoluteVal : ''}`;
+                  const val = absoluteVal.length ? '-' + absoluteVal : '';
+                  const patchedName = `${cls.variants}${important}${minus}${all}${val}`;
                   troubles.push([sameVariantAndValue.map((c) => c.name), patchedName]);
                   cls.name = patchedName;
                   cls.shorthand = 'all';
@@ -255,7 +257,7 @@ module.exports = {
                 } else if (hasY || hasX) {
                   const xOrY = hasX ? 'x' : 'y';
                   const xOrYType = getBodyByShorthand(targetGroups, classname.parentType, xOrY);
-                  const patchedName = `${cls.variants}${minus}${xOrYType}${
+                  const patchedName = `${cls.variants}${important}${minus}${xOrYType}${
                     absoluteVal.length ? '-' + absoluteVal : ''
                   }`;
                   const toBeReplaced = sameVariantAndValue
@@ -270,18 +272,23 @@ module.exports = {
                   });
 
                   troubles.push([toBeReplaced, patchedName]);
-                  cls.name = patchedName;
-                  cls.shorthand = xOrY;
-                  validated.push(cls);
-                  if (toBeKept.length) {
-                    validated.push(...toBeKept);
-                  }
+                  let replaced = false;
+                  sameVariantAndValue.forEach((ref, i) => {
+                    if (toBeKept.find((k) => k.name === ref.name)) {
+                      validated.push(ref);
+                    } else if (!replaced) {
+                      replaced = true;
+                      const cloned = JSON.parse(JSON.stringify(ref));
+                      cloned.name = patchedName;
+                      cloned.shorthand = xOrY;
+                      validated.push(cloned);
+                    }
+                  });
                 } else if (supportCorners && (hasT || hasR || hasB || hasL)) {
                   const side = hasT ? 't' : hasR ? 'r' : hasB ? 'b' : 'l';
                   const sideBody = getBodyByShorthand(targetGroups, classname.parentType, side);
-                  const patchedName = `${cls.variants}${minus}${sideBody}${
-                    absoluteVal.length ? '-' + absoluteVal : ''
-                  }`;
+                  const val = absoluteVal.length ? '-' + absoluteVal : '';
+                  const patchedName = `${cls.variants}${important}${minus}${sideBody}${val}`;
                   const toBeReplaced = sameVariantAndValue
                     .filter((c) => {
                       const candidates = hasT ? ['tl', 'tr'] : hasR ? ['tr', 'br'] : hasB ? ['bl', 'br'] : ['tl', 'bl'];
@@ -294,12 +301,18 @@ module.exports = {
                   });
 
                   troubles.push([toBeReplaced, patchedName]);
-                  cls.name = patchedName;
-                  cls.shorthand = side;
-                  validated.push(cls);
-                  if (toBeKept.length) {
-                    validated.push(...toBeKept);
-                  }
+                  let replaced = false;
+                  sameVariantAndValue.forEach((ref, i) => {
+                    if (toBeKept.find((k) => k.name === ref.name)) {
+                      validated.push(ref);
+                    } else if (!replaced) {
+                      replaced = true;
+                      const cloned = JSON.parse(JSON.stringify(ref));
+                      cloned.name = patchedName;
+                      cloned.shorthand = side;
+                      validated.push(cloned);
+                    }
+                  });
                 } else {
                   validated.push(...sameVariantAndValue);
                 }
@@ -309,6 +322,9 @@ module.exports = {
         }
       });
 
+      // Try to keep the original order
+      validated.sort((a, b) => (a.index < b.index ? -1 : +1));
+
       // Generates the validated attribute value
       const union = validated.map((val) => val.leading + val.name + val.trailing);
 
diff --git a/lib/util/groupMethods.js b/lib/util/groupMethods.js
index d62d5f4..5992ef0 100644
--- a/lib/util/groupMethods.js
+++ b/lib/util/groupMethods.js
@@ -525,6 +525,7 @@ function parseClassname(name, arr, config, index = null) {
     shorthand: slot ? slot.shorthand : '',
     leading: leading,
     trailing: trailing,
+    important: body.substr(0, 1) === '!',
   };
 }
 
diff --git a/package-lock.json b/package-lock.json
index f417e98..d019ac4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-tailwindcss",
-  "version": "3.4.0",
+  "version": "3.4.1-beta.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
diff --git a/package.json b/package.json
index 5c19746..091192d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-tailwindcss",
-  "version": "3.4.0",
+  "version": "3.4.1-beta.0",
   "description": "Rules enforcing best practices while using Tailwind CSS",
   "keywords": [
     "eslint",


Test patch:
diff --git a/tests/lib/rules/enforces-shorthand.js b/tests/lib/rules/enforces-shorthand.js
index 3f4d963..641931c 100644
--- a/tests/lib/rules/enforces-shorthand.js
+++ b/tests/lib/rules/enforces-shorthand.js
@@ -280,7 +280,7 @@ ruleTester.run("shorthands", rule, {
       </div>
       `,
       output: `
-      <div class="rounded-t-sm rounded-br-lg rounded-bl-xl md:rounded-md border-y-4 border-y-4 border-l-0 block">
+      <div class="rounded-t-sm border-r-4 block rounded-br-lg rounded-bl-xl md:rounded-md border-y-4 border-l-0">
         Randomized classnames order
       </div>
       `,
@@ -373,5 +373,30 @@ ruleTester.run("shorthands", rule, {
       output: `classnames({'p-8 w-48 h-48 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})`,
+      errors: [generateError(["!py-8", "!px-8"], "!p-8")],
+    },
+    {
+      code: `classnames({'!pt-8 !pb-8 pr-8 !pl-8': true})`,
+      output: `classnames({'!py-8 pr-8 !pl-8': true})`,
+      errors: [generateError(["!pt-8", "!pb-8"], "!py-8")],
+    },
+    {
+      code: `classnames({'!pt-8 !pb-8 !pr-8 !pl-8': true})`,
+      output: `classnames({'!p-8': true})`,
+      errors: [generateError(["!pt-8", "!pb-8", "!pr-8", "!pl-8"], "!p-8")],
+    },
+    {
+      code: `classnames({'!pt-8 pb-8 pr-8 pl-8': true})`,
+      output: `classnames({'!pt-8 pb-8 px-8': true})`,
+      errors: [generateError(["pr-8", "pl-8"], "px-8")],
+    },
+    {
+      code: `classnames({'md:!rounded-tr block md:rounded-tl md:rounded-br md:rounded-bl': true})`,
+      output: `classnames({'md:!rounded-tr block md:rounded-tl md:rounded-b': true})`,
+      errors: [generateError(["md:rounded-br", "md:rounded-bl"], "md:rounded-b")],
+    },
   ],
 });
diff --git a/tests/lib/util/groupMethods.js b/tests/lib/util/groupMethods.js
index f105b13..31c47c6 100644
--- a/tests/lib/util/groupMethods.js
+++ b/tests/lib/util/groupMethods.js
@@ -38,6 +38,7 @@ describe("parseClassname", function () {
       shorthand: "x",
       leading: "",
       trailing: "",
+      important: false,
     };
     assert.deepEqual(actual, expected);
     name = "md:overflow-y-auto";
@@ -186,6 +187,22 @@ describe("parseClassname", function () {
     expected.leading = "  ";
     expected.trailing = "  ";
     assert.deepEqual(actual, expected);
+
+    // Important
+    name = "md:!p-8";
+    actual = groupUtil.parseClassname(name, targetGroups, mergedConfig, 1);
+    expected.index = 1;
+    expected.name = name;
+    expected.body = "!p-";
+    expected.leading = "";
+    expected.trailing = "";
+    expected.shorthand = "";
+    expected.parentType = "Padding";
+    expected.shorthand = "all";
+    expected.variants = "md:";
+    expected.value = "8";
+    expected.important = true;
+    assert.deepEqual(actual, expected);
   });
 
   it("should support named capture group", function () {