Skip to content

Commit b7b2393

Browse files
authored
Add vue/valid-v-memo rule (#1596)
* Add `vue/valid-v-memo` rule * update
1 parent 3faf520 commit b7b2393

File tree

6 files changed

+343
-0
lines changed

6 files changed

+343
-0
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ For example:
338338
| [vue/valid-define-emits](./valid-define-emits.md) | enforce valid `defineEmits` compiler macro | |
339339
| [vue/valid-define-props](./valid-define-props.md) | enforce valid `defineProps` compiler macro | |
340340
| [vue/valid-next-tick](./valid-next-tick.md) | enforce valid `nextTick` function calls | :wrench: |
341+
| [vue/valid-v-memo](./valid-v-memo.md) | enforce valid `v-memo` directives | |
341342

342343
### Extension Rules
343344

docs/rules/valid-v-memo.md

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/valid-v-memo
5+
description: enforce valid `v-memo` directives
6+
---
7+
# vue/valid-v-memo
8+
9+
> enforce valid `v-memo` directives
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
13+
This rule checks whether every `v-memo` directive is valid.
14+
15+
## :book: Rule Details
16+
17+
This rule reports `v-memo` directives in the following cases:
18+
19+
- The directive has that argument. E.g. `<div v-memo:aaa></div>`
20+
- The directive has that modifier. E.g. `<div v-memo.bbb></div>`
21+
- The directive does not have that attribute value. E.g. `<div v-memo></div>`
22+
- The attribute value of the directive is definitely not array. E.g. `<div v-memo="{x}"></div>`
23+
- The directive was used inside v-for. E.g. `<div v-for="i in items"><div v-memo="[i]" /></div>`
24+
25+
<eslint-code-block :rules="{'vue/valid-v-memo': ['error']}">
26+
27+
```vue
28+
<template>
29+
<!-- ✓ GOOD -->
30+
<div v-memo="[x]"/>
31+
32+
<!-- ✗ BAD -->
33+
<div v-memo/>
34+
<div v-memo:aaa="[x]"/>
35+
<div v-memo.bbb="[x]"/>
36+
<div v-memo="{x}"/>
37+
<div v-for="i in items">
38+
<div v-memo="[i]" />
39+
</div>
40+
</template>
41+
```
42+
43+
</eslint-code-block>
44+
45+
::: warning Note
46+
This rule does not check syntax errors in directives because it's checked by [vue/no-parsing-error] rule.
47+
:::
48+
49+
## :wrench: Options
50+
51+
Nothing.
52+
53+
## :couple: Related Rules
54+
55+
- [vue/no-parsing-error]
56+
57+
[vue/no-parsing-error]: ./no-parsing-error.md
58+
59+
## :books: Further Reading
60+
61+
- [API - v-memo](https://v3.vuejs.org/api/directives.html#v-memo)
62+
63+
## :mag: Implementation
64+
65+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-v-memo.js)
66+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-v-memo.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ module.exports = {
188188
'valid-v-html': require('./rules/valid-v-html'),
189189
'valid-v-if': require('./rules/valid-v-if'),
190190
'valid-v-is': require('./rules/valid-v-is'),
191+
'valid-v-memo': require('./rules/valid-v-memo'),
191192
'valid-v-model': require('./rules/valid-v-model'),
192193
'valid-v-on': require('./rules/valid-v-on'),
193194
'valid-v-once': require('./rules/valid-v-once'),

lib/rules/valid-v-memo.js

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* @author Yosuke Ota <https://github.com/ota-meshi>
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const utils = require('../utils')
12+
13+
// ------------------------------------------------------------------------------
14+
// Rule Definition
15+
// ------------------------------------------------------------------------------
16+
17+
module.exports = {
18+
meta: {
19+
type: 'problem',
20+
docs: {
21+
description: 'enforce valid `v-memo` directives',
22+
// TODO Switch to `vue3-essential` in the major version.
23+
// categories: ['vue3-essential'],
24+
categories: undefined,
25+
url: 'https://eslint.vuejs.org/rules/valid-v-memo.html'
26+
},
27+
fixable: null,
28+
schema: [],
29+
messages: {
30+
unexpectedArgument: "'v-memo' directives require no argument.",
31+
unexpectedModifier: "'v-memo' directives require no modifier.",
32+
expectedValue: "'v-memo' directives require that attribute value.",
33+
expectedArray:
34+
"'v-memo' directives require the attribute value to be an array.",
35+
insideVFor: "'v-memo' directive does not work inside 'v-for'."
36+
}
37+
},
38+
/** @param {RuleContext} context */
39+
create(context) {
40+
/** @type {VElement | null} */
41+
let vForElement = null
42+
return utils.defineTemplateBodyVisitor(context, {
43+
VElement(node) {
44+
if (!vForElement && utils.hasDirective(node, 'for')) {
45+
vForElement = node
46+
}
47+
},
48+
'VElement:exit'(node) {
49+
if (vForElement === node) {
50+
vForElement = null
51+
}
52+
},
53+
/** @param {VDirective} node */
54+
"VAttribute[directive=true][key.name.name='memo']"(node) {
55+
if (vForElement && vForElement !== node.parent.parent) {
56+
context.report({
57+
node: node.key,
58+
messageId: 'insideVFor'
59+
})
60+
}
61+
if (node.key.argument) {
62+
context.report({
63+
node: node.key.argument,
64+
messageId: 'unexpectedArgument'
65+
})
66+
}
67+
if (node.key.modifiers.length > 0) {
68+
context.report({
69+
node,
70+
loc: {
71+
start: node.key.modifiers[0].loc.start,
72+
end: node.key.modifiers[node.key.modifiers.length - 1].loc.end
73+
},
74+
messageId: 'unexpectedModifier'
75+
})
76+
}
77+
if (!node.value || utils.isEmptyValueDirective(node, context)) {
78+
context.report({
79+
node,
80+
messageId: 'expectedValue'
81+
})
82+
return
83+
}
84+
if (!node.value.expression) {
85+
return
86+
}
87+
const expressions = [node.value.expression]
88+
let expression
89+
while ((expression = expressions.pop())) {
90+
if (
91+
expression.type === 'ObjectExpression' ||
92+
expression.type === 'ClassExpression' ||
93+
expression.type === 'ArrowFunctionExpression' ||
94+
expression.type === 'FunctionExpression' ||
95+
expression.type === 'Literal' ||
96+
expression.type === 'TemplateLiteral' ||
97+
expression.type === 'UnaryExpression' ||
98+
expression.type === 'BinaryExpression' ||
99+
expression.type === 'UpdateExpression'
100+
) {
101+
context.report({
102+
node: expression,
103+
messageId: 'expectedArray'
104+
})
105+
} else if (expression.type === 'AssignmentExpression') {
106+
expressions.push(expression.right)
107+
} else if (expression.type === 'TSAsExpression') {
108+
expressions.push(expression.expression)
109+
} else if (expression.type === 'SequenceExpression') {
110+
expressions.push(
111+
expression.expressions[expression.expressions.length - 1]
112+
)
113+
} else if (expression.type === 'ConditionalExpression') {
114+
expressions.push(expression.consequent, expression.alternate)
115+
}
116+
}
117+
}
118+
})
119+
}
120+
}

tests/lib/rules/valid-v-memo.js

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* @author Yosuke Ota <https://github.com/ota-meshi>
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const RuleTester = require('eslint').RuleTester
12+
const rule = require('../../../lib/rules/valid-v-memo')
13+
14+
// ------------------------------------------------------------------------------
15+
// Tests
16+
// ------------------------------------------------------------------------------
17+
18+
const tester = new RuleTester({
19+
parser: require.resolve('vue-eslint-parser'),
20+
parserOptions: { ecmaVersion: 2021 }
21+
})
22+
23+
tester.run('valid-v-memo', rule, {
24+
valid: [
25+
{
26+
filename: 'test.js',
27+
code: 'test'
28+
},
29+
{
30+
filename: 'test.vue',
31+
code: ''
32+
},
33+
{
34+
filename: 'test.vue',
35+
code: '<template><div v-memo="[x]"></div></template>'
36+
},
37+
{
38+
filename: 'test.vue',
39+
code: '<template><div v-memo="x"></div></template>'
40+
},
41+
{
42+
filename: 'test.vue',
43+
code: '<template><div v-memo="x?y:z"></div></template>'
44+
},
45+
// parsing error
46+
{
47+
filename: 'parsing-error.vue',
48+
code: '<template><div v-memo="." /></template>'
49+
},
50+
// comment value (parsing error)
51+
{
52+
filename: 'parsing-error.vue',
53+
code: '<template><div v-memo="/**/" /></template>'
54+
},
55+
// v-for
56+
{
57+
filename: 'test.vue',
58+
code: '<template><div v-for="i in items" v-memo="[x]"></div></template>'
59+
}
60+
],
61+
invalid: [
62+
{
63+
filename: 'test.vue',
64+
code: '<template><div v-memo:aaa="x"></div></template>',
65+
errors: ["'v-memo' directives require no argument."]
66+
},
67+
{
68+
filename: 'test.vue',
69+
code: '<template><div v-memo.aaa="x"></div></template>',
70+
errors: ["'v-memo' directives require no modifier."]
71+
},
72+
{
73+
filename: 'test.vue',
74+
code: '<template><div v-memo></div></template>',
75+
errors: ["'v-memo' directives require that attribute value."]
76+
},
77+
// empty value
78+
{
79+
filename: 'empty-value.vue',
80+
code: '<template><div v-memo="" /></template>',
81+
errors: ["'v-memo' directives require that attribute value."]
82+
},
83+
{
84+
filename: 'test.vue',
85+
code: `
86+
<template>
87+
<div v-memo="{x}" />
88+
<div v-memo="a ? {b}: c+d" />
89+
<div v-memo="(a,{b},c(),d+1)" />
90+
<div v-memo="()=>42" />
91+
<div v-memo="a=42" />
92+
</template>`,
93+
errors: [
94+
{
95+
message:
96+
"'v-memo' directives require the attribute value to be an array.",
97+
line: 3,
98+
column: 22
99+
},
100+
{
101+
message:
102+
"'v-memo' directives require the attribute value to be an array.",
103+
line: 4,
104+
column: 26
105+
},
106+
{
107+
message:
108+
"'v-memo' directives require the attribute value to be an array.",
109+
line: 4,
110+
column: 31
111+
},
112+
{
113+
message:
114+
"'v-memo' directives require the attribute value to be an array.",
115+
line: 5,
116+
column: 33
117+
},
118+
{
119+
message:
120+
"'v-memo' directives require the attribute value to be an array.",
121+
line: 6,
122+
column: 22
123+
},
124+
{
125+
message:
126+
"'v-memo' directives require the attribute value to be an array.",
127+
line: 7,
128+
column: 24
129+
}
130+
]
131+
},
132+
// v-for
133+
{
134+
filename: 'test.vue',
135+
code: `<template><div v-for="i in items"><div v-memo="[x]" /></div></template>`,
136+
errors: [
137+
{
138+
message: "'v-memo' directive does not work inside 'v-for'.",
139+
line: 1,
140+
column: 40
141+
}
142+
]
143+
}
144+
]
145+
})

typings/eslint-plugin-vue/util-types/ast/ast.ts

+10
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ export type VNodeListenerMap = {
7474
| (V.VExpressionContainer & { expression: ES.Expression | null })
7575
| null
7676
}
77+
"VAttribute[directive=true][key.name.name='memo']": V.VDirective & {
78+
value:
79+
| (V.VExpressionContainer & { expression: ES.Expression | null })
80+
| null
81+
}
82+
"VAttribute[directive=true][key.name.name='memo']:exit": V.VDirective & {
83+
value:
84+
| (V.VExpressionContainer & { expression: ES.Expression | null })
85+
| null
86+
}
7787
"VAttribute[directive=true][key.name.name='on']": V.VDirective & {
7888
value:
7989
| (V.VExpressionContainer & {

0 commit comments

Comments
 (0)