Skip to content

Commit

Permalink
Add vue/valid-v-is rule (#1253)
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Jul 19, 2020
1 parent 7ef7de8 commit f626498
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -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 | |
Expand Down
63 changes: 63 additions & 0 deletions 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. `<div v-is:aaa="foo"></div>`
- The directive has that modifier. E.g. `<div v-is.bbb="foo"></div>`
- The directive does not have that attribute value. E.g. `<div v-is></div>`
- The directive is on Vue-components. E.g. `<MyComponent v-is="foo"></MyComponent>`

<eslint-code-block :rules="{'vue/valid-v-is': ['error']}">

```vue
<template>
<!-- ✓ GOOD -->
<tr v-is="'blog-post-row'"></tr>
<tr v-is="foo"></tr>
<!-- ✗ BAD -->
<tr v-is:a="foo"></tr>
<tr v-is.m="foo"></tr>
<tr v-is></tr>
<tr v-is=""></tr>
<MyComponent v-is="foo" />
</template>
```

</eslint-code-block>

::: 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)
1 change: 1 addition & 0 deletions lib/configs/vue3-essential.js
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -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'),
Expand Down
95 changes: 95 additions & 0 deletions 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 }
})
}
}
})
}
}
109 changes: 109 additions & 0 deletions 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: '<template><div v-is="foo" /></template>'
},
{
filename: 'test.vue',
code: '<template><div v-bind:foo="foo" /></template>'
},
{
filename: 'test.vue',
code: `<template><div v-is="'foo'" /></template>`
},
// parsing error
{
filename: 'parsing-error.vue',
code: '<template><div v-is="." /></template>'
},
// comment value (parsing error)
{
filename: 'comment-value.vue',
code: '<template><div v-is="/**/" /></template>'
}
],
invalid: [
{
filename: 'test.vue',
code: '<template><div v-is:a="foo" /></template>',
errors: [
{
message: "'v-is' directives require no argument.",
column: 16,
endColumn: 28
}
]
},
{
filename: 'test.vue',
code: '<template><div v-is.a="foo" /></template>',
errors: [
{
message: "'v-is' directives require no modifier.",
column: 16,
endColumn: 28
}
]
},
{
filename: 'test.vue',
code: '<template><div v-is /></template>',
errors: [
{
message: "'v-is' directives require that attribute value.",
column: 16,
endColumn: 20
}
]
},
{
filename: 'test.vue',
code: '<template><div v-is="" /></template>',
errors: [
{
message: "'v-is' directives require that attribute value.",
column: 16,
endColumn: 23
}
]
},
{
filename: 'test.vue',
code: '<template><MyComponent v-is="foo" /></template>',
errors: [
{
message:
"'v-is' directive must be owned by a native HTML element, but 'mycomponent' is not.",
column: 24,
endColumn: 34
}
]
}
]
})
2 changes: 2 additions & 0 deletions typings/eslint-plugin-vue/util-types/ast/ast.ts
Expand Up @@ -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 })
Expand Down

0 comments on commit f626498

Please sign in to comment.