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: `` }, + { + code: ``, + options: ['forbidden'] + }, + { + code: ``, + options: ['forbidden'] + }, + { + code: ``, + options: ['forbidden'] + }, + { + code: ``, + 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'] + } + ] +})