Skip to content

Commit c2c709d

Browse files
authored
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
1 parent 03ba30e commit c2c709d

File tree

5 files changed

+353
-0
lines changed

5 files changed

+353
-0
lines changed

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ For example:
310310
| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | |
311311
| [vue/no-restricted-block](./no-restricted-block.md) | disallow specific block | |
312312
| [vue/no-restricted-call-after-await](./no-restricted-call-after-await.md) | disallow asynchronously called restricted methods | |
313+
| [vue/no-restricted-class](./no-restricted-class.md) | disallow specific classes in Vue components | |
313314
| [vue/no-restricted-component-options](./no-restricted-component-options.md) | disallow specific component option | |
314315
| [vue/no-restricted-custom-event](./no-restricted-custom-event.md) | disallow specific custom event | |
315316
| [vue/no-restricted-props](./no-restricted-props.md) | disallow specific props | |

docs/rules/no-restricted-class.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-restricted-class
5+
description: disallow specific classes in Vue components
6+
---
7+
# vue/no-restricted-class
8+
9+
> disallow specific classes in Vue components
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
13+
## :book: Rule Details
14+
15+
This rule lets you specify a list of classes that you don't want to allow in your templates.
16+
17+
## :wrench: Options
18+
19+
The simplest way to specify a list of forbidden classes is to pass it directly
20+
in the rule configuration.
21+
22+
```json
23+
{
24+
"vue/no-restricted-props": ["error", "forbidden", "forbidden-two", "forbidden-three"]
25+
}
26+
```
27+
28+
<eslint-code-block :rules="{'vue/no-restricted-class': ['error', 'forbidden']}">
29+
30+
```vue
31+
<template>
32+
<!-- ✗ BAD -->
33+
<div class="forbidden" />
34+
<div :class="{forbidden: someBoolean}" />
35+
<div :class="`forbidden ${someString}`" />
36+
<div :class="'forbidden'" />
37+
<div :class="'forbidden ' + someString" />
38+
<div :class="[someString, 'forbidden']" />
39+
<!-- ✗ GOOD -->
40+
<div class="allowed-class" />
41+
</template>
42+
43+
<script>
44+
export default {
45+
props: {
46+
someBoolean: Boolean,
47+
someString: String,
48+
}
49+
}
50+
</script>
51+
```
52+
53+
</eslint-code-block>
54+
55+
::: warning Note
56+
This rule will only detect classes that are used as strings in your templates. Passing classes via
57+
variables, like below, will not be detected by this rule.
58+
59+
```vue
60+
<template>
61+
<div :class="classes" />
62+
</template>
63+
64+
<script>
65+
export default {
66+
data() {
67+
return {
68+
classes: "forbidden"
69+
}
70+
}
71+
}
72+
</script>
73+
```
74+
:::
75+
76+
## :mag: Implementation
77+
78+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-class.js)
79+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-class.js)

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ module.exports = {
101101
'no-reserved-keys': require('./rules/no-reserved-keys'),
102102
'no-restricted-block': require('./rules/no-restricted-block'),
103103
'no-restricted-call-after-await': require('./rules/no-restricted-call-after-await'),
104+
'no-restricted-class': require('./rules/no-restricted-class'),
104105
'no-restricted-component-options': require('./rules/no-restricted-component-options'),
105106
'no-restricted-custom-event': require('./rules/no-restricted-custom-event'),
106107
'no-restricted-props': require('./rules/no-restricted-props'),

lib/rules/no-restricted-class.js

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/**
2+
* @fileoverview Forbid certain classes from being used
3+
* @author Tao Bojlen
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
const utils = require('../utils')
11+
12+
// ------------------------------------------------------------------------------
13+
// Helpers
14+
// ------------------------------------------------------------------------------
15+
/**
16+
* Report a forbidden class
17+
* @param {string} className
18+
* @param {*} node
19+
* @param {RuleContext} context
20+
* @param {Set<string>} forbiddenClasses
21+
*/
22+
const reportForbiddenClass = (className, node, context, forbiddenClasses) => {
23+
if (forbiddenClasses.has(className)) {
24+
const loc = node.value ? node.value.loc : node.loc
25+
context.report({
26+
node,
27+
loc,
28+
messageId: 'forbiddenClass',
29+
data: {
30+
class: className
31+
}
32+
})
33+
}
34+
}
35+
36+
/**
37+
* @param {Expression} node
38+
* @param {boolean} [textOnly]
39+
* @returns {IterableIterator<{ className:string, reportNode: ESNode }>}
40+
*/
41+
function* extractClassNames(node, textOnly) {
42+
if (node.type === 'Literal') {
43+
yield* `${node.value}`
44+
.split(/\s+/)
45+
.map((className) => ({ className, reportNode: node }))
46+
return
47+
}
48+
if (node.type === 'TemplateLiteral') {
49+
for (const templateElement of node.quasis) {
50+
yield* templateElement.value.cooked
51+
.split(/\s+/)
52+
.map((className) => ({ className, reportNode: templateElement }))
53+
}
54+
for (const expr of node.expressions) {
55+
yield* extractClassNames(expr, true)
56+
}
57+
return
58+
}
59+
if (node.type === 'BinaryExpression') {
60+
if (node.operator !== '+') {
61+
return
62+
}
63+
yield* extractClassNames(node.left, true)
64+
yield* extractClassNames(node.right, true)
65+
return
66+
}
67+
if (textOnly) {
68+
return
69+
}
70+
if (node.type === 'ObjectExpression') {
71+
for (const prop of node.properties) {
72+
if (prop.type !== 'Property') {
73+
continue
74+
}
75+
const classNames = utils.getStaticPropertyName(prop)
76+
if (!classNames) {
77+
continue
78+
}
79+
yield* classNames
80+
.split(/\s+/)
81+
.map((className) => ({ className, reportNode: prop.key }))
82+
}
83+
return
84+
}
85+
if (node.type === 'ArrayExpression') {
86+
for (const element of node.elements) {
87+
if (element == null) {
88+
continue
89+
}
90+
if (element.type === 'SpreadElement') {
91+
continue
92+
}
93+
yield* extractClassNames(element)
94+
}
95+
return
96+
}
97+
}
98+
99+
// ------------------------------------------------------------------------------
100+
// Rule Definition
101+
// ------------------------------------------------------------------------------
102+
module.exports = {
103+
meta: {
104+
type: 'problem',
105+
docs: {
106+
description: 'disallow specific classes in Vue components',
107+
url: 'https://eslint.vuejs.org/rules/no-restricted-class.html',
108+
categories: undefined
109+
},
110+
fixable: null,
111+
messages: {
112+
forbiddenClass: "'{{class}}' class is not allowed."
113+
},
114+
schema: {
115+
type: 'array',
116+
items: {
117+
type: 'string'
118+
}
119+
}
120+
},
121+
122+
/** @param {RuleContext} context */
123+
create(context) {
124+
const forbiddenClasses = new Set(context.options || [])
125+
126+
return utils.defineTemplateBodyVisitor(context, {
127+
/**
128+
* @param {VAttribute & { value: VLiteral } } node
129+
*/
130+
'VAttribute[directive=false][key.name="class"]'(node) {
131+
node.value.value
132+
.split(/\s+/)
133+
.forEach((className) =>
134+
reportForbiddenClass(className, node, context, forbiddenClasses)
135+
)
136+
},
137+
138+
/** @param {VExpressionContainer} node */
139+
"VAttribute[directive=true][key.name.name='bind'][key.argument.name='class'] > VExpressionContainer.value"(
140+
node
141+
) {
142+
if (!node.expression) {
143+
return
144+
}
145+
146+
for (const { className, reportNode } of extractClassNames(
147+
/** @type {Expression} */ (node.expression)
148+
)) {
149+
reportForbiddenClass(className, reportNode, context, forbiddenClasses)
150+
}
151+
}
152+
})
153+
}
154+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* @author Tao Bojlen
3+
*/
4+
5+
'use strict'
6+
7+
const rule = require('../../../lib/rules/no-restricted-class')
8+
const RuleTester = require('eslint').RuleTester
9+
10+
const ruleTester = new RuleTester({
11+
parser: require.resolve('vue-eslint-parser'),
12+
parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
13+
})
14+
15+
ruleTester.run('no-restricted-class', rule, {
16+
valid: [
17+
{ code: `<template><div class="allowed">Content</div></template>` },
18+
{
19+
code: `<template><div class="allowed"">Content</div></template>`,
20+
options: ['forbidden']
21+
},
22+
{
23+
code: `<template><div :class="'allowed' + forbidden">Content</div></template>`,
24+
options: ['forbidden']
25+
},
26+
{
27+
code: `<template><div @class="forbidden">Content</div></template>`,
28+
options: ['forbidden']
29+
},
30+
{
31+
code: `<template><div :class="'' + {forbidden: true}">Content</div></template>`,
32+
options: ['forbidden']
33+
}
34+
],
35+
36+
invalid: [
37+
{
38+
code: `<template><div class="forbidden allowed" /></template>`,
39+
errors: [
40+
{
41+
message: "'forbidden' class is not allowed.",
42+
type: 'VAttribute'
43+
}
44+
],
45+
options: ['forbidden']
46+
},
47+
{
48+
code: `<template><div :class="'forbidden' + ' ' + 'allowed' + someVar" /></template>`,
49+
errors: [
50+
{
51+
message: "'forbidden' class is not allowed.",
52+
type: 'Literal'
53+
}
54+
],
55+
options: ['forbidden']
56+
},
57+
{
58+
code: `<template><div :class="{'forbidden': someBool, someVar: true}" /></template>`,
59+
errors: [
60+
{
61+
message: "'forbidden' class is not allowed.",
62+
type: 'Literal'
63+
}
64+
],
65+
options: ['forbidden']
66+
},
67+
{
68+
code: `<template><div :class="{forbidden: someBool}" /></template>`,
69+
errors: [
70+
{
71+
message: "'forbidden' class is not allowed.",
72+
type: 'Identifier'
73+
}
74+
],
75+
options: ['forbidden']
76+
},
77+
{
78+
code: '<template><div :class="`forbidden ${someVar}`" /></template>',
79+
errors: [
80+
{
81+
message: "'forbidden' class is not allowed.",
82+
type: 'TemplateElement'
83+
}
84+
],
85+
options: ['forbidden']
86+
},
87+
{
88+
code: `<template><div :class="'forbidden'" /></template>`,
89+
errors: [
90+
{
91+
message: "'forbidden' class is not allowed.",
92+
type: 'Literal'
93+
}
94+
],
95+
options: ['forbidden']
96+
},
97+
{
98+
code: `<template><div :class="['forbidden', 'allowed']" /></template>`,
99+
errors: [
100+
{
101+
message: "'forbidden' class is not allowed.",
102+
type: 'Literal'
103+
}
104+
],
105+
options: ['forbidden']
106+
},
107+
{
108+
code: `<template><div :class="['allowed forbidden', someString]" /></template>`,
109+
errors: [
110+
{
111+
message: "'forbidden' class is not allowed.",
112+
type: 'Literal'
113+
}
114+
],
115+
options: ['forbidden']
116+
}
117+
]
118+
})

0 commit comments

Comments
 (0)