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