Repository: vuejs/eslint-plugin-vue
Pull request #1599 (2021-08-07 08:07:11)

Update `vue/no-unsupported-features` rule to support Vue 3.2 syntaxes.
This PR updates `vue/no-unsupported-features` rule to support the Vue 3.2 syntaxes.


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index f7a2e01d..4d87f7cc 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/no-unsupported-features.md b/docs/rules/no-unsupported-features.md
index 858a8445..a70fa2b4 100644
--- a/docs/rules/no-unsupported-features.md
+++ b/docs/rules/no-unsupported-features.md
@@ -29,6 +29,10 @@ This rule reports unsupported Vue.js syntax on the specified version.
 - `version` ... The `version` option accepts [the valid version range of `node-semver`](https://github.com/npm/node-semver#range-grammar). Set the version of Vue.js you are using. This option is required.
 - `ignores` ... You can use this `ignores` option to ignore the given features.
 The `"ignores"` option accepts an array of the following strings.
+  - Vue.js 3.2.0+
+    - `"v-memo"` ... [v-memo](https://v3.vuejs.org/api/directives.html#v-memo) directive.
+    - `"v-bind-prop-modifier-shorthand"` ... `v-bind` with `.prop` modifier shorthand.
+    - `"v-bind-attr-modifier"` ... `.attr` modifier on `v-bind` directive.
   - Vue.js 3.1.0+
     - `"is-attribute-with-vue-prefix"` ... [`is` attribute with `vue:` prefix](https://v3.vuejs.org/api/special-attributes.html#is)
   - Vue.js 3.0.0+
@@ -42,8 +46,6 @@ The `"ignores"` option accepts an array of the following strings.
     - `"v-slot"` ... [v-slot](https://v3.vuejs.org/api/directives.html#v-slot) directive.
   - Vue.js 2.5.0+
     - `"slot-scope-attribute"` ... [slot-scope](https://vuejs.org/v2/api/#slot-scope-deprecated) attributes.
-  - Vue.js `">=2.6.0-beta.1 <=2.6.0-beta.3"` or 2.6 custom build
-    - `"v-bind-prop-modifier-shorthand"` ... `v-bind` with `.prop` modifier shorthand.
 
 ### `{"version": "^2.6.0"}`
 
@@ -97,6 +99,7 @@ The `"ignores"` option accepts an array of the following strings.
 
 ## :books: Further Reading
 
+- [API - v-memo](https://v3.vuejs.org/api/directives.html#v-memo)
 - [API - v-is](https://v3.vuejs.org/api/directives.html#v-is)
 - [API - v-is (Old)](https://github.com/vuejs/docs-next/blob/008613756c3d781128d96b64a2d27f7598f8f548/src/api/directives.md#v-is)
 - [Guide - Dynamic Arguments](https://v3.vuejs.org/guide/template-syntax.html#dynamic-arguments)
diff --git a/docs/rules/valid-v-memo.md b/docs/rules/valid-v-memo.md
new file mode 100644
index 00000000..95d34df7
--- /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: <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)
diff --git a/lib/index.js b/lib/index.js
index 1fe6e4eb..d9352faa 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/no-restricted-v-bind.js b/lib/rules/no-restricted-v-bind.js
index d15bb4ca..3cc46e71 100644
--- a/lib/rules/no-restricted-v-bind.js
+++ b/lib/rules/no-restricted-v-bind.js
@@ -113,7 +113,7 @@ module.exports = {
                 type: 'array',
                 items: {
                   type: 'string',
-                  enum: ['prop', 'camel', 'sync']
+                  enum: ['prop', 'camel', 'sync', 'attr']
                 },
                 uniqueItems: true
               },
diff --git a/lib/rules/no-unsupported-features.js b/lib/rules/no-unsupported-features.js
index 1ddc6934..def5ca3f 100644
--- a/lib/rules/no-unsupported-features.js
+++ b/lib/rules/no-unsupported-features.js
@@ -20,8 +20,6 @@ const FEATURES = {
   // Vue.js 2.6.0+
   'dynamic-directive-arguments': require('./syntaxes/dynamic-directive-arguments'),
   'v-slot': require('./syntaxes/v-slot'),
-  // >=2.6.0-beta.1 <=2.6.0-beta.3
-  'v-bind-prop-modifier-shorthand': require('./syntaxes/v-bind-prop-modifier-shorthand'),
   // Vue.js 3.0.0+
   'v-model-argument': require('./syntaxes/v-model-argument'),
   'v-model-custom-modifiers': require('./syntaxes/v-model-custom-modifiers'),
@@ -29,7 +27,11 @@ const FEATURES = {
   'script-setup': require('./syntaxes/script-setup'),
   'style-css-vars-injection': require('./syntaxes/style-css-vars-injection'),
   // Vue.js 3.1.0+
-  'is-attribute-with-vue-prefix': require('./syntaxes/is-attribute-with-vue-prefix')
+  'is-attribute-with-vue-prefix': require('./syntaxes/is-attribute-with-vue-prefix'),
+  // Vue.js 3.2.0+
+  'v-memo': require('./syntaxes/v-memo'),
+  'v-bind-prop-modifier-shorthand': require('./syntaxes/v-bind-prop-modifier-shorthand'),
+  'v-bind-attr-modifier': require('./syntaxes/v-bind-attr-modifier')
 }
 
 const SYNTAX_NAMES = /** @type {(keyof FEATURES)[]} */ (Object.keys(FEATURES))
@@ -93,9 +95,6 @@ module.exports = {
       forbiddenDynamicDirectiveArguments:
         'Dynamic arguments are not supported until Vue.js "2.6.0".',
       forbiddenVSlot: '`v-slot` are not supported until Vue.js "2.6.0".',
-      // >=2.6.0-beta.1 <=2.6.0-beta.3
-      forbiddenVBindPropModifierShorthand:
-        '`.prop` shorthand are not supported except Vue.js ">=2.6.0-beta.1 <=2.6.0-beta.3".',
       // Vue.js 3.0.0+
       forbiddenVModelArgument:
         'Argument on `v-model` is not supported until Vue.js "3.0.0".',
@@ -108,7 +107,13 @@ module.exports = {
         'SFC CSS variable injection is not supported until Vue.js "3.0.3".',
       // Vue.js 3.1.0+
       forbiddenIsAttributeWithVuePrefix:
-        '`is="vue:"` are not supported until Vue.js "3.1.0".'
+        '`is="vue:"` are not supported until Vue.js "3.1.0".',
+      // Vue.js 3.2.0+
+      forbiddenVMemo: '`v-memo` are not supported until Vue.js "3.2.0".',
+      forbiddenVBindPropModifierShorthand:
+        '`.prop` shorthand are not supported until Vue.js "3.2.0".',
+      forbiddenVBindAttrModifier:
+        '`.attr` modifiers on `v-bind` are not supported until Vue.js "3.2.0".'
     }
   },
   /** @param {RuleContext} context */
diff --git a/lib/rules/no-use-computed-property-like-method.js b/lib/rules/no-use-computed-property-like-method.js
index 1add2c34..c1c8ca76 100644
--- a/lib/rules/no-use-computed-property-like-method.js
+++ b/lib/rules/no-use-computed-property-like-method.js
@@ -228,6 +228,9 @@ module.exports = {
 
         if (node.parent.type !== 'MemberExpression') return
         if (node.parent.property.type !== 'Identifier') return
+        if (node.parent.parent.type !== 'CallExpression') return
+        if (node.parent.parent.callee.type !== 'MemberExpression') return
+        if (!Object.is(node.parent.parent.callee, node.parent)) return
 
         const thisMember = node.parent.property.name
 
diff --git a/lib/rules/syntaxes/v-bind-attr-modifier.js b/lib/rules/syntaxes/v-bind-attr-modifier.js
new file mode 100644
index 00000000..82a0922a
--- /dev/null
+++ b/lib/rules/syntaxes/v-bind-attr-modifier.js
@@ -0,0 +1,32 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+module.exports = {
+  supported: '>=3.2.0',
+  /** @param {RuleContext} context @returns {TemplateListener} */
+  createTemplateBodyVisitor(context) {
+    /**
+     * Reports `v-bind.attr` node
+     * @param { VIdentifier } mod node of `v-bind.attr`
+     * @returns {void}
+     */
+    function report(mod) {
+      context.report({
+        node: mod,
+        messageId: 'forbiddenVBindAttrModifier'
+      })
+    }
+
+    return {
+      "VAttribute[directive=true][key.name.name='bind']"(node) {
+        const attrMod = node.key.modifiers.find((m) => m.name === 'attr')
+        if (attrMod) {
+          report(attrMod)
+        }
+      }
+    }
+  }
+}
diff --git a/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js b/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js
index 5a53a9e3..645ed375 100644
--- a/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js
+++ b/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js
@@ -5,7 +5,7 @@
 'use strict'
 
 module.exports = {
-  supported: '>=2.6.0-beta.1 <=2.6.0-beta.3',
+  supported: '>=3.2.0 || >=2.6.0-beta.1 <=2.6.0-beta.3',
   /** @param {RuleContext} context @returns {TemplateListener} */
   createTemplateBodyVisitor(context) {
     /**
diff --git a/lib/rules/syntaxes/v-is.js b/lib/rules/syntaxes/v-is.js
index f5eed36d..7fb1f862 100644
--- a/lib/rules/syntaxes/v-is.js
+++ b/lib/rules/syntaxes/v-is.js
@@ -10,18 +10,18 @@ module.exports = {
   createTemplateBodyVisitor(context) {
     /**
      * Reports `v-is` node
-     * @param {VDirective} vSlotAttr node of `v-is`
+     * @param {VDirective} vIsAttr node of `v-is`
      * @returns {void}
      */
-    function reportVSlot(vSlotAttr) {
+    function reportVIs(vIsAttr) {
       context.report({
-        node: vSlotAttr.key,
+        node: vIsAttr.key,
         messageId: 'forbiddenVIs'
       })
     }
 
     return {
-      "VAttribute[directive=true][key.name.name='is']": reportVSlot
+      "VAttribute[directive=true][key.name.name='is']": reportVIs
     }
   }
 }
diff --git a/lib/rules/syntaxes/v-memo.js b/lib/rules/syntaxes/v-memo.js
new file mode 100644
index 00000000..958b51cf
--- /dev/null
+++ b/lib/rules/syntaxes/v-memo.js
@@ -0,0 +1,26 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+module.exports = {
+  supported: '>=3.2.0',
+  /** @param {RuleContext} context @returns {TemplateListener} */
+  createTemplateBodyVisitor(context) {
+    /**
+     * Reports `v-is` node
+     * @param {VDirective} vMemoAttr node of `v-is`
+     * @returns {void}
+     */
+    function reportVMemo(vMemoAttr) {
+      context.report({
+        node: vMemoAttr.key,
+        messageId: 'forbiddenVMemo'
+      })
+    }
+
+    return {
+      "VAttribute[directive=true][key.name.name='memo']": reportVMemo
+    }
+  }
+}
diff --git a/lib/rules/valid-v-bind.js b/lib/rules/valid-v-bind.js
index 5b9339c1..6605ed1c 100644
--- a/lib/rules/valid-v-bind.js
+++ b/lib/rules/valid-v-bind.js
@@ -15,7 +15,7 @@ const utils = require('../utils')
 // Helpers
 // ------------------------------------------------------------------------------
 
-const VALID_MODIFIERS = new Set(['prop', 'camel', 'sync'])
+const VALID_MODIFIERS = new Set(['prop', 'camel', 'sync', 'attr'])
 
 // ------------------------------------------------------------------------------
 // Rule Definition
diff --git a/lib/rules/valid-v-memo.js b/lib/rules/valid-v-memo.js
new file mode 100644
index 00000000..b20ba49d
--- /dev/null
+++ b/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)
+          }
+        }
+      }
+    })
+  }
+}
diff --git a/package.json b/package.json
index e5ff5b47..ebee8581 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-vue",
-  "version": "7.15.1",
+  "version": "7.15.2",
   "description": "Official ESLint plugin for Vue.js",
   "main": "lib/index.js",
   "scripts": {
diff --git a/typings/eslint-plugin-vue/util-types/ast/ast.ts b/typings/eslint-plugin-vue/util-types/ast/ast.ts
index 4356ebc5..68d6b884 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 & {


Test patch:
diff --git a/tests/lib/rules/no-restricted-v-bind.js b/tests/lib/rules/no-restricted-v-bind.js
index e6dd72d5..8691b1d0 100644
--- a/tests/lib/rules/no-restricted-v-bind.js
+++ b/tests/lib/rules/no-restricted-v-bind.js
@@ -147,6 +147,13 @@ tester.run('no-restricted-v-bind', rule, {
       </template>`,
       options: [{ argument: 'foo', message: 'foo' }],
       errors: ['foo']
+    },
+
+    {
+      filename: 'test.vue',
+      code: '<template><div :foo.attr="foo" /><div :bar.attr="foo" /></template>',
+      options: [{ argument: 'foo', modifiers: ['attr'] }],
+      errors: ['Using `:foo.attr` is not allowed.']
     }
   ]
 })
diff --git a/tests/lib/rules/no-unsupported-features/v-bind-attr-modifier.js b/tests/lib/rules/no-unsupported-features/v-bind-attr-modifier.js
new file mode 100644
index 00000000..912c98d9
--- /dev/null
+++ b/tests/lib/rules/no-unsupported-features/v-bind-attr-modifier.js
@@ -0,0 +1,62 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../../lib/rules/no-unsupported-features')
+const utils = require('./utils')
+
+const buildOptions = utils.optionsBuilder('v-bind-attr-modifier', '^3.1.0')
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2019
+  }
+})
+
+tester.run('no-unsupported-features/v-bind-attr-modifier', rule, {
+  valid: [
+    {
+      code: `
+      <template>
+        <div :foo.attr="foo" />
+      </template>`,
+      options: buildOptions({ version: '^3.2.0' })
+    },
+    {
+      code: `
+      <template>
+        <div v-bind:attr="foo" />
+      </template>`,
+      options: buildOptions()
+    },
+    {
+      code: `
+      <template>
+        <div :foo.attr="foo" />
+      </template>`,
+      options: buildOptions({
+        version: '^2.5.0',
+        ignores: ['v-bind-attr-modifier']
+      })
+    }
+  ],
+  invalid: [
+    {
+      code: `
+      <template>
+        <div :foo.attr="foo" />
+      </template>`,
+      options: buildOptions(),
+      errors: [
+        {
+          message:
+            '`.attr` modifiers on `v-bind` are not supported until Vue.js "3.2.0".',
+          line: 3
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-unsupported-features/v-bind-prop-modifier-shorthand.js b/tests/lib/rules/no-unsupported-features/v-bind-prop-modifier-shorthand.js
index a49b61e9..1cefc8ff 100644
--- a/tests/lib/rules/no-unsupported-features/v-bind-prop-modifier-shorthand.js
+++ b/tests/lib/rules/no-unsupported-features/v-bind-prop-modifier-shorthand.js
@@ -28,6 +28,13 @@ tester.run('no-unsupported-features/v-bind-prop-modifier-shorthand', rule, {
       </template>`,
       options: buildOptions({ version: '2.6.0-beta.1' })
     },
+    {
+      code: `
+      <template>
+        <a .href="'/xxx'" />
+      </template>`,
+      options: buildOptions({ version: '3.2.0' })
+    },
     {
       code: `
       <template>
@@ -63,8 +70,7 @@ tester.run('no-unsupported-features/v-bind-prop-modifier-shorthand', rule, {
       </template>`,
       errors: [
         {
-          message:
-            '`.prop` shorthand are not supported except Vue.js ">=2.6.0-beta.1 <=2.6.0-beta.3".',
+          message: '`.prop` shorthand are not supported until Vue.js "3.2.0".',
           line: 3
         }
       ]
@@ -81,8 +87,24 @@ tester.run('no-unsupported-features/v-bind-prop-modifier-shorthand', rule, {
       </template>`,
       errors: [
         {
-          message:
-            '`.prop` shorthand are not supported except Vue.js ">=2.6.0-beta.1 <=2.6.0-beta.3".',
+          message: '`.prop` shorthand are not supported until Vue.js "3.2.0".',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <a .href="'/xxx'" />
+      </template>`,
+      options: buildOptions({ version: '3.1.0' }),
+      output: `
+      <template>
+        <a :href.prop="'/xxx'" />
+      </template>`,
+      errors: [
+        {
+          message: '`.prop` shorthand are not supported until Vue.js "3.2.0".',
           line: 3
         }
       ]
diff --git a/tests/lib/rules/no-unsupported-features/v-memo.js b/tests/lib/rules/no-unsupported-features/v-memo.js
new file mode 100644
index 00000000..8c3ea124
--- /dev/null
+++ b/tests/lib/rules/no-unsupported-features/v-memo.js
@@ -0,0 +1,58 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../../lib/rules/no-unsupported-features')
+const utils = require('./utils')
+
+const buildOptions = utils.optionsBuilder('v-memo', '^3.1.0')
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2019
+  }
+})
+
+tester.run('no-unsupported-features/v-memo', rule, {
+  valid: [
+    {
+      code: `
+      <template>
+        <div v-memo="foo" />
+      </template>`,
+      options: buildOptions({ version: '^3.2.0' })
+    },
+    {
+      code: `
+      <template>
+        <div :memo="foo" />
+      </template>`,
+      options: buildOptions()
+    },
+    {
+      code: `
+      <template>
+        <div v-memo="foo" />
+      </template>`,
+      options: buildOptions({ version: '^2.5.0', ignores: ['v-memo'] })
+    }
+  ],
+  invalid: [
+    {
+      code: `
+      <template>
+        <div v-memo="foo" />
+      </template>`,
+      options: buildOptions(),
+      errors: [
+        {
+          message: '`v-memo` are not supported until Vue.js "3.2.0".',
+          line: 3
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-use-computed-property-like-method.js b/tests/lib/rules/no-use-computed-property-like-method.js
index b6e5e695..902f29b1 100644
--- a/tests/lib/rules/no-use-computed-property-like-method.js
+++ b/tests/lib/rules/no-use-computed-property-like-method.js
@@ -418,6 +418,26 @@ tester.run('no-use-computed-property-like-method', rule, {
         }
       </script>
       `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          computed: {
+            computedReturnArray() {
+              return [1,2,3,4,5]
+            },
+            computedReturnArray2() {
+              return [1,2,3,4,5]
+            },
+            computedReturnComputedReturnString() {
+              return this.computedReturnArray.map(() => this.computedReturnArray2)
+            }
+          }
+        }
+      </script>
+      `
     }
   ],
   invalid: [
@@ -698,6 +718,50 @@ tester.run('no-use-computed-property-like-method', rule, {
       errors: [
         'Use this.computedReturnNothing instead of this.computedReturnNothing().'
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          computed: {
+            computedReturnString() {
+              return 'computedReturnString'
+            },
+            computedReturnComputedReturnString() {
+              return this.computedReturnString()
+            }
+          }
+        }
+      </script>
+      `,
+      errors: [
+        'Use this.computedReturnString instead of this.computedReturnString().'
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          computed: {
+            computedReturnArray() {
+              return [1,2,3,4,5]
+            },
+            computedReturnArray2() {
+              return [1,2,3,4,5]
+            },
+            computedReturnComputedReturnString() {
+              return this.computedReturnArray.map([...this.computedReturnArray(), ...this.computedReturnArray2()])
+            }
+          }
+        }
+      </script>
+      `,
+      errors: [
+        'Use this.computedReturnArray instead of this.computedReturnArray().',
+        'Use this.computedReturnArray2 instead of this.computedReturnArray2().'
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/valid-v-bind.js b/tests/lib/rules/valid-v-bind.js
index b5e71af7..24a61bba 100644
--- a/tests/lib/rules/valid-v-bind.js
+++ b/tests/lib/rules/valid-v-bind.js
@@ -71,6 +71,10 @@ tester.run('valid-v-bind', rule, {
       filename: 'test.vue',
       code: "<template><div :aaa.camel='bbb'></div></template>"
     },
+    {
+      filename: 'test.vue',
+      code: "<template><div :aaa.attr='bbb'></div></template>"
+    },
     {
       filename: 'test.vue',
       code: "<template><input v-bind='$attrs' /></template>"
diff --git a/tests/lib/rules/valid-v-memo.js b/tests/lib/rules/valid-v-memo.js
new file mode 100644
index 00000000..51f08621
--- /dev/null
+++ b/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
+        }
+      ]
+    }
+  ]
+})