-
-
Notifications
You must be signed in to change notification settings - Fork 669
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
5 changed files
with
353 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'] | ||
} | ||
] | ||
}) |