From c2c709dfb9ef3f4d482d2cdd84b33d74585f9395 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tao=20Bojl=C3=A9n?=
<66130243+taobojlen@users.noreply.github.com>
Date: Thu, 30 Sep 2021 00:36:10 +0100
Subject: [PATCH 1/6] Add vue/no-restricted-class rule (#1639)
* Add vue/no-restricted-class rule
* don't match '@class'
* accept options in an array
* handle array syntax
* refactor with @ota-meshi's suggestions
* handle objects converted to strings
* run update script
---
docs/rules/README.md | 1 +
docs/rules/no-restricted-class.md | 79 +++++++++++++
lib/index.js | 1 +
lib/rules/no-restricted-class.js | 154 +++++++++++++++++++++++++
tests/lib/rules/no-restricted-class.js | 118 +++++++++++++++++++
5 files changed, 353 insertions(+)
create mode 100644 docs/rules/no-restricted-class.md
create mode 100644 lib/rules/no-restricted-class.js
create mode 100644 tests/lib/rules/no-restricted-class.js
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
new file mode 100644
index 000000000..c7aebce6b
--- /dev/null
+++ b/docs/rules/no-restricted-class.md
@@ -0,0 +1,79 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-restricted-class
+description: disallow specific classes in Vue components
+---
+# vue/no-restricted-class
+
+> disallow specific classes in Vue components
+
+- :exclamation: ***This rule has not been released yet.***
+
+## :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", "forbidden", "forbidden-two", "forbidden-three"]
+}
+```
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+::: 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
+
+
+
+
+
+```
+:::
+
+## :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/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'),
diff --git a/lib/rules/no-restricted-class.js b/lib/rules/no-restricted-class.js
new file mode 100644
index 000000000..b1dcccb07
--- /dev/null
+++ b/lib/rules/no-restricted-class.js
@@ -0,0 +1,154 @@
+/**
+ * @fileoverview Forbid certain classes from being used
+ * @author Tao Bojlen
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+/**
+ * Report a forbidden class
+ * @param {string} className
+ * @param {*} node
+ * @param {RuleContext} context
+ * @param {Set} forbiddenClasses
+ */
+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
+ }
+ })
+ }
+}
+
+/**
+ * @param {Expression} node
+ * @param {boolean} [textOnly]
+ * @returns {IterableIterator<{ className:string, reportNode: ESNode }>}
+ */
+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 (node.type === 'BinaryExpression') {
+ if (node.operator !== '+') {
+ return
+ }
+ yield* extractClassNames(node.left, true)
+ yield* extractClassNames(node.right, true)
+ return
+ }
+ if (textOnly) {
+ 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
+ }
+}
+
+// ------------------------------------------------------------------------------
+// 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: 'array',
+ items: {
+ type: 'string'
+ }
+ }
+ },
+
+ /** @param {RuleContext} context */
+ create(context) {
+ const forbiddenClasses = new Set(context.options || [])
+
+ return utils.defineTemplateBodyVisitor(context, {
+ /**
+ * @param {VAttribute & { value: VLiteral } } node
+ */
+ 'VAttribute[directive=false][key.name="class"]'(node) {
+ node.value.value
+ .split(/\s+/)
+ .forEach((className) =>
+ reportForbiddenClass(className, node, context, forbiddenClasses)
+ )
+ },
+
+ /** @param {VExpressionContainer} node */
+ "VAttribute[directive=true][key.name.name='bind'][key.argument.name='class'] > VExpressionContainer.value"(
+ node
+ ) {
+ if (!node.expression) {
+ return
+ }
+
+ 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
new file mode 100644
index 000000000..baf39778f
--- /dev/null
+++ b/tests/lib/rules/no-restricted-class.js
@@ -0,0 +1,118 @@
+/**
+ * @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: ['forbidden']
+ },
+ {
+ code: `Content
`,
+ options: ['forbidden']
+ },
+ {
+ code: `Content
`,
+ options: ['forbidden']
+ },
+ {
+ code: `Content
`,
+ options: ['forbidden']
+ }
+ ],
+
+ invalid: [
+ {
+ code: ``,
+ errors: [
+ {
+ message: "'forbidden' class is not allowed.",
+ type: 'VAttribute'
+ }
+ ],
+ options: ['forbidden']
+ },
+ {
+ code: ``,
+ errors: [
+ {
+ message: "'forbidden' class is not allowed.",
+ type: 'Literal'
+ }
+ ],
+ options: ['forbidden']
+ },
+ {
+ code: ``,
+ errors: [
+ {
+ message: "'forbidden' class is not allowed.",
+ type: 'Literal'
+ }
+ ],
+ options: ['forbidden']
+ },
+ {
+ code: ``,
+ errors: [
+ {
+ message: "'forbidden' class is not allowed.",
+ type: 'Identifier'
+ }
+ ],
+ options: ['forbidden']
+ },
+ {
+ code: '',
+ errors: [
+ {
+ message: "'forbidden' class is not allowed.",
+ type: 'TemplateElement'
+ }
+ ],
+ options: ['forbidden']
+ },
+ {
+ code: ``,
+ errors: [
+ {
+ message: "'forbidden' class is not allowed.",
+ type: 'Literal'
+ }
+ ],
+ options: ['forbidden']
+ },
+ {
+ code: ``,
+ errors: [
+ {
+ message: "'forbidden' class is not allowed.",
+ type: 'Literal'
+ }
+ ],
+ options: ['forbidden']
+ },
+ {
+ code: ``,
+ errors: [
+ {
+ message: "'forbidden' class is not allowed.",
+ type: 'Literal'
+ }
+ ],
+ options: ['forbidden']
+ }
+ ]
+})
From a56c7ece26236bb672c53da0185314859f2d346f Mon Sep 17 00:00:00 2001
From: Yosuke Ota
Date: Sun, 3 Oct 2021 07:04:01 +0900
Subject: [PATCH 2/6] Chore: add generate new rule command (#1645)
---
docs/developer-guide/README.md | 5 +-
package.json | 1 +
tools/new-rule.js | 160 +++++++++++++++++++++++++++++++++
3 files changed, 164 insertions(+), 2 deletions(-)
create mode 100644 tools/new-rule.js
diff --git a/docs/developer-guide/README.md b/docs/developer-guide/README.md
index f08ecc3a8..c5bfcc99e 100644
--- a/docs/developer-guide/README.md
+++ b/docs/developer-guide/README.md
@@ -13,8 +13,7 @@ Please include as much detail as possible to help us properly address your issue
In order to add a new rule or a rule change, you should:
- Create issue on GitHub with description of proposed rule
-- Generate a new rule using the [official yeoman generator](https://github.com/eslint/generator-eslint)
-- Run `npm start`
+- Generate a new rule using the `npm run new -- [rule-name]` command
- Write test scenarios & implement logic
- Describe the rule in the generated `docs` file
- Make sure all tests are passing
@@ -38,10 +37,12 @@ After opening [astexplorer.net], select `Vue` as the syntax and `vue-eslint-pars
Since single file components in Vue are not plain JavaScript, we can't use the default parser, and we had to introduce additional one: `vue-eslint-parser`, that generates enhanced AST with nodes that represent specific parts of the template syntax, as well as what's inside the `
+
+ `
}
],
@@ -2507,6 +2571,145 @@ tester.run('no-unused-properties', rule, {
"'f' of computed property found, but never used.",
"'h' of method found, but never used."
]
+ },
+
+ // toRef, toRefs
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: ["'bar' of property found, but never used."]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: ["'bar' of property found, but never used."]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: deepDataOptions,
+ errors: ["'foo.baz' of data found, but never used."]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: deepDataOptions,
+ errors: [
+ "'foo.bar.b' of data found, but never used.",
+ "'foo.baz.a' of data found, but never used."
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: deepDataOptions,
+ errors: [
+ "'foo.bar.b' of data found, but never used.",
+ "'foo.baz' of data found, but never used."
+ ]
}
]
})
From 5788883670fc6273512796d5be8b3f2730b43ac0 Mon Sep 17 00:00:00 2001
From: Yosuke Ota
Date: Tue, 5 Oct 2021 11:15:59 +0900
Subject: [PATCH 5/6] Fix unable to autofix event name with `update:`. (#1648)
---
lib/rules/v-on-event-hyphenation.js | 31 ++++-----
tests/lib/rules/v-on-event-hyphenation.js | 80 +++++++++++++++++++++++
2 files changed, 96 insertions(+), 15 deletions(-)
diff --git a/lib/rules/v-on-event-hyphenation.js b/lib/rules/v-on-event-hyphenation.js
index 60823e013..7ef07f1d8 100644
--- a/lib/rules/v-on-event-hyphenation.js
+++ b/lib/rules/v-on-event-hyphenation.js
@@ -51,15 +51,16 @@ module.exports = {
const ignoredAttributes = (optionsPayload && optionsPayload.ignore) || []
const autofix = Boolean(optionsPayload && optionsPayload.autofix)
- const caseConverter = casing.getExactConverter(
+ const caseConverter = casing.getConverter(
useHyphenated ? 'kebab-case' : 'camelCase'
)
/**
* @param {VDirective} node
+ * @param {VIdentifier} argument
* @param {string} name
*/
- function reportIssue(node, name) {
+ function reportIssue(node, argument, name) {
const text = sourceCode.getText(node.key)
context.report({
@@ -71,13 +72,14 @@ module.exports = {
data: {
text
},
- fix: autofix
- ? (fixer) =>
- fixer.replaceText(
- node.key,
- text.replace(name, caseConverter(name))
- )
- : null
+ fix:
+ autofix &&
+ // It cannot be converted in snake_case.
+ !name.includes('_')
+ ? (fixer) => {
+ return fixer.replaceText(argument, caseConverter(name))
+ }
+ : null
})
}
@@ -99,14 +101,13 @@ module.exports = {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='on']"(node) {
if (!utils.isCustomComponent(node.parent.parent)) return
-
- const name =
- node.key.argument &&
- node.key.argument.type === 'VIdentifier' &&
- node.key.argument.rawName
+ if (!node.key.argument || node.key.argument.type !== 'VIdentifier') {
+ return
+ }
+ const name = node.key.argument.rawName
if (!name || isIgnoredAttribute(name)) return
- reportIssue(node, name)
+ reportIssue(node, node.key.argument, name)
}
})
}
diff --git a/tests/lib/rules/v-on-event-hyphenation.js b/tests/lib/rules/v-on-event-hyphenation.js
index c77449971..087e5d00a 100644
--- a/tests/lib/rules/v-on-event-hyphenation.js
+++ b/tests/lib/rules/v-on-event-hyphenation.js
@@ -102,6 +102,86 @@ tester.run('v-on-event-hyphenation', rule, {
`,
errors: ["v-on event 'v-on:custom-event' can't be hyphenated."]
+ },
+ {
+ code: `
+
+
+
+
+ `,
+ options: ['always', { autofix: true }],
+ output: `
+
+
+
+
+ `,
+ errors: ["v-on event '@update:modelValue' must be hyphenated."]
+ },
+ {
+ code: `
+
+
+
+
+ `,
+ options: ['never', { autofix: true }],
+ output: `
+
+
+
+
+ `,
+ errors: ["v-on event '@update:model-value' can't be hyphenated."]
+ },
+ {
+ code: `
+
+
+
+
+
+
+ `,
+ options: ['always', { autofix: true }],
+ output: `
+
+
+
+
+
+
+ `,
+ errors: [
+ "v-on event '@upDate:modelValue' must be hyphenated.",
+ "v-on event '@up-date:modelValue' must be hyphenated.",
+ "v-on event '@upDate:model-value' must be hyphenated."
+ ]
+ },
+ {
+ code: `
+
+
+
+
+
+
+ `,
+ options: ['never', { autofix: true }],
+ output: `
+
+
+
+
+
+
+ `,
+ errors: [
+ "v-on event '@up-date:modelValue' can't be hyphenated.",
+ "v-on event '@upDate:model-value' can't be hyphenated.",
+ "v-on event '@up-date:model-value' can't be hyphenated."
+ ]
}
]
})
From 1ece73e4470f1de1e3691529e4ceadbeecd6fd01 Mon Sep 17 00:00:00 2001
From: yosuke ota
Date: Tue, 5 Oct 2021 11:24:38 +0900
Subject: [PATCH 6/6] 7.19.0
---
docs/rules/no-restricted-class.md | 7 +++++--
docs/rules/no-useless-template-attributes.md | 7 +++++--
package.json | 2 +-
3 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/docs/rules/no-restricted-class.md b/docs/rules/no-restricted-class.md
index c7aebce6b..267bfebaa 100644
--- a/docs/rules/no-restricted-class.md
+++ b/docs/rules/no-restricted-class.md
@@ -3,13 +3,12 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-restricted-class
description: disallow specific classes in Vue components
+since: v7.19.0
---
# vue/no-restricted-class
> disallow specific classes in Vue components
-- :exclamation: ***This rule has not been released yet.***
-
## :book: Rule Details
This rule lets you specify a list of classes that you don't want to allow in your templates.
@@ -73,6 +72,10 @@ 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/docs/rules/no-useless-template-attributes.md b/docs/rules/no-useless-template-attributes.md
index ec24eb70f..c6b71027d 100644
--- a/docs/rules/no-useless-template-attributes.md
+++ b/docs/rules/no-useless-template-attributes.md
@@ -3,13 +3,12 @@ pageClass: rule-details
sidebarDepth: 0
title: vue/no-useless-template-attributes
description: disallow useless attribute on ``
+since: v7.19.0
---
# vue/no-useless-template-attributes
> disallow useless attribute on ``
-- :exclamation: ***This rule has not been released yet.***
-
## :book: Rule Details
This rule to prevent any useless attribute on `` tags.
@@ -58,6 +57,10 @@ Nothing.
[vue/no-lone-template]: ./no-lone-template.md
+## :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-useless-template-attributes.js)
diff --git a/package.json b/package.json
index fc5b22a13..16e6ead6e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-vue",
- "version": "7.18.0",
+ "version": "7.19.0",
"description": "Official ESLint plugin for Vue.js",
"main": "lib/index.js",
"scripts": {