From e805ab6d3dd898bea707e07be37e0334c700ea7e Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sun, 19 Jul 2020 16:24:12 +0900 Subject: [PATCH] Add `vue/valid-v-is` rule --- docs/rules/README.md | 1 + docs/rules/valid-v-is.md | 63 ++++++++++ lib/configs/vue3-essential.js | 1 + lib/index.js | 1 + lib/rules/valid-v-is.js | 95 +++++++++++++++ tests/lib/rules/valid-v-is.js | 109 ++++++++++++++++++ .../eslint-plugin-vue/util-types/ast/ast.ts | 2 + 7 files changed, 272 insertions(+) create mode 100644 docs/rules/valid-v-is.md create mode 100644 lib/rules/valid-v-is.js create mode 100644 tests/lib/rules/valid-v-is.js diff --git a/docs/rules/README.md b/docs/rules/README.md index 33285ae25..0a052569c 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -92,6 +92,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi | [vue/valid-v-for](./valid-v-for.md) | enforce valid `v-for` directives | | | [vue/valid-v-html](./valid-v-html.md) | enforce valid `v-html` directives | | | [vue/valid-v-if](./valid-v-if.md) | enforce valid `v-if` directives | | +| [vue/valid-v-is](./valid-v-is.md) | enforce valid `v-is` directives | | | [vue/valid-v-model](./valid-v-model.md) | enforce valid `v-model` directives | | | [vue/valid-v-on](./valid-v-on.md) | enforce valid `v-on` directives | | | [vue/valid-v-once](./valid-v-once.md) | enforce valid `v-once` directives | | diff --git a/docs/rules/valid-v-is.md b/docs/rules/valid-v-is.md new file mode 100644 index 000000000..c8b44d213 --- /dev/null +++ b/docs/rules/valid-v-is.md @@ -0,0 +1,63 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/valid-v-is +description: enforce valid `v-is` directives +--- +# vue/valid-v-is +> enforce valid `v-is` directives + +- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`. + +This rule checks whether every `v-is` directive is valid. + +## :book: Rule Details + +This rule reports `v-is` directives in the following cases: + +- The directive has that argument. E.g. `
` +- The directive has that modifier. E.g. `
` +- The directive does not have that attribute value. E.g. `
` +- The directive is on Vue-components. E.g. `` + + + +```vue + +``` + + + +::: warning Note +This rule does not check syntax errors in directives because it's checked by [vue/no-parsing-error] rule. +::: + +## :wrench: Options + +Nothing. + +## :couple: Related Rules + +- [vue/no-parsing-error] + +[vue/no-parsing-error]: ./no-parsing-error.md + +## :books: Further Reading + +- [API - v-is](https://v3.vuejs.org/api/directives.html#v-is) + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-v-is.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-v-is.js) diff --git a/lib/configs/vue3-essential.js b/lib/configs/vue3-essential.js index 5adbbcd23..8b2d6a131 100644 --- a/lib/configs/vue3-essential.js +++ b/lib/configs/vue3-essential.js @@ -60,6 +60,7 @@ module.exports = { 'vue/valid-v-for': 'error', 'vue/valid-v-html': 'error', 'vue/valid-v-if': 'error', + 'vue/valid-v-is': 'error', 'vue/valid-v-model': 'error', 'vue/valid-v-on': 'error', 'vue/valid-v-once': 'error', diff --git a/lib/index.js b/lib/index.js index 530c05c58..5f285be69 100644 --- a/lib/index.js +++ b/lib/index.js @@ -158,6 +158,7 @@ module.exports = { 'valid-v-for': require('./rules/valid-v-for'), 'valid-v-html': require('./rules/valid-v-html'), 'valid-v-if': require('./rules/valid-v-if'), + 'valid-v-is': require('./rules/valid-v-is'), 'valid-v-model': require('./rules/valid-v-model'), 'valid-v-on': require('./rules/valid-v-on'), 'valid-v-once': require('./rules/valid-v-once'), diff --git a/lib/rules/valid-v-is.js b/lib/rules/valid-v-is.js new file mode 100644 index 000000000..50fa1fb5e --- /dev/null +++ b/lib/rules/valid-v-is.js @@ -0,0 +1,95 @@ +/** + * @fileoverview enforce valid `v-is` directives + * @author Yosuke Ota + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const utils = require('../utils') + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +/** + * Check whether the given node is valid or not. + * @param {VElement} node The element node to check. + * @returns {boolean} `true` if the node is valid. + */ +function isValidElement(node) { + if ( + utils.isHtmlElementNode(node) && + !utils.isHtmlWellKnownElementName(node.rawName) + ) { + // Vue-component + return false + } + return true +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'enforce valid `v-is` directives', + categories: ['vue3-essential'], + url: 'https://eslint.vuejs.org/rules/valid-v-is.html' + }, + fixable: null, + schema: [], + messages: { + unexpectedArgument: "'v-is' directives require no argument.", + unexpectedModifier: "'v-is' directives require no modifier.", + expectedValue: "'v-is' directives require that attribute value.", + ownerMustBeHTMLElement: + "'v-is' directive must be owned by a native HTML element, but '{{name}}' is not." + } + }, + /** @param {RuleContext} context */ + create(context) { + return utils.defineTemplateBodyVisitor(context, { + "VAttribute[directive=true][key.name.name='is']"(node) { + if (node.key.argument) { + context.report({ + node, + loc: node.loc, + messageId: 'unexpectedArgument' + }) + } + if (node.key.modifiers.length > 0) { + context.report({ + node, + loc: node.loc, + messageId: 'unexpectedModifier' + }) + } + if (!node.value || utils.isEmptyValueDirective(node, context)) { + context.report({ + node, + loc: node.loc, + messageId: 'expectedValue' + }) + } + + const element = node.parent.parent + + if (!isValidElement(element)) { + const name = element.name + context.report({ + node, + loc: node.loc, + messageId: 'ownerMustBeHTMLElement', + data: { name } + }) + } + } + }) + } +} diff --git a/tests/lib/rules/valid-v-is.js b/tests/lib/rules/valid-v-is.js new file mode 100644 index 000000000..1a3a45e5e --- /dev/null +++ b/tests/lib/rules/valid-v-is.js @@ -0,0 +1,109 @@ +/** + * @author Yosuke Ota + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/valid-v-is') + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { ecmaVersion: 2020 } +}) + +tester.run('valid-v-is', rule, { + valid: [ + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: `` + }, + // parsing error + { + filename: 'parsing-error.vue', + code: '' + }, + // comment value (parsing error) + { + filename: 'comment-value.vue', + code: '' + } + ], + invalid: [ + { + filename: 'test.vue', + code: '', + errors: [ + { + message: "'v-is' directives require no argument.", + column: 16, + endColumn: 28 + } + ] + }, + { + filename: 'test.vue', + code: '', + errors: [ + { + message: "'v-is' directives require no modifier.", + column: 16, + endColumn: 28 + } + ] + }, + { + filename: 'test.vue', + code: '', + errors: [ + { + message: "'v-is' directives require that attribute value.", + column: 16, + endColumn: 20 + } + ] + }, + { + filename: 'test.vue', + code: '', + errors: [ + { + message: "'v-is' directives require that attribute value.", + column: 16, + endColumn: 23 + } + ] + }, + { + filename: 'test.vue', + code: '', + errors: [ + { + message: + "'v-is' directive must be owned by a native HTML element, but 'mycomponent' is not.", + column: 24, + endColumn: 34 + } + ] + } + ] +}) diff --git a/typings/eslint-plugin-vue/util-types/ast/ast.ts b/typings/eslint-plugin-vue/util-types/ast/ast.ts index 357b56400..d8f21315c 100644 --- a/typings/eslint-plugin-vue/util-types/ast/ast.ts +++ b/typings/eslint-plugin-vue/util-types/ast/ast.ts @@ -62,6 +62,8 @@ export type VNodeListenerMap = { | (V.VExpressionContainer & { expression: ES.Expression | null }) | null } + "VAttribute[directive=true][key.name.name='is']": V.VDirective + "VAttribute[directive=true][key.name.name='is']:exit": V.VDirective "VAttribute[directive=true][key.name.name='model']": V.VDirective & { value: | (V.VExpressionContainer & { expression: ES.Expression | null })