Skip to content

Commit

Permalink
Add vue/require-explicit-emits rule (#1124)
Browse files Browse the repository at this point in the history
* Add `vue/require-explicit-emits` rule

* Refactor
  • Loading branch information
ota-meshi committed May 15, 2020
1 parent a7c6696 commit e785bdb
Show file tree
Hide file tree
Showing 16 changed files with 2,555 additions and 513 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -287,6 +287,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 @@ -88,6 +88,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'
})
}
})
})
}
)
}
}

0 comments on commit e785bdb

Please sign in to comment.