Skip to content

Commit 10824ec

Browse files
authored
Add vue/valid-next-tick rule (#1404)
* 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
1 parent ec2dc79 commit 10824ec

File tree

5 files changed

+699
-0
lines changed

5 files changed

+699
-0
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ For example:
325325
| [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: |
326326
| [vue/v-on-event-hyphenation](./v-on-event-hyphenation.md) | enforce v-on event naming style on custom components in template | :wrench: |
327327
| [vue/v-on-function-call](./v-on-function-call.md) | enforce or forbid parentheses after method calls without arguments in `v-on` directives | :wrench: |
328+
| [vue/valid-next-tick](./valid-next-tick.md) | enforce valid `nextTick` function calls | :wrench: |
328329

329330
### Extension Rules
330331

docs/rules/valid-next-tick.md

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/valid-next-tick
5+
description: enforce valid `nextTick` function calls
6+
---
7+
# vue/valid-next-tick
8+
9+
> enforce valid `nextTick` function calls
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
- :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.
13+
14+
## :book: Rule Details
15+
16+
Calling `Vue.nextTick` or `vm.$nextTick` without passing a callback and without awaiting the returned Promise is likely a mistake (probably a missing `await`).
17+
18+
<eslint-code-block fix :rules="{'vue/valid-next-tick': ['error']}">
19+
20+
```vue
21+
<script>
22+
import { nextTick as nt } from 'vue';
23+
24+
export default {
25+
async mounted() {
26+
/* ✗ BAD: no callback function or awaited Promise */
27+
nt();
28+
Vue.nextTick();
29+
this.$nextTick();
30+
31+
/* ✗ BAD: too many parameters */
32+
nt(callback, anotherCallback);
33+
Vue.nextTick(callback, anotherCallback);
34+
this.$nextTick(callback, anotherCallback);
35+
36+
/* ✗ BAD: no function call */
37+
nt.then(callback);
38+
Vue.nextTick.then(callback);
39+
this.$nextTick.then(callback);
40+
await nt;
41+
await Vue.nextTick;
42+
await this.$nextTick;
43+
44+
/* ✗ BAD: both callback function and awaited Promise */
45+
nt(callback).then(anotherCallback);
46+
Vue.nextTick(callback).then(anotherCallback);
47+
this.$nextTick(callback).then(anotherCallback);
48+
await nt(callback);
49+
await Vue.nextTick(callback);
50+
await this.$nextTick(callback);
51+
52+
/* ✓ GOOD */
53+
await nt();
54+
await Vue.nextTick();
55+
await this.$nextTick();
56+
57+
/* ✓ GOOD */
58+
nt().then(callback);
59+
Vue.nextTick().then(callback);
60+
this.$nextTick().then(callback);
61+
62+
/* ✓ GOOD */
63+
nt(callback);
64+
Vue.nextTick(callback);
65+
this.$nextTick(callback);
66+
}
67+
}
68+
</script>
69+
```
70+
71+
</eslint-code-block>
72+
73+
## :wrench: Options
74+
75+
Nothing.
76+
77+
## :books: Further Reading
78+
79+
- [`Vue.nextTick` API in Vue 2](https://vuejs.org/v2/api/#Vue-nextTick)
80+
- [`vm.$nextTick` API in Vue 2](https://vuejs.org/v2/api/#vm-nextTick)
81+
- [Global API Treeshaking](https://v3.vuejs.org/guide/migration/global-api-treeshaking.html)
82+
- [Global `nextTick` API in Vue 3](https://v3.vuejs.org/api/global-api.html#nexttick)
83+
- [Instance `$nextTick` API in Vue 3](https://v3.vuejs.org/api/instance-methods.html#nexttick)
84+
85+
## :mag: Implementation
86+
87+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-next-tick.js)
88+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-next-tick.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ module.exports = {
164164
'v-on-function-call': require('./rules/v-on-function-call'),
165165
'v-on-style': require('./rules/v-on-style'),
166166
'v-slot-style': require('./rules/v-slot-style'),
167+
'valid-next-tick': require('./rules/valid-next-tick'),
167168
'valid-template-root': require('./rules/valid-template-root'),
168169
'valid-v-bind-sync': require('./rules/valid-v-bind-sync'),
169170
'valid-v-bind': require('./rules/valid-v-bind'),

lib/rules/valid-next-tick.js

+204
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/**
2+
* @fileoverview enforce valid `nextTick` function calls
3+
* @author Flo Edelmann
4+
* @copyright 2021 Flo Edelmann. All rights reserved.
5+
* See LICENSE file in root directory for full license.
6+
*/
7+
'use strict'
8+
9+
// ------------------------------------------------------------------------------
10+
// Requirements
11+
// ------------------------------------------------------------------------------
12+
13+
const utils = require('../utils')
14+
const { findVariable } = require('eslint-utils')
15+
16+
// ------------------------------------------------------------------------------
17+
// Helpers
18+
// ------------------------------------------------------------------------------
19+
20+
/**
21+
* @param {Identifier} identifier
22+
* @param {RuleContext} context
23+
* @returns {ASTNode|undefined}
24+
*/
25+
function getVueNextTickNode(identifier, context) {
26+
// Instance API: this.$nextTick()
27+
if (
28+
identifier.name === '$nextTick' &&
29+
identifier.parent.type === 'MemberExpression' &&
30+
utils.isThis(identifier.parent.object, context)
31+
) {
32+
return identifier.parent
33+
}
34+
35+
// Vue 2 Global API: Vue.nextTick()
36+
if (
37+
identifier.name === 'nextTick' &&
38+
identifier.parent.type === 'MemberExpression' &&
39+
identifier.parent.object.type === 'Identifier' &&
40+
identifier.parent.object.name === 'Vue'
41+
) {
42+
return identifier.parent
43+
}
44+
45+
// Vue 3 Global API: import { nextTick as nt } from 'vue'; nt()
46+
const variable = findVariable(context.getScope(), identifier)
47+
48+
if (variable != null && variable.defs.length === 1) {
49+
const def = variable.defs[0]
50+
if (
51+
def.type === 'ImportBinding' &&
52+
def.node.type === 'ImportSpecifier' &&
53+
def.node.imported.type === 'Identifier' &&
54+
def.node.imported.name === 'nextTick' &&
55+
def.node.parent.type === 'ImportDeclaration' &&
56+
def.node.parent.source.value === 'vue'
57+
) {
58+
return identifier
59+
}
60+
}
61+
62+
return undefined
63+
}
64+
65+
/**
66+
* @param {CallExpression} callExpression
67+
* @returns {boolean}
68+
*/
69+
function isAwaitedPromise(callExpression) {
70+
if (callExpression.parent.type === 'AwaitExpression') {
71+
// cases like `await nextTick()`
72+
return true
73+
}
74+
75+
if (callExpression.parent.type === 'ReturnStatement') {
76+
// cases like `return nextTick()`
77+
return true
78+
}
79+
80+
if (
81+
callExpression.parent.type === 'MemberExpression' &&
82+
callExpression.parent.property.type === 'Identifier' &&
83+
callExpression.parent.property.name === 'then'
84+
) {
85+
// cases like `nextTick().then()`
86+
return true
87+
}
88+
89+
if (
90+
callExpression.parent.type === 'VariableDeclarator' ||
91+
callExpression.parent.type === 'AssignmentExpression'
92+
) {
93+
// cases like `let foo = nextTick()` or `foo = nextTick()`
94+
return true
95+
}
96+
97+
if (
98+
callExpression.parent.type === 'ArrayExpression' &&
99+
callExpression.parent.parent.type === 'CallExpression' &&
100+
callExpression.parent.parent.callee.type === 'MemberExpression' &&
101+
callExpression.parent.parent.callee.object.type === 'Identifier' &&
102+
callExpression.parent.parent.callee.object.name === 'Promise' &&
103+
callExpression.parent.parent.callee.property.type === 'Identifier'
104+
) {
105+
// cases like `Promise.all([nextTick()])`
106+
return true
107+
}
108+
109+
return false
110+
}
111+
112+
// ------------------------------------------------------------------------------
113+
// Rule Definition
114+
// ------------------------------------------------------------------------------
115+
116+
module.exports = {
117+
meta: {
118+
type: 'problem',
119+
docs: {
120+
description: 'enforce valid `nextTick` function calls',
121+
// categories: ['vue3-essential', 'essential'],
122+
categories: undefined,
123+
url: 'https://eslint.vuejs.org/rules/valid-next-tick.html'
124+
},
125+
fixable: 'code',
126+
schema: []
127+
},
128+
/** @param {RuleContext} context */
129+
create(context) {
130+
return utils.defineVueVisitor(context, {
131+
/** @param {Identifier} node */
132+
Identifier(node) {
133+
const nextTickNode = getVueNextTickNode(node, context)
134+
if (!nextTickNode || !nextTickNode.parent) {
135+
return
136+
}
137+
138+
const parentNode = nextTickNode.parent
139+
140+
if (
141+
parentNode.type === 'CallExpression' &&
142+
parentNode.callee !== nextTickNode
143+
) {
144+
// cases like `foo.then(nextTick)` are allowed
145+
return
146+
}
147+
148+
if (
149+
parentNode.type === 'VariableDeclarator' ||
150+
parentNode.type === 'AssignmentExpression'
151+
) {
152+
// cases like `let foo = nextTick` or `foo = nextTick` are allowed
153+
return
154+
}
155+
156+
if (parentNode.type !== 'CallExpression') {
157+
context.report({
158+
node,
159+
message: '`nextTick` is a function.',
160+
fix(fixer) {
161+
return fixer.insertTextAfter(node, '()')
162+
}
163+
})
164+
return
165+
}
166+
167+
if (parentNode.arguments.length === 0) {
168+
if (!isAwaitedPromise(parentNode)) {
169+
context.report({
170+
node,
171+
message:
172+
'Await the Promise returned by `nextTick` or pass a callback function.',
173+
suggest: [
174+
{
175+
desc: 'Add missing `await` statement.',
176+
fix(fixer) {
177+
return fixer.insertTextBefore(parentNode, 'await ')
178+
}
179+
}
180+
]
181+
})
182+
}
183+
return
184+
}
185+
186+
if (parentNode.arguments.length > 1) {
187+
context.report({
188+
node,
189+
message: '`nextTick` expects zero or one parameters.'
190+
})
191+
return
192+
}
193+
194+
if (isAwaitedPromise(parentNode)) {
195+
context.report({
196+
node,
197+
message:
198+
'Either await the Promise or pass a callback function to `nextTick`.'
199+
})
200+
}
201+
}
202+
})
203+
}
204+
}

0 commit comments

Comments
 (0)