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/require-explicit-emits rule #1124

Merged
merged 2 commits into from May 15, 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 @@ -286,6 +286,7 @@ For example:
| [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | |
| [vue/require-explicit-emits](./require-explicit-emits.md) | require `emits` option with name triggered by `$emit()` | |
| [vue/require-name-property](./require-name-property.md) | require a name property in Vue components | |
| [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: |
| [vue/sort-keys](./sort-keys.md) | enforce sort-keys in a manner that is compatible with order-in-components | |
Expand Down
10 changes: 6 additions & 4 deletions docs/rules/no-deprecated-vue-config-keycodes.md
Expand Up @@ -13,7 +13,7 @@ description: disallow using deprecated `Vue.config.keyCodes` (in Vue.js 3.0.0+)

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

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

```js
/* ✗ BAD */
Expand All @@ -31,14 +31,16 @@ Nothing.
## :couple: Related rules

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

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

## :books: Further reading

- [Vue RFCs - 0014-drop-keycode-support](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0014-drop-keycode-support.md)
- [Vue RFCs - 0014-drop-keycode-support]
- [API - Global Config - keyCodes]

[Vue RFCs - 0014-drop-keycode-support]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0014-drop-keycode-support.md
[API - Global Config - keyCodes]: https://vuejs.org/v2/api/#keyCodes

## :mag: Implementation

Expand Down
84 changes: 84 additions & 0 deletions docs/rules/require-explicit-emits.md
@@ -0,0 +1,84 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/require-explicit-emits
description: require `emits` option with name triggered by `$emit()`
---
# vue/require-explicit-emits
> require `emits` option with name triggered by `$emit()`

## :book: Rule Details

This rule reports event triggers not declared with the `emits` option. (The `emits` option is a new in Vue.js 3.0.0+)

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.
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.

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

```vue
<template>
<!-- ✓ GOOD -->
<div @click="$emit('good')"/>
<!-- ✗ BAD -->
<div @click="$emit('bad')"/>
</template>
<script>
export default {
emits: ['good']
}
</script>
```

</eslint-code-block>

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

```vue
<script>
export default {
emits: ['good'],
methods: {
foo () {
// ✓ GOOD
this.$emit('good')
// ✗ BAD
this.$emit('bad')
}
}
}
</script>
```

</eslint-code-block>

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

```vue
<script>
export default {
emits: ['good'],
setup (props, context) {
// ✓ GOOD
context.emit('good')
// ✗ BAD
context.emit('bad')
}
}
</script>
```

</eslint-code-block>

## :wrench: Options

Nothing.

## :books: Further reading

- [Vue RFCs - 0030-emits-option](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0030-emits-option.md)

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-explicit-emits.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/require-explicit-emits.js)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -87,6 +87,7 @@ module.exports = {
'require-component-is': require('./rules/require-component-is'),
'require-default-prop': require('./rules/require-default-prop'),
'require-direct-export': require('./rules/require-direct-export'),
'require-explicit-emits': require('./rules/require-explicit-emits'),
'require-name-property': require('./rules/require-name-property'),
'require-prop-type-constructor': require('./rules/require-prop-type-constructor'),
'require-prop-types': require('./rules/require-prop-types'),
Expand Down
113 changes: 47 additions & 66 deletions lib/rules/no-async-in-computed-properties.js
Expand Up @@ -72,7 +72,7 @@ module.exports = {
},

create (context) {
const forbiddenNodes = []
const computedPropertiesMap = new Map()
let scopeStack = { upper: null, body: null }

const expressionTypes = {
Expand All @@ -83,13 +83,9 @@ module.exports = {
timed: 'timed function'
}

function onFunctionEnter (node) {
function onFunctionEnter (node, { node: vueNode }) {
if (node.async) {
forbiddenNodes.push({
node: node,
type: 'async',
targetBody: node.body
})
verify(node, node.body, 'async', computedPropertiesMap.get(vueNode))
}

scopeStack = { upper: scopeStack, body: node.body }
Expand All @@ -98,68 +94,53 @@ module.exports = {
function onFunctionExit () {
scopeStack = scopeStack.upper
}
return Object.assign({},
{
':function': onFunctionEnter,
':function:exit': onFunctionExit,

NewExpression (node) {
if (node.callee.name === 'Promise') {
forbiddenNodes.push({
node: node,
type: 'new',
targetBody: scopeStack.body
})
}
},

CallExpression (node) {
if (isPromise(node)) {
forbiddenNodes.push({
node: node,
type: 'promise',
targetBody: scopeStack.body
})
} else if (isTimedFunction(node)) {
forbiddenNodes.push({
node: node,
type: 'timed',
targetBody: scopeStack.body
})
}
},

AwaitExpression (node) {
forbiddenNodes.push({

function verify (node, targetBody, type, computedProperties) {
computedProperties.forEach(cp => {
if (
cp.value &&
node.loc.start.line >= cp.value.loc.start.line &&
node.loc.end.line <= cp.value.loc.end.line &&
targetBody === cp.value
) {
context.report({
node: node,
type: 'await',
targetBody: scopeStack.body
})
}
},
utils.executeOnVue(context, (obj) => {
const computedProperties = utils.getComputedProperties(obj)

computedProperties.forEach(cp => {
forbiddenNodes.forEach(el => {
if (
cp.value &&
el.node.loc.start.line >= cp.value.loc.start.line &&
el.node.loc.end.line <= cp.value.loc.end.line &&
el.targetBody === cp.value
) {
context.report({
node: el.node,
message: 'Unexpected {{expressionName}} in "{{propertyName}}" computed property.',
data: {
expressionName: expressionTypes[el.type],
propertyName: cp.key
}
})
message: 'Unexpected {{expressionName}} in "{{propertyName}}" computed property.',
data: {
expressionName: expressionTypes[type],
propertyName: cp.key
}
})
})
}
})
)
}
return utils.defineVueVisitor(context, {
ObjectExpression (node, { node: vueNode }) {
if (node !== vueNode) {
return
}
computedPropertiesMap.set(node, utils.getComputedProperties(node))
},
':function': onFunctionEnter,
':function:exit': onFunctionExit,

NewExpression (node, { node: vueNode }) {
if (node.callee.name === 'Promise') {
verify(node, scopeStack.body, 'new', computedPropertiesMap.get(vueNode))
}
},

CallExpression (node, { node: vueNode }) {
if (isPromise(node)) {
verify(node, scopeStack.body, 'promise', computedPropertiesMap.get(vueNode))
} else if (isTimedFunction(node)) {
verify(node, scopeStack.body, 'timed', computedPropertiesMap.get(vueNode))
}
},

AwaitExpression (node, { node: vueNode }) {
verify(node, scopeStack.body, 'await', computedPropertiesMap.get(vueNode))
}
})
}
}
25 changes: 7 additions & 18 deletions lib/rules/no-deprecated-events-api.js
Expand Up @@ -30,28 +30,17 @@ module.exports = {
},

create (context) {
const forbiddenNodes = []

return Object.assign(
return utils.defineVueVisitor(context,
{
'CallExpression > MemberExpression > ThisExpression' (node) {
if (!['$on', '$off', '$once'].includes(node.parent.property.name)) return
forbiddenNodes.push(node.parent.parent)

context.report({
node: node.parent.parent,
messageId: 'noDeprecatedEventsApi'
})
}
},
utils.executeOnVue(context, (obj) => {
forbiddenNodes.forEach(node => {
if (
node.loc.start.line >= obj.loc.start.line &&
node.loc.end.line <= obj.loc.end.line
) {
context.report({
node,
messageId: 'noDeprecatedEventsApi'
})
}
})
})
}
)
}
}