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/valid-define-emits rule #1561

Merged
merged 1 commit into from Jul 6, 2021
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 @@ -331,6 +331,7 @@ For example:
| [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: |
| [vue/v-on-event-hyphenation](./v-on-event-hyphenation.md) | enforce v-on event naming style on custom components in template | :wrench: |
| [vue/v-on-function-call](./v-on-function-call.md) | enforce or forbid parentheses after method calls without arguments in `v-on` directives | :wrench: |
| [vue/valid-define-emits](./valid-define-emits.md) | enforce valid `defineEmits` compiler macro | |
| [vue/valid-define-props](./valid-define-props.md) | enforce valid `defineProps` compiler macro | |
| [vue/valid-next-tick](./valid-next-tick.md) | enforce valid `nextTick` function calls | :wrench: |

Expand Down
133 changes: 133 additions & 0 deletions docs/rules/valid-define-emits.md
@@ -0,0 +1,133 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/valid-define-emits
description: enforce valid `defineEmits` compiler macro
---
# vue/valid-define-emits

> enforce valid `defineEmits` compiler macro

- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>

This rule checks whether `defineEmits` compiler macro is valid.

## :book: Rule Details

This rule reports `defineEmits` compiler macros in the following cases:

- `defineEmits` are referencing locally declared variables.
- `defineEmits` has both a literal type and an argument. e.g. `defineEmits<(e: 'foo')=>void>(['bar'])`
- `defineEmits` has been called multiple times.
- Custom events are defined in both `defineEmits` and `export default {}`.
- Custom events are not defined in either `defineEmits` or `export default {}`.

<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">

```vue
<script setup>
/* ✓ GOOD */
defineEmits({ notify: null })
</script>
```

</eslint-code-block>

<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">

```vue
<script setup>
/* ✓ GOOD */
defineEmits(['notify'])
</script>
```

</eslint-code-block>

```vue
<script setup lang="ts">
/* ✓ GOOD */
defineEmits<(e: 'notify')=>void>()
</script>
```

<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">

```vue
<script>
const def = { notify: null }
</script>
<script setup>
/* ✓ GOOD */
defineEmits(def)
</script>
```

</eslint-code-block>

<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">

```vue
<script setup>
/* ✗ BAD */
const def = { notify: null }
defineEmits(def)
</script>
```

</eslint-code-block>

```vue
<script setup lang="ts">
/* ✗ BAD */
defineEmits<(e: 'notify')=>void>({ submit: null })
</script>
```

<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">

```vue
<script setup>
/* ✗ BAD */
defineEmits({ notify: null })
defineEmits({ submit: null })
</script>
```

</eslint-code-block>

<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">

```vue
<script>
export default {
emits: { notify: null }
}
</script>
<script setup>
/* ✗ BAD */
defineEmits({ submit: null })
</script>
```

</eslint-code-block>

<eslint-code-block :rules="{'vue/valid-define-emits': ['error']}">

```vue
<script setup>
/* ✗ BAD */
defineEmits()
</script>
```

</eslint-code-block>

## :wrench: Options

Nothing.

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-define-emits.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-define-emits.js)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -171,6 +171,7 @@ module.exports = {
'v-on-function-call': require('./rules/v-on-function-call'),
'v-on-style': require('./rules/v-on-style'),
'v-slot-style': require('./rules/v-slot-style'),
'valid-define-emits': require('./rules/valid-define-emits'),
'valid-define-props': require('./rules/valid-define-props'),
'valid-next-tick': require('./rules/valid-next-tick'),
'valid-template-root': require('./rules/valid-template-root'),
Expand Down
144 changes: 144 additions & 0 deletions lib/rules/valid-define-emits.js
@@ -0,0 +1,144 @@
/**
* @author Yosuke Ota <https://github.com/ota-meshi>
* See LICENSE file in root directory for full license.
*/
'use strict'

const { findVariable } = require('eslint-utils')
const utils = require('../utils')

module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `defineEmits` compiler macro',
// TODO Switch in the major version.
// categories: ['vue3-essential'],
categories: undefined,
url: 'https://eslint.vuejs.org/rules/valid-define-emits.html'
},
fixable: null,
schema: [],
messages: {
hasTypeAndArg: '`defineEmits` has both a type-only emit and an argument.',
referencingLocally:
'`defineEmits` are referencing locally declared variables.',
multiple: '`defineEmits` has been called multiple times.',
notDefined: 'Custom events are not defined.',
definedInBoth:
'Custom events are defined in both `defineEmits` and `export default {}`.'
}
},
/** @param {RuleContext} context */
create(context) {
const scriptSetup = utils.getScriptSetupElement(context)
if (!scriptSetup) {
return {}
}

/** @type {Set<Expression | SpreadElement>} */
const emitsDefExpressions = new Set()
let hasDefaultExport = false
/** @type {CallExpression[]} */
const defineEmitsNodes = []
/** @type {CallExpression | null} */
let emptyDefineEmits = null

return utils.compositingVisitors(
utils.defineScriptSetupVisitor(context, {
onDefineEmitsEnter(node) {
defineEmitsNodes.push(node)

if (node.arguments.length >= 1) {
if (node.typeParameters && node.typeParameters.params.length >= 1) {
// `defineEmits` has both a literal type and an argument.
context.report({
node,
messageId: 'hasTypeAndArg'
})
return
}

emitsDefExpressions.add(node.arguments[0])
} else {
if (
!node.typeParameters ||
node.typeParameters.params.length === 0
) {
emptyDefineEmits = node
}
}
},
Identifier(node) {
for (const def of emitsDefExpressions) {
if (utils.inRange(def.range, node)) {
const variable = findVariable(context.getScope(), node)
if (
variable &&
variable.references.some((ref) => ref.identifier === node)
) {
if (
variable.defs.length &&
variable.defs.every((def) =>
utils.inRange(scriptSetup.range, def.name)
)
) {
//`defineEmits` are referencing locally declared variables.
context.report({
node,
messageId: 'referencingLocally'
})
}
}
}
}
}
}),
utils.defineVueVisitor(context, {
onVueObjectEnter(node, { type }) {
if (type !== 'export' || utils.inRange(scriptSetup.range, node)) {
return
}

hasDefaultExport = Boolean(utils.findProperty(node, 'emits'))
}
}),
{
'Program:exit'() {
if (!defineEmitsNodes.length) {
return
}
if (defineEmitsNodes.length > 1) {
// `defineEmits` has been called multiple times.
for (const node of defineEmitsNodes) {
context.report({
node,
messageId: 'multiple'
})
}
return
}
if (emptyDefineEmits) {
if (!hasDefaultExport) {
// Custom events are not defined.
context.report({
node: emptyDefineEmits,
messageId: 'notDefined'
})
}
} else {
if (hasDefaultExport) {
// Custom events are defined in both `defineEmits` and `export default {}`.
for (const node of defineEmitsNodes) {
context.report({
node,
messageId: 'definedInBoth'
})
}
}
}
}
}
)
}
}