Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add vue/next-tick-style rule #1400

Merged
merged 17 commits into from Jan 21, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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` | |
| [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
99 changes: 99 additions & 0 deletions docs/rules/next-tick-style.md
@@ -0,0 +1,99 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/next-tick-style
description: enforce `v-for` directive's delimiter style
FloEdelmann marked this conversation as resolved.
Show resolved Hide resolved
since: v7.5.0
FloEdelmann marked this conversation as resolved.
Show resolved Hide resolved
---
# vue/next-tick-style

> enforce Promise or callback style in `nextTick`

- :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 } from 'vue';

export default {
async mounted() {
/* ✓ GOOD */
nextTick().then(() => callback());
await nextTick(); callback();
this.$nextTick().then(() => callback());
await this.$nextTick(); callback();

/* ✗ BAD */
nextTick(() => callback());
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 :rules="{'vue/next-tick-style': ['error', 'callback']}">

```vue
<script>
import { nextTick } from 'vue';

export default {
async mounted() {
/* ✓ GOOD */
nextTick(() => callback());
nextTick(callback);
this.$nextTick(() => callback());
this.$nextTick(callback);

/* ✗ BAD */
nextTick().then(() => callback());
await 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)

## :rocket: Version

This rule was introduced in eslint-plugin-vue v7.5.0

## :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)
3 changes: 2 additions & 1 deletion lib/configs/no-layout-rules.js
Expand Up @@ -45,6 +45,7 @@ module.exports = {
'vue/space-infix-ops': 'off',
'vue/space-unary-ops': 'off',
'vue/template-curly-spacing': 'off',
'vue/v-for-delimiter-style': 'off'
'vue/v-for-delimiter-style': 'off',
'vue/next-tick-style': 'off'
}
}
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
142 changes: 142 additions & 0 deletions lib/rules/next-tick-style.js
@@ -0,0 +1,142 @@
/**
* @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/index.js')

// ------------------------------------------------------------------------------
// 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'
) {
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'
) {
return identifier.parent.parent
}

// Vue 3 Global API: import { nextTick as nt } from 'vue'; nt()
if (identifier.parent.type === 'CallExpression') {
FloEdelmann marked this conversation as resolved.
Show resolved Hide resolved
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: 'layout',
FloEdelmann marked this conversation as resolved.
Show resolved Hide resolved
docs: {
description: 'enforce Promise or callback style in `nextTick`',
categories: undefined,
recommended: false,
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) {
yield fixer.insertTextAfter(node, '().then')
FloEdelmann marked this conversation as resolved.
Show resolved Hide resolved
}
})
}
}
})
}
}