From c240877d1033922553a1e0bbdea604d20357d593 Mon Sep 17 00:00:00 2001 From: Flo Edelmann Date: Wed, 30 Dec 2020 14:06:49 +0100 Subject: [PATCH 01/17] Add rule docs --- docs/rules/next-tick-style.md | 99 +++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 docs/rules/next-tick-style.md diff --git a/docs/rules/next-tick-style.md b/docs/rules/next-tick-style.md new file mode 100644 index 000000000..efe8528a2 --- /dev/null +++ b/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 +since: v7.5.0 +--- +# vue/next-tick-style + +> enforce `Vue.nextTick` / `this.$nextTick` style + +- :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`. + + + +```vue + +``` + + + +## :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"` + + + +```vue + +``` + + + +## :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) From af38d7ece52434919eff7f1cf688875c20c7c2df Mon Sep 17 00:00:00 2001 From: Flo Edelmann Date: Wed, 30 Dec 2020 14:06:29 +0100 Subject: [PATCH 02/17] Add rule tests --- tests/lib/rules/next-tick-style.js | 281 +++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 tests/lib/rules/next-tick-style.js diff --git a/tests/lib/rules/next-tick-style.js b/tests/lib/rules/next-tick-style.js new file mode 100644 index 000000000..3fed63c45 --- /dev/null +++ b/tests/lib/rules/next-tick-style.js @@ -0,0 +1,281 @@ +/** + * @fileoverview enforce `Vue.nextTick` / `this.$nextTick` style + * @author Flo Edelmann + * @copyright 2020 Flo Edelmann. All rights reserved. + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/next-tick-style') + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2017, + sourceType: 'module' + } +}) + +tester.run('next-tick-style', rule, { + valid: [ + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + options: ['promise'], + code: `` + }, + { + filename: 'test.vue', + options: ['callback'], + code: `` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ``, + output: ``, + errors: [ + { + message: + 'Use the Promise returned by `nextTick` instead of passing a callback function.', + line: 4, + column: 16 + }, + { + message: + 'Use the Promise returned by `nextTick` instead of passing a callback function.', + line: 5, + column: 15 + }, + { + message: + 'Use the Promise returned by `nextTick` instead of passing a callback function.', + line: 6, + column: 11 + }, + { + message: + 'Use the Promise returned by `nextTick` instead of passing a callback function.', + line: 8, + column: 16 + }, + { + message: + 'Use the Promise returned by `nextTick` instead of passing a callback function.', + line: 9, + column: 15 + }, + { + message: + 'Use the Promise returned by `nextTick` instead of passing a callback function.', + line: 10, + column: 11 + } + ] + }, + { + filename: 'test.vue', + options: ['promise'], + code: ``, + output: ``, + errors: [ + { + message: + 'Use the Promise returned by `nextTick` instead of passing a callback function.', + line: 4, + column: 16 + }, + { + message: + 'Use the Promise returned by `nextTick` instead of passing a callback function.', + line: 5, + column: 15 + }, + { + message: + 'Use the Promise returned by `nextTick` instead of passing a callback function.', + line: 6, + column: 11 + }, + { + message: + 'Use the Promise returned by `nextTick` instead of passing a callback function.', + line: 8, + column: 16 + }, + { + message: + 'Use the Promise returned by `nextTick` instead of passing a callback function.', + line: 9, + column: 15 + }, + { + message: + 'Use the Promise returned by `nextTick` instead of passing a callback function.', + line: 10, + column: 11 + } + ] + }, + { + filename: 'test.vue', + options: ['callback'], + code: ``, + output: ``, + errors: [ + { + message: + 'Pass a callback function to `nextTick` instead of using the returned Promise.', + line: 4, + column: 16 + }, + { + message: + 'Pass a callback function to `nextTick` instead of using the returned Promise.', + line: 5, + column: 15 + }, + { + message: + 'Pass a callback function to `nextTick` instead of using the returned Promise.', + line: 6, + column: 11 + }, + { + message: + 'Pass a callback function to `nextTick` instead of using the returned Promise.', + line: 8, + column: 22 + }, + { + message: + 'Pass a callback function to `nextTick` instead of using the returned Promise.', + line: 9, + column: 21 + }, + { + message: + 'Pass a callback function to `nextTick` instead of using the returned Promise.', + line: 10, + column: 17 + } + ] + } + ] +}) From 3bbb7f07848517bebb640dc80eb8e1832ed56159 Mon Sep 17 00:00:00 2001 From: Flo Edelmann Date: Wed, 30 Dec 2020 14:58:13 +0100 Subject: [PATCH 03/17] Initial implementation: detect nextTick calls --- lib/rules/next-tick-style.js | 105 +++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 lib/rules/next-tick-style.js diff --git a/lib/rules/next-tick-style.js b/lib/rules/next-tick-style.js new file mode 100644 index 000000000..e8ecf07eb --- /dev/null +++ b/lib/rules/next-tick-style.js @@ -0,0 +1,105 @@ +/** + * @fileoverview enforce `Vue.nextTick` / `this.$nextTick` style + * @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') { + 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 +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'layout', + docs: { + description: 'enforce `Vue.nextTick` / `this.$nextTick` style', + categories: undefined, + recommended: false, + url: 'https://eslint.vuejs.org/rules/next-tick-style.html' + }, + fixable: null, + 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) { + if (!getVueNextTickCallExpression(node, context)) { + return + } + + context.report({ + node, + message: + 'Use the Promise returned by `nextTick` instead of passing a callback function.' + }) + } + }) + } +} From db2ff3bf2ee73f9e8b855cb32cc1a1aad3cfaf3e Mon Sep 17 00:00:00 2001 From: Flo Edelmann Date: Wed, 30 Dec 2020 16:10:20 +0100 Subject: [PATCH 04/17] Finish rule implementation --- lib/rules/next-tick-style.js | 46 +++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/lib/rules/next-tick-style.js b/lib/rules/next-tick-style.js index e8ecf07eb..119004130 100644 --- a/lib/rules/next-tick-style.js +++ b/lib/rules/next-tick-style.js @@ -66,6 +66,19 @@ function getVueNextTickCallExpression(identifier, context) { 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 // ------------------------------------------------------------------------------ @@ -90,15 +103,36 @@ module.exports = { return utils.defineVueVisitor(context, { /** @param {Identifier} node */ Identifier(node) { - if (!getVueNextTickCallExpression(node, context)) { + const callExpression = getVueNextTickCallExpression(node, context) + if (!callExpression) { return } - context.report({ - node, - message: - 'Use the Promise returned by `nextTick` instead of passing a callback function.' - }) + 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.' + }) + } } }) } From c9767081b3de2a69ab79bf5d3b41298950d6a8bf Mon Sep 17 00:00:00 2001 From: Flo Edelmann Date: Wed, 30 Dec 2020 18:26:33 +0100 Subject: [PATCH 05/17] Add fixer for promise style --- lib/rules/next-tick-style.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/rules/next-tick-style.js b/lib/rules/next-tick-style.js index 119004130..2f3a06995 100644 --- a/lib/rules/next-tick-style.js +++ b/lib/rules/next-tick-style.js @@ -92,7 +92,7 @@ module.exports = { recommended: false, url: 'https://eslint.vuejs.org/rules/next-tick-style.html' }, - fixable: null, + fixable: 'code', schema: [{ enum: ['promise', 'callback'] }] }, /** @param {RuleContext} context */ @@ -130,7 +130,10 @@ module.exports = { context.report({ node, message: - 'Use the Promise returned by `nextTick` instead of passing a callback function.' + 'Use the Promise returned by `nextTick` instead of passing a callback function.', + *fix(fixer) { + yield fixer.insertTextAfter(node, '().then') + } }) } } From 8485c3acdc738c54e8e5084fcc1a71be2933d327 Mon Sep 17 00:00:00 2001 From: Flo Edelmann Date: Wed, 30 Dec 2020 16:14:17 +0100 Subject: [PATCH 06/17] Add new rule to rule collections --- docs/rules/README.md | 1 + lib/configs/no-layout-rules.js | 3 ++- lib/index.js | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/rules/README.md b/docs/rules/README.md index 5f848467b..06f8a7f6c 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -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 `Vue.nextTick` / `this.$nextTick` style | | | [vue/no-bare-strings-in-template](./no-bare-strings-in-template.md) | disallow the use of bare strings in `