Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c7efaef

Browse files
committedJul 6, 2021
Add vue/valid-define-emits rule
1 parent fbf0194 commit c7efaef

File tree

5 files changed

+435
-0
lines changed

5 files changed

+435
-0
lines changed
 

‎docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ For example:
331331
| [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: |
332332
| [vue/v-on-event-hyphenation](./v-on-event-hyphenation.md) | enforce v-on event naming style on custom components in template | :wrench: |
333333
| [vue/v-on-function-call](./v-on-function-call.md) | enforce or forbid parentheses after method calls without arguments in `v-on` directives | :wrench: |
334+
| [vue/valid-define-emits](./valid-define-emits.md) | enforce valid `defineEmits` compiler macro | |
334335
| [vue/valid-define-props](./valid-define-props.md) | enforce valid `defineProps` compiler macro | |
335336
| [vue/valid-next-tick](./valid-next-tick.md) | enforce valid `nextTick` function calls | :wrench: |
336337

‎docs/rules/valid-define-emits.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/valid-define-emits
5+
description: enforce valid `defineEmits` compiler macro
6+
---
7+
# vue/valid-define-emits
8+
9+
> enforce valid `defineEmits` compiler macro
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+
This rule checks whether `defineEmits` compiler macro is valid.
14+
15+
## :book: Rule Details
16+
17+
This rule reports `defineEmits` compiler macros in the following cases:
18+
19+
- `defineEmits` are referencing locally declared variables.
20+
- `defineEmits` has both a literal type and an argument. e.g. `defineEmits<(e: 'foo')=>void>(['bar'])`
21+
- `defineEmits` has been called multiple times.
22+
- Custom events are defined in both `defineEmits` and `export default {}`.
23+
- Custom events are not defined in either `defineEmits` or `export default {}`.
24+
25+
<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">
26+
27+
```vue
28+
<script setup>
29+
/* ✓ GOOD */
30+
defineEmits({ notify: null })
31+
</script>
32+
```
33+
34+
</eslint-code-block>
35+
36+
<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">
37+
38+
```vue
39+
<script setup>
40+
/* ✓ GOOD */
41+
defineEmits(['notify'])
42+
</script>
43+
```
44+
45+
</eslint-code-block>
46+
47+
```vue
48+
<script setup lang="ts">
49+
/* ✓ GOOD */
50+
defineEmits<(e: 'notify')=>void>()
51+
</script>
52+
```
53+
54+
<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">
55+
56+
```vue
57+
<script>
58+
const def = { notify: null }
59+
</script>
60+
<script setup>
61+
/* ✓ GOOD */
62+
defineEmits(def)
63+
</script>
64+
```
65+
66+
</eslint-code-block>
67+
68+
<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">
69+
70+
```vue
71+
<script setup>
72+
/* ✗ BAD */
73+
const def = { notify: null }
74+
defineEmits(def)
75+
</script>
76+
```
77+
78+
</eslint-code-block>
79+
80+
```vue
81+
<script setup lang="ts">
82+
/* ✗ BAD */
83+
defineEmits<(e: 'notify')=>void>({ submit: null })
84+
</script>
85+
```
86+
87+
<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">
88+
89+
```vue
90+
<script setup>
91+
/* ✗ BAD */
92+
defineEmits({ notify: null })
93+
defineEmits({ submit: null })
94+
</script>
95+
```
96+
97+
</eslint-code-block>
98+
99+
<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">
100+
101+
```vue
102+
<script>
103+
export default {
104+
emits: { notify: null }
105+
}
106+
</script>
107+
<script setup>
108+
/* ✗ BAD */
109+
defineEmits({ submit: null })
110+
</script>
111+
```
112+
113+
</eslint-code-block>
114+
115+
<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">
116+
117+
```vue
118+
<script setup>
119+
/* ✗ BAD */
120+
defineEmits()
121+
</script>
122+
```
123+
124+
</eslint-code-block>
125+
126+
## :wrench: Options
127+
128+
Nothing.
129+
130+
## :mag: Implementation
131+
132+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-define-emits.js)
133+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-define-emits.js)

‎lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ module.exports = {
171171
'v-on-function-call': require('./rules/v-on-function-call'),
172172
'v-on-style': require('./rules/v-on-style'),
173173
'v-slot-style': require('./rules/v-slot-style'),
174+
'valid-define-emits': require('./rules/valid-define-emits'),
174175
'valid-define-props': require('./rules/valid-define-props'),
175176
'valid-next-tick': require('./rules/valid-next-tick'),
176177
'valid-template-root': require('./rules/valid-template-root'),

‎lib/rules/valid-define-emits.js

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/**
2+
* @author Yosuke Ota <https://github.com/ota-meshi>
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const { findVariable } = require('eslint-utils')
8+
const utils = require('../utils')
9+
10+
module.exports = {
11+
meta: {
12+
type: 'problem',
13+
docs: {
14+
description: 'enforce valid `defineEmits` compiler macro',
15+
// TODO Switch in the major version.
16+
// categories: ['vue3-essential'],
17+
categories: undefined,
18+
url: 'https://eslint.vuejs.org/rules/valid-define-emits.html'
19+
},
20+
fixable: null,
21+
schema: [],
22+
messages: {
23+
hasTypeAndArg: '`defineEmits` has both a type-only emit and an argument.',
24+
referencingLocally:
25+
'`defineEmits` are referencing locally declared variables.',
26+
multiple: '`defineEmits` has been called multiple times.',
27+
notDefined: 'Custom events are not defined.',
28+
definedInBoth:
29+
'Custom events are defined in both `defineEmits` and `export default {}`.'
30+
}
31+
},
32+
/** @param {RuleContext} context */
33+
create(context) {
34+
const scriptSetup = utils.getScriptSetupElement(context)
35+
if (!scriptSetup) {
36+
return {}
37+
}
38+
39+
/** @type {Set<Expression | SpreadElement>} */
40+
const emitsDefExpressions = new Set()
41+
let hasDefaultExport = false
42+
/** @type {CallExpression[]} */
43+
const defineEmitsNodes = []
44+
/** @type {CallExpression | null} */
45+
let emptyDefineEmits = null
46+
47+
return utils.compositingVisitors(
48+
utils.defineScriptSetupVisitor(context, {
49+
onDefineEmitsEnter(node) {
50+
defineEmitsNodes.push(node)
51+
52+
if (node.arguments.length >= 1) {
53+
if (node.typeParameters && node.typeParameters.params.length >= 1) {
54+
// `defineEmits` has both a literal type and an argument.
55+
context.report({
56+
node,
57+
messageId: 'hasTypeAndArg'
58+
})
59+
return
60+
}
61+
62+
emitsDefExpressions.add(node.arguments[0])
63+
} else {
64+
if (
65+
!node.typeParameters ||
66+
node.typeParameters.params.length === 0
67+
) {
68+
emptyDefineEmits = node
69+
}
70+
}
71+
},
72+
Identifier(node) {
73+
for (const def of emitsDefExpressions) {
74+
if (utils.inRange(def.range, node)) {
75+
const variable = findVariable(context.getScope(), node)
76+
if (
77+
variable &&
78+
variable.references.some((ref) => ref.identifier === node)
79+
) {
80+
if (
81+
variable.defs.length &&
82+
variable.defs.every((def) =>
83+
utils.inRange(scriptSetup.range, def.name)
84+
)
85+
) {
86+
//`defineEmits` are referencing locally declared variables.
87+
context.report({
88+
node,
89+
messageId: 'referencingLocally'
90+
})
91+
}
92+
}
93+
}
94+
}
95+
}
96+
}),
97+
utils.defineVueVisitor(context, {
98+
onVueObjectEnter(node, { type }) {
99+
if (type !== 'export' || utils.inRange(scriptSetup.range, node)) {
100+
return
101+
}
102+
103+
hasDefaultExport = Boolean(utils.findProperty(node, 'emits'))
104+
}
105+
}),
106+
{
107+
'Program:exit'() {
108+
if (!defineEmitsNodes.length) {
109+
return
110+
}
111+
if (defineEmitsNodes.length > 1) {
112+
// `defineEmits` has been called multiple times.
113+
for (const node of defineEmitsNodes) {
114+
context.report({
115+
node,
116+
messageId: 'multiple'
117+
})
118+
}
119+
return
120+
}
121+
if (emptyDefineEmits) {
122+
if (!hasDefaultExport) {
123+
// Custom events are not defined.
124+
context.report({
125+
node: emptyDefineEmits,
126+
messageId: 'notDefined'
127+
})
128+
}
129+
} else {
130+
if (hasDefaultExport) {
131+
// Custom events are defined in both `defineEmits` and `export default {}`.
132+
for (const node of defineEmitsNodes) {
133+
context.report({
134+
node,
135+
messageId: 'definedInBoth'
136+
})
137+
}
138+
}
139+
}
140+
}
141+
}
142+
)
143+
}
144+
}

‎tests/lib/rules/valid-define-emits.js

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* @author Yosuke Ota <https://github.com/ota-meshi>
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const RuleTester = require('eslint').RuleTester
12+
const rule = require('../../../lib/rules/valid-define-emits')
13+
14+
// ------------------------------------------------------------------------------
15+
// Tests
16+
// ------------------------------------------------------------------------------
17+
18+
const tester = new RuleTester({
19+
parser: require.resolve('vue-eslint-parser'),
20+
parserOptions: { ecmaVersion: 2015, sourceType: 'module' }
21+
})
22+
23+
tester.run('valid-define-emits', rule, {
24+
valid: [
25+
{
26+
filename: 'test.vue',
27+
code: `
28+
<script setup>
29+
/* ✓ GOOD */
30+
defineEmits({ notify: null })
31+
</script>
32+
`
33+
},
34+
{
35+
filename: 'test.vue',
36+
code: `
37+
<script setup>
38+
/* ✓ GOOD */
39+
defineEmits(['notify'])
40+
</script>
41+
`
42+
},
43+
{
44+
filename: 'test.vue',
45+
code: `
46+
<script setup lang="ts">
47+
/* ✓ GOOD */
48+
defineEmits<(e: 'notify')=>void>()
49+
</script>
50+
`,
51+
parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
52+
},
53+
{
54+
filename: 'test.vue',
55+
code: `
56+
<script>
57+
const def = { notify: null }
58+
</script>
59+
<script setup>
60+
/* ✓ GOOD */
61+
defineEmits(def)
62+
</script>
63+
`
64+
}
65+
],
66+
invalid: [
67+
{
68+
filename: 'test.vue',
69+
code: `
70+
<script setup>
71+
/* ✗ BAD */
72+
const def = { notify: null }
73+
defineEmits(def)
74+
</script>
75+
`,
76+
errors: [
77+
{
78+
message: '`defineEmits` are referencing locally declared variables.',
79+
line: 5
80+
}
81+
]
82+
},
83+
{
84+
filename: 'test.vue',
85+
code: `
86+
<script setup lang="ts">
87+
/* ✗ BAD */
88+
defineEmits<(e: 'notify')=>void>({ submit: null })
89+
</script>
90+
`,
91+
parserOptions: { parser: require.resolve('@typescript-eslint/parser') },
92+
errors: [
93+
{
94+
message: '`defineEmits` has both a type-only emit and an argument.',
95+
line: 4
96+
}
97+
]
98+
},
99+
{
100+
filename: 'test.vue',
101+
code: `
102+
<script setup>
103+
/* ✗ BAD */
104+
defineEmits({ notify: null })
105+
defineEmits({ submit: null })
106+
</script>
107+
`,
108+
errors: [
109+
{
110+
message: '`defineEmits` has been called multiple times.',
111+
line: 4
112+
},
113+
{
114+
message: '`defineEmits` has been called multiple times.',
115+
line: 5
116+
}
117+
]
118+
},
119+
{
120+
filename: 'test.vue',
121+
code: `
122+
<script>
123+
export default {
124+
emits: ['notify']
125+
}
126+
</script>
127+
<script setup>
128+
/* ✗ BAD */
129+
defineEmits({ submit: null })
130+
</script>
131+
`,
132+
errors: [
133+
{
134+
message:
135+
'Custom events are defined in both `defineEmits` and `export default {}`.',
136+
line: 9
137+
}
138+
]
139+
},
140+
{
141+
filename: 'test.vue',
142+
code: `
143+
<script setup>
144+
/* ✗ BAD */
145+
defineEmits()
146+
</script>
147+
`,
148+
errors: [
149+
{
150+
message: 'Custom events are not defined.',
151+
line: 4
152+
}
153+
]
154+
}
155+
]
156+
})

0 commit comments

Comments
 (0)
Please sign in to comment.