Skip to content

Commit ec2dc79

Browse files
authored
Add vue/next-tick-style rule (#1400)
* Add rule docs * Add rule tests * Initial implementation: detect nextTick calls * Finish rule implementation * Add fixer for promise style * Add new rule to rule collections * Improve short description * Use `output: null` instead of copying the unchanged code * Update documentation with update tool * Simplify rule fix code * Drop unused `recommended` property * Fix require path * Update docs * Fix docs * Prevent false positives for `foo.then(nextTick)` * Update rule type * Add `foo.then(nextTick, catchHandler)` test case
1 parent 24eccfb commit ec2dc79

File tree

5 files changed

+555
-0
lines changed

5 files changed

+555
-0
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ For example:
294294
| [vue/html-comment-indent](./html-comment-indent.md) | enforce consistent indentation in HTML comments | :wrench: |
295295
| [vue/match-component-file-name](./match-component-file-name.md) | require component name property to match its file name | |
296296
| [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: |
297+
| [vue/next-tick-style](./next-tick-style.md) | enforce Promise or callback style in `nextTick` | :wrench: |
297298
| [vue/no-bare-strings-in-template](./no-bare-strings-in-template.md) | disallow the use of bare strings in `<template>` | |
298299
| [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: |
299300
| [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` | |

docs/rules/next-tick-style.md

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/next-tick-style
5+
description: enforce Promise or callback style in `nextTick`
6+
---
7+
# vue/next-tick-style
8+
9+
> enforce Promise or callback style in `nextTick`
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+
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`.
17+
18+
<eslint-code-block fix :rules="{'vue/next-tick-style': ['error']}">
19+
20+
```vue
21+
<script>
22+
import { nextTick as nt } from 'vue';
23+
24+
export default {
25+
async mounted() {
26+
/* ✓ GOOD */
27+
nt().then(() => callback());
28+
await nt(); callback();
29+
Vue.nextTick().then(() => callback());
30+
await Vue.nextTick(); callback();
31+
this.$nextTick().then(() => callback());
32+
await this.$nextTick(); callback();
33+
34+
/* ✗ BAD */
35+
nt(() => callback());
36+
nt(callback);
37+
Vue.nextTick(() => callback());
38+
Vue.nextTick(callback);
39+
this.$nextTick(() => callback());
40+
this.$nextTick(callback);
41+
}
42+
}
43+
</script>
44+
```
45+
46+
</eslint-code-block>
47+
48+
## :wrench: Options
49+
Default is set to `promise`.
50+
51+
```json
52+
{
53+
"vue/next-tick-style": ["error", "promise" | "callback"]
54+
}
55+
```
56+
57+
- `"promise"` (default) ... requires using the promise version.
58+
- `"callback"` ... requires using the callback version. Use this if you use a Vue version below v2.1.0.
59+
60+
### `"callback"`
61+
62+
<eslint-code-block fix :rules="{'vue/next-tick-style': ['error', 'callback']}">
63+
64+
```vue
65+
<script>
66+
import { nextTick as nt } from 'vue';
67+
68+
export default {
69+
async mounted() {
70+
/* ✓ GOOD */
71+
nt(() => callback());
72+
nt(callback);
73+
Vue.nextTick(() => callback());
74+
Vue.nextTick(callback);
75+
this.$nextTick(() => callback());
76+
this.$nextTick(callback);
77+
78+
/* ✗ BAD */
79+
nt().then(() => callback());
80+
await nt(); callback();
81+
Vue.nextTick().then(() => callback());
82+
await Vue.nextTick(); callback();
83+
this.$nextTick().then(() => callback());
84+
await this.$nextTick(); callback();
85+
}
86+
}
87+
</script>
88+
```
89+
90+
</eslint-code-block>
91+
92+
## :books: Further Reading
93+
94+
- [`Vue.nextTick` API in Vue 2](https://vuejs.org/v2/api/#Vue-nextTick)
95+
- [`vm.$nextTick` API in Vue 2](https://vuejs.org/v2/api/#vm-nextTick)
96+
- [Global API Treeshaking](https://v3.vuejs.org/guide/migration/global-api-treeshaking.html)
97+
- [Global `nextTick` API in Vue 3](https://v3.vuejs.org/api/global-api.html#nexttick)
98+
- [Instance `$nextTick` API in Vue 3](https://v3.vuejs.org/api/instance-methods.html#nexttick)
99+
100+
## :mag: Implementation
101+
102+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/next-tick-style.js)
103+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/next-tick-style.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ module.exports = {
4848
'mustache-interpolation-spacing': require('./rules/mustache-interpolation-spacing'),
4949
'name-property-casing': require('./rules/name-property-casing'),
5050
'new-line-between-multi-line-property': require('./rules/new-line-between-multi-line-property'),
51+
'next-tick-style': require('./rules/next-tick-style'),
5152
'no-arrow-functions-in-watch': require('./rules/no-arrow-functions-in-watch'),
5253
'no-async-in-computed-properties': require('./rules/no-async-in-computed-properties'),
5354
'no-bare-strings-in-template': require('./rules/no-bare-strings-in-template'),

lib/rules/next-tick-style.js

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* @fileoverview enforce Promise or callback style in `nextTick`
3+
* @author Flo Edelmann
4+
* @copyright 2020 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 {CallExpression|undefined}
24+
*/
25+
function getVueNextTickCallExpression(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+
identifier.parent.parent.type === 'CallExpression' &&
32+
identifier.parent.parent.callee === identifier.parent
33+
) {
34+
return identifier.parent.parent
35+
}
36+
37+
// Vue 2 Global API: Vue.nextTick()
38+
if (
39+
identifier.name === 'nextTick' &&
40+
identifier.parent.type === 'MemberExpression' &&
41+
identifier.parent.object.type === 'Identifier' &&
42+
identifier.parent.object.name === 'Vue' &&
43+
identifier.parent.parent.type === 'CallExpression' &&
44+
identifier.parent.parent.callee === identifier.parent
45+
) {
46+
return identifier.parent.parent
47+
}
48+
49+
// Vue 3 Global API: import { nextTick as nt } from 'vue'; nt()
50+
if (
51+
identifier.parent.type === 'CallExpression' &&
52+
identifier.parent.callee === identifier
53+
) {
54+
const variable = findVariable(context.getScope(), identifier)
55+
56+
if (variable != null && variable.defs.length === 1) {
57+
const def = variable.defs[0]
58+
if (
59+
def.type === 'ImportBinding' &&
60+
def.node.type === 'ImportSpecifier' &&
61+
def.node.imported.type === 'Identifier' &&
62+
def.node.imported.name === 'nextTick' &&
63+
def.node.parent.type === 'ImportDeclaration' &&
64+
def.node.parent.source.value === 'vue'
65+
) {
66+
return identifier.parent
67+
}
68+
}
69+
}
70+
71+
return undefined
72+
}
73+
74+
/**
75+
* @param {CallExpression} callExpression
76+
* @returns {boolean}
77+
*/
78+
function isAwaitedPromise(callExpression) {
79+
return (
80+
callExpression.parent.type === 'AwaitExpression' ||
81+
(callExpression.parent.type === 'MemberExpression' &&
82+
callExpression.parent.property.type === 'Identifier' &&
83+
callExpression.parent.property.name === 'then')
84+
)
85+
}
86+
87+
// ------------------------------------------------------------------------------
88+
// Rule Definition
89+
// ------------------------------------------------------------------------------
90+
91+
module.exports = {
92+
meta: {
93+
type: 'suggestion',
94+
docs: {
95+
description: 'enforce Promise or callback style in `nextTick`',
96+
categories: undefined,
97+
url: 'https://eslint.vuejs.org/rules/next-tick-style.html'
98+
},
99+
fixable: 'code',
100+
schema: [{ enum: ['promise', 'callback'] }]
101+
},
102+
/** @param {RuleContext} context */
103+
create(context) {
104+
const preferredStyle =
105+
/** @type {string|undefined} */ (context.options[0]) || 'promise'
106+
107+
return utils.defineVueVisitor(context, {
108+
/** @param {Identifier} node */
109+
Identifier(node) {
110+
const callExpression = getVueNextTickCallExpression(node, context)
111+
if (!callExpression) {
112+
return
113+
}
114+
115+
if (preferredStyle === 'callback') {
116+
if (
117+
callExpression.arguments.length !== 1 ||
118+
isAwaitedPromise(callExpression)
119+
) {
120+
context.report({
121+
node,
122+
message:
123+
'Pass a callback function to `nextTick` instead of using the returned Promise.'
124+
})
125+
}
126+
127+
return
128+
}
129+
130+
if (
131+
callExpression.arguments.length !== 0 ||
132+
!isAwaitedPromise(callExpression)
133+
) {
134+
context.report({
135+
node,
136+
message:
137+
'Use the Promise returned by `nextTick` instead of passing a callback function.',
138+
fix(fixer) {
139+
return fixer.insertTextAfter(node, '().then')
140+
}
141+
})
142+
}
143+
}
144+
})
145+
}
146+
}

0 commit comments

Comments
 (0)