Skip to content

Commit e785bdb

Browse files
authored
Add vue/require-explicit-emits rule (#1124)
* Add `vue/require-explicit-emits` rule * Refactor
1 parent a7c6696 commit e785bdb

16 files changed

+2555
-513
lines changed

Diff for: docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ For example:
287287
| [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
288288
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
289289
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | |
290+
| [vue/require-explicit-emits](./require-explicit-emits.md) | require `emits` option with name triggered by `$emit()` | |
290291
| [vue/require-name-property](./require-name-property.md) | require a name property in Vue components | |
291292
| [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: |
292293
| [vue/sort-keys](./sort-keys.md) | enforce sort-keys in a manner that is compatible with order-in-components | |

Diff for: docs/rules/no-deprecated-vue-config-keycodes.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ description: disallow using deprecated `Vue.config.keyCodes` (in Vue.js 3.0.0+)
1313

1414
This rule reports use of deprecated `Vue.config.keyCodes` (in Vue.js 3.0.0+)
1515

16-
<eslint-code-block filename="a.js" language="javascript ":rules="{'vue/no-deprecated-vue-config-keycodes': ['error']}">
16+
<eslint-code-block filename="a.js" language="javascript" :rules="{'vue/no-deprecated-vue-config-keycodes': ['error']}">
1717

1818
```js
1919
/* ✗ BAD */
@@ -31,14 +31,16 @@ Nothing.
3131
## :couple: Related rules
3232

3333
- [vue/no-deprecated-v-on-number-modifiers]
34-
- [API - Global Config - keyCodes]
3534

3635
[vue/no-deprecated-v-on-number-modifiers]: ./no-deprecated-v-on-number-modifiers.md
37-
[API - Global Config - keyCodes]: https://vuejs.org/v2/api/#keyCodes
3836

3937
## :books: Further reading
4038

41-
- [Vue RFCs - 0014-drop-keycode-support](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0014-drop-keycode-support.md)
39+
- [Vue RFCs - 0014-drop-keycode-support]
40+
- [API - Global Config - keyCodes]
41+
42+
[Vue RFCs - 0014-drop-keycode-support]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0014-drop-keycode-support.md
43+
[API - Global Config - keyCodes]: https://vuejs.org/v2/api/#keyCodes
4244

4345
## :mag: Implementation
4446

Diff for: docs/rules/require-explicit-emits.md

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/require-explicit-emits
5+
description: require `emits` option with name triggered by `$emit()`
6+
---
7+
# vue/require-explicit-emits
8+
> require `emits` option with name triggered by `$emit()`
9+
10+
## :book: Rule Details
11+
12+
This rule reports event triggers not declared with the `emits` option. (The `emits` option is a new in Vue.js 3.0.0+)
13+
14+
Explicit `emits` declaration serves as self-documenting code. This can be useful for other developers to instantly understand what events the component is supposed to emit.
15+
Also, with attribute fallthrough changes in Vue.js 3.0.0+, `v-on` listeners on components will fallthrough as native listeners by default. Declare it as a component-only event in `emits` to avoid unnecessary registration of native listeners.
16+
17+
<eslint-code-block :rules="{'vue/require-explicit-emits': ['error']}">
18+
19+
```vue
20+
<template>
21+
<!-- ✓ GOOD -->
22+
<div @click="$emit('good')"/>
23+
<!-- ✗ BAD -->
24+
<div @click="$emit('bad')"/>
25+
</template>
26+
<script>
27+
export default {
28+
emits: ['good']
29+
}
30+
</script>
31+
```
32+
33+
</eslint-code-block>
34+
35+
<eslint-code-block :rules="{'vue/require-explicit-emits': ['error']}">
36+
37+
```vue
38+
<script>
39+
export default {
40+
emits: ['good'],
41+
methods: {
42+
foo () {
43+
// ✓ GOOD
44+
this.$emit('good')
45+
// ✗ BAD
46+
this.$emit('bad')
47+
}
48+
}
49+
}
50+
</script>
51+
```
52+
53+
</eslint-code-block>
54+
55+
<eslint-code-block :rules="{'vue/require-explicit-emits': ['error']}">
56+
57+
```vue
58+
<script>
59+
export default {
60+
emits: ['good'],
61+
setup (props, context) {
62+
// ✓ GOOD
63+
context.emit('good')
64+
// ✗ BAD
65+
context.emit('bad')
66+
}
67+
}
68+
</script>
69+
```
70+
71+
</eslint-code-block>
72+
73+
## :wrench: Options
74+
75+
Nothing.
76+
77+
## :books: Further reading
78+
79+
- [Vue RFCs - 0030-emits-option](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0030-emits-option.md)
80+
81+
## :mag: Implementation
82+
83+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-explicit-emits.js)
84+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/require-explicit-emits.js)

Diff for: lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ module.exports = {
8888
'require-component-is': require('./rules/require-component-is'),
8989
'require-default-prop': require('./rules/require-default-prop'),
9090
'require-direct-export': require('./rules/require-direct-export'),
91+
'require-explicit-emits': require('./rules/require-explicit-emits'),
9192
'require-name-property': require('./rules/require-name-property'),
9293
'require-prop-type-constructor': require('./rules/require-prop-type-constructor'),
9394
'require-prop-types': require('./rules/require-prop-types'),

Diff for: lib/rules/no-async-in-computed-properties.js

+47-66
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ module.exports = {
7272
},
7373

7474
create (context) {
75-
const forbiddenNodes = []
75+
const computedPropertiesMap = new Map()
7676
let scopeStack = { upper: null, body: null }
7777

7878
const expressionTypes = {
@@ -83,13 +83,9 @@ module.exports = {
8383
timed: 'timed function'
8484
}
8585

86-
function onFunctionEnter (node) {
86+
function onFunctionEnter (node, { node: vueNode }) {
8787
if (node.async) {
88-
forbiddenNodes.push({
89-
node: node,
90-
type: 'async',
91-
targetBody: node.body
92-
})
88+
verify(node, node.body, 'async', computedPropertiesMap.get(vueNode))
9389
}
9490

9591
scopeStack = { upper: scopeStack, body: node.body }
@@ -98,68 +94,53 @@ module.exports = {
9894
function onFunctionExit () {
9995
scopeStack = scopeStack.upper
10096
}
101-
return Object.assign({},
102-
{
103-
':function': onFunctionEnter,
104-
':function:exit': onFunctionExit,
105-
106-
NewExpression (node) {
107-
if (node.callee.name === 'Promise') {
108-
forbiddenNodes.push({
109-
node: node,
110-
type: 'new',
111-
targetBody: scopeStack.body
112-
})
113-
}
114-
},
115-
116-
CallExpression (node) {
117-
if (isPromise(node)) {
118-
forbiddenNodes.push({
119-
node: node,
120-
type: 'promise',
121-
targetBody: scopeStack.body
122-
})
123-
} else if (isTimedFunction(node)) {
124-
forbiddenNodes.push({
125-
node: node,
126-
type: 'timed',
127-
targetBody: scopeStack.body
128-
})
129-
}
130-
},
131-
132-
AwaitExpression (node) {
133-
forbiddenNodes.push({
97+
98+
function verify (node, targetBody, type, computedProperties) {
99+
computedProperties.forEach(cp => {
100+
if (
101+
cp.value &&
102+
node.loc.start.line >= cp.value.loc.start.line &&
103+
node.loc.end.line <= cp.value.loc.end.line &&
104+
targetBody === cp.value
105+
) {
106+
context.report({
134107
node: node,
135-
type: 'await',
136-
targetBody: scopeStack.body
137-
})
138-
}
139-
},
140-
utils.executeOnVue(context, (obj) => {
141-
const computedProperties = utils.getComputedProperties(obj)
142-
143-
computedProperties.forEach(cp => {
144-
forbiddenNodes.forEach(el => {
145-
if (
146-
cp.value &&
147-
el.node.loc.start.line >= cp.value.loc.start.line &&
148-
el.node.loc.end.line <= cp.value.loc.end.line &&
149-
el.targetBody === cp.value
150-
) {
151-
context.report({
152-
node: el.node,
153-
message: 'Unexpected {{expressionName}} in "{{propertyName}}" computed property.',
154-
data: {
155-
expressionName: expressionTypes[el.type],
156-
propertyName: cp.key
157-
}
158-
})
108+
message: 'Unexpected {{expressionName}} in "{{propertyName}}" computed property.',
109+
data: {
110+
expressionName: expressionTypes[type],
111+
propertyName: cp.key
159112
}
160113
})
161-
})
114+
}
162115
})
163-
)
116+
}
117+
return utils.defineVueVisitor(context, {
118+
ObjectExpression (node, { node: vueNode }) {
119+
if (node !== vueNode) {
120+
return
121+
}
122+
computedPropertiesMap.set(node, utils.getComputedProperties(node))
123+
},
124+
':function': onFunctionEnter,
125+
':function:exit': onFunctionExit,
126+
127+
NewExpression (node, { node: vueNode }) {
128+
if (node.callee.name === 'Promise') {
129+
verify(node, scopeStack.body, 'new', computedPropertiesMap.get(vueNode))
130+
}
131+
},
132+
133+
CallExpression (node, { node: vueNode }) {
134+
if (isPromise(node)) {
135+
verify(node, scopeStack.body, 'promise', computedPropertiesMap.get(vueNode))
136+
} else if (isTimedFunction(node)) {
137+
verify(node, scopeStack.body, 'timed', computedPropertiesMap.get(vueNode))
138+
}
139+
},
140+
141+
AwaitExpression (node, { node: vueNode }) {
142+
verify(node, scopeStack.body, 'await', computedPropertiesMap.get(vueNode))
143+
}
144+
})
164145
}
165146
}

Diff for: lib/rules/no-deprecated-events-api.js

+7-18
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,17 @@ module.exports = {
3030
},
3131

3232
create (context) {
33-
const forbiddenNodes = []
34-
35-
return Object.assign(
33+
return utils.defineVueVisitor(context,
3634
{
3735
'CallExpression > MemberExpression > ThisExpression' (node) {
3836
if (!['$on', '$off', '$once'].includes(node.parent.property.name)) return
39-
forbiddenNodes.push(node.parent.parent)
37+
38+
context.report({
39+
node: node.parent.parent,
40+
messageId: 'noDeprecatedEventsApi'
41+
})
4042
}
41-
},
42-
utils.executeOnVue(context, (obj) => {
43-
forbiddenNodes.forEach(node => {
44-
if (
45-
node.loc.start.line >= obj.loc.start.line &&
46-
node.loc.end.line <= obj.loc.end.line
47-
) {
48-
context.report({
49-
node,
50-
messageId: 'noDeprecatedEventsApi'
51-
})
52-
}
53-
})
54-
})
43+
}
5544
)
5645
}
5746
}

0 commit comments

Comments
 (0)