Repository: vuejs/eslint-plugin-vue
Pull request #1406 (2021-01-03 08:32:34)

Add composition api's computed function support to vue/return-in-computed-property refs #1393
This PR adds support for composition api's computed function to `vue/return-in-computed-property `.
refs #1393



Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index c96f44a0..0fd8e2fa 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -294,6 +294,7 @@ For example:
 | [vue/html-comment-indent](./html-comment-indent.md) | enforce consistent indentation in HTML comments | :wrench: |
 | [vue/match-component-file-name](./match-component-file-name.md) | require component name property to match its file name |  |
 | [vue/new-line-between-multi-line-property](./new-line-between-multi-line-property.md) | enforce new lines between multi-line properties in Vue components | :wrench: |
+| [vue/next-tick-style](./next-tick-style.md) | enforce Promise or callback style in `nextTick` | :wrench: |
 | [vue/no-bare-strings-in-template](./no-bare-strings-in-template.md) | disallow the use of bare strings in `<template>` |  |
 | [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: |
 | [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` |  |
@@ -324,6 +325,7 @@ For example:
 | [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: |
 | [vue/v-on-event-hyphenation](./v-on-event-hyphenation.md) | enforce v-on event naming style on custom components in template | :wrench: |
 | [vue/v-on-function-call](./v-on-function-call.md) | enforce or forbid parentheses after method calls without arguments in `v-on` directives | :wrench: |
+| [vue/valid-next-tick](./valid-next-tick.md) | enforce valid `nextTick` function calls | :wrench: |
 
 ### Extension Rules
 
diff --git a/docs/rules/next-tick-style.md b/docs/rules/next-tick-style.md
new file mode 100644
index 00000000..52188fd0
--- /dev/null
+++ b/docs/rules/next-tick-style.md
@@ -0,0 +1,103 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/next-tick-style
+description: enforce Promise or callback style in `nextTick`
+---
+# vue/next-tick-style
+
+> enforce Promise or callback style in `nextTick`
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+This rule enforces whether the callback version or Promise version (which was introduced in Vue v2.1.0) should be used in `Vue.nextTick` and `this.$nextTick`.
+
+<eslint-code-block fix :rules="{'vue/next-tick-style': ['error']}">
+
+```vue
+<script>
+import { nextTick as nt } from 'vue';
+
+export default {
+  async mounted() {
+    /* ✓ GOOD */
+    nt().then(() => callback());
+    await nt(); callback();
+    Vue.nextTick().then(() => callback());
+    await Vue.nextTick(); callback();
+    this.$nextTick().then(() => callback());
+    await this.$nextTick(); callback();
+
+    /* ✗ BAD */
+    nt(() => callback());
+    nt(callback);
+    Vue.nextTick(() => callback());
+    Vue.nextTick(callback);
+    this.$nextTick(() => callback());
+    this.$nextTick(callback);
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+Default is set to `promise`.
+
+```json
+{
+  "vue/next-tick-style": ["error", "promise" | "callback"]
+}
+```
+
+- `"promise"` (default) ... requires using the promise version.
+- `"callback"` ... requires using the callback version. Use this if you use a Vue version below v2.1.0.
+
+### `"callback"`
+
+<eslint-code-block fix :rules="{'vue/next-tick-style': ['error', 'callback']}">
+
+```vue
+<script>
+import { nextTick as nt } from 'vue';
+
+export default {
+  async mounted() {
+    /* ✓ GOOD */
+    nt(() => callback());
+    nt(callback);
+    Vue.nextTick(() => callback());
+    Vue.nextTick(callback);
+    this.$nextTick(() => callback());
+    this.$nextTick(callback);
+
+    /* ✗ BAD */
+    nt().then(() => callback());
+    await nt(); callback();
+    Vue.nextTick().then(() => callback());
+    await Vue.nextTick(); callback();
+    this.$nextTick().then(() => callback());
+    await this.$nextTick(); callback();
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :books: Further Reading
+
+- [`Vue.nextTick` API in Vue 2](https://vuejs.org/v2/api/#Vue-nextTick)
+- [`vm.$nextTick` API in Vue 2](https://vuejs.org/v2/api/#vm-nextTick)
+- [Global API Treeshaking](https://v3.vuejs.org/guide/migration/global-api-treeshaking.html)
+- [Global `nextTick` API in Vue 3](https://v3.vuejs.org/api/global-api.html#nexttick)
+- [Instance `$nextTick` API in Vue 3](https://v3.vuejs.org/api/instance-methods.html#nexttick)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/next-tick-style.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/next-tick-style.js)
diff --git a/docs/rules/return-in-computed-property.md b/docs/rules/return-in-computed-property.md
index d2522579..0ade8e5a 100644
--- a/docs/rules/return-in-computed-property.md
+++ b/docs/rules/return-in-computed-property.md
@@ -2,18 +2,18 @@
 pageClass: rule-details
 sidebarDepth: 0
 title: vue/return-in-computed-property
-description: enforce that a return statement is present in computed property
+description: enforce that a return statement is present in computed property and function
 since: v3.7.0
 ---
 # vue/return-in-computed-property
 
-> enforce that a return statement is present in computed property
+> enforce that a return statement is present in computed property and function
 
 - :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
 
 ## :book: Rule Details
 
-This rule enforces that a `return` statement is present in `computed` properties.
+This rule enforces that a `return` statement is present in `computed` properties and functions.
 
 <eslint-code-block :rules="{'vue/return-in-computed-property': ['error']}">
 
@@ -46,6 +46,39 @@ export default {
 
 </eslint-code-block>
 
+<eslint-code-block :rules="{'vue/return-in-computed-property': ['error']}">
+
+```vue
+<script>
+import {computed} from 'vue'
+export default {
+  setup() {
+    const foobar = useFoobar()
+
+    /* ✓ GOOD */
+    const foo = computed(() => {
+      if (foobar.bar) {
+        return foobar.baz
+      } else {
+        return foobar.baf
+      }
+    })
+    const bar = computed(() => false)
+
+    /* ✗ BAD */
+    const baz = computed(() => {
+      if (foobar.baf) {
+        return foobar.baf
+      }
+    })
+    const baf = computed(() => {})
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
 ## :wrench: Options
 
 ```json
diff --git a/docs/rules/valid-next-tick.md b/docs/rules/valid-next-tick.md
new file mode 100644
index 00000000..076fcc79
--- /dev/null
+++ b/docs/rules/valid-next-tick.md
@@ -0,0 +1,88 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/valid-next-tick
+description: enforce valid `nextTick` function calls
+---
+# vue/valid-next-tick
+
+> enforce valid `nextTick` function calls
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+Calling `Vue.nextTick` or `vm.$nextTick` without passing a callback and without awaiting the returned Promise is likely a mistake (probably a missing `await`).
+
+<eslint-code-block fix :rules="{'vue/valid-next-tick': ['error']}">
+
+```vue
+<script>
+import { nextTick as nt } from 'vue';
+
+export default {
+  async mounted() {
+    /* ✗ BAD: no callback function or awaited Promise */
+    nt();
+    Vue.nextTick();
+    this.$nextTick();
+
+    /* ✗ BAD: too many parameters */
+    nt(callback, anotherCallback);
+    Vue.nextTick(callback, anotherCallback);
+    this.$nextTick(callback, anotherCallback);
+
+    /* ✗ BAD: no function call */
+    nt.then(callback);
+    Vue.nextTick.then(callback);
+    this.$nextTick.then(callback);
+    await nt;
+    await Vue.nextTick;
+    await this.$nextTick;
+
+    /* ✗ BAD: both callback function and awaited Promise */
+    nt(callback).then(anotherCallback);
+    Vue.nextTick(callback).then(anotherCallback);
+    this.$nextTick(callback).then(anotherCallback);
+    await nt(callback);
+    await Vue.nextTick(callback);
+    await this.$nextTick(callback);
+
+    /* ✓ GOOD */
+    await nt();
+    await Vue.nextTick();
+    await this.$nextTick();
+
+    /* ✓ GOOD */
+    nt().then(callback);
+    Vue.nextTick().then(callback);
+    this.$nextTick().then(callback);
+
+    /* ✓ GOOD */
+    nt(callback);
+    Vue.nextTick(callback);
+    this.$nextTick(callback);
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further Reading
+
+- [`Vue.nextTick` API in Vue 2](https://vuejs.org/v2/api/#Vue-nextTick)
+- [`vm.$nextTick` API in Vue 2](https://vuejs.org/v2/api/#vm-nextTick)
+- [Global API Treeshaking](https://v3.vuejs.org/guide/migration/global-api-treeshaking.html)
+- [Global `nextTick` API in Vue 3](https://v3.vuejs.org/api/global-api.html#nexttick)
+- [Instance `$nextTick` API in Vue 3](https://v3.vuejs.org/api/instance-methods.html#nexttick)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-next-tick.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-next-tick.js)
diff --git a/docs/user-guide/README.md b/docs/user-guide/README.md
index 2457534f..a4e9acd9 100644
--- a/docs/user-guide/README.md
+++ b/docs/user-guide/README.md
@@ -338,4 +338,4 @@ Note that you cannot use angle-bracket type assertion style (`var x = <foo>bar;`
 
 You need to turn off Vetur's template validation by adding `vetur.validation.template: false` to your `.vscode/settings.json`.
 
-See also: "[Visual Studio Code](#editor-integrations)" section and [Vetur - Linting](https://vuejs.github.io/vetur/linting-error.html#linting).
+See also: "[Visual Studio Code](#editor-integrations)" section and [Vetur - Linting](https://vuejs.github.io/vetur/guide/linting-error.html#linting).
diff --git a/lib/index.js b/lib/index.js
index 699d2f40..2d600117 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -48,6 +48,7 @@ module.exports = {
     'mustache-interpolation-spacing': require('./rules/mustache-interpolation-spacing'),
     'name-property-casing': require('./rules/name-property-casing'),
     'new-line-between-multi-line-property': require('./rules/new-line-between-multi-line-property'),
+    'next-tick-style': require('./rules/next-tick-style'),
     'no-arrow-functions-in-watch': require('./rules/no-arrow-functions-in-watch'),
     'no-async-in-computed-properties': require('./rules/no-async-in-computed-properties'),
     'no-bare-strings-in-template': require('./rules/no-bare-strings-in-template'),
@@ -163,6 +164,7 @@ module.exports = {
     'v-on-function-call': require('./rules/v-on-function-call'),
     'v-on-style': require('./rules/v-on-style'),
     'v-slot-style': require('./rules/v-slot-style'),
+    'valid-next-tick': require('./rules/valid-next-tick'),
     'valid-template-root': require('./rules/valid-template-root'),
     'valid-v-bind-sync': require('./rules/valid-v-bind-sync'),
     'valid-v-bind': require('./rules/valid-v-bind'),
diff --git a/lib/rules/next-tick-style.js b/lib/rules/next-tick-style.js
new file mode 100644
index 00000000..d015bb5b
--- /dev/null
+++ b/lib/rules/next-tick-style.js
@@ -0,0 +1,146 @@
+/**
+ * @fileoverview enforce Promise or callback style in `nextTick`
+ * @author Flo Edelmann
+ * @copyright 2020 Flo Edelmann. All rights reserved.
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+const { findVariable } = require('eslint-utils')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * @param {Identifier} identifier
+ * @param {RuleContext} context
+ * @returns {CallExpression|undefined}
+ */
+function getVueNextTickCallExpression(identifier, context) {
+  // Instance API: this.$nextTick()
+  if (
+    identifier.name === '$nextTick' &&
+    identifier.parent.type === 'MemberExpression' &&
+    utils.isThis(identifier.parent.object, context) &&
+    identifier.parent.parent.type === 'CallExpression' &&
+    identifier.parent.parent.callee === identifier.parent
+  ) {
+    return identifier.parent.parent
+  }
+
+  // Vue 2 Global API: Vue.nextTick()
+  if (
+    identifier.name === 'nextTick' &&
+    identifier.parent.type === 'MemberExpression' &&
+    identifier.parent.object.type === 'Identifier' &&
+    identifier.parent.object.name === 'Vue' &&
+    identifier.parent.parent.type === 'CallExpression' &&
+    identifier.parent.parent.callee === identifier.parent
+  ) {
+    return identifier.parent.parent
+  }
+
+  // Vue 3 Global API: import { nextTick as nt } from 'vue'; nt()
+  if (
+    identifier.parent.type === 'CallExpression' &&
+    identifier.parent.callee === identifier
+  ) {
+    const variable = findVariable(context.getScope(), identifier)
+
+    if (variable != null && variable.defs.length === 1) {
+      const def = variable.defs[0]
+      if (
+        def.type === 'ImportBinding' &&
+        def.node.type === 'ImportSpecifier' &&
+        def.node.imported.type === 'Identifier' &&
+        def.node.imported.name === 'nextTick' &&
+        def.node.parent.type === 'ImportDeclaration' &&
+        def.node.parent.source.value === 'vue'
+      ) {
+        return identifier.parent
+      }
+    }
+  }
+
+  return undefined
+}
+
+/**
+ * @param {CallExpression} callExpression
+ * @returns {boolean}
+ */
+function isAwaitedPromise(callExpression) {
+  return (
+    callExpression.parent.type === 'AwaitExpression' ||
+    (callExpression.parent.type === 'MemberExpression' &&
+      callExpression.parent.property.type === 'Identifier' &&
+      callExpression.parent.property.name === 'then')
+  )
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'enforce Promise or callback style in `nextTick`',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/next-tick-style.html'
+    },
+    fixable: 'code',
+    schema: [{ enum: ['promise', 'callback'] }]
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    const preferredStyle =
+      /** @type {string|undefined} */ (context.options[0]) || 'promise'
+
+    return utils.defineVueVisitor(context, {
+      /** @param {Identifier} node */
+      Identifier(node) {
+        const callExpression = getVueNextTickCallExpression(node, context)
+        if (!callExpression) {
+          return
+        }
+
+        if (preferredStyle === 'callback') {
+          if (
+            callExpression.arguments.length !== 1 ||
+            isAwaitedPromise(callExpression)
+          ) {
+            context.report({
+              node,
+              message:
+                'Pass a callback function to `nextTick` instead of using the returned Promise.'
+            })
+          }
+
+          return
+        }
+
+        if (
+          callExpression.arguments.length !== 0 ||
+          !isAwaitedPromise(callExpression)
+        ) {
+          context.report({
+            node,
+            message:
+              'Use the Promise returned by `nextTick` instead of passing a callback function.',
+            fix(fixer) {
+              return fixer.insertTextAfter(node, '().then')
+            }
+          })
+        }
+      }
+    })
+  }
+}
diff --git a/lib/rules/no-ref-as-operand.js b/lib/rules/no-ref-as-operand.js
index d93d0422..465ec50b 100644
--- a/lib/rules/no-ref-as-operand.js
+++ b/lib/rules/no-ref-as-operand.js
@@ -134,8 +134,11 @@ module.exports = {
         reportIfRefWrapped(node)
       },
       // refValue+=1, refValue-=1, foo+=refValue, foo-=refValue
-      /** @param {Identifier} node */
+      /** @param {Identifier & {parent: AssignmentExpression}} node */
       'AssignmentExpression>Identifier'(node) {
+        if (node.parent.operator === '=' && node.parent.left !== node) {
+          return
+        }
         reportIfRefWrapped(node)
       },
       // refValue || other, refValue && other. ignore: other || refValue
diff --git a/lib/rules/return-in-computed-property.js b/lib/rules/return-in-computed-property.js
index 770e0c59..9908fe4f 100644
--- a/lib/rules/return-in-computed-property.js
+++ b/lib/rules/return-in-computed-property.js
@@ -3,7 +3,7 @@
  * @author Armano
  */
 'use strict'
-
+const { ReferenceTracker } = require('eslint-utils')
 const utils = require('../utils')
 
 /**
@@ -47,13 +47,36 @@ module.exports = {
      * @type {Set<ComponentComputedProperty>}
      */
     const computedProperties = new Set()
+    /** @type {(FunctionExpression | ArrowFunctionExpression)[]} */
+    const computedFunctionNodes = []
 
     // ----------------------------------------------------------------------
     // Public
     // ----------------------------------------------------------------------
 
     return Object.assign(
-      {},
+      {
+        Program() {
+          const tracker = new ReferenceTracker(context.getScope())
+          const traceMap = utils.createCompositionApiTraceMap({
+            [ReferenceTracker.ESM]: true,
+            computed: {
+              [ReferenceTracker.CALL]: true
+            }
+          })
+
+          for (const { node } of tracker.iterateEsmReferences(traceMap)) {
+            if (node.type !== 'CallExpression') {
+              continue
+            }
+
+            const getter = utils.getGetterBodyFromComputedFunction(node)
+            if (getter) {
+              computedFunctionNodes.push(getter)
+            }
+          }
+        }
+      },
       utils.defineVueVisitor(context, {
         onVueObjectEnter(obj) {
           for (const computedProperty of utils.getComputedProperties(obj)) {
@@ -76,6 +99,14 @@ module.exports = {
               })
             }
           })
+          computedFunctionNodes.forEach((cf) => {
+            if (cf === node) {
+              context.report({
+                node,
+                message: 'Expected to return a value in computed function.'
+              })
+            }
+          })
         }
       )
     )
diff --git a/lib/rules/valid-next-tick.js b/lib/rules/valid-next-tick.js
new file mode 100644
index 00000000..4e41abbf
--- /dev/null
+++ b/lib/rules/valid-next-tick.js
@@ -0,0 +1,204 @@
+/**
+ * @fileoverview enforce valid `nextTick` function calls
+ * @author Flo Edelmann
+ * @copyright 2021 Flo Edelmann. All rights reserved.
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+const { findVariable } = require('eslint-utils')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * @param {Identifier} identifier
+ * @param {RuleContext} context
+ * @returns {ASTNode|undefined}
+ */
+function getVueNextTickNode(identifier, context) {
+  // Instance API: this.$nextTick()
+  if (
+    identifier.name === '$nextTick' &&
+    identifier.parent.type === 'MemberExpression' &&
+    utils.isThis(identifier.parent.object, context)
+  ) {
+    return identifier.parent
+  }
+
+  // Vue 2 Global API: Vue.nextTick()
+  if (
+    identifier.name === 'nextTick' &&
+    identifier.parent.type === 'MemberExpression' &&
+    identifier.parent.object.type === 'Identifier' &&
+    identifier.parent.object.name === 'Vue'
+  ) {
+    return identifier.parent
+  }
+
+  // Vue 3 Global API: import { nextTick as nt } from 'vue'; nt()
+  const variable = findVariable(context.getScope(), identifier)
+
+  if (variable != null && variable.defs.length === 1) {
+    const def = variable.defs[0]
+    if (
+      def.type === 'ImportBinding' &&
+      def.node.type === 'ImportSpecifier' &&
+      def.node.imported.type === 'Identifier' &&
+      def.node.imported.name === 'nextTick' &&
+      def.node.parent.type === 'ImportDeclaration' &&
+      def.node.parent.source.value === 'vue'
+    ) {
+      return identifier
+    }
+  }
+
+  return undefined
+}
+
+/**
+ * @param {CallExpression} callExpression
+ * @returns {boolean}
+ */
+function isAwaitedPromise(callExpression) {
+  if (callExpression.parent.type === 'AwaitExpression') {
+    // cases like `await nextTick()`
+    return true
+  }
+
+  if (callExpression.parent.type === 'ReturnStatement') {
+    // cases like `return nextTick()`
+    return true
+  }
+
+  if (
+    callExpression.parent.type === 'MemberExpression' &&
+    callExpression.parent.property.type === 'Identifier' &&
+    callExpression.parent.property.name === 'then'
+  ) {
+    // cases like `nextTick().then()`
+    return true
+  }
+
+  if (
+    callExpression.parent.type === 'VariableDeclarator' ||
+    callExpression.parent.type === 'AssignmentExpression'
+  ) {
+    // cases like `let foo = nextTick()` or `foo = nextTick()`
+    return true
+  }
+
+  if (
+    callExpression.parent.type === 'ArrayExpression' &&
+    callExpression.parent.parent.type === 'CallExpression' &&
+    callExpression.parent.parent.callee.type === 'MemberExpression' &&
+    callExpression.parent.parent.callee.object.type === 'Identifier' &&
+    callExpression.parent.parent.callee.object.name === 'Promise' &&
+    callExpression.parent.parent.callee.property.type === 'Identifier'
+  ) {
+    // cases like `Promise.all([nextTick()])`
+    return true
+  }
+
+  return false
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'enforce valid `nextTick` function calls',
+      // categories: ['vue3-essential', 'essential'],
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/valid-next-tick.html'
+    },
+    fixable: 'code',
+    schema: []
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    return utils.defineVueVisitor(context, {
+      /** @param {Identifier} node */
+      Identifier(node) {
+        const nextTickNode = getVueNextTickNode(node, context)
+        if (!nextTickNode || !nextTickNode.parent) {
+          return
+        }
+
+        const parentNode = nextTickNode.parent
+
+        if (
+          parentNode.type === 'CallExpression' &&
+          parentNode.callee !== nextTickNode
+        ) {
+          // cases like `foo.then(nextTick)` are allowed
+          return
+        }
+
+        if (
+          parentNode.type === 'VariableDeclarator' ||
+          parentNode.type === 'AssignmentExpression'
+        ) {
+          // cases like `let foo = nextTick` or `foo = nextTick` are allowed
+          return
+        }
+
+        if (parentNode.type !== 'CallExpression') {
+          context.report({
+            node,
+            message: '`nextTick` is a function.',
+            fix(fixer) {
+              return fixer.insertTextAfter(node, '()')
+            }
+          })
+          return
+        }
+
+        if (parentNode.arguments.length === 0) {
+          if (!isAwaitedPromise(parentNode)) {
+            context.report({
+              node,
+              message:
+                'Await the Promise returned by `nextTick` or pass a callback function.',
+              suggest: [
+                {
+                  desc: 'Add missing `await` statement.',
+                  fix(fixer) {
+                    return fixer.insertTextBefore(parentNode, 'await ')
+                  }
+                }
+              ]
+            })
+          }
+          return
+        }
+
+        if (parentNode.arguments.length > 1) {
+          context.report({
+            node,
+            message: '`nextTick` expects zero or one parameters.'
+          })
+          return
+        }
+
+        if (isAwaitedPromise(parentNode)) {
+          context.report({
+            node,
+            message:
+              'Either await the Promise or pass a callback function to `nextTick`.'
+          })
+        }
+      }
+    })
+  }
+}
diff --git a/package.json b/package.json
index b907051f..8467ccbf 100644
--- a/package.json
+++ b/package.json
@@ -56,7 +56,7 @@
     "eslint-utils": "^2.1.0",
     "natural-compare": "^1.4.0",
     "semver": "^7.3.2",
-    "vue-eslint-parser": "^7.3.0"
+    "vue-eslint-parser": "^7.4.1"
   },
   "devDependencies": {
     "@types/eslint": "^7.2.0",


Test patch:
diff --git a/tests/lib/rules/next-tick-style.js b/tests/lib/rules/next-tick-style.js
new file mode 100644
index 00000000..9f2d0ae5
--- /dev/null
+++ b/tests/lib/rules/next-tick-style.js
@@ -0,0 +1,304 @@
+/**
+ * @fileoverview enforce Promise or callback style in `nextTick`
+ * @author Flo Edelmann
+ * @copyright 2020 Flo Edelmann. All rights reserved.
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/next-tick-style')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2017,
+    sourceType: 'module'
+  }
+})
+
+tester.run('next-tick-style', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: ''
+    },
+    {
+      filename: 'test.vue',
+      code: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        async mounted() {
+          this.$nextTick().then(() => callback());
+          Vue.nextTick().then(() => callback());
+          nt().then(() => callback());
+
+          await this.$nextTick(); callback();
+          await Vue.nextTick(); callback();
+          await nt(); callback();
+        }
+      }</script>`
+    },
+    {
+      filename: 'test.vue',
+      options: ['promise'],
+      code: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        async mounted() {
+          this.$nextTick().then(() => callback());
+          Vue.nextTick().then(() => callback());
+          nt().then(() => callback());
+
+          await this.$nextTick(); callback();
+          await Vue.nextTick(); callback();
+          await nt(); callback();
+        }
+      }</script>`
+    },
+    {
+      filename: 'test.vue',
+      options: ['callback'],
+      code: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        mounted() {
+          this.$nextTick(() => callback());
+          Vue.nextTick(() => callback());
+          nt(() => callback());
+
+          this.$nextTick(callback);
+          Vue.nextTick(callback);
+          nt(callback);
+        }
+      }</script>`
+    },
+
+    // https://github.com/vuejs/eslint-plugin-vue/pull/1400#discussion_r550937977
+    {
+      filename: 'test.vue',
+      options: ['promise'],
+      code: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        mounted() {
+          foo.then(this.$nextTick);
+          foo.then(Vue.nextTick);
+          foo.then(nt);
+
+          foo.then(nt, catchHandler);
+          foo.then(Vue.nextTick, catchHandler);
+          foo.then(this.$nextTick, catchHandler);
+        }
+      }</script>`
+    },
+    {
+      filename: 'test.vue',
+      options: ['callback'],
+      code: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        mounted() {
+          foo.then(this.$nextTick);
+          foo.then(Vue.nextTick);
+          foo.then(nt);
+
+          foo.then(nt, catchHandler);
+          foo.then(Vue.nextTick, catchHandler);
+          foo.then(this.$nextTick, catchHandler);
+        }
+      }</script>`
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        mounted() {
+          this.$nextTick(() => callback());
+          Vue.nextTick(() => callback());
+          nt(() => callback());
+
+          this.$nextTick(callback);
+          Vue.nextTick(callback);
+          nt(callback);
+        }
+      }</script>`,
+      output: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        mounted() {
+          this.$nextTick().then(() => callback());
+          Vue.nextTick().then(() => callback());
+          nt().then(() => callback());
+
+          this.$nextTick().then(callback);
+          Vue.nextTick().then(callback);
+          nt().then(callback);
+        }
+      }</script>`,
+      errors: [
+        {
+          message:
+            'Use the Promise returned by `nextTick` instead of passing a callback function.',
+          line: 4,
+          column: 16
+        },
+        {
+          message:
+            'Use the Promise returned by `nextTick` instead of passing a callback function.',
+          line: 5,
+          column: 15
+        },
+        {
+          message:
+            'Use the Promise returned by `nextTick` instead of passing a callback function.',
+          line: 6,
+          column: 11
+        },
+        {
+          message:
+            'Use the Promise returned by `nextTick` instead of passing a callback function.',
+          line: 8,
+          column: 16
+        },
+        {
+          message:
+            'Use the Promise returned by `nextTick` instead of passing a callback function.',
+          line: 9,
+          column: 15
+        },
+        {
+          message:
+            'Use the Promise returned by `nextTick` instead of passing a callback function.',
+          line: 10,
+          column: 11
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      options: ['promise'],
+      code: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        mounted() {
+          this.$nextTick(() => callback());
+          Vue.nextTick(() => callback());
+          nt(() => callback());
+
+          this.$nextTick(callback);
+          Vue.nextTick(callback);
+          nt(callback);
+        }
+      }</script>`,
+      output: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        mounted() {
+          this.$nextTick().then(() => callback());
+          Vue.nextTick().then(() => callback());
+          nt().then(() => callback());
+
+          this.$nextTick().then(callback);
+          Vue.nextTick().then(callback);
+          nt().then(callback);
+        }
+      }</script>`,
+      errors: [
+        {
+          message:
+            'Use the Promise returned by `nextTick` instead of passing a callback function.',
+          line: 4,
+          column: 16
+        },
+        {
+          message:
+            'Use the Promise returned by `nextTick` instead of passing a callback function.',
+          line: 5,
+          column: 15
+        },
+        {
+          message:
+            'Use the Promise returned by `nextTick` instead of passing a callback function.',
+          line: 6,
+          column: 11
+        },
+        {
+          message:
+            'Use the Promise returned by `nextTick` instead of passing a callback function.',
+          line: 8,
+          column: 16
+        },
+        {
+          message:
+            'Use the Promise returned by `nextTick` instead of passing a callback function.',
+          line: 9,
+          column: 15
+        },
+        {
+          message:
+            'Use the Promise returned by `nextTick` instead of passing a callback function.',
+          line: 10,
+          column: 11
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      options: ['callback'],
+      code: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        async mounted() {
+          this.$nextTick().then(() => callback());
+          Vue.nextTick().then(() => callback());
+          nt().then(() => callback());
+
+          await this.$nextTick(); callback();
+          await Vue.nextTick(); callback();
+          await nt(); callback();
+        }
+      }</script>`,
+      output: null,
+      errors: [
+        {
+          message:
+            'Pass a callback function to `nextTick` instead of using the returned Promise.',
+          line: 4,
+          column: 16
+        },
+        {
+          message:
+            'Pass a callback function to `nextTick` instead of using the returned Promise.',
+          line: 5,
+          column: 15
+        },
+        {
+          message:
+            'Pass a callback function to `nextTick` instead of using the returned Promise.',
+          line: 6,
+          column: 11
+        },
+        {
+          message:
+            'Pass a callback function to `nextTick` instead of using the returned Promise.',
+          line: 8,
+          column: 22
+        },
+        {
+          message:
+            'Pass a callback function to `nextTick` instead of using the returned Promise.',
+          line: 9,
+          column: 21
+        },
+        {
+          message:
+            'Pass a callback function to `nextTick` instead of using the returned Promise.',
+          line: 10,
+          column: 17
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-parsing-error.js b/tests/lib/rules/no-parsing-error.js
index c0c1ce61..46733a3f 100644
--- a/tests/lib/rules/no-parsing-error.js
+++ b/tests/lib/rules/no-parsing-error.js
@@ -249,7 +249,10 @@ tester.run('no-parsing-error', rule, {
       filename: 'test.vue',
       code: '<template><div v-for="foo">hello</div></template>',
       errors: [
-        { message: 'Parsing error: Unexpected end of expression.', column: 26 }
+        {
+          message: 'Parsing error: Expected to be an alias, but got empty.',
+          column: 23
+        }
       ]
     },
     {
diff --git a/tests/lib/rules/no-ref-as-operand.js b/tests/lib/rules/no-ref-as-operand.js
index 7cdc61b7..fb564ce1 100644
--- a/tests/lib/rules/no-ref-as-operand.js
+++ b/tests/lib/rules/no-ref-as-operand.js
@@ -121,6 +121,16 @@ tester.run('no-ref-as-operand', rule, {
     const count = ref
     count++
     `,
+    `
+    import { ref } from 'vue'
+    const count = ref(0)
+    foo = count
+    `,
+    `
+    import { ref } from 'vue'
+    const count = ref(0)
+    const foo = count
+    `,
     {
       code: `
       <script>
diff --git a/tests/lib/rules/return-in-computed-property.js b/tests/lib/rules/return-in-computed-property.js
index a7c14a51..df6b1b68 100644
--- a/tests/lib/rules/return-in-computed-property.js
+++ b/tests/lib/rules/return-in-computed-property.js
@@ -104,6 +104,56 @@ ruleTester.run('return-in-computed-property', rule, {
       `,
       parserOptions,
       options: [{ treatUndefinedAsUnspecified: false }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        import {computed} from 'vue'
+        export default {
+          setup() {
+            const foo = computed(() => true)
+            const bar = computed(function() {
+              return false
+            })
+            const bar3 = computed({
+              set: () => true,
+              get: () => true
+            })
+            const bar4 = computed(() => {
+              if (foo) {
+                return true
+              } else {
+                return false
+              }
+            })
+            const foo2 = computed(() => {
+              const options = []
+              this.matches.forEach((match) => {
+                options.push(match)
+              })
+              return options
+            })
+          }
+        }
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        import {computed} from 'vue'
+        export default {
+          setup() {
+            const foo = computed({
+              get: () => {
+                return
+              }
+            })
+          }
+        }
+      `,
+      parserOptions,
+      options: [{ treatUndefinedAsUnspecified: false }]
     }
   ],
 
@@ -272,6 +322,78 @@ ruleTester.run('return-in-computed-property', rule, {
           line: 5
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        import {computed} from 'vue'
+        export default {
+          setup() {
+            const foo = computed(() => {})
+            const foo2 = computed(function() {})
+            const foo3 = computed(() => {
+              if (a) {
+                return
+              }
+            })
+            const foo4 = computed({
+              set: () => {},
+              get: () => {}
+            })
+            const foo5 = computed(() => {
+              const bar = () => {
+                return this.baz * 2
+              }
+              bar()
+            })
+          }
+        }
+      `,
+      parserOptions,
+      errors: [
+        {
+          message: 'Expected to return a value in computed function.',
+          line: 5
+        },
+        {
+          message: 'Expected to return a value in computed function.',
+          line: 6
+        },
+        {
+          message: 'Expected to return a value in computed function.',
+          line: 7
+        },
+        {
+          message: 'Expected to return a value in computed function.',
+          line: 14
+        },
+        {
+          message: 'Expected to return a value in computed function.',
+          line: 16
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        import {computed} from 'vue'
+        export default {
+          setup() {
+            const foo = computed(() => {})
+            const baz = computed(() => {
+              return
+            })
+          }
+        }
+      `,
+      parserOptions,
+      options: [{ treatUndefinedAsUnspecified: false }],
+      errors: [
+        {
+          message: 'Expected to return a value in computed function.',
+          line: 5
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/valid-next-tick.js b/tests/lib/rules/valid-next-tick.js
new file mode 100644
index 00000000..220e4341
--- /dev/null
+++ b/tests/lib/rules/valid-next-tick.js
@@ -0,0 +1,405 @@
+/**
+ * @fileoverview enforce valid `nextTick` function calls
+ * @author Flo Edelmann
+ * @copyright 2021 Flo Edelmann. All rights reserved.
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/valid-next-tick')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2017,
+    sourceType: 'module'
+  }
+})
+
+tester.run('valid-next-tick', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: ''
+    },
+    {
+      filename: 'test.vue',
+      code: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        async mounted() {
+          await nt();
+          await Vue.nextTick();
+          await this.$nextTick();
+
+          nt().then(callback);
+          Vue.nextTick().then(callback);
+          this.$nextTick().then(callback);
+
+          nt(callback);
+          Vue.nextTick(callback);
+          this.$nextTick(callback);
+        }
+      }</script>`
+    },
+
+    // https://github.com/vuejs/eslint-plugin-vue/pull/1404#discussion_r550937500
+    {
+      filename: 'test.vue',
+      code: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        mounted() {
+          foo.then(nt);
+          foo.then(Vue.nextTick);
+          foo.then(this.$nextTick);
+
+          foo.then(nt, catchHandler);
+          foo.then(Vue.nextTick, catchHandler);
+          foo.then(this.$nextTick, catchHandler);
+        }
+      }</script>`
+    },
+
+    // https://github.com/vuejs/eslint-plugin-vue/pull/1404#discussion_r550936410
+    {
+      filename: 'test.vue',
+      code: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        mounted() {
+          let foo = nt;
+          foo = Vue.nextTick;
+          foo = this.$nextTick;
+        }
+      }</script>`
+    },
+
+    // https://github.com/vuejs/eslint-plugin-vue/pull/1404#discussion_r550936933
+    {
+      filename: 'test.vue',
+      code: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        mounted() {
+          Promise.all([nt(), someOtherPromise]);
+          Promise.all([Vue.nextTick(), someOtherPromise]);
+          Promise.all([this.$nextTick(), someOtherPromise]);
+        }
+      }</script>`
+    },
+
+    // https://github.com/vuejs/eslint-plugin-vue/pull/1404#discussion_r551769969
+    {
+      filename: 'test.vue',
+      code: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        created() {
+          let queue = nt();
+          queue = queue.then(nt);
+          return nt();
+        },
+        mounted() {
+          const queue = Vue.nextTick();
+          return Vue.nextTick();
+        },
+        updated() {
+          const queue = this.$nextTick();
+          return this.$nextTick();
+        }
+      }</script>`
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        async mounted() {
+          nt();
+          Vue.nextTick();
+          this.$nextTick();
+        }
+      }</script>`,
+      output: null,
+      errors: [
+        {
+          message:
+            'Await the Promise returned by `nextTick` or pass a callback function.',
+          line: 4,
+          column: 11,
+          suggestions: [
+            {
+              output: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        async mounted() {
+          await nt();
+          Vue.nextTick();
+          this.$nextTick();
+        }
+      }</script>`
+            }
+          ]
+        },
+        {
+          message:
+            'Await the Promise returned by `nextTick` or pass a callback function.',
+          line: 5,
+          column: 15,
+          suggestions: [
+            {
+              output: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        async mounted() {
+          nt();
+          await Vue.nextTick();
+          this.$nextTick();
+        }
+      }</script>`
+            }
+          ]
+        },
+        {
+          message:
+            'Await the Promise returned by `nextTick` or pass a callback function.',
+          line: 6,
+          column: 16,
+          suggestions: [
+            {
+              output: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        async mounted() {
+          nt();
+          Vue.nextTick();
+          await this.$nextTick();
+        }
+      }</script>`
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        mounted() {
+          nt;
+          Vue.nextTick;
+          this.$nextTick;
+
+          nt.then(callback);
+          Vue.nextTick.then(callback);
+          this.$nextTick.then(callback);
+        }
+      }</script>`,
+      output: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        mounted() {
+          nt();
+          Vue.nextTick();
+          this.$nextTick();
+
+          nt().then(callback);
+          Vue.nextTick().then(callback);
+          this.$nextTick().then(callback);
+        }
+      }</script>`,
+      errors: [
+        {
+          message: '`nextTick` is a function.',
+          line: 4,
+          column: 11
+        },
+        {
+          message: '`nextTick` is a function.',
+          line: 5,
+          column: 15
+        },
+        {
+          message: '`nextTick` is a function.',
+          line: 6,
+          column: 16
+        },
+        {
+          message: '`nextTick` is a function.',
+          line: 8,
+          column: 11
+        },
+        {
+          message: '`nextTick` is a function.',
+          line: 9,
+          column: 15
+        },
+        {
+          message: '`nextTick` is a function.',
+          line: 10,
+          column: 16
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        async mounted() {
+          await nt;
+          await Vue.nextTick;
+          return this.$nextTick;
+        }
+      }</script>`,
+      output: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        async mounted() {
+          await nt();
+          await Vue.nextTick();
+          return this.$nextTick();
+        }
+      }</script>`,
+      errors: [
+        {
+          message: '`nextTick` is a function.',
+          line: 4,
+          column: 17
+        },
+        {
+          message: '`nextTick` is a function.',
+          line: 5,
+          column: 21
+        },
+        {
+          message: '`nextTick` is a function.',
+          line: 6,
+          column: 23
+        }
+      ]
+    },
+
+    // https://github.com/vuejs/eslint-plugin-vue/pull/1404#discussion_r550936933
+    {
+      filename: 'test.vue',
+      code: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        mounted() {
+          Promise.all([nt, someOtherPromise]);
+          Promise.all([Vue.nextTick, someOtherPromise]);
+          Promise.all([this.$nextTick, someOtherPromise]);
+        }
+      }</script>`,
+      output: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        mounted() {
+          Promise.all([nt(), someOtherPromise]);
+          Promise.all([Vue.nextTick(), someOtherPromise]);
+          Promise.all([this.$nextTick(), someOtherPromise]);
+        }
+      }</script>`,
+      errors: [
+        {
+          message: '`nextTick` is a function.',
+          line: 4,
+          column: 24
+        },
+        {
+          message: '`nextTick` is a function.',
+          line: 5,
+          column: 28
+        },
+        {
+          message: '`nextTick` is a function.',
+          line: 6,
+          column: 29
+        }
+      ]
+    },
+
+    {
+      filename: 'test.vue',
+      code: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        mounted() {
+          nt(callback, anotherCallback);
+          Vue.nextTick(callback, anotherCallback);
+          this.$nextTick(callback, anotherCallback);
+        }
+      }</script>`,
+      output: null,
+      errors: [
+        {
+          message: '`nextTick` expects zero or one parameters.',
+          line: 4,
+          column: 11
+        },
+        {
+          message: '`nextTick` expects zero or one parameters.',
+          line: 5,
+          column: 15
+        },
+        {
+          message: '`nextTick` expects zero or one parameters.',
+          line: 6,
+          column: 16
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<script>import { nextTick as nt } from 'vue';
+      export default {
+        async mounted() {
+          nt(callback).then(anotherCallback);
+          Vue.nextTick(callback).then(anotherCallback);
+          this.$nextTick(callback).then(anotherCallback);
+
+          await nt(callback);
+          await Vue.nextTick(callback);
+          await this.$nextTick(callback);
+        }
+      }</script>`,
+      output: null,
+      errors: [
+        {
+          message:
+            'Either await the Promise or pass a callback function to `nextTick`.',
+          line: 4,
+          column: 11
+        },
+        {
+          message:
+            'Either await the Promise or pass a callback function to `nextTick`.',
+          line: 5,
+          column: 15
+        },
+        {
+          message:
+            'Either await the Promise or pass a callback function to `nextTick`.',
+          line: 6,
+          column: 16
+        },
+        {
+          message:
+            'Either await the Promise or pass a callback function to `nextTick`.',
+          line: 8,
+          column: 17
+        },
+        {
+          message:
+            'Either await the Promise or pass a callback function to `nextTick`.',
+          line: 9,
+          column: 21
+        },
+        {
+          message:
+            'Either await the Promise or pass a callback function to `nextTick`.',
+          line: 10,
+          column: 22
+        }
+      ]
+    }
+  ]
+})