From f7d9fd0ed057e5ea5dea74cfeb88e128238c3ffa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tao=20Bojl=C3=A9n?=
<66130243+taobojlen@users.noreply.github.com>
Date: Mon, 27 Sep 2021 19:01:00 +0100
Subject: [PATCH 1/7] Add vue/no-restricted-class rule
---
docs/rules/no-restricted-class.md | 104 +++++++++
lib/rules/no-restricted-class.js | 221 ++++++++++++++++++
.../no-restricted-class/forbidden.json | 1 +
tests/lib/rules/no-restricted-class.js | 102 ++++++++
4 files changed, 428 insertions(+)
create mode 100644 docs/rules/no-restricted-class.md
create mode 100644 lib/rules/no-restricted-class.js
create mode 100644 tests/fixtures/no-restricted-class/forbidden.json
create mode 100644 tests/lib/rules/no-restricted-class.js
diff --git a/docs/rules/no-restricted-class.md b/docs/rules/no-restricted-class.md
new file mode 100644
index 000000000..d722a5618
--- /dev/null
+++ b/docs/rules/no-restricted-class.md
@@ -0,0 +1,104 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-restricted-class
+description: disallow specific classes
+since: v7.19.0
+---
+# vue/no-restricted-classes
+
+> disallow specific classes
+
+## :book: Rule Details
+
+This rule lets you specify a list of classes that you don't want to allow in your templates.
+
+## :wrench: Options
+
+The simplest way to specify a list of forbidden classes is to pass it directly
+in the rule configuration.
+
+```json
+{
+ "vue/no-restricted-props": ["error", { classes: ["forbidden"] }]
+}
+```
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+
+Alternatively, you can also specify a list of files that contain forbidden classes. Each file
+must contain a JSON-formatted array of strings.
+
+```json
+{
+ "vue/no-restricted-props": ["error",
+ {
+ files: [".eslint/forbidden-classes.json"]
+ },
+ ]
+}
+```
+
+`.eslint/forbidden-classes.json`:
+
+```
+[
+ "forbidden-class",
+ "another-forbidden-class"
+]
+```
+
+::: warning Note
+This rule will only detect classes that are used as strings in your templates. Passing classes via
+variables, like below, will not be detected by this rule.
+
+```vue
+
+
+
+
+
+```
+:::
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v7.19.0.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-class.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-class.js)
diff --git a/lib/rules/no-restricted-class.js b/lib/rules/no-restricted-class.js
new file mode 100644
index 000000000..426a236cd
--- /dev/null
+++ b/lib/rules/no-restricted-class.js
@@ -0,0 +1,221 @@
+/**
+ * @fileoverview Forbid certain classes from being used
+ * @author Tao Bojlen
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+const fs = require('fs')
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+/**
+ * Report a forbidden class
+ * @param {Set} classes
+ * @param {*} node
+ * @param {RuleContext} context
+ * @param {Set} forbiddenClasses
+ */
+const reportForbiddenClasses = (classes, node, context, forbiddenClasses) => {
+ classes.forEach((className) => {
+ if (forbiddenClasses.has(className)) {
+ context.report({
+ node,
+ loc: node.value.loc,
+ messageId: 'forbiddenClass',
+ data: {
+ class: className
+ }
+ })
+ }
+ })
+}
+
+/**
+ * Recursively flatten a binary Expression into a string
+ * @param {BinaryExpression | Expression} left
+ * @param {BinaryExpression | Expression} right
+ * @returns {string}
+ */
+const flattenBinaryExpression = (left, right) => {
+ const result = []
+ if (left) {
+ if (left.type === 'Literal') {
+ result.push(left.value)
+ } else if (
+ left.type === 'BinaryExpression' &&
+ left.left.type !== 'PrivateIdentifier'
+ ) {
+ result.push(flattenBinaryExpression(left.left, left.right))
+ }
+ }
+
+ if (right) {
+ if (right.type === 'Literal') {
+ result.push(right.value)
+ } else if (
+ right.type === 'BinaryExpression' &&
+ right.left.type !== 'PrivateIdentifier'
+ ) {
+ result.push(flattenBinaryExpression(right.left, right.right))
+ }
+ }
+
+ return result.join(' ')
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'disallow specific classes in Vue components',
+ url: 'https://eslint.vuejs.org/rules/no-restricted-class.html',
+ categories: undefined
+ },
+ fixable: null,
+ messages: {
+ forbiddenClass: "'{{class}}' class is not allowed."
+ },
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ classes: {
+ type: 'array',
+ items: {
+ type: 'string'
+ }
+ },
+ files: {
+ type: 'array',
+ items: { type: 'string' }
+ }
+ },
+ additionalProperties: false
+ }
+ ]
+ },
+
+ /** @param {RuleContext} context */
+ create(context) {
+ const config = context.options[0] || {}
+ const forbiddenClasses = new Set(config.classes || [])
+ const forbiddenClassFiles = config.files || []
+
+ forbiddenClassFiles.forEach((/** @type {string} */ filePath) => {
+ if (!fs.existsSync(filePath)) {
+ throw new Error(`File "${filePath}" does not exist.`)
+ }
+ const rawData = fs.readFileSync(filePath, 'utf8')
+ JSON.parse(rawData).forEach((/** @type {string} */ forbiddenClass) => {
+ forbiddenClasses.add(forbiddenClass)
+ })
+ })
+
+ return utils.defineTemplateBodyVisitor(context, {
+ //
+ /**
+ * @param {VAttribute & { value: VLiteral } } node
+ */
+ 'VAttribute[directive=false][key.name="class"]'(node) {
+ const classes = new Set(node.value.value.split(/\s+/))
+ reportForbiddenClasses(classes, node, context, forbiddenClasses)
+ },
+
+ //
+ /**
+ * @param {VAttribute & { value: { expression: ObjectExpression } } } node
+ */
+ 'VAttribute[directive=true][key.argument.name="class"][value.expression.type="ObjectExpression"]'(
+ node
+ ) {
+ const classes = node.value.expression.properties
+ .filter(
+ (property) =>
+ property.type === 'Property' &&
+ (property.key.type === 'Literal' ||
+ property.key.type === 'Identifier')
+ )
+ .reduce((acc, property) => {
+ // Ugly early return to make TS happy
+ if (
+ property.type !== 'Property' ||
+ (property.key.type !== 'Literal' &&
+ property.key.type !== 'Identifier')
+ )
+ return acc
+ let value = ''
+ if (property.key.type === 'Literal') {
+ value = (property.key.value || '').toString()
+ } else {
+ value = property.key.name
+ }
+ const values = value.split(/\s+/)
+ values.forEach((/** @type {string} */ className) => {
+ acc.add(className)
+ })
+ return acc
+ }, new Set())
+ reportForbiddenClasses(classes, node, context, forbiddenClasses)
+ },
+
+ //
+ /**
+ * @param {VAttribute & { value: { expression: TemplateLiteral } } } node
+ */
+ 'VAttribute[directive=true][key.argument.name="class"][value.expression.type="TemplateLiteral"]'(
+ node
+ ) {
+ const strings = node.value.expression.quasis.reduce(
+ (acc, templateElement) => {
+ const classNames = templateElement.value.raw.split(/\s+/)
+ classNames.forEach((className) => {
+ acc.add(className)
+ })
+ return acc
+ },
+ new Set()
+ )
+ reportForbiddenClasses(strings, node, context, forbiddenClasses)
+ },
+
+ //
+ /**
+ * @param {VAttribute & { value: { expression: Literal } } } node
+ */
+ 'VAttribute[directive=true][key.argument.name="class"][value.expression.type="Literal"]'(
+ node
+ ) {
+ if (!node.value.expression.value) return
+ const classNames = new Set(
+ node.value.expression.value.toString().split(/\s+/)
+ )
+ reportForbiddenClasses(classNames, node, context, forbiddenClasses)
+ },
+
+ //
+ /**
+ * @param {VAttribute & { value: { expression: BinaryExpression } } } node
+ */
+ 'VAttribute[directive=true][key.argument.name="class"][value.expression.type="BinaryExpression"]'(
+ node
+ ) {
+ if (node.value.expression.left.type === 'PrivateIdentifier') return
+ const classNames = new Set(
+ flattenBinaryExpression(
+ node.value.expression.left,
+ node.value.expression.right
+ ).split(/\s+/)
+ )
+ reportForbiddenClasses(classNames, node, context, forbiddenClasses)
+ }
+ })
+ }
+}
diff --git a/tests/fixtures/no-restricted-class/forbidden.json b/tests/fixtures/no-restricted-class/forbidden.json
new file mode 100644
index 000000000..b2c833993
--- /dev/null
+++ b/tests/fixtures/no-restricted-class/forbidden.json
@@ -0,0 +1 @@
+["forbidden"]
diff --git a/tests/lib/rules/no-restricted-class.js b/tests/lib/rules/no-restricted-class.js
new file mode 100644
index 000000000..adf46874b
--- /dev/null
+++ b/tests/lib/rules/no-restricted-class.js
@@ -0,0 +1,102 @@
+/**
+ * @author Tao Bojlen
+ */
+
+'use strict'
+
+const rule = require('../../../lib/rules/no-restricted-class')
+const RuleTester = require('eslint').RuleTester
+
+const ruleTester = new RuleTester({
+ parser: require.resolve('vue-eslint-parser'),
+ parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
+})
+
+ruleTester.run('no-restricted-class', rule, {
+ valid: [
+ { code: `Content
` },
+ {
+ code: `Content
`,
+ options: [{ classes: ['forbidden'] }]
+ },
+ {
+ code: `Content
`,
+ options: [{ classes: ['forbidden'] }]
+ }
+ ],
+
+ invalid: [
+ {
+ code: ``,
+ errors: [
+ {
+ message: "'forbidden' class is not allowed.",
+ type: 'VAttribute'
+ }
+ ],
+ options: [{ classes: ['forbidden'] }]
+ },
+ {
+ code: ``,
+ errors: [
+ {
+ message: "'forbidden' class is not allowed.",
+ type: 'VAttribute'
+ }
+ ],
+ options: [{ classes: ['forbidden'] }]
+ },
+ {
+ code: ``,
+ errors: [
+ {
+ message: "'forbidden' class is not allowed.",
+ type: 'VAttribute'
+ }
+ ],
+ options: [{ classes: ['forbidden'] }]
+ },
+ {
+ code: ``,
+ errors: [
+ {
+ message: "'forbidden' class is not allowed.",
+ type: 'VAttribute'
+ }
+ ],
+ options: [{ classes: ['forbidden'] }]
+ },
+ {
+ code: '',
+ errors: [
+ {
+ message: "'forbidden' class is not allowed.",
+ type: 'VAttribute'
+ }
+ ],
+ options: [{ classes: ['forbidden'] }]
+ },
+ {
+ code: ``,
+ errors: [
+ {
+ message: "'forbidden' class is not allowed.",
+ type: 'VAttribute'
+ }
+ ],
+ options: [{ classes: ['forbidden'] }]
+ },
+ {
+ code: ``,
+ errors: [
+ {
+ message: "'forbidden' class is not allowed.",
+ type: 'VAttribute'
+ }
+ ],
+ options: [
+ { files: ['tests/fixtures/no-restricted-class/forbidden.json'] }
+ ]
+ }
+ ]
+})
From c1b550291e3edfac820fd2e3947061fdc9a7f82f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tao=20Bojl=C3=A9n?=
<66130243+taobojlen@users.noreply.github.com>
Date: Tue, 28 Sep 2021 14:18:51 +0100
Subject: [PATCH 2/7] don't match '@class'
---
lib/rules/no-restricted-class.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/rules/no-restricted-class.js b/lib/rules/no-restricted-class.js
index 426a236cd..c87ebe326 100644
--- a/lib/rules/no-restricted-class.js
+++ b/lib/rules/no-restricted-class.js
@@ -133,7 +133,7 @@ module.exports = {
/**
* @param {VAttribute & { value: { expression: ObjectExpression } } } node
*/
- 'VAttribute[directive=true][key.argument.name="class"][value.expression.type="ObjectExpression"]'(
+ 'VAttribute[directive=true][key.name.name="bind"][key.argument.name="class"][value.expression.type="ObjectExpression"]'(
node
) {
const classes = node.value.expression.properties
From 7fe694d90fe1712ab248bafce51741bdaafb8abb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tao=20Bojl=C3=A9n?=
<66130243+taobojlen@users.noreply.github.com>
Date: Tue, 28 Sep 2021 14:19:07 +0100
Subject: [PATCH 3/7] accept options in an array
---
docs/rules/no-restricted-class.md | 28 +-------------
lib/rules/no-restricted-class.js | 37 +++----------------
.../no-restricted-class/forbidden.json | 1 -
tests/lib/rules/no-restricted-class.js | 28 ++++----------
4 files changed, 16 insertions(+), 78 deletions(-)
delete mode 100644 tests/fixtures/no-restricted-class/forbidden.json
diff --git a/docs/rules/no-restricted-class.md b/docs/rules/no-restricted-class.md
index d722a5618..b21f697ee 100644
--- a/docs/rules/no-restricted-class.md
+++ b/docs/rules/no-restricted-class.md
@@ -3,7 +3,6 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-restricted-class
description: disallow specific classes
-since: v7.19.0
---
# vue/no-restricted-classes
@@ -20,11 +19,11 @@ in the rule configuration.
```json
{
- "vue/no-restricted-props": ["error", { classes: ["forbidden"] }]
+ "vue/no-restricted-props": ["error", "forbidden", "forbidden-two", "forbidden-three"]
}
```
-
+
```vue
@@ -50,29 +49,6 @@ export default {
-
-Alternatively, you can also specify a list of files that contain forbidden classes. Each file
-must contain a JSON-formatted array of strings.
-
-```json
-{
- "vue/no-restricted-props": ["error",
- {
- files: [".eslint/forbidden-classes.json"]
- },
- ]
-}
-```
-
-`.eslint/forbidden-classes.json`:
-
-```
-[
- "forbidden-class",
- "another-forbidden-class"
-]
-```
-
::: warning Note
This rule will only detect classes that are used as strings in your templates. Passing classes via
variables, like below, will not be detected by this rule.
diff --git a/lib/rules/no-restricted-class.js b/lib/rules/no-restricted-class.js
index c87ebe326..7ac0d37f2 100644
--- a/lib/rules/no-restricted-class.js
+++ b/lib/rules/no-restricted-class.js
@@ -7,7 +7,6 @@
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
-const fs = require('fs')
const utils = require('../utils')
// ------------------------------------------------------------------------------
@@ -83,41 +82,17 @@ module.exports = {
messages: {
forbiddenClass: "'{{class}}' class is not allowed."
},
- schema: [
- {
- type: 'object',
- properties: {
- classes: {
- type: 'array',
- items: {
- type: 'string'
- }
- },
- files: {
- type: 'array',
- items: { type: 'string' }
- }
- },
- additionalProperties: false
+ schema: {
+ type: 'array',
+ items: {
+ type: 'string'
}
- ]
+ }
},
/** @param {RuleContext} context */
create(context) {
- const config = context.options[0] || {}
- const forbiddenClasses = new Set(config.classes || [])
- const forbiddenClassFiles = config.files || []
-
- forbiddenClassFiles.forEach((/** @type {string} */ filePath) => {
- if (!fs.existsSync(filePath)) {
- throw new Error(`File "${filePath}" does not exist.`)
- }
- const rawData = fs.readFileSync(filePath, 'utf8')
- JSON.parse(rawData).forEach((/** @type {string} */ forbiddenClass) => {
- forbiddenClasses.add(forbiddenClass)
- })
- })
+ const forbiddenClasses = new Set(context.options || [])
return utils.defineTemplateBodyVisitor(context, {
//
diff --git a/tests/fixtures/no-restricted-class/forbidden.json b/tests/fixtures/no-restricted-class/forbidden.json
deleted file mode 100644
index b2c833993..000000000
--- a/tests/fixtures/no-restricted-class/forbidden.json
+++ /dev/null
@@ -1 +0,0 @@
-["forbidden"]
diff --git a/tests/lib/rules/no-restricted-class.js b/tests/lib/rules/no-restricted-class.js
index adf46874b..80e227a0f 100644
--- a/tests/lib/rules/no-restricted-class.js
+++ b/tests/lib/rules/no-restricted-class.js
@@ -17,11 +17,11 @@ ruleTester.run('no-restricted-class', rule, {
{ code: `Content
` },
{
code: `Content
`,
- options: [{ classes: ['forbidden'] }]
+ options: ['forbidden']
},
{
code: `Content
`,
- options: [{ classes: ['forbidden'] }]
+ options: ['forbidden']
}
],
@@ -34,7 +34,7 @@ ruleTester.run('no-restricted-class', rule, {
type: 'VAttribute'
}
],
- options: [{ classes: ['forbidden'] }]
+ options: ['forbidden']
},
{
code: ``,
@@ -44,7 +44,7 @@ ruleTester.run('no-restricted-class', rule, {
type: 'VAttribute'
}
],
- options: [{ classes: ['forbidden'] }]
+ options: ['forbidden']
},
{
code: ``,
@@ -54,7 +54,7 @@ ruleTester.run('no-restricted-class', rule, {
type: 'VAttribute'
}
],
- options: [{ classes: ['forbidden'] }]
+ options: ['forbidden']
},
{
code: ``,
@@ -64,7 +64,7 @@ ruleTester.run('no-restricted-class', rule, {
type: 'VAttribute'
}
],
- options: [{ classes: ['forbidden'] }]
+ options: ['forbidden']
},
{
code: '',
@@ -74,7 +74,7 @@ ruleTester.run('no-restricted-class', rule, {
type: 'VAttribute'
}
],
- options: [{ classes: ['forbidden'] }]
+ options: ['forbidden']
},
{
code: ``,
@@ -84,19 +84,7 @@ ruleTester.run('no-restricted-class', rule, {
type: 'VAttribute'
}
],
- options: [{ classes: ['forbidden'] }]
- },
- {
- code: ``,
- errors: [
- {
- message: "'forbidden' class is not allowed.",
- type: 'VAttribute'
- }
- ],
- options: [
- { files: ['tests/fixtures/no-restricted-class/forbidden.json'] }
- ]
+ options: ['forbidden']
}
]
})
From 4a1ba340dce4591f927b4e1f9a89298cff25e8c0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tao=20Bojl=C3=A9n?=
<66130243+taobojlen@users.noreply.github.com>
Date: Tue, 28 Sep 2021 14:29:50 +0100
Subject: [PATCH 4/7] handle array syntax
---
docs/rules/no-restricted-class.md | 1 +
lib/rules/no-restricted-class.js | 17 +++++++++++++++++
tests/lib/rules/no-restricted-class.js | 20 ++++++++++++++++++++
3 files changed, 38 insertions(+)
diff --git a/docs/rules/no-restricted-class.md b/docs/rules/no-restricted-class.md
index b21f697ee..027d86abf 100644
--- a/docs/rules/no-restricted-class.md
+++ b/docs/rules/no-restricted-class.md
@@ -33,6 +33,7 @@ in the rule configuration.
+
diff --git a/lib/rules/no-restricted-class.js b/lib/rules/no-restricted-class.js
index 7ac0d37f2..eb72e250c 100644
--- a/lib/rules/no-restricted-class.js
+++ b/lib/rules/no-restricted-class.js
@@ -190,6 +190,23 @@ module.exports = {
).split(/\s+/)
)
reportForbiddenClasses(classNames, node, context, forbiddenClasses)
+ },
+
+ //
+ /**
+ * @param {VAttribute & { value: { expression: ArrayExpression} } } node
+ */
+ 'VAttribute[directive=true][key.argument.name="class"][value.expression.type="ArrayExpression"]'(
+ node
+ ) {
+ const classNames = new Set(
+ node.value.expression.elements.flatMap((item) => {
+ // It's not possible to use a filter here because Typescript won't understand the types.
+ if (!item || item.type !== 'Literal') return []
+ return (item.value || '').toString().split(/\s+/)
+ })
+ )
+ reportForbiddenClasses(classNames, node, context, forbiddenClasses)
}
})
}
diff --git a/tests/lib/rules/no-restricted-class.js b/tests/lib/rules/no-restricted-class.js
index 80e227a0f..23e28049a 100644
--- a/tests/lib/rules/no-restricted-class.js
+++ b/tests/lib/rules/no-restricted-class.js
@@ -85,6 +85,26 @@ ruleTester.run('no-restricted-class', rule, {
}
],
options: ['forbidden']
+ },
+ {
+ code: ``,
+ errors: [
+ {
+ message: "'forbidden' class is not allowed.",
+ type: 'VAttribute'
+ }
+ ],
+ options: ['forbidden']
+ },
+ {
+ code: ``,
+ errors: [
+ {
+ message: "'forbidden' class is not allowed.",
+ type: 'VAttribute'
+ }
+ ],
+ options: ['forbidden']
}
]
})
From 330c3141486f979015a96b0d85156788c552b856 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tao=20Bojl=C3=A9n?=
<66130243+taobojlen@users.noreply.github.com>
Date: Tue, 28 Sep 2021 15:17:38 +0100
Subject: [PATCH 5/7] refactor with @ota-meshi's suggestions
---
lib/rules/no-restricted-class.js | 223 +++++++++----------------
tests/lib/rules/no-restricted-class.js | 18 +-
2 files changed, 93 insertions(+), 148 deletions(-)
diff --git a/lib/rules/no-restricted-class.js b/lib/rules/no-restricted-class.js
index eb72e250c..a4fef0da5 100644
--- a/lib/rules/no-restricted-class.js
+++ b/lib/rules/no-restricted-class.js
@@ -14,57 +14,86 @@ const utils = require('../utils')
// ------------------------------------------------------------------------------
/**
* Report a forbidden class
- * @param {Set} classes
+ * @param {string} className
* @param {*} node
* @param {RuleContext} context
* @param {Set} forbiddenClasses
*/
-const reportForbiddenClasses = (classes, node, context, forbiddenClasses) => {
- classes.forEach((className) => {
- if (forbiddenClasses.has(className)) {
- context.report({
- node,
- loc: node.value.loc,
- messageId: 'forbiddenClass',
- data: {
- class: className
- }
- })
- }
- })
+const reportForbiddenClass = (className, node, context, forbiddenClasses) => {
+ if (forbiddenClasses.has(className)) {
+ const loc = node.value ? node.value.loc : node.loc
+ context.report({
+ node,
+ loc,
+ messageId: 'forbiddenClass',
+ data: {
+ class: className
+ }
+ })
+ }
}
/**
- * Recursively flatten a binary Expression into a string
- * @param {BinaryExpression | Expression} left
- * @param {BinaryExpression | Expression} right
- * @returns {string}
+ * @param {Expression} node
+ * @param {boolean} [textOnly]
+ * @returns {IterableIterator<{ className:string, reportNode: ESNode }>}
*/
-const flattenBinaryExpression = (left, right) => {
- const result = []
- if (left) {
- if (left.type === 'Literal') {
- result.push(left.value)
- } else if (
- left.type === 'BinaryExpression' &&
- left.left.type !== 'PrivateIdentifier'
- ) {
- result.push(flattenBinaryExpression(left.left, left.right))
+function* extractClassNames(node, textOnly) {
+ if (node.type === 'Literal') {
+ yield* `${node.value}`
+ .split(/\s+/)
+ .map((className) => ({ className, reportNode: node }))
+ return
+ }
+ if (node.type === 'TemplateLiteral') {
+ for (const templateElement of node.quasis) {
+ yield* templateElement.value.cooked
+ .split(/\s+/)
+ .map((className) => ({ className, reportNode: templateElement }))
}
+ for (const expr of node.expressions) {
+ yield* extractClassNames(expr, true)
+ }
+ return
}
-
- if (right) {
- if (right.type === 'Literal') {
- result.push(right.value)
- } else if (
- right.type === 'BinaryExpression' &&
- right.left.type !== 'PrivateIdentifier'
- ) {
- result.push(flattenBinaryExpression(right.left, right.right))
+ if (node.type === 'BinaryExpression') {
+ if (node.operator !== '+') {
+ return
}
+ yield* extractClassNames(node.left, true)
+ yield* extractClassNames(node.right, true)
+ return
+ }
+ if (node.type === 'ObjectExpression') {
+ for (const prop of node.properties) {
+ if (prop.type !== 'Property') {
+ continue
+ }
+ const classNames = utils.getStaticPropertyName(prop)
+ if (!classNames) {
+ continue
+ }
+ yield* classNames
+ .split(/\s+/)
+ .map((className) => ({ className, reportNode: prop.key }))
+ }
+ return
+ }
+ if (node.type === 'ArrayExpression') {
+ for (const element of node.elements) {
+ if (element == null) {
+ continue
+ }
+ if (element.type === 'SpreadElement') {
+ continue
+ }
+ yield* extractClassNames(element)
+ }
+ return
+ }
+ if (!textOnly) {
+ return
}
-
- return result.join(' ')
}
// ------------------------------------------------------------------------------
@@ -95,118 +124,30 @@ module.exports = {
const forbiddenClasses = new Set(context.options || [])
return utils.defineTemplateBodyVisitor(context, {
- //
/**
* @param {VAttribute & { value: VLiteral } } node
*/
'VAttribute[directive=false][key.name="class"]'(node) {
- const classes = new Set(node.value.value.split(/\s+/))
- reportForbiddenClasses(classes, node, context, forbiddenClasses)
- },
-
- //
- /**
- * @param {VAttribute & { value: { expression: ObjectExpression } } } node
- */
- 'VAttribute[directive=true][key.name.name="bind"][key.argument.name="class"][value.expression.type="ObjectExpression"]'(
- node
- ) {
- const classes = node.value.expression.properties
- .filter(
- (property) =>
- property.type === 'Property' &&
- (property.key.type === 'Literal' ||
- property.key.type === 'Identifier')
+ node.value.value
+ .split(/\s+/)
+ .forEach((className) =>
+ reportForbiddenClass(className, node, context, forbiddenClasses)
)
- .reduce((acc, property) => {
- // Ugly early return to make TS happy
- if (
- property.type !== 'Property' ||
- (property.key.type !== 'Literal' &&
- property.key.type !== 'Identifier')
- )
- return acc
- let value = ''
- if (property.key.type === 'Literal') {
- value = (property.key.value || '').toString()
- } else {
- value = property.key.name
- }
- const values = value.split(/\s+/)
- values.forEach((/** @type {string} */ className) => {
- acc.add(className)
- })
- return acc
- }, new Set())
- reportForbiddenClasses(classes, node, context, forbiddenClasses)
- },
-
- //
- /**
- * @param {VAttribute & { value: { expression: TemplateLiteral } } } node
- */
- 'VAttribute[directive=true][key.argument.name="class"][value.expression.type="TemplateLiteral"]'(
- node
- ) {
- const strings = node.value.expression.quasis.reduce(
- (acc, templateElement) => {
- const classNames = templateElement.value.raw.split(/\s+/)
- classNames.forEach((className) => {
- acc.add(className)
- })
- return acc
- },
- new Set()
- )
- reportForbiddenClasses(strings, node, context, forbiddenClasses)
- },
-
- //
- /**
- * @param {VAttribute & { value: { expression: Literal } } } node
- */
- 'VAttribute[directive=true][key.argument.name="class"][value.expression.type="Literal"]'(
- node
- ) {
- if (!node.value.expression.value) return
- const classNames = new Set(
- node.value.expression.value.toString().split(/\s+/)
- )
- reportForbiddenClasses(classNames, node, context, forbiddenClasses)
},
- //
- /**
- * @param {VAttribute & { value: { expression: BinaryExpression } } } node
- */
- 'VAttribute[directive=true][key.argument.name="class"][value.expression.type="BinaryExpression"]'(
+ /** @param {VExpressionContainer} node */
+ "VAttribute[directive=true][key.name.name='bind'][key.argument.name='class'] > VExpressionContainer.value"(
node
) {
- if (node.value.expression.left.type === 'PrivateIdentifier') return
- const classNames = new Set(
- flattenBinaryExpression(
- node.value.expression.left,
- node.value.expression.right
- ).split(/\s+/)
- )
- reportForbiddenClasses(classNames, node, context, forbiddenClasses)
- },
+ if (!node.expression) {
+ return
+ }
- //
- /**
- * @param {VAttribute & { value: { expression: ArrayExpression} } } node
- */
- 'VAttribute[directive=true][key.argument.name="class"][value.expression.type="ArrayExpression"]'(
- node
- ) {
- const classNames = new Set(
- node.value.expression.elements.flatMap((item) => {
- // It's not possible to use a filter here because Typescript won't understand the types.
- if (!item || item.type !== 'Literal') return []
- return (item.value || '').toString().split(/\s+/)
- })
- )
- reportForbiddenClasses(classNames, node, context, forbiddenClasses)
+ for (const { className, reportNode } of extractClassNames(
+ /** @type {Expression} */ (node.expression)
+ )) {
+ reportForbiddenClass(className, reportNode, context, forbiddenClasses)
+ }
}
})
}
diff --git a/tests/lib/rules/no-restricted-class.js b/tests/lib/rules/no-restricted-class.js
index 23e28049a..a69081c43 100644
--- a/tests/lib/rules/no-restricted-class.js
+++ b/tests/lib/rules/no-restricted-class.js
@@ -22,6 +22,10 @@ ruleTester.run('no-restricted-class', rule, {
{
code: `Content
`,
options: ['forbidden']
+ },
+ {
+ code: `Content
`,
+ options: ['forbidden']
}
],
@@ -41,7 +45,7 @@ ruleTester.run('no-restricted-class', rule, {
errors: [
{
message: "'forbidden' class is not allowed.",
- type: 'VAttribute'
+ type: 'Literal'
}
],
options: ['forbidden']
@@ -51,7 +55,7 @@ ruleTester.run('no-restricted-class', rule, {
errors: [
{
message: "'forbidden' class is not allowed.",
- type: 'VAttribute'
+ type: 'Literal'
}
],
options: ['forbidden']
@@ -61,7 +65,7 @@ ruleTester.run('no-restricted-class', rule, {
errors: [
{
message: "'forbidden' class is not allowed.",
- type: 'VAttribute'
+ type: 'Identifier'
}
],
options: ['forbidden']
@@ -71,7 +75,7 @@ ruleTester.run('no-restricted-class', rule, {
errors: [
{
message: "'forbidden' class is not allowed.",
- type: 'VAttribute'
+ type: 'TemplateElement'
}
],
options: ['forbidden']
@@ -81,7 +85,7 @@ ruleTester.run('no-restricted-class', rule, {
errors: [
{
message: "'forbidden' class is not allowed.",
- type: 'VAttribute'
+ type: 'Literal'
}
],
options: ['forbidden']
@@ -91,7 +95,7 @@ ruleTester.run('no-restricted-class', rule, {
errors: [
{
message: "'forbidden' class is not allowed.",
- type: 'VAttribute'
+ type: 'Literal'
}
],
options: ['forbidden']
@@ -101,7 +105,7 @@ ruleTester.run('no-restricted-class', rule, {
errors: [
{
message: "'forbidden' class is not allowed.",
- type: 'VAttribute'
+ type: 'Literal'
}
],
options: ['forbidden']
From b5fb51014f2c97e78a125d1850519bebd8a4e7fa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tao=20Bojl=C3=A9n?=
<66130243+taobojlen@users.noreply.github.com>
Date: Wed, 29 Sep 2021 13:10:42 +0100
Subject: [PATCH 6/7] handle objects converted to strings
---
lib/rules/no-restricted-class.js | 6 +++---
tests/lib/rules/no-restricted-class.js | 4 ++++
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/lib/rules/no-restricted-class.js b/lib/rules/no-restricted-class.js
index a4fef0da5..b1dcccb07 100644
--- a/lib/rules/no-restricted-class.js
+++ b/lib/rules/no-restricted-class.js
@@ -64,6 +64,9 @@ function* extractClassNames(node, textOnly) {
yield* extractClassNames(node.right, true)
return
}
+ if (textOnly) {
+ return
+ }
if (node.type === 'ObjectExpression') {
for (const prop of node.properties) {
if (prop.type !== 'Property') {
@@ -91,9 +94,6 @@ function* extractClassNames(node, textOnly) {
}
return
}
- if (!textOnly) {
- return
- }
}
// ------------------------------------------------------------------------------
diff --git a/tests/lib/rules/no-restricted-class.js b/tests/lib/rules/no-restricted-class.js
index a69081c43..baf39778f 100644
--- a/tests/lib/rules/no-restricted-class.js
+++ b/tests/lib/rules/no-restricted-class.js
@@ -26,6 +26,10 @@ ruleTester.run('no-restricted-class', rule, {
{
code: `Content
`,
options: ['forbidden']
+ },
+ {
+ code: `Content
`,
+ options: ['forbidden']
}
],
From 3235306563d17dc9cc40e5f3161047eacc21dd91 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tao=20Bojl=C3=A9n?=
<66130243+taobojlen@users.noreply.github.com>
Date: Wed, 29 Sep 2021 13:11:03 +0100
Subject: [PATCH 7/7] run update script
---
docs/rules/README.md | 1 +
docs/rules/no-restricted-class.md | 12 +++++-------
lib/index.js | 1 +
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 85f6e661b..2f89e3a53 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -310,6 +310,7 @@ For example:
| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | |
| [vue/no-restricted-block](./no-restricted-block.md) | disallow specific block | |
| [vue/no-restricted-call-after-await](./no-restricted-call-after-await.md) | disallow asynchronously called restricted methods | |
+| [vue/no-restricted-class](./no-restricted-class.md) | disallow specific classes in Vue components | |
| [vue/no-restricted-component-options](./no-restricted-component-options.md) | disallow specific component option | |
| [vue/no-restricted-custom-event](./no-restricted-custom-event.md) | disallow specific custom event | |
| [vue/no-restricted-props](./no-restricted-props.md) | disallow specific props | |
diff --git a/docs/rules/no-restricted-class.md b/docs/rules/no-restricted-class.md
index 027d86abf..c7aebce6b 100644
--- a/docs/rules/no-restricted-class.md
+++ b/docs/rules/no-restricted-class.md
@@ -2,11 +2,13 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/no-restricted-class
-description: disallow specific classes
+description: disallow specific classes in Vue components
---
-# vue/no-restricted-classes
+# vue/no-restricted-class
-> disallow specific classes
+> disallow specific classes in Vue components
+
+- :exclamation: ***This rule has not been released yet.***
## :book: Rule Details
@@ -71,10 +73,6 @@ export default {
```
:::
-## :rocket: Version
-
-This rule was introduced in eslint-plugin-vue v7.19.0.
-
## :mag: Implementation
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-class.js)
diff --git a/lib/index.js b/lib/index.js
index 0015fb579..ef111732d 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -101,6 +101,7 @@ module.exports = {
'no-reserved-keys': require('./rules/no-reserved-keys'),
'no-restricted-block': require('./rules/no-restricted-block'),
'no-restricted-call-after-await': require('./rules/no-restricted-call-after-await'),
+ 'no-restricted-class': require('./rules/no-restricted-class'),
'no-restricted-component-options': require('./rules/no-restricted-component-options'),
'no-restricted-custom-event': require('./rules/no-restricted-custom-event'),
'no-restricted-props': require('./rules/no-restricted-props'),