diff --git a/docs/rules/README.md b/docs/rules/README.md
index f7a2e01d5..4d87f7cc9 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -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
diff --git a/docs/rules/valid-v-memo.md b/docs/rules/valid-v-memo.md
new file mode 100644
index 000000000..95d34df70
--- /dev/null
+++ b/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: ***This rule has not been released yet.***
+
+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. `
`
+- The directive has that modifier. E.g. ``
+- The directive does not have that attribute value. E.g. ``
+- The attribute value of the directive is definitely not array. E.g. ``
+- The directive was used inside v-for. 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-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)
diff --git a/lib/index.js b/lib/index.js
index 1fe6e4eb3..d9352faa1 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -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'),
diff --git a/lib/rules/valid-v-memo.js b/lib/rules/valid-v-memo.js
new file mode 100644
index 000000000..b20ba49d4
--- /dev/null
+++ b/lib/rules/valid-v-memo.js
@@ -0,0 +1,120 @@
+/**
+ * @author Yosuke Ota
+ * 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)
+ }
+ }
+ }
+ })
+ }
+}
diff --git a/tests/lib/rules/valid-v-memo.js b/tests/lib/rules/valid-v-memo.js
new file mode 100644
index 000000000..51f086213
--- /dev/null
+++ b/tests/lib/rules/valid-v-memo.js
@@ -0,0 +1,145 @@
+/**
+ * @author Yosuke Ota
+ * 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: ''
+ },
+ {
+ filename: 'test.vue',
+ code: ''
+ },
+ {
+ filename: 'test.vue',
+ code: ''
+ },
+ // parsing error
+ {
+ filename: 'parsing-error.vue',
+ code: ''
+ },
+ // comment value (parsing error)
+ {
+ filename: 'parsing-error.vue',
+ code: ''
+ },
+ // v-for
+ {
+ filename: 'test.vue',
+ code: ''
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: '',
+ errors: ["'v-memo' directives require no argument."]
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ errors: ["'v-memo' directives require no modifier."]
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ errors: ["'v-memo' directives require that attribute value."]
+ },
+ // empty value
+ {
+ filename: 'empty-value.vue',
+ code: '',
+ errors: ["'v-memo' directives require that attribute value."]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+
+ `,
+ 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: ``,
+ errors: [
+ {
+ message: "'v-memo' directive does not work inside 'v-for'.",
+ line: 1,
+ column: 40
+ }
+ ]
+ }
+ ]
+})
diff --git a/typings/eslint-plugin-vue/util-types/ast/ast.ts b/typings/eslint-plugin-vue/util-types/ast/ast.ts
index 4356ebc55..68d6b8843 100644
--- a/typings/eslint-plugin-vue/util-types/ast/ast.ts
+++ b/typings/eslint-plugin-vue/util-types/ast/ast.ts
@@ -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 & {