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/no-restricted-call-after-await rule #1381

Merged
merged 3 commits into from Dec 25, 2020
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -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 | |
Expand Down
98 changes: 98 additions & 0 deletions 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: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>

## :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...
]
}
```

<eslint-code-block :rules="{'vue/no-restricted-call-after-await': ['error', { module: 'vue-i18n', path: ['useI18n'] }]}">

```vue
<script>
import { useI18n } from 'vue-i18n'
export default {
async setup() {
/* ✓ GOOD */
useI18n({})

await doSomething()

/* ✗ BAD */
useI18n({})
}
}
</script>
```

</eslint-code-block>

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.
]
}
```

<eslint-code-block :rules="{'vue/no-restricted-call-after-await': ['error', { module: 'a', path: 'foo' }, { module: 'b', path: ['bar', 'baz'] }, { module: 'c' }, { module: 'd', path: 'default' }]}">

```vue
<script>
import { foo as fooOfA } from 'a'
import { bar as barOfB } from 'b'
import defaultOfC from 'c'
import defaultOfD from 'd'
export default {
async setup() {
await doSomething()

/* ✗ BAD */
fooOfA()
barOfB.baz()
defaultOfC()
defaultOfD()
}
}
</script>
```

</eslint-code-block>

## :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)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -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'),
Expand Down
227 changes: 227 additions & 0 deletions 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<ESNode, string>} */
const restrictedCallNodes = new Map()
/** @type {Map<FunctionExpression | ArrowFunctionExpression | FunctionDeclaration, { setupProperty: Property, afterAwait: boolean }>} */
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<string, string[]> | 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)
}
})
)
}
}