Skip to content

Commit

Permalink
Add vue/no-restricted-class rule (#1639)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
taobojlen committed Sep 29, 2021
1 parent 03ba30e commit c2c709d
Show file tree
Hide file tree
Showing 5 changed files with 353 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -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 | |
Expand Down
79 changes: 79 additions & 0 deletions 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: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>

## :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"]
}
```

<eslint-code-block :rules="{'vue/no-restricted-class': ['error', 'forbidden']}">

```vue
<template>
<!-- ✗ BAD -->
<div class="forbidden" />
<div :class="{forbidden: someBoolean}" />
<div :class="`forbidden ${someString}`" />
<div :class="'forbidden'" />
<div :class="'forbidden ' + someString" />
<div :class="[someString, 'forbidden']" />
<!-- ✗ GOOD -->
<div class="allowed-class" />
</template>
<script>
export default {
props: {
someBoolean: Boolean,
someString: String,
}
}
</script>
```

</eslint-code-block>

::: 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
<template>
<div :class="classes" />
</template>
<script>
export default {
data() {
return {
classes: "forbidden"
}
}
}
</script>
```
:::

## :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)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -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'),
Expand Down
154 changes: 154 additions & 0 deletions 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<string>} 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)
}
}
})
}
}
118 changes: 118 additions & 0 deletions 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: `<template><div class="allowed">Content</div></template>` },
{
code: `<template><div class="allowed"">Content</div></template>`,
options: ['forbidden']
},
{
code: `<template><div :class="'allowed' + forbidden">Content</div></template>`,
options: ['forbidden']
},
{
code: `<template><div @class="forbidden">Content</div></template>`,
options: ['forbidden']
},
{
code: `<template><div :class="'' + {forbidden: true}">Content</div></template>`,
options: ['forbidden']
}
],

invalid: [
{
code: `<template><div class="forbidden allowed" /></template>`,
errors: [
{
message: "'forbidden' class is not allowed.",
type: 'VAttribute'
}
],
options: ['forbidden']
},
{
code: `<template><div :class="'forbidden' + ' ' + 'allowed' + someVar" /></template>`,
errors: [
{
message: "'forbidden' class is not allowed.",
type: 'Literal'
}
],
options: ['forbidden']
},
{
code: `<template><div :class="{'forbidden': someBool, someVar: true}" /></template>`,
errors: [
{
message: "'forbidden' class is not allowed.",
type: 'Literal'
}
],
options: ['forbidden']
},
{
code: `<template><div :class="{forbidden: someBool}" /></template>`,
errors: [
{
message: "'forbidden' class is not allowed.",
type: 'Identifier'
}
],
options: ['forbidden']
},
{
code: '<template><div :class="`forbidden ${someVar}`" /></template>',
errors: [
{
message: "'forbidden' class is not allowed.",
type: 'TemplateElement'
}
],
options: ['forbidden']
},
{
code: `<template><div :class="'forbidden'" /></template>`,
errors: [
{
message: "'forbidden' class is not allowed.",
type: 'Literal'
}
],
options: ['forbidden']
},
{
code: `<template><div :class="['forbidden', 'allowed']" /></template>`,
errors: [
{
message: "'forbidden' class is not allowed.",
type: 'Literal'
}
],
options: ['forbidden']
},
{
code: `<template><div :class="['allowed forbidden', someString]" /></template>`,
errors: [
{
message: "'forbidden' class is not allowed.",
type: 'Literal'
}
],
options: ['forbidden']
}
]
})

0 comments on commit c2c709d

Please sign in to comment.