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

Issue:
[Feature request] Lint values in a object
**Is your feature request related to a problem? Please describe.**
Can't lint values in an object passed to a callee.

My list of callees: `["clsx", "cva"]`.

This doesn't work:

```js
const buttons = cva({
  variants: {
    variant: {
      primary: "bg-white text-black font-bold px-4",
      secondary: "bg-black text-white font-semibold px-5",
    },
  },
});
```

This does:

```js
const buttons = cva({
  variants: {
    variant: {
      primary: clsx("bg-white text-black font-bold px-4"),
      secondary: clsx("bg-black text-white font-semibold px-5"),
    },
  },
});
```

**Describe the solution you'd like**
I would like for the first example to work.


Pull request:
feat: Support for Object syntax in custom callees beside `classnames`


# Support for nested object for custom callees

## Description

This plugin offers support for [classnames](https://www.npmjs.com/package/classnames), a widely used package. But this package handles keys as classnames. This is different then any other plugin like [CVA](https://github.com/joe-bell/cva). 

PR contains a check if the callee is from classnames. Maybe not the best check but only classnames plugin handles this different, so I think it is valid to check for this specific callee. 
Fixes #135

## Type of change

Please delete options that are not relevant.

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] 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?

Added two tests in no-custom-className based on custom callee

- [x] no-custom-className (cva nested object)
- [x] no-custom-className (cva simple object)

**Test Configuration**:
* OS + version: e.g. macOS Mojave
* NPM version: 16.14.2
* Node version: 8.5

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [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
- [x] Any dependent changes have been merged and published in downstream modules
- [x] I have checked my code and corrected any misspellings

Patch:
diff --git a/lib/rules/classnames-order.js b/lib/rules/classnames-order.js
index d1e324f..c877be7 100644
--- a/lib/rules/classnames-order.js
+++ b/lib/rules/classnames-order.js
@@ -127,8 +127,10 @@ module.exports = {
             });
             return;
           case 'ObjectExpression':
+            const isUsedByClassNamesPlugin = node.callee && node.callee.name === 'classnames';
+
             arg.properties.forEach((prop) => {
-              sortNodeArgumentValue(node, prop.key);
+              sortNodeArgumentValue(node, isUsedByClassNamesPlugin ? prop.key : prop.value);
             });
             return;
           case 'Literal':
diff --git a/lib/rules/enforces-negative-arbitrary-values.js b/lib/rules/enforces-negative-arbitrary-values.js
index 5f297f0..ad5b99f 100644
--- a/lib/rules/enforces-negative-arbitrary-values.js
+++ b/lib/rules/enforces-negative-arbitrary-values.js
@@ -102,8 +102,10 @@ module.exports = {
             });
             return;
           case 'ObjectExpression':
+            const isUsedByClassNamesPlugin = node.callee && node.callee.name === 'classnames';
+
             arg.properties.forEach((prop) => {
-              parseForNegativeArbitraryClassNames(node, prop.key);
+              parseForNegativeArbitraryClassNames(node, isUsedByClassNamesPlugin ? prop.key : prop.value);
             });
             return;
           case 'Literal':
diff --git a/lib/rules/enforces-shorthand.js b/lib/rules/enforces-shorthand.js
index b389688..97c8e33 100644
--- a/lib/rules/enforces-shorthand.js
+++ b/lib/rules/enforces-shorthand.js
@@ -154,8 +154,10 @@ module.exports = {
             });
             return;
           case 'ObjectExpression':
+            const isUsedByClassNamesPlugin = node.callee && node.callee.name === 'classnames';
+
             arg.properties.forEach((prop) => {
-              parseForShorthandCandidates(node, prop.key);
+              parseForShorthandCandidates(node, isUsedByClassNamesPlugin ? prop.key : prop.value);
             });
             return;
           case 'Literal':
diff --git a/lib/rules/no-arbitrary-value.js b/lib/rules/no-arbitrary-value.js
index e448e41..7b766e0 100644
--- a/lib/rules/no-arbitrary-value.js
+++ b/lib/rules/no-arbitrary-value.js
@@ -101,8 +101,10 @@ module.exports = {
             });
             return;
           case 'ObjectExpression':
+            const isUsedByClassNamesPlugin = node.callee && node.callee.name === 'classnames';
+
             arg.properties.forEach((prop) => {
-              parseForArbitraryValues(node, prop.key);
+              parseForArbitraryValues(node, isUsedByClassNamesPlugin ? prop.key : prop.value);
             });
             return;
           case 'Literal':
diff --git a/lib/util/ast.js b/lib/util/ast.js
index 225f2ba..ae62098 100644
--- a/lib/util/ast.js
+++ b/lib/util/ast.js
@@ -209,7 +209,15 @@ function parseNodeRecursive(node, arg, cb, skipConditional = false, isolate = fa
         return;
       case 'ObjectExpression':
         arg.properties.forEach((prop) => {
-          parseNodeRecursive(node, prop.key, cb, skipConditional, forceIsolation);
+          const isUsedByClassNamesPlugin = node.callee && node.callee.name === 'classnames';
+
+          parseNodeRecursive(
+            node,
+            isUsedByClassNamesPlugin ? prop.key : prop.value,
+            cb,
+            skipConditional,
+            forceIsolation
+          );
         });
         return;
       case 'Literal':


Test patch:
diff --git a/tests/lib/rules/classnames-order.js b/tests/lib/rules/classnames-order.js
index 7680258..59e253a 100644
--- a/tests/lib/rules/classnames-order.js
+++ b/tests/lib/rules/classnames-order.js
@@ -507,6 +507,20 @@ ruleTester.run("classnames-order", rule, {
       ],
       errors: errors,
     },
+    {
+      code: `cva({
+          primary: ["absolute bottom-0 w-full h-[70px] flex flex-col"],
+        })`,
+      output: `cva({
+          primary: ["absolute bottom-0 flex h-[70px] w-full flex-col"],
+        })`,
+      options: [
+        {
+          callees: ["cva"],
+        },
+      ],
+      errors: errors,
+    },
     {
       code: `<div className={clsx(\`absolute bottom-0 w-full h-[270px] flex flex-col\`)}>clsx</div>`,
       output: `<div className={clsx(\`absolute bottom-0 flex h-[270px] w-full flex-col\`)}>clsx</div>`,
diff --git a/tests/lib/rules/enforces-shorthand.js b/tests/lib/rules/enforces-shorthand.js
index 4fe139d..a6fa258 100644
--- a/tests/lib/rules/enforces-shorthand.js
+++ b/tests/lib/rules/enforces-shorthand.js
@@ -491,5 +491,19 @@ ruleTester.run("shorthands", rule, {
       `,
       errors: [generateError(["p-2", "pl-2", "pr-2"], "p-2")],
     },
+    {
+      code: `cva({
+          primary: ["border-l-0 border-r-0"],
+        });`,
+      output: `cva({
+          primary: ["border-x-0"],
+        });`,
+      options: [
+        {
+          callees: ["cva"],
+        },
+      ],
+      errors: [generateError(["border-l-0", "border-r-0"], "border-x-0")],
+    },
   ],
 });
diff --git a/tests/lib/rules/no-arbitrary-value.js b/tests/lib/rules/no-arbitrary-value.js
index 1959c5e..222759e 100644
--- a/tests/lib/rules/no-arbitrary-value.js
+++ b/tests/lib/rules/no-arbitrary-value.js
@@ -93,16 +93,11 @@ ruleTester.run("no-arbitrary-value", rule, {
     {
       code: `
       <nav
-        className={cns("flex relative flex-row rounded-lg select-none", {
+        className={classnames("flex relative flex-row rounded-lg select-none", {
           "bg-gray-200 p-1": !size,
           "bg-gray-100 p-[3px]": size === "sm",
         })}
       />`,
-      options: [
-        {
-          callees: ["cns"],
-        },
-      ],
       errors: generateErrors("p-[3px]"),
     },
     {
@@ -116,6 +111,18 @@ ruleTester.run("no-arbitrary-value", rule, {
       );`,
       errors: generateErrors("text-[length:var(--font-size)] rounded-[2em] p-[4vw] leading-[1]"),
     },
+    {
+      code: `
+      cva({
+        primary: ["[mask-type:luminance] container flex bg-[rgba(10,20,30,0.5)] w-12 sm:w-6 lg:w-4"],
+      });`,
+      options: [
+        {
+          callees: ["cva"],
+        },
+      ],
+      errors: generateErrors("[mask-type:luminance] bg-[rgba(10,20,30,0.5)]"),
+    },
     {
       code: `<div className={ctl('w-[10px]')}>Skip class attribute</div>`,
       options: skipClassAttributeOptions,
diff --git a/tests/lib/rules/no-contradicting-classname.js b/tests/lib/rules/no-contradicting-classname.js
index f8f90a1..cad9008 100644
--- a/tests/lib/rules/no-contradicting-classname.js
+++ b/tests/lib/rules/no-contradicting-classname.js
@@ -618,6 +618,18 @@ ruleTester.run("no-contradicting-classname", rule, {
       </div>`,
       errors: generateErrors("-outline-offset-2 outline-offset-4"),
     },
+    {
+      code: `
+      cva({
+        primary: ["px-2 px-4"],
+      });`,
+      options: [
+        {
+          callees: ["cva"],
+        },
+      ],
+      errors: generateErrors(["px-2 px-4"]),
+    },
     {
       code: `
       <div className={\`stroke-white stroke-none \${ctl('-outline-offset-2 outline-offset-4')}\`}>
diff --git a/tests/lib/rules/no-custom-classname.js b/tests/lib/rules/no-custom-classname.js
index 80cb615..abadd96 100644
--- a/tests/lib/rules/no-custom-classname.js
+++ b/tests/lib/rules/no-custom-classname.js
@@ -785,7 +785,7 @@ ruleTester.run("no-custom-classname", rule, {
     {
       code: `
       <div class="grid-flow-dense mix-blend-plus-lighter border-separate border-spacing-4">
-      grid-flow-dense, mix-blend-plus-lighter	
+      grid-flow-dense, mix-blend-plus-lighter
       </div>`,
     },
     {
@@ -820,6 +820,35 @@ ruleTester.run("no-custom-classname", rule, {
         },
       ],
     },
+    {
+      code: `
+        cva({
+          variants: {
+            variant: {
+              primary: ["sm:w-6 w-8 container w-12 flex lg:w-4"],
+              primary: ["sm:w-6 w-8 container w-12 flex lg:w-4"],
+            },
+          },
+      });
+      `,
+      options: [
+        {
+          callees: ["cva"],
+        },
+      ],
+    },
+    {
+      code: `
+        cva({
+          primary: ["sm:w-6 w-8 container w-12 flex lg:w-4"],
+        });
+      `,
+      options: [
+        {
+          callees: ["cva"],
+        },
+      ],
+    },
   ],
 
   invalid: [
@@ -1088,5 +1117,36 @@ ruleTester.run("no-custom-classname", rule, {
       ],
       errors: generateErrors("bg-404 lg:text-unknown text-color-not-found"),
     },
+    {
+      code: `
+        cva({
+          variants: {
+            variant: {
+              primary: ["sm:w-6 w-8 container w-12 flex lg:w-4 hello"],
+              primary: ["sm:w-6 w-8 container w-12 flex lg:w-4 world"],
+            },
+          },
+      });
+      `,
+      options: [
+        {
+          callees: ["cva"],
+        },
+      ],
+      errors: generateErrors("hello world"),
+    },
+    {
+      code: `
+        cva({
+          primary: ["sm:w-6 w-8 container w-12 flex lg:w-4 hello"],
+        });
+      `,
+      options: [
+        {
+          callees: ["cva"],
+        },
+      ],
+      errors: generateErrors("hello"),
+    },
   ],
 });