Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
24eccfb
commit ec2dc79
Showing
5 changed files
with
555 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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') | ||
} | ||
}) | ||
} | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.