Skip to content

Commit

Permalink
Add new vue/no-restricted-v-on rule (#2367)
Browse files Browse the repository at this point in the history
Co-authored-by: Flo Edelmann <git@flo-edelmann.de>
  • Loading branch information
thesheppard and FloEdelmann authored Jan 16, 2024
1 parent 634f38d commit 7d13ce3
Show file tree
Hide file tree
Showing 7 changed files with 480 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ For example:
| [vue/no-restricted-props](./no-restricted-props.md) | disallow specific props | :bulb: | :hammer: |
| [vue/no-restricted-static-attribute](./no-restricted-static-attribute.md) | disallow specific attribute | | :hammer: |
| [vue/no-restricted-v-bind](./no-restricted-v-bind.md) | disallow specific argument in `v-bind` | | :hammer: |
| [vue/no-restricted-v-on](./no-restricted-v-on.md) | disallow specific argument in `v-on` | | :hammer: |
| [vue/no-root-v-if](./no-root-v-if.md) | disallow `v-if` directives on root element | | :hammer: |
| [vue/no-setup-props-reactivity-loss](./no-setup-props-reactivity-loss.md) | disallow usages that lose the reactivity of `props` passed to `setup` | | :hammer: |
| [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | | :hammer: |
Expand Down
6 changes: 5 additions & 1 deletion docs/rules/no-restricted-static-attribute.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ title: vue/no-restricted-static-attribute
description: disallow specific attribute
since: v7.0.0
---

# vue/no-restricted-static-attribute

> disallow specific attribute
Expand Down Expand Up @@ -39,7 +40,8 @@ Alternatively, the rule also accepts objects.

```json
{
"vue/no-restricted-static-attribute": ["error",
"vue/no-restricted-static-attribute": [
"error",
{
"key": "stlye",
"message": "Using \"stlye\" is not allowed. Use \"style\" instead."
Expand Down Expand Up @@ -95,8 +97,10 @@ The following properties can be specified for the object.
## :couple: Related Rules

- [vue/no-restricted-v-bind]
- [vue/no-restricted-v-on]

[vue/no-restricted-v-bind]: ./no-restricted-v-bind.md
[vue/no-restricted-v-on]: ./no-restricted-v-on.md

## :rocket: Version

Expand Down
6 changes: 5 additions & 1 deletion docs/rules/no-restricted-v-bind.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ title: vue/no-restricted-v-bind
description: disallow specific argument in `v-bind`
since: v7.0.0
---

# vue/no-restricted-v-bind

> disallow specific argument in `v-bind`
Expand Down Expand Up @@ -53,7 +54,8 @@ Alternatively, the rule also accepts objects.

```json
{
"vue/no-restricted-v-bind": ["error",
"vue/no-restricted-v-bind": [
"error",
{
"argument": "/^v-/",
"message": "Using `:v-xxx` is not allowed. Instead, remove `:` and use it as directive."
Expand Down Expand Up @@ -112,8 +114,10 @@ The following properties can be specified for the object.
## :couple: Related Rules

- [vue/no-restricted-static-attribute]
- [vue/no-restricted-v-on]

[vue/no-restricted-static-attribute]: ./no-restricted-static-attribute.md
[vue/no-restricted-v-on]: ./no-restricted-v-on.md

## :rocket: Version

Expand Down
111 changes: 111 additions & 0 deletions docs/rules/no-restricted-v-on.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/no-restricted-v-on
description: disallow specific argument in `v-on`
---
# vue/no-restricted-v-on

> disallow specific argument in `v-on`
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>

## :book: Rule Details

This rule allows you to specify `v-on` argument names that you don't want to use in your application.

## :wrench: Options

This rule takes a list of strings, where each string is a argument name or pattern to be restricted:

```json
{
"vue/no-restricted-v-on": ["error", "foo", "/^bar/"]
}
```

<eslint-code-block :rules="{'vue/no-restricted-v-on': ['error', 'foo', '/^bar/']}">

```vue
<template>
<!-- ✓ GOOD -->
<div v-on:click="x" />
<div @tap="x" />
<!-- ✗ BAD -->
<div v-on:foo="x" />
<div @bar-baz="x" />
</template>
```

</eslint-code-block>

Alternatively, the rule also accepts objects.

```json
{
"vue/no-restricted-v-on": [
"error",
{
"argument": "foo",
"message": "Use \"v-on:x\" instead."
},
{
"argument": "bar",
"message": "\"@bar\" is deprecated."
}
]
}
```

The following properties can be specified for the object.

- `argument` ... Specify the argument name or pattern or `null`. If `null` is specified, it matches `v-on=`.
- `modifiers` ... Specifies an array of the modifier names. If specified, it will only be reported if the specified modifier is used.
- `element` ... Specify the element name or pattern. If specified, it will only be reported if used on the specified element.
- `message` ... Specify an optional custom message.

### `{ "argument": "foo", "modifiers": ["prevent"] }`

<eslint-code-block :rules="{'vue/no-restricted-v-on': ['error', { argument: 'foo', modifiers: ['prevent'] }]}">

```vue
<template>
<!-- ✓ GOOD -->
<div @foo="x" />
<!-- ✗ BAD -->
<div @foo.prevent="x" />
</template>
```

</eslint-code-block>

### `{ "argument": "foo", "element": "MyButton" }`

<eslint-code-block :rules="{'vue/no-restricted-v-on': ['error', { argument: 'foo', element: 'MyButton' }]}">

```vue
<template>
<!-- ✓ GOOD -->
<CoolButton @foo="x" />
<!-- ✗ BAD -->
<MyButton @foo="x" />
</template>
```

</eslint-code-block>

## :couple: Related Rules

- [vue/no-restricted-static-attribute]
- [vue/no-restricted-v-bind]

[vue/no-restricted-static-attribute]: ./no-restricted-static-attribute.md
[vue/no-restricted-v-bind]: ./no-restricted-v-bind.md

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-v-on.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-v-on.js)
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ module.exports = {
'no-restricted-static-attribute': require('./rules/no-restricted-static-attribute'),
'no-restricted-syntax': require('./rules/no-restricted-syntax'),
'no-restricted-v-bind': require('./rules/no-restricted-v-bind'),
'no-restricted-v-on': require('./rules/no-restricted-v-on'),
'no-root-v-if': require('./rules/no-root-v-if'),
'no-setup-props-destructure': require('./rules/no-setup-props-destructure'),
'no-setup-props-reactivity-loss': require('./rules/no-setup-props-reactivity-loss'),
Expand Down
184 changes: 184 additions & 0 deletions lib/rules/no-restricted-v-on.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/**
* @author Kamogelo Moalusi <github.com/thesheppard>
* See LICENSE file in root directory for full license.
*/
'use strict'

const utils = require('../utils')
const regexp = require('../utils/regexp')

/**
* @typedef {object} ParsedOption
* @property { (key: VDirectiveKey) => boolean } test
* @property {string[]} [modifiers]
* @property {boolean} [useElement]
* @property {string} [message]
*/

/**
* @param {string} str
* @returns {(str: string) => boolean}
*/
function buildMatcher(str) {
if (regexp.isRegExp(str)) {
const re = regexp.toRegExp(str)
return (s) => {
re.lastIndex = 0
return re.test(s)
}
}
return (s) => s === str
}

/**
* @param {any} option
* @returns {ParsedOption}
*/
function parseOption(option) {
if (typeof option === 'string') {
const matcher = buildMatcher(option)
return {
test(key) {
return Boolean(
key.argument &&
key.argument.type === 'VIdentifier' &&
matcher(key.argument.rawName)
)
}
}
}
if (option === null) {
return {
test(key) {
return key.argument === null
}
}
}
const parsed = parseOption(option.argument)

if (option.modifiers) {
const argTest = parsed.test
parsed.test = (key) => {
if (!argTest(key)) {
return false
}
return /** @type {string[]} */ (option.modifiers).every((modName) =>
key.modifiers.some((mid) => mid.name === modName)
)
}
parsed.modifiers = option.modifiers
}
if (option.element) {
const argTest = parsed.test
const tagMatcher = buildMatcher(option.element)
parsed.test = (key) => {
if (!argTest(key)) {
return false
}
return tagMatcher(key.parent.parent.parent.rawName)
}
parsed.useElement = true
}
parsed.message = option.message
return parsed
}

/**
* @param {VDirectiveKey} key
* @param {ParsedOption} option
*/
function defaultMessage(key, option) {
const von = key.name.rawName === '@' ? '' : 'v-on'
const arg =
key.argument != null && key.argument.type === 'VIdentifier'
? `${key.name.rawName === '@' ? '@' : ':'}${key.argument.rawName}`
: ''
const mod =
option.modifiers != null && option.modifiers.length > 0
? `.${option.modifiers.join('.')}`
: ''
let element = 'element'
if (option.useElement) {
element = `<${key.parent.parent.parent.rawName}>`
}
return `Using \`${von + arg + mod}\` is not allowed on this ${element}.`
}

module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow specific argument in `v-on`',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/no-restricted-v-on.html'
},
fixable: null,
schema: {
type: 'array',
items: {
oneOf: [
{ type: ['string', 'null'] },
{
type: 'object',
properties: {
argument: { type: ['string', 'null'] },
element: { type: 'string' },
message: { type: 'string', minLength: 1 },
modifiers: {
type: 'array',
items: {
type: 'string',
enum: [
'prevent',
'stop',
'capture',
'self',
'once',
'passive'
]
},
uniqueItems: true,
minItems: 1
}
},
required: ['argument'],
additionalProperties: false
}
]
},
uniqueItems: true
},
messages: {
// eslint-disable-next-line eslint-plugin/report-message-format
restrictedVOn: '{{message}}'
}
},

/** @param {RuleContext} context */
create(context) {
if (context.options.length === 0) {
return {}
}
/** @type {ParsedOption[]} */
const options = context.options.map(parseOption)

return utils.defineTemplateBodyVisitor(context, {
/**
* @param {VDirectiveKey} node
*/
"VAttribute[directive=true][key.name.name='on'] > VDirectiveKey"(node) {
for (const option of options) {
if (option.test(node)) {
const message = option.message || defaultMessage(node, option)
context.report({
node,
messageId: 'restrictedVOn',
data: { message }
})
return
}
}
}
})
}
}
Loading

0 comments on commit 7d13ce3

Please sign in to comment.