/** * @author Yosuke Ota * See LICENSE file in root directory for full license. */ 'use strict' const fs = require('fs') const path = require('path') const { ReferenceTracker } = require('eslint-utils') const utils = require('../utils') /** * @typedef {import('eslint-utils').TYPES.TraceMap} TraceMap * @typedef {import('eslint-utils').TYPES.TraceKind} TraceKind */ module.exports = { meta: { type: 'suggestion', docs: { description: 'disallow asynchronously called restricted methods', categories: undefined, url: 'https://eslint.vuejs.org/rules/no-restricted-call-after-await.html' }, fixable: null, schema: { type: 'array', items: { type: 'object', properties: { module: { type: 'string' }, path: { anyOf: [ { type: 'string' }, { type: 'array', items: { type: 'string' } } ] }, message: { type: 'string', minLength: 1 } }, required: ['module'], additionalProperties: false }, uniqueItems: true, minItems: 0 }, messages: { // eslint-disable-next-line eslint-plugin/report-message-format restricted: '{{message}}' } }, /** @param {RuleContext} context */ create(context) { /** * @typedef {object} SetupScopeData * @property {boolean} afterAwait * @property {[number,number]} range */ /** @type {Map} */ const restrictedCallNodes = new Map() /** @type {Map} */ const setupScopes = new Map() /**x * @typedef {object} ScopeStack * @property {ScopeStack | null} upper * @property {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} scopeNode */ /** @type {ScopeStack | null} */ let scopeStack = null /** @type {Record | null} */ let allLocalImports = null /** * @param {string} id */ function safeRequireResolve(id) { try { if (fs.statSync(id).isDirectory()) { return require.resolve(id) } } catch (_e) { // ignore } return id } /** * @param {Program} ast */ function getAllLocalImports(ast) { if (!allLocalImports) { allLocalImports = {} const dir = path.dirname(context.getFilename()) for (const body of ast.body) { if (body.type !== 'ImportDeclaration') { continue } const source = String(body.source.value) if (!source.startsWith('.')) { continue } const modulePath = safeRequireResolve(path.join(dir, source)) const list = allLocalImports[modulePath] || (allLocalImports[modulePath] = []) list.push(source) } } return allLocalImports } function getCwd() { if (context.getCwd) { return context.getCwd() } return path.resolve('') } /** * @param {string} moduleName * @param {Program} ast * @returns {string[]} */ function normalizeModules(moduleName, ast) { /** @type {string} */ let modulePath if (moduleName.startsWith('.')) { modulePath = safeRequireResolve(path.join(getCwd(), moduleName)) } else if (path.isAbsolute(moduleName)) { modulePath = safeRequireResolve(moduleName) } else { return [moduleName] } return getAllLocalImports(ast)[modulePath] || [] } return utils.compositingVisitors( { /** @param {Program} node */ Program(node) { const tracker = new ReferenceTracker(context.getScope()) for (const option of context.options) { const modules = normalizeModules(option.module, node) for (const module of modules) { /** @type {TraceMap} */ const traceMap = { [module]: { [ReferenceTracker.ESM]: true } } /** @type {TraceKind & TraceMap} */ const mod = traceMap[module] let local = mod const paths = Array.isArray(option.path) ? option.path : [option.path || 'default'] for (const path of paths) { local = local[path] || (local[path] = {}) } local[ReferenceTracker.CALL] = true const message = option.message || `The \`${[`import("${module}")`, ...paths].join( '.' )}\` after \`await\` expression are forbidden.` for (const { node } of tracker.iterateEsmReferences(traceMap)) { restrictedCallNodes.set(node, message) } } } } }, utils.defineVueVisitor(context, { onSetupFunctionEnter(node) { setupScopes.set(node, { afterAwait: false, range: node.range }) }, /** @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} node */ ':function'(node) { scopeStack = { upper: scopeStack, scopeNode: node } }, ':function:exit'() { scopeStack = scopeStack && scopeStack.upper }, /** @param {AwaitExpression} node */ AwaitExpression(node) { if (!scopeStack) { return } const setupScope = setupScopes.get(scopeStack.scopeNode) if (!setupScope || !utils.inRange(setupScope.range, node)) { return } setupScope.afterAwait = true }, /** @param {CallExpression} node */ CallExpression(node) { if (!scopeStack) { return } const setupScope = setupScopes.get(scopeStack.scopeNode) if ( !setupScope || !setupScope.afterAwait || !utils.inRange(setupScope.range, node) ) { return } const message = restrictedCallNodes.get(node) if (message) { context.report({ node, messageId: 'restricted', data: { message } }) } }, onSetupFunctionExit(node) { setupScopes.delete(node) } }) ) } }