Skip to content

Commit

Permalink
Add vue/next-tick-style rule (#1400)
Browse files Browse the repository at this point in the history
* 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
FloEdelmann committed Jan 21, 2021
1 parent 24eccfb commit ec2dc79
Show file tree
Hide file tree
Showing 5 changed files with 555 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -294,6 +294,7 @@ For example:
| [vue/html-comment-indent](./html-comment-indent.md) | enforce consistent indentation in HTML comments | :wrench: |
| [vue/match-component-file-name](./match-component-file-name.md) | require component name property to match its file name | |
| [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: |
| [vue/next-tick-style](./next-tick-style.md) | enforce Promise or callback style in `nextTick` | :wrench: |
| [vue/no-bare-strings-in-template](./no-bare-strings-in-template.md) | disallow the use of bare strings in `<template>` | |
| [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: |
| [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` | |
Expand Down
103 changes: 103 additions & 0 deletions docs/rules/next-tick-style.md
@@ -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)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -48,6 +48,7 @@ module.exports = {
'mustache-interpolation-spacing': require('./rules/mustache-interpolation-spacing'),
'name-property-casing': require('./rules/name-property-casing'),
'new-line-between-multi-line-property': require('./rules/new-line-between-multi-line-property'),
'next-tick-style': require('./rules/next-tick-style'),
'no-arrow-functions-in-watch': require('./rules/no-arrow-functions-in-watch'),
'no-async-in-computed-properties': require('./rules/no-async-in-computed-properties'),
'no-bare-strings-in-template': require('./rules/no-bare-strings-in-template'),
Expand Down
146 changes: 146 additions & 0 deletions lib/rules/next-tick-style.js
@@ -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')
}
})
}
}
})
}
}

0 comments on commit ec2dc79

Please sign in to comment.