Repository: francoismassart/eslint-plugin-tailwindcss
Pull request #308 (2024-01-15 18:36:57)

Fix edge cases with whitespace in `enforces-shorthand`
## Description

I noticed a small bug when updating a side project to `eslint-plugin-tailwindcss@3.14.0` and running `pnpm fix:eslint`. See commit history in https://github.com/kachkaev/website/pull/311.

**Input:**

```jsx
<body className={`relative flex h-full w-full flex-col overflow-y-scroll px-5 pt-4 ${locale}`}>
```

**Ouput:**

```jsx
<body className={`relative flex size-full flex-col overflow-y-scroll px-5 pt-4${locale}`}>
```

**Expected output:**

```jsx
<body className={`relative flex size-full flex-col overflow-y-scroll px-5 pt-4 ${locale}`}>
```

This PR makes sure that a space is not removed between `pt-4` and `${locale}` when `tailwindcss/enforces-shorthand` does an autofix.

## Type of change

Please delete options that are not relevant.

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

See new unit tests

**Test Configuration**:

CI

## 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
- ~~I have made corresponding changes to the documentation~~ N/A
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- ~~Any dependent changes have been merged and published in downstream modules~~ N/A
- [x] I have checked my code and corrected any misspellings



Patch:
diff --git a/lib/rules/enforces-shorthand.js b/lib/rules/enforces-shorthand.js
index 1fcc366..e7703c3 100644
--- a/lib/rules/enforces-shorthand.js
+++ b/lib/rules/enforces-shorthand.js
@@ -434,7 +434,7 @@ module.exports = {
           const w = whitespaces[i] ?? '';
           const cls = union[i];
           validatedClassNamesValue += headSpace ? `${w}${cls}` : isLast ? `${cls}` : `${cls}${w}`;
-          if (headSpace && tailSpace && isLast) {
+          if (tailSpace && isLast) {
             validatedClassNamesValue += whitespaces[whitespaces.length - 1] ?? '';
           }
         }
diff --git a/lib/util/ast.js b/lib/util/ast.js
index e420ef1..4648f8d 100644
--- a/lib/util/ast.js
+++ b/lib/util/ast.js
@@ -184,6 +184,9 @@ function extractRangeFromNode(node) {
   if (node.type === 'TextAttribute' && node.name === 'class') {
     return [node.valueSpan.fullStart.offset, node.valueSpan.end.offset];
   }
+  if (node.value === undefined) {
+    return [0,0];
+  }
   switch (node.value.type) {
     case 'JSXExpressionContainer':
       return node.value.expression.range;
@@ -196,6 +199,10 @@ function extractValueFromNode(node) {
   if (node.type === 'TextAttribute' && node.name === 'class') {
     return node.value;
   }
+  if (node.value === undefined) {
+    return node.value;
+  }
+
   switch (node.value.type) {
     case 'JSXExpressionContainer':
       return node.value.expression.value;


Test patch:
diff --git a/tests/lib/rules/classnames-order.js b/tests/lib/rules/classnames-order.js
index b797f01..c4bba5d 100644
--- a/tests/lib/rules/classnames-order.js
+++ b/tests/lib/rules/classnames-order.js
@@ -281,6 +281,14 @@ ruleTester.run("classnames-order", rule, {
     {
       code: `<div class="py-1\u3000px-2 block">Do not treat full width space as class separator</div>`,
     },
+    {
+      code: `
+        const func = () => ({ a: 12 });
+        <div className={clsx(['text-sm', {
+          ...func()
+        }])}>Spread of a function return inside clsx</div>
+      `,
+    },
   ],
   invalid: [
     {
diff --git a/tests/lib/rules/enforces-shorthand.js b/tests/lib/rules/enforces-shorthand.js
index d14368b..8d14347 100644
--- a/tests/lib/rules/enforces-shorthand.js
+++ b/tests/lib/rules/enforces-shorthand.js
@@ -691,6 +691,26 @@ ruleTester.run("shorthands", rule, {
       `,
       errors: [generateError(["overflow-hidden", "text-ellipsis", "whitespace-nowrap"], "truncate")],
     },
+    {
+      code: "<div className={ctl(`${live && 'bg-white'} w-full px-10 py-10`)}>Leading space trim issue with fix</div>",
+      output: "<div className={ctl(`${live && 'bg-white'} w-full p-10`)}>Leading space trim issue with fix</div>",
+      errors: [generateError(["px-10", "py-10"], "p-10")],
+    },
+    {
+      code: "<div className={ctl(`${live && 'bg-white'} w-full px-10 py-10 `)}>Leading space trim issue with fix (2)</div>",
+      output: "<div className={ctl(`${live && 'bg-white'} w-full p-10 `)}>Leading space trim issue with fix (2)</div>",
+      errors: [generateError(["px-10", "py-10"], "p-10")],
+    },
+    {
+      code: "<div className={ctl(`w-full px-10 py-10 ${live && 'bg-white'}`)}>Trailing space trim issue with fix</div>",
+      output: "<div className={ctl(`w-full p-10 ${live && 'bg-white'}`)}>Trailing space trim issue with fix</div>",
+      errors: [generateError(["px-10", "py-10"], "p-10")],
+    },
+    {
+      code: "<div className={ctl(` w-full px-10 py-10 ${live && 'bg-white'}`)}>Trailing space trim issue with fix (2)</div>",
+      output: "<div className={ctl(` w-full p-10 ${live && 'bg-white'}`)}>Trailing space trim issue with fix (2)</div>",
+      errors: [generateError(["px-10", "py-10"], "p-10")],
+    },
     {
       code: `<div class="h-10 w-10">New size-* utilities</div>`,
       output: `<div class="size-10">New size-* utilities</div>`,