Repository: francoismassart/eslint-plugin-tailwindcss
Pull request #153 (2022-08-12 13:41:35)

FIX: Conflicting rules with ambiguous arbitrary values
FIX: Conflicting rules with arbitrary values

## Description

This changes the algorithm on the rule `lib/rules/no-contradicting-classname.js`, to detect conflicting rules when using ambiguous arbitrary values.


Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context.
List any dependencies that are required for this change.

Fixes # (issue)

## 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?
I've added a test to tests/lib/rules/no-contradicting-classname.js.

**Test Configuration**:
* OS + version: macOS monterey
* NPM version: 6.14.11
* Node version: v14.16.0

## 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
- [ ] 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
- [ ] 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 8463110..01a2575 100644
--- a/lib/rules/classnames-order.js
+++ b/lib/rules/classnames-order.js
@@ -204,7 +204,7 @@ module.exports = {
       }
       if (astUtil.isLiteralAttributeValue(node)) {
         sortNodeArgumentValue(node);
-      } else if (node.value.type === 'JSXExpressionContainer') {
+      } else if (node.value && node.value.type === 'JSXExpressionContainer') {
         sortNodeArgumentValue(node, node.value.expression);
       }
     };
diff --git a/lib/rules/enforces-negative-arbitrary-values.js b/lib/rules/enforces-negative-arbitrary-values.js
index 9a2b249..778c5f2 100644
--- a/lib/rules/enforces-negative-arbitrary-values.js
+++ b/lib/rules/enforces-negative-arbitrary-values.js
@@ -147,7 +147,7 @@ module.exports = {
       }
       if (astUtil.isLiteralAttributeValue(node)) {
         parseForNegativeArbitraryClassNames(node);
-      } else if (node.value.type === 'JSXExpressionContainer') {
+      } else if (node.value && node.value.type === 'JSXExpressionContainer') {
         parseForNegativeArbitraryClassNames(node, node.value.expression);
       }
     };
diff --git a/lib/rules/enforces-shorthand.js b/lib/rules/enforces-shorthand.js
index baceba1..698af31 100644
--- a/lib/rules/enforces-shorthand.js
+++ b/lib/rules/enforces-shorthand.js
@@ -375,7 +375,7 @@ module.exports = {
       }
       if (astUtil.isLiteralAttributeValue(node)) {
         parseForShorthandCandidates(node);
-      } else if (node.value.type === 'JSXExpressionContainer') {
+      } else if (node.value && node.value.type === 'JSXExpressionContainer') {
         parseForShorthandCandidates(node, node.value.expression);
       }
     };
diff --git a/lib/rules/migration-from-tailwind-2.js b/lib/rules/migration-from-tailwind-2.js
index d40ebcf..06482c2 100644
--- a/lib/rules/migration-from-tailwind-2.js
+++ b/lib/rules/migration-from-tailwind-2.js
@@ -260,7 +260,7 @@ module.exports = {
       }
       if (astUtil.isLiteralAttributeValue(node)) {
         parseForObsoleteClassNames(node);
-      } else if (node.value.type === 'JSXExpressionContainer') {
+      } else if (node.value && node.value.type === 'JSXExpressionContainer') {
         parseForObsoleteClassNames(node, node.value.expression);
       }
     };
diff --git a/lib/rules/no-arbitrary-value.js b/lib/rules/no-arbitrary-value.js
index faa4036..551543e 100644
--- a/lib/rules/no-arbitrary-value.js
+++ b/lib/rules/no-arbitrary-value.js
@@ -146,7 +146,7 @@ module.exports = {
       }
       if (astUtil.isLiteralAttributeValue(node)) {
         parseForArbitraryValues(node);
-      } else if (node.value.type === 'JSXExpressionContainer') {
+      } else if (node.value && node.value.type === 'JSXExpressionContainer') {
         parseForArbitraryValues(node, node.value.expression);
       }
     };
diff --git a/lib/rules/no-contradicting-classname.js b/lib/rules/no-contradicting-classname.js
index fd33eb5..8810f89 100644
--- a/lib/rules/no-contradicting-classname.js
+++ b/lib/rules/no-contradicting-classname.js
@@ -89,13 +89,18 @@ module.exports = {
       classNames = sorted.filter((slot) => slot.length > 1);
 
       // Sorts each groups' classnames
+      const ambiguousArbitraryValues = String.raw`(\[(.*${mergedConfig.separator}))`
+      const ambiguousArbitraryValuesOrClasses = String.raw`(\[(.*${mergedConfig.separator}))|(^((?!:).)*$)`
+      const arbitraryRegex = new RegExp(ambiguousArbitraryValues);
+
       classNames.forEach((slot) => {
         const variants = [];
         slot.forEach((cls) => {
-          const start = cls.lastIndexOf(mergedConfig.separator) + 1;
+          const arbiratySeparator = arbitraryRegex.test(cls);
+          const start = arbiratySeparator? 0 : cls.lastIndexOf(mergedConfig.separator) + 1;
           const prefix = cls.substr(0, start);
           const name = cls.substr(start);
-          const rePrefix = prefix === '' ? '((?!:).)*$' : prefix;
+          const rePrefix = prefix === '' ? ambiguousArbitraryValuesOrClasses : '^' + prefix;
           const idx = variants.findIndex((v) => v.prefix === rePrefix);
           if (idx === -1) {
             variants.push({
@@ -109,7 +114,7 @@ module.exports = {
         const troubles = variants.filter((v) => v.name.length > 1);
         if (troubles.length) {
           troubles.forEach((issue) => {
-            const re = new RegExp('^' + issue.prefix);
+            const re = new RegExp(issue.prefix);
             const conflicting = slot.filter((c) => re.test(c));
             context.report({
               node: node,
@@ -133,7 +138,7 @@ module.exports = {
       }
       if (astUtil.isLiteralAttributeValue(node)) {
         astUtil.parseNodeRecursive(node, null, parseForContradictingClassNames, true);
-      } else if (node.value.type === 'JSXExpressionContainer') {
+      } else if (node.value && node.value.type === 'JSXExpressionContainer') {
         astUtil.parseNodeRecursive(node, node.value.expression, parseForContradictingClassNames, true);
       }
     };
diff --git a/lib/rules/no-custom-classname.js b/lib/rules/no-custom-classname.js
index cc44a85..af60b3e 100644
--- a/lib/rules/no-custom-classname.js
+++ b/lib/rules/no-custom-classname.js
@@ -142,7 +142,7 @@ module.exports = {
       }
       if (astUtil.isLiteralAttributeValue(node)) {
         astUtil.parseNodeRecursive(node, null, parseForCustomClassNames);
-      } else if (node.value.type === 'JSXExpressionContainer') {
+      } else if (node.value && node.value.type === 'JSXExpressionContainer') {
         astUtil.parseNodeRecursive(node, node.value.expression, parseForCustomClassNames);
       }
     };
diff --git a/lib/util/ast.js b/lib/util/ast.js
index 49c4a32..225f2ba 100644
--- a/lib/util/ast.js
+++ b/lib/util/ast.js
@@ -189,7 +189,7 @@ function parseNodeRecursive(node, arg, cb, skipConditional = false, isolate = fa
     switch (arg.type) {
       case 'TemplateLiteral':
         arg.expressions.forEach((exp) => {
-          parseNodeRecursive(node, exp.right, cb, skipConditional, forceIsolation);
+          parseNodeRecursive(node, exp, cb, skipConditional, forceIsolation);
         });
         arg.quasis.forEach((quasis) => {
           parseNodeRecursive(node, quasis, cb, skipConditional, isolate);


Test patch:
diff --git a/tests/lib/rules/classnames-order.js b/tests/lib/rules/classnames-order.js
index aa944db..a8ba0ee 100644
--- a/tests/lib/rules/classnames-order.js
+++ b/tests/lib/rules/classnames-order.js
@@ -241,6 +241,9 @@ ruleTester.run("classnames-order", rule, {
       </div>
       `,
     },
+    {
+      code: `<div class>No errors while typing</div>`,
+    },
   ],
   invalid: [
     {
diff --git a/tests/lib/rules/enforces-negative-arbitrary-values.js b/tests/lib/rules/enforces-negative-arbitrary-values.js
index 141d526..00dae9b 100644
--- a/tests/lib/rules/enforces-negative-arbitrary-values.js
+++ b/tests/lib/rules/enforces-negative-arbitrary-values.js
@@ -49,6 +49,9 @@ ruleTester.run("enforces-negative-arbitrary-values", rule, {
     {
       code: `<div className={\`top-[-50px]\`}>top-[-50px]</div>`,
     },
+    {
+      code: `<div className>No errors while typing</div>`,
+    },
   ],
   invalid: [
     {
diff --git a/tests/lib/rules/enforces-shorthand.js b/tests/lib/rules/enforces-shorthand.js
index 672f02c..31c30dc 100644
--- a/tests/lib/rules/enforces-shorthand.js
+++ b/tests/lib/rules/enforces-shorthand.js
@@ -86,6 +86,9 @@ ruleTester.run("shorthands", rule, {
     {
       code: `<div className="py-2.5 md:py-3 pl-2.5 md:pl-4 font-medium uppercase">issue #91</div>`,
     },
+    {
+      code: `<div class>No errors while typing</div>`,
+    },
   ],
 
   invalid: [
diff --git a/tests/lib/rules/migration-from-tailwind-2.js b/tests/lib/rules/migration-from-tailwind-2.js
index 257c029..89a0c32 100644
--- a/tests/lib/rules/migration-from-tailwind-2.js
+++ b/tests/lib/rules/migration-from-tailwind-2.js
@@ -36,6 +36,9 @@ ruleTester.run("migration-from-tailwind-2", rule, {
     {
       code: `<div class="transition-[var(--transform)]">transition-[var(--transform)]</div>`,
     },
+    {
+      code: `<div class>No errors while typing</div>`,
+    },
   ],
   invalid: [
     {
diff --git a/tests/lib/rules/no-arbitrary-value.js b/tests/lib/rules/no-arbitrary-value.js
index a83508f..ee29e43 100644
--- a/tests/lib/rules/no-arbitrary-value.js
+++ b/tests/lib/rules/no-arbitrary-value.js
@@ -46,6 +46,9 @@ ruleTester.run("no-arbitrary-value", rule, {
     {
       code: `<div class="flex shrink-0 flex-col">No arbitrary value</div>`,
     },
+    {
+      code: `<div class>No errors while typing</div>`,
+    },
   ],
 
   invalid: [
diff --git a/tests/lib/rules/no-contradicting-classname.js b/tests/lib/rules/no-contradicting-classname.js
index 1d47921..60ab11a 100644
--- a/tests/lib/rules/no-contradicting-classname.js
+++ b/tests/lib/rules/no-contradicting-classname.js
@@ -170,6 +170,12 @@ ruleTester.run("no-contradicting-classname", rule, {
         Same class prefix, type prefix may be required for resolving ambiguous values
       </div>`,
     },
+    {
+      code: `
+      <div class="text-left text-lg text-[color:var(--my-var,#ccc)]]">
+        Same class prefix, type prefix may be required for resolving ambiguous values
+      </div>`,
+    },
     {
       code: `
       <div class="scale-75 translate-x-4 skew-y-3 motion-reduce:transform-none">
@@ -235,6 +241,9 @@ ruleTester.run("no-contradicting-classname", rule, {
         },
       ],
     },
+    {
+      code: "<div class>No errors while typing</div>",
+    },
   ],
 
   invalid: [
@@ -243,11 +252,11 @@ ruleTester.run("no-contradicting-classname", rule, {
       export interface FakePropsInterface {
         readonly name?: string;
       }
-      
+
       function Fake({
         name = 'yolo'
       }: FakeProps) {
-      
+
         return (
           <>
             <h1 className={"container w-1 w-2"}>Welcome {name}</h1>
@@ -255,7 +264,7 @@ ruleTester.run("no-contradicting-classname", rule, {
           </>
         );
       }
-      
+
       export default Fake;
       `,
       parser: require.resolve("@typescript-eslint/parser"),
@@ -403,6 +412,11 @@ ruleTester.run("no-contradicting-classname", rule, {
       options: [{ tags: ["myTag"] }],
       errors: generateErrors("px-2 px-0"),
     },
+    {
+      code: `myTag\`\${enabled ? "px-2 px-0" : ""}\``,
+      options: [{ tags: ["myTag"] }],
+      errors: generateErrors("px-2 px-0"),
+    },
     {
       code: `
       <div class="shrink shrink-0 shrink-[inherit] shrink-[initial] shrink-[unset] shrink-[var(--some)] shrink-[0.5] shrink-[5]">
@@ -452,6 +466,30 @@ ruleTester.run("no-contradicting-classname", rule, {
       options: config,
       errors: generateErrors("rounded-xl rounded-[50%/10%] rounded-[10%,30%,50%,70%] rounded-[var(--some)]"),
     },
+    {
+      code: `
+      <div class="text-white text-[color:var(--my-var,#ccc)]">
+        Arbitrary values for text color
+      </div>
+      `,
+      errors: generateErrors("text-white text-[color:var(--my-var,#ccc)]"),
+    },
+    {
+      code: `
+      <div class="text-lg text-[length:var(--my-var)]">
+        Arbitrary values for text font size
+      </div>
+      `,
+      errors: generateErrors("text-lg text-[length:var(--my-var)]"),
+    },
+    {
+      code: `
+      <div class="bg-white bg-[color:var(--donno)]">
+        Arbitrary values for background color
+      </div>
+      `,
+      errors: generateErrors("bg-white bg-[color:var(--donno)]"),
+    },
     {
       code: `<div class="aspect-none aspect-w-16 aspect-w-10 aspect-h-9">aspect</div>`,
       options: config,
diff --git a/tests/lib/rules/no-custom-classname.js b/tests/lib/rules/no-custom-classname.js
index 1095367..37a22b3 100644
--- a/tests/lib/rules/no-custom-classname.js
+++ b/tests/lib/rules/no-custom-classname.js
@@ -791,6 +791,9 @@ ruleTester.run("no-custom-classname", rule, {
         border-spacing
       </div>`,
     },
+    {
+      code: `<div class>No errors while typing</div>`,
+    },
   ],
 
   invalid: [
@@ -985,6 +988,11 @@ ruleTester.run("no-custom-classname", rule, {
       options: [{ tags: ["myTag"] }],
       errors: generateErrors("custom-2 custom-3 custom-1"),
     },
+    {
+      code: `myTag\`custom-1 \${isDisabled ? "custom-2" : "m-4"}\``,
+      options: [{ tags: ["myTag"] }],
+      errors: generateErrors("custom-2 custom-1"),
+    },
     {
       code: `
       <div class="bg-red-600 p-10">