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

Issue:
[BUG] Prefix from tailwind.config.js not working with named groups
**Describe the bug**
A colleague of mine discovered this issue. We use the ESlint plugin at work and have a shared component library using a tailwind with a prefix to avoid collisions with other tailwind projects. While developing a new feature, we wanted to use the recently introduced syntax for named groups, but ESlint kept nagging about a non-existent class.
I looked into the code for `no-custom-classname`, and the regex responsible doesn't include the prefix from the tailwind config. I believe this is the issue.

**To Reproduce**
```
// HTML
<div class="tw-group/edit">
  <div class="group-hover/edit:tw-text-red-500">Some content</div>
</div>


// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  prefix: "tw-"
}
``` 

The plugin will report a false positive unknown class name `tw-group/edit` and `group-hover/edit:tw-text-red-500`

**Expected behavior**
Support the specified prefix specified inside the `tailwind.config.js` together with the named groups and peer syntax.

Pull request description:
Fix support for prefixes in named group/peer syntax
# Fix support for prefixes in named group/peer syntax

## Description

Fixes #243 

## Type of change

Please delete options that are not relevant.

- [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?

Tests added inside `no-custom-classname`

## 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] 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/no-custom-classname.js b/lib/rules/no-custom-classname.js
index e38cfa4..aceb171 100644
--- a/lib/rules/no-custom-classname.js
+++ b/lib/rules/no-custom-classname.js
@@ -14,6 +14,7 @@ const parserUtil = require('../util/parser');
 const getClassnamesFromCSS = require('../util/cssFiles');
 const createContextFallback = require('tailwindcss/lib/lib/setupContextUtils').createContext;
 const generated = require('../util/generated');
+const escapeRegex = require('../util/regex').escapeRegex;
 
 //------------------------------------------------------------------------------
 // Rule Definition
@@ -26,7 +27,8 @@ const CUSTOM_CLASSNAME_DETECTED_MSG = `Classname '{{classname}}' is not a Tailwi
 // Group/peer names can be arbitrarily named and are not
 // generated by generateRules. Using a custom regexp to
 // validate these avoids false reports.
-const GROUP_NAME_REGEXP = /^(group|peer)\/[\w\$\#\@\%\^\&\*\_\-]+$/i;
+const getGroupNameRegex = (prefix = '') =>
+  new RegExp(`^${escapeRegex(prefix)}(group|peer)\/[\\w\\$\\#\\@\\%\\^\\&\\*\\_\\-]+$`, 'i');
 
 const contextFallbackCache = new WeakMap();
 
@@ -110,6 +112,7 @@ module.exports = {
     // Init assets before sorting
     const groups = groupUtil.getGroups(defaultGroups, mergedConfig);
     const classnamesFromFiles = getClassnamesFromCSS(cssFiles, cssFilesRefreshRate);
+    const groupNameRegex = getGroupNameRegex(mergedConfig.prefix);
 
     /**
      * Parse the classnames and report found conflicts
@@ -134,7 +137,7 @@ module.exports = {
         if (fromFilesIdx >= 0) {
           return; // Lazier is faster... processing next className!
         }
-        if (GROUP_NAME_REGEXP.test(className)) {
+        if (groupNameRegex.test(className)) {
           return; // Lazier is faster... processing next className!
         }
 
diff --git a/lib/util/regex.js b/lib/util/regex.js
new file mode 100644
index 0000000..823efd8
--- /dev/null
+++ b/lib/util/regex.js
@@ -0,0 +1,13 @@
+/**
+ * Escapes a string to be used in a regular expression
+ * Copied from https://stackoverflow.com/a/3561711.
+ * @param {string} input
+ * @returns {string}
+ */
+function escapeRegex(input) {
+  return input.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
+}
+
+module.exports = {
+  escapeRegex,
+};


Test patch:
diff --git a/tests/lib/rules/no-custom-classname.js b/tests/lib/rules/no-custom-classname.js
index f20988c..00bff45 100644
--- a/tests/lib/rules/no-custom-classname.js
+++ b/tests/lib/rules/no-custom-classname.js
@@ -877,7 +877,23 @@ ruleTester.run("no-custom-classname", rule, {
       ],
     },
     { code: `<div className="group/edit">Custom group name</div>` },
+    {
+      code: `<div className="tw-group/edit">Prefix and custom group name</div>`,
+      options: [
+        {
+          config: { prefix: "tw-" },
+        },
+      ],
+    },
     { code: `<div className="group-hover/edit:hidden">Custom group name variant</div>` },
+    {
+      code: `<div className="group-hover/edit:tw-hidden">Prefix and custom group name variant</div>`,
+      options: [
+        {
+          config: { prefix: "tw-" },
+        },
+      ],
+    },
     {
       code: `
       <div class="group/nav123$#@%^&*_-">
@@ -885,6 +901,18 @@ ruleTester.run("no-custom-classname", rule, {
       </div>
       `,
     },
+    {
+      code: `
+      <div class="tw-group/nav123$#@%^&*_-">
+        <div class="group-hover/nav123$#@%^&*_-:tw-h-0">group/nav123$#@%^&*_-</div>
+      </div>
+      `,
+      options: [
+        {
+          config: { prefix: "tw-" },
+        },
+      ],
+    },
     {
       code: `<div className="group-hover/edit:custom-class">Custom group name variant</div>`,
       options: [
@@ -894,7 +922,23 @@ ruleTester.run("no-custom-classname", rule, {
       ],
     },
     { code: `<div className="peer/draft">Custom peer name</div>` },
+    {
+      code: `<div className="tw-peer/draft">Prefix and custom peer name</div>`,
+      options: [
+        {
+          config: { prefix: "tw-" },
+        },
+      ],
+    },
     { code: `<div className="peer-checked/draft:hidden">Custom peer name variant</div>` },
+    {
+      code: `<div className="peer-checked/draft:tw-hidden">Prefix and custom peer name variant</div>`,
+      options: [
+        {
+          config: { prefix: "tw-" },
+        },
+      ],
+    },
     {
       code: `<div className="peer-checked/draft:custom-class">Custom peer name variant</div>`,
       options: [