Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add vue/valid-next-tick rule #1404

Merged
merged 11 commits into from Jan 21, 2021
2 changes: 2 additions & 0 deletions docs/rules/README.md
Expand Up @@ -86,6 +86,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
| [vue/return-in-computed-property](./return-in-computed-property.md) | enforce that a return statement is present in computed property | |
| [vue/return-in-emits-validator](./return-in-emits-validator.md) | enforce that a return statement is present in emits validator | |
| [vue/use-v-on-exact](./use-v-on-exact.md) | enforce usage of `exact` modifier on `v-on` | |
| [vue/valid-next-tick](./valid-next-tick.md) | enforce valid `nextTick` function calls | :wrench: |
| [vue/valid-template-root](./valid-template-root.md) | enforce valid template root | |
| [vue/valid-v-bind](./valid-v-bind.md) | enforce valid `v-bind` directives | |
| [vue/valid-v-cloak](./valid-v-cloak.md) | enforce valid `v-cloak` directives | |
Expand Down Expand Up @@ -197,6 +198,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
| [vue/require-valid-default-prop](./require-valid-default-prop.md) | enforce props default values to be valid | |
| [vue/return-in-computed-property](./return-in-computed-property.md) | enforce that a return statement is present in computed property | |
| [vue/use-v-on-exact](./use-v-on-exact.md) | enforce usage of `exact` modifier on `v-on` | |
| [vue/valid-next-tick](./valid-next-tick.md) | enforce valid `nextTick` function calls | :wrench: |
| [vue/valid-template-root](./valid-template-root.md) | enforce valid template root | |
| [vue/valid-v-bind-sync](./valid-v-bind-sync.md) | enforce valid `.sync` modifier on `v-bind` directives | |
| [vue/valid-v-bind](./valid-v-bind.md) | enforce valid `v-bind` directives | |
Expand Down
89 changes: 89 additions & 0 deletions docs/rules/valid-next-tick.md
@@ -0,0 +1,89 @@
---
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>
- :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"`.
- :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)
1 change: 1 addition & 0 deletions lib/configs/essential.js
Expand Up @@ -32,6 +32,7 @@ module.exports = {
'vue/require-valid-default-prop': 'error',
'vue/return-in-computed-property': 'error',
'vue/use-v-on-exact': 'error',
'vue/valid-next-tick': 'error',
'vue/valid-template-root': 'error',
'vue/valid-v-bind-sync': 'error',
'vue/valid-v-bind': 'error',
Expand Down
1 change: 1 addition & 0 deletions lib/configs/vue3-essential.js
Expand Up @@ -53,6 +53,7 @@ module.exports = {
'vue/return-in-computed-property': 'error',
'vue/return-in-emits-validator': 'error',
'vue/use-v-on-exact': 'error',
'vue/valid-next-tick': 'error',
'vue/valid-template-root': 'error',
'vue/valid-v-bind': 'error',
'vue/valid-v-cloak': 'error',
Expand Down
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -162,6 +162,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'),
Expand Down
167 changes: 167 additions & 0 deletions lib/rules/valid-next-tick.js
@@ -0,0 +1,167 @@
/**
* @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 getVueNextTickParentNode(identifier, context) {
// Instance API: this.$nextTick()
if (
identifier.name === '$nextTick' &&
identifier.parent.type === 'MemberExpression' &&
utils.isThis(identifier.parent.object, context)
) {
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'
) {
return identifier.parent.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.parent
}
}

return undefined
}

/**
* @param {CallExpression} callExpression
* @returns {boolean}
*/
function isAwaitedPromise(callExpression) {
return (
FloEdelmann marked this conversation as resolved.
Show resolved Hide resolved
callExpression.parent.type === 'AwaitExpression' ||
(callExpression.parent.type === 'MemberExpression' &&
callExpression.parent.property.type === 'Identifier' &&
callExpression.parent.property.name === 'then')
)
}

/**
* @param {ASTNode} node
* @returns {FunctionExpression|undefined}
*/
function getClosestFunction(node) {
while (node.parent !== null) {
node = /** @type {ASTNode} */ (node.parent)

if (node.type === 'FunctionExpression') {
return node
}
}

return undefined
}

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

module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `nextTick` function calls',
categories: ['vue3-essential', 'essential'],
FloEdelmann marked this conversation as resolved.
Show resolved Hide resolved
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 parentNode = getVueNextTickParentNode(node, context)
if (!parentNode) {
return
}

if (parentNode.type !== 'CallExpression') {
context.report({
FloEdelmann marked this conversation as resolved.
Show resolved Hide resolved
node,
message: '`nextTick` is a function.',
fix(fixer) {
return fixer.insertTextAfter(node, '()')
}
})
return
}

if (parentNode.arguments.length === 0) {
FloEdelmann marked this conversation as resolved.
Show resolved Hide resolved
if (!isAwaitedPromise(parentNode)) {
const closestFunction = getClosestFunction(parentNode)
const isAsyncFunction = closestFunction
? closestFunction.async
: false

context.report({
node,
message:
'Await the Promise returned by `nextTick` or pass a callback function.',
fix: isAsyncFunction
? (fixer) => fixer.insertTextBefore(parentNode, 'await ')
FloEdelmann marked this conversation as resolved.
Show resolved Hide resolved
: undefined
})
}
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`.'
})
}
}
})
}
}