Repository: francoismassart/eslint-plugin-tailwindcss
Pull request #3 (2021-03-29 20:27:53)

rules/no-contradicting-classname


Patch:
diff --git a/README.md b/README.md
index 2cf2358..d93cd51 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,7 @@ Then configure the rules you want to use under the rules section.
     "rules": {
         "tailwindcss/classnames-order": 2,
         "tailwindcss/no-custom-classname": 2,
+        "tailwindcss/no-contradicting-classname": 2,
     }
 }
 ```
@@ -48,10 +49,10 @@ Learn more about [Configuring Rules](https://eslint.org/docs/user-guide/configur
 
 * [`classnames-order`](docs/rules/classnames-order.md): order classnames by target properties then by variants (`[size:][theme:][state:]`)
 * [`no-custom-classname`](docs/rules/no-custom-classname.md): only allow classnames from Tailwind CSS
+* [`no-contradicting-classname`](docs/rules/no-contradicting-classname.md): e.g. avoid `p-2 p-3`, different Tailwind CSS classnames (`pt-2` & `pt-3`) but targeting the same property several times for the same variant.
 
 ## Upcoming Rules
 
-* `no-contradicting-classname`: e.g. avoid `p-2 p-3`, different Tailwind CSS classnames (`pt-2` & `pt-3`) but targeting the same property
 * `no-redundant-variant`: e.g. avoid `mx-5 sm:mx-5`, no need to redefine `mx` in `sm:` variant as it uses the same value (`5`)
 
 ## Alternatives
diff --git a/docs/rules/no-contradicting-classname.md b/docs/rules/no-contradicting-classname.md
new file mode 100644
index 0000000..e01b119
--- /dev/null
+++ b/docs/rules/no-contradicting-classname.md
@@ -0,0 +1,25 @@
+# Avoid contradicting Tailwind CSS classnames (e.g. "w-3 w-5") (no-contradicting-classname)
+
+The majority of the Tailwind CSS classes only affect a single CSS property.
+Using two or more classnames which affect the same property for the same variant means trouble and confusion.
+
+
+## Rule Details
+
+The rule aims to warn you about contradictions in the classnames you are attaching to an element.
+
+Examples of **incorrect** code for this rule:
+
+```html
+<div class="container w-1 w-2">width ?</div>
+```
+
+Examples of **correct** code for this rule:
+
+```html
+<div class="container p-1 lg:p-4">padding is defined once per variant max.</div>
+```
+
+## Further Reading
+
+This rule will not fix the issue but will let you know about the issue.
\ No newline at end of file
diff --git a/lib/rules/classnames-order.js b/lib/rules/classnames-order.js
index 5e4b494..7523c7d 100644
--- a/lib/rules/classnames-order.js
+++ b/lib/rules/classnames-order.js
@@ -47,22 +47,14 @@ module.exports = {
   },
 
   create: function (context) {
-
-    // variables should be defined here
-
-    /**
-     * 1. Group by property
-     * 2. Sort each groups based on config
-     * 3. Sort elements within group based on:
-     *    3.1. size
-     *    3.2. theme
-     *    3.3. state
-     */
+    const configuration = context.options[0] || {};
+    const groupsConfig = configuration.groups || defaultGroups;
+    const prependCustom = configuration.prependCustom || false;
+    const removeDuplicates = !(configuration.removeDuplicates === false);
 
     //----------------------------------------------------------------------
     // Helpers
     //----------------------------------------------------------------------
-
     const getPrefixIndex = (str, arr, beginning = false) => {
       const start = beginning ? '^' : '';
       let idx = arr.findIndex(el => {
@@ -107,10 +99,6 @@ module.exports = {
     //----------------------------------------------------------------------
     // Public
     //----------------------------------------------------------------------
-    const configuration = context.options[0] || {};
-    const groupsConfig = configuration.groups || defaultGroups;
-    const prependCustom = configuration.prependCustom || false;
-    const removeDuplicates = !(configuration.removeDuplicates === false);
     return {
       JSXAttribute: function (node) {
         if (!astUtil.isValidJSXAttribute(node)) {
diff --git a/lib/rules/no-contradicting-classname.js b/lib/rules/no-contradicting-classname.js
new file mode 100644
index 0000000..f478ad4
--- /dev/null
+++ b/lib/rules/no-contradicting-classname.js
@@ -0,0 +1,115 @@
+/**
+ * @fileoverview Avoid contradicting Tailwind CSS classnames (e.g. "w-3 w-5")
+ * @author François Massart
+ */
+'use strict';
+
+const docsUrl = require('../util/docsUrl');
+const defaultGroups = require('../config/groups').groups;
+const astUtil = require('../util/ast');
+const attrUtil = require('../util/attr');
+const groupUtil = require('../util/groupMethods');
+const removeDuplicatesFromArray = require('../util/removeDuplicatesFromArray');
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+// Predefine message for use in context.report conditional.
+// messageId will still be usable in tests.
+const CONFLICTING_CLASSNAMES_DETECTED_MSG = `Classnames {{classnames}} are conflicting!`;
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Avoid contradicting Tailwind CSS classnames (e.g. "w- 3 w- 5")',
+      category: 'Possible Errors',
+      recommended: false,
+      url: docsUrl('no-contradicting-classname'),
+    },
+    messages: {
+      conflictingClassnames: CONFLICTING_CLASSNAMES_DETECTED_MSG
+    },
+    fixable: null,
+      schema: []
+  },
+
+create: function(context) {
+
+  // variables should be defined here
+
+  //----------------------------------------------------------------------
+  // Helpers
+  //----------------------------------------------------------------------
+
+  // any helper functions should go here or else delete this section
+
+  //----------------------------------------------------------------------
+  // Public
+  //----------------------------------------------------------------------
+
+  return {
+    JSXAttribute: function (node) {
+      if (!astUtil.isValidJSXAttribute(node)) {
+        return;
+      }
+
+      const originalClassNamesValue = node.value.value;
+      let classNames = attrUtil.getClassNamesFromAttribute(originalClassNamesValue);
+      classNames = removeDuplicatesFromArray(classNames);
+      if (classNames.length <= 1) {
+        // Don't run for a single or empty className
+        return;
+      }
+
+      // Init assets before sorting
+      const groups = groupUtil.getGroups(defaultGroups);
+      const sorted = groupUtil.initGroupSlots(groups);
+
+      // Move each classname inside its dedicated group
+      classNames.forEach(className => {
+        const idx = groupUtil.getGroupIndex(className, groups);
+        if (idx > -1) {
+          sorted[idx].push(className);
+        }
+      });
+
+      classNames = sorted.filter(slot => slot.length > 1);
+
+      // Sorts each groups' classnames
+      classNames.forEach(slot => {
+        const variants = [];
+        slot.forEach(cls => {
+          const start = cls.lastIndexOf(':') + 1;
+          const prefix = cls.substr(0, start);
+          const name = cls.substr(start);
+          const rePrefix = prefix === '' ? '((?!:).)*$' : prefix;
+          const idx = variants.findIndex(v => v.prefix === rePrefix);
+          if (idx === -1) {
+            variants.push({
+              prefix: rePrefix,
+              name: [name]
+            });
+          } else {
+            variants[idx].name.push(name);
+          }
+        });
+        const troubles = variants.filter(v => v.name.length > 1);
+        if (troubles.length) {
+          troubles.forEach(t => {
+            const re = new RegExp('^' + t.prefix);
+            const conflicting = slot.filter(c => re.test(c));
+            context.report({
+              node: node,
+              messageId: 'conflictingClassnames',
+              data: {
+                classnames: conflicting.join(', ')
+              }
+            });
+          });
+        }
+      });
+    }
+  };
+}
+};


Test patch:
diff --git a/tests/lib/rules/no-contradicting-classname.js b/tests/lib/rules/no-contradicting-classname.js
new file mode 100644
index 0000000..a626321
--- /dev/null
+++ b/tests/lib/rules/no-contradicting-classname.js
@@ -0,0 +1,71 @@
+/**
+ * @fileoverview Avoid contradicting Tailwind CSS classnames (e.g. 'w-3 w-5')
+ * @author François Massart
+ */
+'use strict';
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+var rule = require('../../../lib/rules/no-contradicting-classname'),
+
+  RuleTester = require('eslint').RuleTester;
+
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+var parserOptions = {
+  ecmaVersion: 2019,
+  sourceType: 'module',
+  ecmaFeatures: {
+    jsx: true,
+  },
+};
+
+var ruleTester = new RuleTester({ parserOptions });
+
+ruleTester.run('no-contradicting-classname', rule, {
+
+  valid: [
+    {
+      code: '<div class="overflow-auto overflow-x-hidden lg:overflow-x-auto lg:dark:overflow-x-visible lg:dark:active:overflow-x-visible active:overflow-auto"></div>',
+    },
+    {
+      code: '<div class="p-1 px-2 sm:px-3 sm:pt-0">Accepts shorthands</div>',
+    },
+  ],
+
+  invalid: [
+    {
+      code: '<div class="container w-1 w-2"></div>',
+      errors: [
+        {
+          messageId: 'conflictingClassnames',
+          data: {
+            classnames: 'w-1, w-2'
+          }
+        }
+      ]
+    },
+    {
+      code: '<div class="flex-1 order-first order-11 sm:order-last flex-none "></div>',
+      errors: [
+        {
+          messageId: 'conflictingClassnames',
+          data: {
+            classnames: 'flex-1, flex-none'
+          }
+        },
+        {
+          messageId: 'conflictingClassnames',
+          data: {
+            classnames: 'order-first, order-11'
+          }
+        },
+      ]
+    },
+  ]
+});
diff --git a/tests/lib/rules/no-custom-classname.js b/tests/lib/rules/no-custom-classname.js
index 13ec520..38e4f45 100644
--- a/tests/lib/rules/no-custom-classname.js
+++ b/tests/lib/rules/no-custom-classname.js
@@ -42,7 +42,7 @@ ruleTester.run('no-custom-classname', rule, {
         data: {
           classname: 'my-custom'
         }
-    }]
+      }]
     },
     {
       code: `<div class="hello world">2 classnames are not defined in Tailwind CSS!</div>`,