Skip to content

Commit

Permalink
Add vue/no-useless-v-bind rule (#1186)
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Jun 5, 2020
1 parent e5c835e commit e644855
Show file tree
Hide file tree
Showing 5 changed files with 391 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -292,6 +292,7 @@ For example:
| [vue/no-unregistered-components](./no-unregistered-components.md) | disallow using components that are not registered inside templates | |
| [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
| [vue/no-unused-properties](./no-unused-properties.md) | disallow unused properties | |
| [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :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()` | |
Expand Down
87 changes: 87 additions & 0 deletions docs/rules/no-useless-v-bind.md
@@ -0,0 +1,87 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/no-useless-v-bind
description: disallow unnecessary `v-bind` directives
---
# vue/no-useless-v-bind
> disallow unnecessary `v-bind` directives
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.

## :book: Rule Details

This rule reports `v-bind` with a string literal value.
The `v-bind` with a string literal value can be changed to a static attribute definition.

<eslint-code-block fix :rules="{'vue/no-useless-v-bind': ['error']}">

```vue
<template>
<!-- ✓ GOOD -->
<div foo="bar"/>
<div :foo="bar"/>
<!-- ✗ BAD -->
<div v-bind:foo="'bar'"/>
<div :foo="'bar'"/>
</template>
```

</eslint-code-block>

## :wrench: Options

```js
{
"vue/no-useless-v-bind": ["error", {
"ignoreIncludesComment": false,
"ignoreStringEscape": false
}]
}
```

- `ignoreIncludesComment` ... If `true`, do not report expressions containing comments. default `false`.
- `ignoreStringEscape` ... If `true`, do not report string literals with useful escapes. default `false`.

### `"ignoreIncludesComment": true`

<eslint-code-block fix :rules="{'vue/no-useless-v-bind': ['error', {ignoreIncludesComment: true}]}">

```vue
<template>
<!-- ✓ GOOD -->
<div v-bind:foo="'bar'/* comment */"/>
<!-- ✗ BAD -->
<div v-bind:foo="'bar'"/>
</template>
```

</eslint-code-block>

### `"ignoreStringEscape": true`

<eslint-code-block fix :rules="{'vue/no-useless-v-bind': ['error', {ignoreStringEscape: true}]}">

```vue
<template>
<!-- ✓ GOOD -->
<div v-bind:foo="'bar\nbaz'"/>
</template>
```

</eslint-code-block>

## :couple: Related rules

- [vue/no-useless-mustaches]
- [vue/no-useless-concat]

[vue/no-useless-mustaches]: ./no-useless-mustaches.md
[vue/no-useless-concat]: ./no-useless-concat.md

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-useless-v-bind.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-useless-v-bind.js)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -97,6 +97,7 @@ module.exports = {
'no-unused-vars': require('./rules/no-unused-vars'),
'no-use-v-if-with-v-for': require('./rules/no-use-v-if-with-v-for'),
'no-useless-concat': require('./rules/no-useless-concat'),
'no-useless-v-bind': require('./rules/no-useless-v-bind'),
'no-v-html': require('./rules/no-v-html'),
'no-v-model-argument': require('./rules/no-v-model-argument'),
'no-watch-after-await': require('./rules/no-watch-after-await'),
Expand Down
153 changes: 153 additions & 0 deletions lib/rules/no-useless-v-bind.js
@@ -0,0 +1,153 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'

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

const DOUBLE_QUOTES_RE = /"/gu
const SINGLE_QUOTES_RE = /'/gu

/**
* @typedef {import('eslint').Rule.RuleContext} RuleContext
* @typedef {import('vue-eslint-parser').AST.VDirective} VDirective
*/

module.exports = {
meta: {
docs: {
description: 'disallow unnecessary `v-bind` directives',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/no-useless-v-bind.html'
},
fixable: 'code',
messages: {
unexpected: 'Unexpected `v-bind` with a string literal value.'
},
schema: [
{
type: 'object',
properties: {
ignoreIncludesComment: {
type: 'boolean'
},
ignoreStringEscape: {
type: 'boolean'
}
}
}
],
type: 'suggestion'
},
/** @param {RuleContext} context */
create(context) {
const opts = context.options[0] || {}
const ignoreIncludesComment = opts.ignoreIncludesComment
const ignoreStringEscape = opts.ignoreStringEscape
const sourceCode = context.getSourceCode()

/**
* Report if the value expression is string literals
* @param {VDirective} node the node to check
*/
function verify(node) {
if (!node.value || node.key.modifiers.length) {
return
}
const { expression } = node.value
if (!expression) {
return
}
let strValue, rawValue
if (expression.type === 'Literal') {
if (typeof expression.value !== 'string') {
return
}
strValue = expression.value
rawValue = expression.raw.slice(1, -1)
} else if (expression.type === 'TemplateLiteral') {
if (expression.expressions.length > 0) {
return
}
strValue = expression.quasis[0].value.cooked
rawValue = expression.quasis[0].value.raw
} else {
return
}

const tokenStore = context.parserServices.getTemplateBodyTokenStore()
const hasComment = tokenStore
.getTokens(node.value, { includeComments: true })
.some((t) => t.type === 'Block' || t.type === 'Line')
if (ignoreIncludesComment && hasComment) {
return
}

let hasEscape = false
if (rawValue !== strValue) {
// check escapes
const chars = [...rawValue]
let c = chars.shift()
while (c) {
if (c === '\\') {
c = chars.shift()
if (
c == null ||
// ignore "\\", '"', "'", "`" and "$"
'nrvtbfux'.includes(c)
) {
// has useful escape.
hasEscape = true
break
}
}
c = chars.shift()
}
}
if (ignoreStringEscape && hasEscape) {
return
}

context.report({
// @ts-ignore
node,
messageId: 'unexpected',
fix(fixer) {
if (hasComment || hasEscape) {
// cannot fix
return null
}
const text = sourceCode.getText(node.value)
const quoteChar = text[0]

const shorthand = node.key.name.rawName === ':'
/** @type { [number, number] } */
const keyDirectiveRange = [
node.key.name.range[0],
node.key.name.range[1] + (shorthand ? 0 : 1)
]

let attrValue
if (quoteChar === '"') {
attrValue = strValue.replace(DOUBLE_QUOTES_RE, '&quot;')
} else if (quoteChar === "'") {
attrValue = strValue.replace(SINGLE_QUOTES_RE, '&apos;')
} else {
attrValue = strValue
.replace(DOUBLE_QUOTES_RE, '&quot;')
.replace(SINGLE_QUOTES_RE, '&apos;')
}
return [
fixer.removeRange(keyDirectiveRange),
fixer.replaceText(expression, attrValue)
]
}
})
}

return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='bind'][key.argument!=null]": verify
})
}
}

0 comments on commit e644855

Please sign in to comment.