/** * @fileoverview enforce valid `nextTick` function calls * @author Flo Edelmann * @copyright 2021 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 {ASTNode|undefined} */ function getVueNextTickNode(identifier, context) { // Instance API: this.$nextTick() if ( identifier.name === '$nextTick' && identifier.parent.type === 'MemberExpression' && utils.isThis(identifier.parent.object, context) ) { return identifier.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' ) { return identifier.parent } // Vue 3 Global API: import { nextTick as nt } from 'vue'; nt() 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 } } return undefined } /** * @param {CallExpression} callExpression * @returns {boolean} */ function isAwaitedPromise(callExpression) { if (callExpression.parent.type === 'AwaitExpression') { // cases like `await nextTick()` return true } if (callExpression.parent.type === 'ReturnStatement') { // cases like `return nextTick()` return true } if ( callExpression.parent.type === 'ArrowFunctionExpression' && callExpression.parent.body === callExpression ) { // cases like `() => nextTick()` return true } if ( callExpression.parent.type === 'MemberExpression' && callExpression.parent.property.type === 'Identifier' && callExpression.parent.property.name === 'then' ) { // cases like `nextTick().then()` return true } if ( callExpression.parent.type === 'VariableDeclarator' || callExpression.parent.type === 'AssignmentExpression' ) { // cases like `let foo = nextTick()` or `foo = nextTick()` return true } if ( callExpression.parent.type === 'ArrayExpression' && callExpression.parent.parent.type === 'CallExpression' && callExpression.parent.parent.callee.type === 'MemberExpression' && callExpression.parent.parent.callee.object.type === 'Identifier' && callExpression.parent.parent.callee.object.name === 'Promise' && callExpression.parent.parent.callee.property.type === 'Identifier' ) { // cases like `Promise.all([nextTick()])` return true } return false } // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ module.exports = { meta: { hasSuggestions: true, type: 'problem', docs: { description: 'enforce valid `nextTick` function calls', categories: ['vue3-essential', 'essential'], url: 'https://eslint.vuejs.org/rules/valid-next-tick.html' }, fixable: 'code', schema: [] }, /** @param {RuleContext} context */ create(context) { return utils.defineVueVisitor(context, { /** @param {Identifier} node */ Identifier(node) { const nextTickNode = getVueNextTickNode(node, context) if (!nextTickNode || !nextTickNode.parent) { return } let parentNode = nextTickNode.parent // skip conditional expressions like `foo ? nextTick : bar` if (parentNode.type === 'ConditionalExpression') { parentNode = parentNode.parent } if ( parentNode.type === 'CallExpression' && parentNode.callee !== nextTickNode ) { // cases like `foo.then(nextTick)` are allowed return } if ( parentNode.type === 'VariableDeclarator' || parentNode.type === 'AssignmentExpression' ) { // cases like `let foo = nextTick` or `foo = nextTick` are allowed return } if (parentNode.type !== 'CallExpression') { context.report({ node, message: '`nextTick` is a function.', fix(fixer) { return fixer.insertTextAfter(node, '()') } }) return } if (parentNode.arguments.length === 0) { if (!isAwaitedPromise(parentNode)) { context.report({ node, message: 'Await the Promise returned by `nextTick` or pass a callback function.', suggest: [ { desc: 'Add missing `await` statement.', fix(fixer) { return fixer.insertTextBefore(parentNode, 'await ') } } ] }) } return } if (parentNode.arguments.length > 1) { context.report({ node, message: '`nextTick` expects zero or one parameters.' }) return } if (isAwaitedPromise(parentNode)) { context.report({ node, message: 'Either await the Promise or pass a callback function to `nextTick`.' }) } } }) } }