diff --git a/docs/rules/README.md b/docs/rules/README.md
index 68677d252..0d1fccd31 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -301,6 +301,7 @@ For example:
| [vue/no-multiple-objects-in-class](./no-multiple-objects-in-class.md) | disallow to pass multiple objects into array to class | |
| [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | |
| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | |
+| [vue/no-restricted-call-after-await](./no-restricted-call-after-await.md) | disallow asynchronously called restricted methods | |
| [vue/no-restricted-component-options](./no-restricted-component-options.md) | disallow specific component option | |
| [vue/no-restricted-custom-event](./no-restricted-custom-event.md) | disallow specific custom event | |
| [vue/no-restricted-props](./no-restricted-props.md) | disallow specific props | |
diff --git a/docs/rules/no-restricted-call-after-await.md b/docs/rules/no-restricted-call-after-await.md
new file mode 100644
index 000000000..735fc87b0
--- /dev/null
+++ b/docs/rules/no-restricted-call-after-await.md
@@ -0,0 +1,98 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-restricted-call-after-await
+description: disallow asynchronously called restricted methods
+---
+# vue/no-restricted-call-after-await
+
+> disallow asynchronously called restricted methods
+
+- :exclamation: ***This rule has not been released yet.***
+
+## :book: Rule Details
+
+This rule reports your restricted calls after the `await` expression.
+In `setup()` function, you need to call your restricted functions synchronously.
+
+## :wrench: Options
+
+This rule takes a list of objects, where each object specifies a restricted module name and an exported name:
+
+```json5
+{
+ "vue/no-restricted-call-after-await": ["error",
+ { "module": "vue-i18n", "path": "useI18n" },
+ { ... } // You can specify more...
+ ]
+}
+```
+
+
+
+```vue
+
+```
+
+
+
+The following properties can be specified for the object.
+
+- `module` ... Specify the module name.
+- `path` ... Specify the imported name or the path that points to the method.
+- `message` ... Specify an optional custom message.
+
+For examples:
+
+```json5
+{
+ "vue/no-restricted-call-after-await": ["error",
+ { "module": "a", "path": "foo" },
+ { "module": "b", "path": ["bar", "baz"] },
+ { "module": "c" }, // Checks the default import.
+ { "module": "d", "path": "default" }, // Checks the default import.
+ ]
+}
+```
+
+
+
+```vue
+
+```
+
+
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-call-after-await.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-call-after-await.js)
diff --git a/lib/index.js b/lib/index.js
index 95ffd1dd6..0cc2ef271 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -91,6 +91,7 @@ module.exports = {
'no-ref-as-operand': require('./rules/no-ref-as-operand'),
'no-reserved-component-names': require('./rules/no-reserved-component-names'),
'no-reserved-keys': require('./rules/no-reserved-keys'),
+ 'no-restricted-call-after-await': require('./rules/no-restricted-call-after-await'),
'no-restricted-component-options': require('./rules/no-restricted-component-options'),
'no-restricted-custom-event': require('./rules/no-restricted-custom-event'),
'no-restricted-props': require('./rules/no-restricted-props'),
diff --git a/lib/rules/no-restricted-call-after-await.js b/lib/rules/no-restricted-call-after-await.js
new file mode 100644
index 000000000..6723d91ce
--- /dev/null
+++ b/lib/rules/no-restricted-call-after-await.js
@@ -0,0 +1,227 @@
+/**
+ * @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) {
+ /** @type {Map} */
+ const restrictedCallNodes = new Map()
+ /** @type {Map} */
+ const setupFunctions = new Map()
+
+ /**x
+ * @typedef {object} ScopeStack
+ * @property {ScopeStack | null} upper
+ * @property {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} functionNode
+ */
+ /** @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, {
+ /** @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} node */
+ ':function'(node) {
+ scopeStack = {
+ upper: scopeStack,
+ functionNode: node
+ }
+ },
+ onSetupFunctionEnter(node) {
+ setupFunctions.set(node, {
+ setupProperty: node.parent,
+ afterAwait: false
+ })
+ },
+ AwaitExpression() {
+ if (!scopeStack) {
+ return
+ }
+ const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
+ if (!setupFunctionData) {
+ return
+ }
+ setupFunctionData.afterAwait = true
+ },
+ /** @param {CallExpression} node */
+ CallExpression(node) {
+ if (!scopeStack) {
+ return
+ }
+ const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
+ if (!setupFunctionData || !setupFunctionData.afterAwait) {
+ return
+ }
+
+ const message = restrictedCallNodes.get(node)
+ if (message) {
+ context.report({
+ node,
+ messageId: 'restricted',
+ data: { message }
+ })
+ }
+ },
+ /** @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} node */
+ ':function:exit'(node) {
+ scopeStack = scopeStack && scopeStack.upper
+
+ setupFunctions.delete(node)
+ }
+ })
+ )
+ }
+}
diff --git a/tests/lib/rules/no-restricted-call-after-await.js b/tests/lib/rules/no-restricted-call-after-await.js
new file mode 100644
index 000000000..530e7cd39
--- /dev/null
+++ b/tests/lib/rules/no-restricted-call-after-await.js
@@ -0,0 +1,382 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const path = require('path')
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-restricted-call-after-await')
+
+const tester = new RuleTester({
+ parser: require.resolve('vue-eslint-parser'),
+ parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
+})
+
+tester.run('no-restricted-call-after-await', rule, {
+ valid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ module: 'vue-i18n', path: 'useI18n' }]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ module: 'vue-i18n', path: 'useI18n' }]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ module: 'foo', path: ['bar', 'baz'] }]
+ },
+ {
+ filename: path.join(process.cwd(), 'test.vue'),
+ code: `
+
+ `,
+ options: [
+ { module: './foo', path: 'bar' },
+ { module: './baz', path: 'qux' },
+ { module: 'vue-i18n', path: 'useI18n' }
+ ]
+ },
+ {
+ filename: path.join(process.cwd(), 'test.vue'),
+ code: `
+
+ `,
+ options: [
+ { module: './foo', path: 'bar' },
+ { module: './baz', path: 'qux' },
+ { module: 'vue-i18n', path: 'useI18n' }
+ ]
+ },
+ {
+ filename: path.join(process.cwd(), 'test.vue'),
+ code: `
+
+ `,
+ options: [
+ { module: './foo', path: 'bar' },
+ { module: './baz', path: 'qux' },
+ { module: 'vue-i18n', path: 'useI18n' }
+ ]
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ module: 'vue-i18n', path: 'useI18n' }],
+ errors: [
+ {
+ message:
+ 'The `import("vue-i18n").useI18n` after `await` expression are forbidden.',
+ line: 7,
+ column: 25,
+ endLine: 7,
+ endColumn: 37
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ module: 'foo', path: ['bar', 'baz'] }],
+ errors: [
+ {
+ message:
+ 'The `import("foo").bar.baz` after `await` expression are forbidden.',
+ line: 7
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [
+ { module: 'vue-i18n', path: 'useI18n' },
+ { module: 'foo', path: ['bar', 'baz'] }
+ ],
+ errors: [
+ {
+ message:
+ 'The `import("vue-i18n").useI18n` after `await` expression are forbidden.',
+ line: 8
+ },
+ {
+ message:
+ 'The `import("foo").bar.baz` after `await` expression are forbidden.',
+ line: 9
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ module: 'foo', path: 'default' }],
+ errors: [
+ {
+ message:
+ 'The `import("foo").default` after `await` expression are forbidden.',
+ line: 7
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ module: 'foo' }],
+ errors: [
+ {
+ message:
+ 'The `import("foo").default` after `await` expression are forbidden.',
+ line: 7
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ module: 'vue-i18n', path: 'useI18n' }],
+ errors: [
+ {
+ message:
+ 'The `import("vue-i18n").useI18n` after `await` expression are forbidden.',
+ line: 7
+ }
+ ]
+ },
+ {
+ filename: path.join(process.cwd(), 'test.vue'),
+ code: `
+
+ `,
+ options: [
+ { module: './foo', path: 'bar' },
+ { module: './baz', path: 'qux' },
+ { module: 'vue-i18n', path: 'useI18n' }
+ ],
+ errors: [
+ {
+ message:
+ 'The `import("./foo").bar` after `await` expression are forbidden.',
+ line: 10
+ },
+ {
+ message:
+ 'The `import("./baz").qux` after `await` expression are forbidden.',
+ line: 11
+ },
+ {
+ message:
+ 'The `import("vue-i18n").useI18n` after `await` expression are forbidden.',
+ line: 12
+ }
+ ]
+ },
+ {
+ filename: path.join(process.cwd(), 'test.vue'),
+ code: `
+
+ `,
+ options: [
+ { module: path.join(process.cwd(), './foo'), path: 'bar' },
+ { module: path.join(process.cwd(), './baz'), path: 'qux' },
+ { module: 'vue-i18n', path: 'useI18n' }
+ ],
+ errors: [
+ {
+ message:
+ 'The `import("./foo").bar` after `await` expression are forbidden.',
+ line: 10
+ },
+ {
+ message:
+ 'The `import("./baz").qux` after `await` expression are forbidden.',
+ line: 11
+ },
+ {
+ message:
+ 'The `import("vue-i18n").useI18n` after `await` expression are forbidden.',
+ line: 12
+ }
+ ]
+ },
+ {
+ filename: path.join(__dirname, '../../../test/test.vue'),
+ code: `
+
+ `,
+ options: [{ module: require.resolve('../../..'), path: 'foo' }],
+ errors: [
+ {
+ message:
+ 'The `import("..").foo` after `await` expression are forbidden.',
+ line: 7
+ }
+ ]
+ }
+ ]
+})
diff --git a/typings/eslint/index.d.ts b/typings/eslint/index.d.ts
index f977b45ae..fdbca8503 100644
--- a/typings/eslint/index.d.ts
+++ b/typings/eslint/index.d.ts
@@ -327,6 +327,9 @@ export namespace Rule {
getSourceCode(): SourceCode
markVariableAsUsed(name: string): boolean
report(descriptor: ReportDescriptor): void
+
+ // eslint@6 does not have this method.
+ getCwd?: () => string
}
type ReportDescriptor =