Skip to content

Commit f0e13a4

Browse files
authored
Add vue/no-restricted-call-after-await rule (#1381)
* Add vue/no-restricted-call-after-await rule * Fix bug in eslint 6 * update
1 parent 30e89ec commit f0e13a4

File tree

6 files changed

+712
-0
lines changed

6 files changed

+712
-0
lines changed

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ For example:
301301
| [vue/no-multiple-objects-in-class](./no-multiple-objects-in-class.md) | disallow to pass multiple objects into array to class | |
302302
| [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | |
303303
| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | |
304+
| [vue/no-restricted-call-after-await](./no-restricted-call-after-await.md) | disallow asynchronously called restricted methods | |
304305
| [vue/no-restricted-component-options](./no-restricted-component-options.md) | disallow specific component option | |
305306
| [vue/no-restricted-custom-event](./no-restricted-custom-event.md) | disallow specific custom event | |
306307
| [vue/no-restricted-props](./no-restricted-props.md) | disallow specific props | |
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-restricted-call-after-await
5+
description: disallow asynchronously called restricted methods
6+
---
7+
# vue/no-restricted-call-after-await
8+
9+
> disallow asynchronously called restricted methods
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
13+
## :book: Rule Details
14+
15+
This rule reports your restricted calls after the `await` expression.
16+
In `setup()` function, you need to call your restricted functions synchronously.
17+
18+
## :wrench: Options
19+
20+
This rule takes a list of objects, where each object specifies a restricted module name and an exported name:
21+
22+
```json5
23+
{
24+
"vue/no-restricted-call-after-await": ["error",
25+
{ "module": "vue-i18n", "path": "useI18n" },
26+
{ ... } // You can specify more...
27+
]
28+
}
29+
```
30+
31+
<eslint-code-block :rules="{'vue/no-restricted-call-after-await': ['error', { module: 'vue-i18n', path: ['useI18n'] }]}">
32+
33+
```vue
34+
<script>
35+
import { useI18n } from 'vue-i18n'
36+
export default {
37+
async setup() {
38+
/* ✓ GOOD */
39+
useI18n({})
40+
41+
await doSomething()
42+
43+
/* ✗ BAD */
44+
useI18n({})
45+
}
46+
}
47+
</script>
48+
```
49+
50+
</eslint-code-block>
51+
52+
The following properties can be specified for the object.
53+
54+
- `module` ... Specify the module name.
55+
- `path` ... Specify the imported name or the path that points to the method.
56+
- `message` ... Specify an optional custom message.
57+
58+
For examples:
59+
60+
```json5
61+
{
62+
"vue/no-restricted-call-after-await": ["error",
63+
{ "module": "a", "path": "foo" },
64+
{ "module": "b", "path": ["bar", "baz"] },
65+
{ "module": "c" }, // Checks the default import.
66+
{ "module": "d", "path": "default" }, // Checks the default import.
67+
]
68+
}
69+
```
70+
71+
<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' }]}">
72+
73+
```vue
74+
<script>
75+
import { foo as fooOfA } from 'a'
76+
import { bar as barOfB } from 'b'
77+
import defaultOfC from 'c'
78+
import defaultOfD from 'd'
79+
export default {
80+
async setup() {
81+
await doSomething()
82+
83+
/* ✗ BAD */
84+
fooOfA()
85+
barOfB.baz()
86+
defaultOfC()
87+
defaultOfD()
88+
}
89+
}
90+
</script>
91+
```
92+
93+
</eslint-code-block>
94+
95+
## :mag: Implementation
96+
97+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-call-after-await.js)
98+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-call-after-await.js)

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ module.exports = {
9191
'no-ref-as-operand': require('./rules/no-ref-as-operand'),
9292
'no-reserved-component-names': require('./rules/no-reserved-component-names'),
9393
'no-reserved-keys': require('./rules/no-reserved-keys'),
94+
'no-restricted-call-after-await': require('./rules/no-restricted-call-after-await'),
9495
'no-restricted-component-options': require('./rules/no-restricted-component-options'),
9596
'no-restricted-custom-event': require('./rules/no-restricted-custom-event'),
9697
'no-restricted-props': require('./rules/no-restricted-props'),
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
const fs = require('fs')
7+
const path = require('path')
8+
const { ReferenceTracker } = require('eslint-utils')
9+
const utils = require('../utils')
10+
11+
/**
12+
* @typedef {import('eslint-utils').TYPES.TraceMap} TraceMap
13+
* @typedef {import('eslint-utils').TYPES.TraceKind} TraceKind
14+
*/
15+
16+
module.exports = {
17+
meta: {
18+
type: 'suggestion',
19+
docs: {
20+
description: 'disallow asynchronously called restricted methods',
21+
categories: undefined,
22+
url: 'https://eslint.vuejs.org/rules/no-restricted-call-after-await.html'
23+
},
24+
fixable: null,
25+
schema: {
26+
type: 'array',
27+
items: {
28+
type: 'object',
29+
properties: {
30+
module: { type: 'string' },
31+
path: {
32+
anyOf: [
33+
{ type: 'string' },
34+
{
35+
type: 'array',
36+
items: {
37+
type: 'string'
38+
}
39+
}
40+
]
41+
},
42+
message: { type: 'string', minLength: 1 }
43+
},
44+
required: ['module'],
45+
additionalProperties: false
46+
},
47+
uniqueItems: true,
48+
minItems: 0
49+
},
50+
messages: {
51+
// eslint-disable-next-line eslint-plugin/report-message-format
52+
restricted: '{{message}}'
53+
}
54+
},
55+
/** @param {RuleContext} context */
56+
create(context) {
57+
/** @type {Map<ESNode, string>} */
58+
const restrictedCallNodes = new Map()
59+
/** @type {Map<FunctionExpression | ArrowFunctionExpression | FunctionDeclaration, { setupProperty: Property, afterAwait: boolean }>} */
60+
const setupFunctions = new Map()
61+
62+
/**x
63+
* @typedef {object} ScopeStack
64+
* @property {ScopeStack | null} upper
65+
* @property {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} functionNode
66+
*/
67+
/** @type {ScopeStack | null} */
68+
let scopeStack = null
69+
70+
/** @type {Record<string, string[]> | null} */
71+
let allLocalImports = null
72+
/**
73+
* @param {string} id
74+
*/
75+
function safeRequireResolve(id) {
76+
try {
77+
if (fs.statSync(id).isDirectory()) {
78+
return require.resolve(id)
79+
}
80+
} catch (_e) {
81+
// ignore
82+
}
83+
return id
84+
}
85+
/**
86+
* @param {Program} ast
87+
*/
88+
function getAllLocalImports(ast) {
89+
if (!allLocalImports) {
90+
allLocalImports = {}
91+
const dir = path.dirname(context.getFilename())
92+
for (const body of ast.body) {
93+
if (body.type !== 'ImportDeclaration') {
94+
continue
95+
}
96+
const source = String(body.source.value)
97+
if (!source.startsWith('.')) {
98+
continue
99+
}
100+
const modulePath = safeRequireResolve(path.join(dir, source))
101+
const list =
102+
allLocalImports[modulePath] || (allLocalImports[modulePath] = [])
103+
list.push(source)
104+
}
105+
}
106+
107+
return allLocalImports
108+
}
109+
110+
function getCwd() {
111+
if (context.getCwd) {
112+
return context.getCwd()
113+
}
114+
return path.resolve('')
115+
}
116+
117+
/**
118+
* @param {string} moduleName
119+
* @param {Program} ast
120+
* @returns {string[]}
121+
*/
122+
function normalizeModules(moduleName, ast) {
123+
/** @type {string} */
124+
let modulePath
125+
if (moduleName.startsWith('.')) {
126+
modulePath = safeRequireResolve(path.join(getCwd(), moduleName))
127+
} else if (path.isAbsolute(moduleName)) {
128+
modulePath = safeRequireResolve(moduleName)
129+
} else {
130+
return [moduleName]
131+
}
132+
return getAllLocalImports(ast)[modulePath] || []
133+
}
134+
135+
return utils.compositingVisitors(
136+
{
137+
/** @param {Program} node */
138+
Program(node) {
139+
const tracker = new ReferenceTracker(context.getScope())
140+
141+
for (const option of context.options) {
142+
const modules = normalizeModules(option.module, node)
143+
144+
for (const module of modules) {
145+
/** @type {TraceMap} */
146+
const traceMap = {
147+
[module]: {
148+
[ReferenceTracker.ESM]: true
149+
}
150+
}
151+
152+
/** @type {TraceKind & TraceMap} */
153+
const mod = traceMap[module]
154+
let local = mod
155+
const paths = Array.isArray(option.path)
156+
? option.path
157+
: [option.path || 'default']
158+
for (const path of paths) {
159+
local = local[path] || (local[path] = {})
160+
}
161+
local[ReferenceTracker.CALL] = true
162+
const message =
163+
option.message ||
164+
`The \`${[`import("${module}")`, ...paths].join(
165+
'.'
166+
)}\` after \`await\` expression are forbidden.`
167+
168+
for (const { node } of tracker.iterateEsmReferences(traceMap)) {
169+
restrictedCallNodes.set(node, message)
170+
}
171+
}
172+
}
173+
}
174+
},
175+
utils.defineVueVisitor(context, {
176+
/** @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} node */
177+
':function'(node) {
178+
scopeStack = {
179+
upper: scopeStack,
180+
functionNode: node
181+
}
182+
},
183+
onSetupFunctionEnter(node) {
184+
setupFunctions.set(node, {
185+
setupProperty: node.parent,
186+
afterAwait: false
187+
})
188+
},
189+
AwaitExpression() {
190+
if (!scopeStack) {
191+
return
192+
}
193+
const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
194+
if (!setupFunctionData) {
195+
return
196+
}
197+
setupFunctionData.afterAwait = true
198+
},
199+
/** @param {CallExpression} node */
200+
CallExpression(node) {
201+
if (!scopeStack) {
202+
return
203+
}
204+
const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
205+
if (!setupFunctionData || !setupFunctionData.afterAwait) {
206+
return
207+
}
208+
209+
const message = restrictedCallNodes.get(node)
210+
if (message) {
211+
context.report({
212+
node,
213+
messageId: 'restricted',
214+
data: { message }
215+
})
216+
}
217+
},
218+
/** @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} node */
219+
':function:exit'(node) {
220+
scopeStack = scopeStack && scopeStack.upper
221+
222+
setupFunctions.delete(node)
223+
}
224+
})
225+
)
226+
}
227+
}

0 commit comments

Comments
 (0)