Skip to content

Commit

Permalink
Add vue/valid-v-memo rule (#1596)
Browse files Browse the repository at this point in the history
* Add `vue/valid-v-memo` rule

* update
  • Loading branch information
ota-meshi committed Aug 10, 2021
1 parent 3faf520 commit b7b2393
Show file tree
Hide file tree
Showing 6 changed files with 343 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -338,6 +338,7 @@ For example:
| [vue/valid-define-emits](./valid-define-emits.md) | enforce valid `defineEmits` compiler macro | |
| [vue/valid-define-props](./valid-define-props.md) | enforce valid `defineProps` compiler macro | |
| [vue/valid-next-tick](./valid-next-tick.md) | enforce valid `nextTick` function calls | :wrench: |
| [vue/valid-v-memo](./valid-v-memo.md) | enforce valid `v-memo` directives | |

### Extension Rules

Expand Down
66 changes: 66 additions & 0 deletions docs/rules/valid-v-memo.md
@@ -0,0 +1,66 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/valid-v-memo
description: enforce valid `v-memo` directives
---
# vue/valid-v-memo

> enforce valid `v-memo` directives
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>

This rule checks whether every `v-memo` directive is valid.

## :book: Rule Details

This rule reports `v-memo` directives in the following cases:

- The directive has that argument. E.g. `<div v-memo:aaa></div>`
- The directive has that modifier. E.g. `<div v-memo.bbb></div>`
- The directive does not have that attribute value. E.g. `<div v-memo></div>`
- The attribute value of the directive is definitely not array. E.g. `<div v-memo="{x}"></div>`
- The directive was used inside v-for. E.g. `<div v-for="i in items"><div v-memo="[i]" /></div>`

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

```vue
<template>
<!-- ✓ GOOD -->
<div v-memo="[x]"/>
<!-- ✗ BAD -->
<div v-memo/>
<div v-memo:aaa="[x]"/>
<div v-memo.bbb="[x]"/>
<div v-memo="{x}"/>
<div v-for="i in items">
<div v-memo="[i]" />
</div>
</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-memo](https://v3.vuejs.org/api/directives.html#v-memo)

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-v-memo.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-v-memo.js)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -188,6 +188,7 @@ module.exports = {
'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-memo': require('./rules/valid-v-memo'),
'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
120 changes: 120 additions & 0 deletions lib/rules/valid-v-memo.js
@@ -0,0 +1,120 @@
/**
* @author Yosuke Ota <https://github.com/ota-meshi>
* See LICENSE file in root directory for full license.
*/
'use strict'

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const utils = require('../utils')

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `v-memo` directives',
// TODO Switch to `vue3-essential` in the major version.
// categories: ['vue3-essential'],
categories: undefined,
url: 'https://eslint.vuejs.org/rules/valid-v-memo.html'
},
fixable: null,
schema: [],
messages: {
unexpectedArgument: "'v-memo' directives require no argument.",
unexpectedModifier: "'v-memo' directives require no modifier.",
expectedValue: "'v-memo' directives require that attribute value.",
expectedArray:
"'v-memo' directives require the attribute value to be an array.",
insideVFor: "'v-memo' directive does not work inside 'v-for'."
}
},
/** @param {RuleContext} context */
create(context) {
/** @type {VElement | null} */
let vForElement = null
return utils.defineTemplateBodyVisitor(context, {
VElement(node) {
if (!vForElement && utils.hasDirective(node, 'for')) {
vForElement = node
}
},
'VElement:exit'(node) {
if (vForElement === node) {
vForElement = null
}
},
/** @param {VDirective} node */
"VAttribute[directive=true][key.name.name='memo']"(node) {
if (vForElement && vForElement !== node.parent.parent) {
context.report({
node: node.key,
messageId: 'insideVFor'
})
}
if (node.key.argument) {
context.report({
node: node.key.argument,
messageId: 'unexpectedArgument'
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: {
start: node.key.modifiers[0].loc.start,
end: node.key.modifiers[node.key.modifiers.length - 1].loc.end
},
messageId: 'unexpectedModifier'
})
}
if (!node.value || utils.isEmptyValueDirective(node, context)) {
context.report({
node,
messageId: 'expectedValue'
})
return
}
if (!node.value.expression) {
return
}
const expressions = [node.value.expression]
let expression
while ((expression = expressions.pop())) {
if (
expression.type === 'ObjectExpression' ||
expression.type === 'ClassExpression' ||
expression.type === 'ArrowFunctionExpression' ||
expression.type === 'FunctionExpression' ||
expression.type === 'Literal' ||
expression.type === 'TemplateLiteral' ||
expression.type === 'UnaryExpression' ||
expression.type === 'BinaryExpression' ||
expression.type === 'UpdateExpression'
) {
context.report({
node: expression,
messageId: 'expectedArray'
})
} else if (expression.type === 'AssignmentExpression') {
expressions.push(expression.right)
} else if (expression.type === 'TSAsExpression') {
expressions.push(expression.expression)
} else if (expression.type === 'SequenceExpression') {
expressions.push(
expression.expressions[expression.expressions.length - 1]
)
} else if (expression.type === 'ConditionalExpression') {
expressions.push(expression.consequent, expression.alternate)
}
}
}
})
}
}
145 changes: 145 additions & 0 deletions tests/lib/rules/valid-v-memo.js
@@ -0,0 +1,145 @@
/**
* @author Yosuke Ota <https://github.com/ota-meshi>
* See LICENSE file in root directory for full license.
*/
'use strict'

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const RuleTester = require('eslint').RuleTester
const rule = require('../../../lib/rules/valid-v-memo')

// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------

const tester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
parserOptions: { ecmaVersion: 2021 }
})

tester.run('valid-v-memo', rule, {
valid: [
{
filename: 'test.js',
code: 'test'
},
{
filename: 'test.vue',
code: ''
},
{
filename: 'test.vue',
code: '<template><div v-memo="[x]"></div></template>'
},
{
filename: 'test.vue',
code: '<template><div v-memo="x"></div></template>'
},
{
filename: 'test.vue',
code: '<template><div v-memo="x?y:z"></div></template>'
},
// parsing error
{
filename: 'parsing-error.vue',
code: '<template><div v-memo="." /></template>'
},
// comment value (parsing error)
{
filename: 'parsing-error.vue',
code: '<template><div v-memo="/**/" /></template>'
},
// v-for
{
filename: 'test.vue',
code: '<template><div v-for="i in items" v-memo="[x]"></div></template>'
}
],
invalid: [
{
filename: 'test.vue',
code: '<template><div v-memo:aaa="x"></div></template>',
errors: ["'v-memo' directives require no argument."]
},
{
filename: 'test.vue',
code: '<template><div v-memo.aaa="x"></div></template>',
errors: ["'v-memo' directives require no modifier."]
},
{
filename: 'test.vue',
code: '<template><div v-memo></div></template>',
errors: ["'v-memo' directives require that attribute value."]
},
// empty value
{
filename: 'empty-value.vue',
code: '<template><div v-memo="" /></template>',
errors: ["'v-memo' directives require that attribute value."]
},
{
filename: 'test.vue',
code: `
<template>
<div v-memo="{x}" />
<div v-memo="a ? {b}: c+d" />
<div v-memo="(a,{b},c(),d+1)" />
<div v-memo="()=>42" />
<div v-memo="a=42" />
</template>`,
errors: [
{
message:
"'v-memo' directives require the attribute value to be an array.",
line: 3,
column: 22
},
{
message:
"'v-memo' directives require the attribute value to be an array.",
line: 4,
column: 26
},
{
message:
"'v-memo' directives require the attribute value to be an array.",
line: 4,
column: 31
},
{
message:
"'v-memo' directives require the attribute value to be an array.",
line: 5,
column: 33
},
{
message:
"'v-memo' directives require the attribute value to be an array.",
line: 6,
column: 22
},
{
message:
"'v-memo' directives require the attribute value to be an array.",
line: 7,
column: 24
}
]
},
// v-for
{
filename: 'test.vue',
code: `<template><div v-for="i in items"><div v-memo="[x]" /></div></template>`,
errors: [
{
message: "'v-memo' directive does not work inside 'v-for'.",
line: 1,
column: 40
}
]
}
]
})
10 changes: 10 additions & 0 deletions typings/eslint-plugin-vue/util-types/ast/ast.ts
Expand Up @@ -74,6 +74,16 @@ export type VNodeListenerMap = {
| (V.VExpressionContainer & { expression: ES.Expression | null })
| null
}
"VAttribute[directive=true][key.name.name='memo']": V.VDirective & {
value:
| (V.VExpressionContainer & { expression: ES.Expression | null })
| null
}
"VAttribute[directive=true][key.name.name='memo']:exit": V.VDirective & {
value:
| (V.VExpressionContainer & { expression: ES.Expression | null })
| null
}
"VAttribute[directive=true][key.name.name='on']": V.VDirective & {
value:
| (V.VExpressionContainer & {
Expand Down

0 comments on commit b7b2393

Please sign in to comment.