Repository: vuejs/eslint-plugin-vue (js)

Issue:
Allow regex in `vue/no-restricted-class`
<!--
  Before proposing rule changes, please make sure it hasn't been posted already.
  You can see all open propositions here:
  https://github.com/vuejs/eslint-plugin-vue/issues?q=is%3Aopen+is%3Aissue+label%3A%22new+rule+proposition%22
-->

**What rule do you want to change?**
[vue/no-restricted-class](https://eslint.vuejs.org/rules/no-restricted-class.html)

**Does this change cause the rule to produce more or fewer warnings?**
No

**How will the change be implemented? (New option, new default behavior, etc.)?**
Allow a regex pattern just like we did [here](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-custom-event.js#L30)

**Please provide some example code that this change will affect:**

<!-- Put your code examples here -->
```vue
<div class="ml-2" />
```

**What does the rule currently do for this code?**
Right now to restrict the classes which are not RTL friendly I have to write long list of them:
```js
'vue/no-restricted-class': ['error', 'ml-1', 'ml-2', /* And more... */],
```

**What will the rule do after it's changed?**
Once this rule accept regex pattern we don't have to write long list, just below is enough:
```js
'vue/no-restricted-class': ['error', '/^(m|p)(l|r)-/'],
```

**Additional context**
None


Pull request description:
Allow restricting classes by regexp in `vue/no-restricted-class`
Closes #1877

This PR adds support for RegEx to the `no-restricted-class` rule and an update to the example in the docs.

I didn't add all the `buildMatcher` logic like in the example linked in the Issue (`no-restricted-custom-event`: https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-custom-event.js) because this rule seemed a little more straightforward.

Please let me know if you need anything else. Thanks!

Patch:
diff --git a/docs/rules/no-restricted-class.md b/docs/rules/no-restricted-class.md
index 849c71e8..b6fa69b3 100644
--- a/docs/rules/no-restricted-class.md
+++ b/docs/rules/no-restricted-class.md
@@ -20,7 +20,7 @@ in the rule configuration.
 
 ```json
 {
-  "vue/no-restricted-class": ["error", "forbidden", "forbidden-two", "forbidden-three"]
+  "vue/no-restricted-class": ["error", "forbidden", "forbidden-two", "forbidden-three", "/^for(bidden|gotten)/"]
 }
 ```
 
diff --git a/lib/rules/no-restricted-class.js b/lib/rules/no-restricted-class.js
index 4d6c3c42..f25ad649 100644
--- a/lib/rules/no-restricted-class.js
+++ b/lib/rules/no-restricted-class.js
@@ -5,6 +5,7 @@
 'use strict'
 
 const utils = require('../utils')
+const regexp = require('../utils/regexp')
 
 /**
  * Report a forbidden class
@@ -12,9 +13,19 @@ const utils = require('../utils')
  * @param {*} node
  * @param {RuleContext} context
  * @param {Set<string>} forbiddenClasses
+ * @param {Array<RegExp>} forbiddenClassesRegexps
  */
-const reportForbiddenClass = (className, node, context, forbiddenClasses) => {
-  if (forbiddenClasses.has(className)) {
+const reportForbiddenClass = (
+  className,
+  node,
+  context,
+  forbiddenClasses,
+  forbiddenClassesRegexps
+) => {
+  if (
+    forbiddenClasses.has(className) ||
+    forbiddenClassesRegexps.some((re) => re.test(className))
+  ) {
     const loc = node.value ? node.value.loc : node.loc
     context.report({
       node,
@@ -113,6 +124,9 @@ module.exports = {
   /** @param {RuleContext} context */
   create(context) {
     const forbiddenClasses = new Set(context.options || [])
+    const forbiddenClassesRegexps = (context.options || [])
+      .filter((cl) => regexp.isRegExp(cl))
+      .map((cl) => regexp.toRegExp(cl))
 
     return utils.defineTemplateBodyVisitor(context, {
       /**
@@ -120,7 +134,13 @@ module.exports = {
        */
       'VAttribute[directive=false][key.name="class"]'(node) {
         for (const className of node.value.value.split(/\s+/)) {
-          reportForbiddenClass(className, node, context, forbiddenClasses)
+          reportForbiddenClass(
+            className,
+            node,
+            context,
+            forbiddenClasses,
+            forbiddenClassesRegexps
+          )
         }
       },
 
@@ -135,7 +155,13 @@ module.exports = {
         for (const { className, reportNode } of extractClassNames(
           /** @type {Expression} */ (node.expression)
         )) {
-          reportForbiddenClass(className, reportNode, context, forbiddenClasses)
+          reportForbiddenClass(
+            className,
+            reportNode,
+            context,
+            forbiddenClasses,
+            forbiddenClassesRegexps
+          )
         }
       }
     })


Test patch:
diff --git a/tests/lib/rules/no-restricted-class.js b/tests/lib/rules/no-restricted-class.js
index baf39778..02815b2c 100644
--- a/tests/lib/rules/no-restricted-class.js
+++ b/tests/lib/rules/no-restricted-class.js
@@ -30,6 +30,10 @@ ruleTester.run('no-restricted-class', rule, {
     {
       code: `<template><div :class="'' + {forbidden: true}">Content</div></template>`,
       options: ['forbidden']
+    },
+    {
+      code: `<template><div class="allowed">Content</div></template>`,
+      options: ['/^for(bidden|gotten)/']
     }
   ],
 
@@ -113,6 +117,16 @@ ruleTester.run('no-restricted-class', rule, {
         }
       ],
       options: ['forbidden']
+    },
+    {
+      code: `<template><div class="forbidden allowed" /></template>`,
+      errors: [
+        {
+          message: "'forbidden' class is not allowed.",
+          type: 'VAttribute'
+        }
+      ],
+      options: ['/^for(bidden|gotten)/']
     }
   ]
 })