Skip to content

Commit

Permalink
Add vue/valid-next-tick rule (#1404)
Browse files Browse the repository at this point in the history
* Add rule docs

* Add rule to collections

* Add rule tests

* Add more test cases

* Add rule implementation

* Fix docs

* Update docs

* Remove categories

* Prevent false positives for `foo.then(nextTick)`

* Suggest `await` instead of fixing it

* Add tests and logic for more false positives
  • Loading branch information
FloEdelmann committed Jan 21, 2021
1 parent ec2dc79 commit 10824ec
Show file tree
Hide file tree
Showing 5 changed files with 699 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -325,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

Expand Down
88 changes: 88 additions & 0 deletions 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)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -164,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'),
Expand Down
204 changes: 204 additions & 0 deletions 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`.'
})
}
}
})
}
}

0 comments on commit 10824ec

Please sign in to comment.